Skip to main content

Officiating Governor

import { officiatingGovernor } from 'tods-competition-factory';

The officiatingGovernor re-exports all officiating mutation and query functions for use outside the officiating engine. While the officiatingEngine provides a complete stateful API, the governor exports individual functions that can be called directly with an officialRecord parameter.

For full engine documentation including state management, executionQueue, state machines, and workflow examples, see Officiating Engine.


Mutations

createOfficialRecord

Creates a new OfficialRecord with empty arrays for certifications, evaluations, assignments, and suspensions.

{ personId: string; organisationId?: string; officialRecordId?: string }

Returns: { success, officialRecord }


addCertification / modifyCertification / removeCertification

CRUD operations for certifications within an official record.

// Add
{ officialRecord; organisationId: string; certificationFamily: string; certificationLevel?: string; status?: string; validFrom?: string; validUntil?: string }
// Returns: { success, certification }

// Modify
{ officialRecord; certificationId: string; updates: Partial<OfficialCertification> }

// Remove
{ officialRecord; certificationId: string }

transitionCertificationStatus

Validates transition against VALID_CERTIFICATION_TRANSITIONS and records status history.

{ officialRecord; certificationId: string; toStatus: CertificationStatus; transitionedBy?: string; reason?: string }

Returns: { success, certification }


addEvaluation / modifyEvaluation / removeEvaluation

CRUD operations for performance evaluations. Modify is restricted to DRAFT or REJECTED status.

// Add
{ officialRecord; evaluatorPersonId: string; overallRating: number; scores?: EvaluationScore[]; policyName?: string; tournamentId?: string; matchUpId?: string; comments?: string }
// Returns: { success, evaluation }

// Modify
{ officialRecord; evaluationId: string; updates: Partial<OfficialEvaluation> }

// Remove
{ officialRecord; evaluationId: string }

transitionEvaluationStatus

Validates transition. On SUBMITTED, validates required criterion scores against the linked evaluation policy.

{ officialRecord; evaluationId: string; toStatus: EvaluationStatus; transitionedBy?: string; reason?: string }

assignOfficial / removeOfficialAssignment

// Assign
{ officialRecord; tournamentId: string; roleSubtype: string; assignedDate?: string; startDate?: string; endDate?: string;
// Conflict-of-interest gate (opt-in) — see getOfficialConflicts
policyDefinitions?: PolicyDefinitions; participants?: Participant[]; nationalityCode?: string; organisationIds?: string[] }
// Returns: { success, assignment, conflicts? }

// Remove
{ officialRecord; assignmentId: string }
// tournament-scoped declarations work on this route too
{ officialRecord; tournamentId; roleSubtype;
policyDefinitions?; participants?;
officialParticipantId?; groupParticipants?; // SHARED_GROUPING inputs
nationalityCode?; organisationIds? }

One input set, forwarded whole

Every route that can evaluate a conflict accepts ConflictEvaluationInputs and forwards it whole via conflictInputsFrom(). Adding a new input means editing CONFLICT_INPUT_KEYS once; every route inherits it.

This is structural, not stylistic. Routes used to hand-list the fields they forwarded, and when the tournament-scoped inputs (officialParticipantId + groupParticipants) were added, one route forwarded them and the other did not — so the same conflict blocked a per-matchUp assignment and passed on the tournament-level one. A rule that applies or not depending on which route the operator used is worse than no rule, because it looks enforced. A conformance test now fails, naming the route and the key, if any route drops an input.

participants is deliberately outside the set: it is route-specific (supplied by the caller on assignOfficial, derived from the matchUp's sides on getMatchUpOfficialConflicts).

Both assignment routes see the same declarations. Without officialParticipantId + groupParticipants this route would see only registry declarations, so a GROUP-expressed conflict would block per-matchUp and pass here — same feature, two routes, different answers.

Note officialRecord is required here, unlike getOfficialConflicts: assignOfficial mutates the record (it pushes an assignment onto it).

The conflict gate is opt-in: with no policyDefinitions the assignment behaves exactly as before. Supply a conflict policy and participants and the assignment is checked first — a BLOCK-severity conflict returns { error: OFFICIAL_CONFLICT_OF_INTEREST, conflicts } and records nothing, while WARN-severity conflicts are returned alongside a successful assignment so the caller can surface them.

Supplying a policy without participants is an error (MISSING_CONFLICT_PARTICIPANTS), not a silent pass: a check that could not run must not be indistinguishable from a check that found nothing.


transitionAssignmentStatus

{ officialRecord; assignmentId: string; toStatus: AssignmentStatus; transitionedBy?: string; reason?: string }

addConflictDeclaration / removeConflictDeclaration

Records a relationship the official has declared. Self-declaration is how federations administer conflicts of interest — the factory cannot infer that an official coaches a player, so the declaration is the record of it.

// Add — one of personId, participantId or organisationId is REQUIRED
{ officialRecord; personId?: string; participantId?: string; organisationId?: string;
relationship?: string; declaredAt?: string; declaredBy?: string; notes?: string }
// Returns: { success, declaration }

// Remove
{ officialRecord; declarationId: string }

A declaration that identifies nothing is refused (INVALID_VALUES) — it could never match a participant, so accepting it would create a record that looks like a disclosure while checking nothing.


addSuspension / removeSuspension

// Add
{ officialRecord; suspensionType?: string; suspensionNotes?: string; suspendedFrom?: string; suspendedUntil?: string }
// Returns: { success, suspension }

// Remove
{ officialRecord; suspensionId: string }

addCertificationRequirement

Defines organisational prerequisites for a certification level. Used by getOfficialEligibility.

{ officialRecord; certificationFamily: string; certificationLevel: string; organisationId: string; requirements: string[]; prerequisiteLevels?: string[]; minimumAssignments?: number; minimumEvaluationScore?: number }

addEvaluationPolicy

Attaches an evaluation template (sections, criteria, scoring method) to the official record.

{
officialRecord;
evaluationPolicy: EvaluationPolicy;
}

Queries

queryOfficialRecord

{
officialRecord;
}

Returns: { success, officialRecord } — the full record structure.


getOfficialCertifications

{ officialRecord; certificationFamily?: string; certificationLevel?: string; organisationId?: string; activeOnly?: boolean }

Returns: { success, certifications }


validateCertification

Checks status and date validity for a specific certification.

{ officialRecord; certificationId: string; asOfDate?: string }

Returns: { success, valid, reasons, certification }


getEvaluationSummary

Computes average rating from APPROVED evaluations.

{ officialRecord; policyName?: string }

Returns: { success, summary: { evaluationCount, averageRating } }


getEvaluationTemplate

Converts an evaluation policy into a flat array of EvaluationFormField objects for UI rendering.

{ officialRecord?; policyName?: string; evaluationPolicy?: EvaluationPolicy }

Returns: { success, fields, evaluationPolicy }


getOfficialEligibility

Checks whether an official meets all requirements for a certification: active cert, no suspensions, minimum assignments, minimum evaluation score, prerequisite levels.

{ officialRecord; certificationFamily: string; certificationLevel?: string; organisationId?: string; asOfDate?: string }

Returns: { success, eligible, reasons }reasons is empty when eligible.


getOfficialAssignments

{ officialRecord; tournamentId?: string; roleSubtype?: string; status?: string }

Returns: { success, assignments }


getOfficialConflicts

Evaluates an official against a tournament's entered participants and returns every conflict of interest the supplied policy declares interest in. Pure and side-effect free — the caller supplies the participants, so the query makes no assumption about where the tournament record lives.

{ officialRecord; participants?: Participant[]; nationalityCode?: string; organisationIds?: string[];
policyDefinitions?: PolicyDefinitions }

Returns: { success, conflicts, blocked }blocked is true when any conflict carries BLOCK severity.

Rules

RuleDetects
SAME_PERSONThe official is themselves entered in the tournament.
DECLARED_RELATIONSHIPA declaration matches an entered participant by personId or participantId.
NATIONALITYThe official's nationalityCode matches a participant's nationality (or representing).
ORGANISATIONA declared — or supplied — organisation matches a participant's person.parentOrganisationId.

nationalityCode must be supplied by the caller: an OfficialRecord carries no person detail, so the official's own nationality cannot be derived from it.

Two declaration sources — either is sufficient

officialRecord is optional. A conflict declaration can come from either:

sourcescopesupplied as
registry (courthive-ams)durable, cross-tournamentofficialRecord.conflictDeclarations
tournamentRecordtransient, this event onlyofficialParticipantId + groupParticipants

Supplying neither is an error (MISSING_CONFLICT_SOURCE).

The tournament-scoped source exists because expecting officials to keep a global registry current is unrealistic — and an empty registry would otherwise make every check return "no conflicts", which is indistinguishable from a check that passed. A tournament assignment is the natural moment to capture a relationship, so the declaration lives where it is created.

A GROUP participant containing the official and one or more competitors is the declaration. Create it with createGroupParticipant({ groupName, individualParticipantIds, participantRole }).

participantRole marks an authored relationship

SHARED_GROUPING defaults to WARN, because GROUP is a general primitive — squads and attribute-derived groupings are legitimate and would otherwise false-positive. The GROUP's own participantRole distinguishes an authored relationship from an incidental one, and ConflictRule.roleSeverity escalates per role:

[CONFLICT_SHARED_GROUPING]: {
enabled: true,
severity: CONFLICT_WARN, // incidental groupings
roleSeverity: { COACH: 'BLOCK', MEDICAL: 'BLOCK', PHYSIO: 'BLOCK', TRAINER: 'BLOCK' },
}

createGroupParticipant defaults participantRole to OTHER, so an unspecified grouping carries OTHER, is absent from roleSeverity, and falls through to the base severity.

A rule that is absent from the policy, or present with enabled: false, is not evaluated at all — a policy is an allow-list of checks, never a silent partial application.

Policies

POLICY_OFFICIATING_CONFLICT_OF_INTEREST (default) blocks on SAME_PERSON and DECLARED_RELATIONSHIP, warns on ORGANISATION, and leaves NATIONALITY disabled. Shared nationality is disqualifying at ITF-level international events and meaningless at national ones, where every official necessarily shares the players' nationality — enabling it by default would make the check noise at most events.

POLICY_OFFICIATING_CONFLICT_OF_INTEREST_ITF enables all four rules at BLOCK.


getMatchUpOfficialConflicts

The per-matchUp counterpart to getOfficialConflicts. Resolves the sides of a single matchUp — expanding any PAIR/TEAM side to the individuals within it — and evaluates the official against just those participants.

{ officialRecord; tournamentRecord; drawDefinition; matchUpId: string; event?: Event;
nationalityCode?: string; organisationIds?: string[]; policyDefinitions?: PolicyDefinitions }

Returns: { success, conflicts, blocked, checkedParticipants }

Scoping to one matchUp is the sharper check — a chair umpire who shares a nationality with someone in a different quarter of the draw is not a conflict for this assignment. Only participants actually assigned to the matchUp's sides are evaluated; potential participants (those who could still advance into it) are deliberately excluded, since treating every possible opponent as a conflict would block most early-round assignments.

Note this resolves the matchUp inContext: a raw drawDefinition matchUp carries only drawPosition on its sides, and participantIds come from the structure's positionAssignments during hydration.

Gating addMatchUpOfficial

addMatchUpOfficial (scheduleGovernor) accepts the same opt-in gate as assignOfficial:

{ matchUpId; participantId; officialType?;
policyDefinitions?; officialRecord?; nationalityCode?; organisationIds? }
// Returns: { success, conflicts? } | { error: OFFICIAL_CONFLICT_OF_INTEREST, conflicts }

With no policyDefinitions the behaviour is unchanged. With a policy, an officialRecord is required — a policy supplied without one is an error, not a pass. BLOCK refuses the assignment and writes nothing; WARN conflicts return alongside the successful assignment.

The two routes cover different scopes and are independent:

routescope
assignOfficial (officiatingGovernor)the official's tournament-level engagement
addMatchUpOfficial (scheduleGovernor)a specific matchUp — matchUp.schedule.official