Type Definitions
Overview
This reference is generated from the factory's canonical TypeScript tournament model, including optional markers, arrays, unions and comments. Import the published types instead of copying these declarations into your application. Referenced enum names are described in Enums. Referenced types imported from other modules retain their source names. TypeScript declarations and runtime JSON-schema validation are separate contracts: schema acceptance alone does not describe every operational field (including exit provenance).
Structure is ItemStructure | ContainerStructure, discriminated by structureType: a CONTAINER
holds child structures, not its own matchUps or positionAssignments. An ITEM has no child
structures; its discriminator remains optional for stored-record compatibility. DrawLink is
RoundDrawLink | PositionDrawLink: WINNER/LOSER links name a source round, POSITION links name
source finishing positions. The soft unions allow reads before narrowing while rejecting malformed
construction. This is a type contract, not a runtime conversion.
Maintainers: run node scripts/generateModelDocs.mjs after model changes, and
node scripts/generateModelDocs.mjs --check to verify this page has not drifted.
Common
Extension
export interface Extension {
// Written by `addExtension` whenever `creationTime` is not disabled, and honoured when
// the caller supplies one. Declared here to match runtime: the field has always been
// written, but an `Object.assign` bypassed the checker, so the type was silently out of
// sync for as long as the field existed. Mirrors {@link TimeItem}.
createdAt?: Date | string;
description?: string;
name: string;
value: any;
}
TimeItem
export interface TimeItem {
createdAt?: Date | string;
itemDate?: Date | string;
itemSubTypes?: string[];
itemType?: string;
itemValue?: any;
}
Generic
Address
export interface Address {
addressLine1?: string;
addressLine2?: string;
addressLine3?: string;
addressName?: string;
addressType?: AddressTypeUnion;
city?: string;
countryCode?: CountryCodeUnion;
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
/**
* Stored as a number in production records, but written as a string by some
* producers (e.g. `activateFromSanctioning` stringifies sanctioning coordinates).
* Both forms are real; consumers must coerce rather than assume.
*/
latitude?: string | number;
longitude?: string | number;
notes?: string;
postalCode?: string;
state?: string;
timeItems?: TimeItem[];
timeZone?: string;
updatedAt?: Date | string;
}
Category
export interface Category {
ageCategoryCode?: string;
ageMax?: number;
ageMaxDate?: string;
ageMin?: number;
ageMinDate?: string;
ballType?: BallTypeUnion;
categoryName?: string;
categoryType?: string;
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
ratingMax?: number;
ratingMin?: number;
ratingType?: string;
/**
* Bounds on the SUM of a pair's ratings, for events priced as a combined band —
* "NTRP 7.0 combined doubles", where a 3.0 and a 4.0 may enter together.
*
* Distinct from `ratingMin`/`ratingMax`, which bound an INDIVIDUAL. Modelling a combined event
* with `ratingMax: 7.0` does not merely lose information, it produces a wrong answer: the
* individual check rejects the 4.0 half of a legal 3.0 + 4.0 pair. `validateParticipantRating`
* therefore skips individual rating validation whenever a combined bound is stated, mirroring the
* skip already applied to combined AGE categories (`ageCategoryCode` matching `C##-##`).
*
* Both may legitimately be present: a federation can cap the pair at 7.0 *and* bar anyone above
* 5.0 individually. Where both are set the individual bounds still apply — the skip is keyed on
* `ratingType` being absent from the individual pair, not on the combined bound existing. See
* `validateParticipantRating`.
*
* NOT enforced at pair grain yet. Nothing sums a pair's ratings and compares it to these bounds;
* that belongs at entry time where both partners are known. Stating the bound is what stops the
* wrong rejection — enforcing it is a later change, deliberately not smuggled in here.
*/
combinedRatingMax?: number;
combinedRatingMin?: number;
subType?: string;
timeItems?: TimeItem[];
type?: CategoryUnion;
updatedAt?: Date | string;
}
Entry
export interface Entry {
createdAt?: Date | string;
entryId?: string;
entryPosition?: number;
entryStage?: StageTypeUnion;
entryStageSequence?: number;
entryStatus?: EntryStatusUnion;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
participantId: string;
// CODES first-class: previously stored as `roundTarget` extension on entry
roundTarget?: number;
timeItems?: TimeItem[];
updatedAt?: Date | string;
scaleValue?: number;
}
OnlineResource
export interface OnlineResource {
createdAt?: Date | string;
extensions?: Extension[];
identifier?: string;
isMock?: boolean;
name?: string;
notes?: string;
provider?: string;
resourceSubType?: string;
resourceType?: OnlineResourceTypeUnion;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
tournament
Tournament
export interface Tournament {
activeDates?: Date[] | string[]; // dates from startDate to endDate on which the tournament is active
createdAt?: Date | string;
endDate?: string;
events?: Event[];
extensions?: Extension[];
// CODES first-class: previously stored as `factory` extension (processor versioning)
factory?: { createdVersion?: string; version?: string; timeStamp?: number; [key: string]: any };
formalName?: string;
hostCountryCode?: CountryCodeUnion;
indoorOutdoor?: IndoorOutdoorUnion;
isMock?: boolean;
// CODES first-class: previously stored as `linkedTournamentsIds` extension
// with shape `{tournamentIds: string[]}`; CODES flattens that wrapper away.
linkedTournamentIds?: string[];
localTimeZone?: string;
matchUps?: MatchUp[];
notes?: string;
onlineResources?: OnlineResource[];
parentOrganisationId?: string;
parentOrganisation?: Organisation;
participants?: Participant[];
/**
* What each finishing position was worth, where an authority publishes ONE ladder for the whole
* competition rather than one per event. The counterpart to `totalPrizeMoney`, at the same
* grain: without it a reader can find the total at tournament level but must go to the events
* for the shape, and for a single-event competition there are no events to go to.
*
* NOT an aggregation of `Event.prizeMoneyAwards`. A ladder cannot be summed or merged across
* events — see `PrizeMoneyAward` — so this holds only what the source itself stated at
* tournament grain. Where an event carries its own ladder, the event's ladder governs that
* event; this one never overrides it.
*/
prizeMoneyAwards?: PrizeMoneyAward[];
processCodes?: string[];
promotionalName?: string;
registrationProfile?: RegistrationProfile;
/**
* What a governing body decided about this competition — see `TournamentSanction`.
* Supersedes the binary `processCodes: ['SANCTIONED']` marker, which cannot express a
* decision, an authority chain, or what the sanction confers. `processCodes` is left in
* place for existing consumers.
*/
sanction?: TournamentSanction;
/** Additional sanctions where a competition is approved by more than one body. */
sanctions?: TournamentSanction[];
// CODES first-class group leaf: previously stored as separate
// `schedulingProfile`, `scheduleLimits`, and `scheduleTiming` extensions.
scheduling?: {
profile?: any;
dailyLimits?: any;
timing?: any;
/** transit between pairs of venues (scheduling-pipeline.md § 3.4): first-class, never a legacy extension */
venueTransits?: VenueTransitEntry[];
// Alternate ("contingency") scheduling plans a tournament director builds
// off the current schedule, cycles through, and can commit as the official
// schedule (uncompleted matchUps only). Persisted first-class — NEVER a
// legacy extension, and NEVER emitted to public / arena read surfaces.
scenarios?: ScheduleScenario[];
practice?: {
defaultCapacity?: number | null;
};
[key: string]: any;
};
season?: string;
startDate?: string;
surfaceCategory?: SurfaceCategoryUnion;
timeItems?: TimeItem[];
totalPrizeMoney?: PrizeMoney[];
tournamentCategories?: Category[];
tournamentGroups?: string[];
tournamentId: string;
tournamentLevel?: TournamentLevelUnion;
tournamentName?: string;
tournamentOtherIds?: UnifiedTournamentID[];
tournamentRank?: string;
/**
* CODES first-class: the same tournament across its editions — the same value every year it is held, where
* `tournamentId` is one edition. A ranking that counts only the better of two editions of one tournament (ITF
* Juniors Reg. 10) reads it from the point awards (`CountingBucket.bestPerSeries`).
*/
tournamentSeriesId?: string;
tournamentStatus?: TournamentStatusUnion;
/**
* When the competition was cancelled.
*
* `TournamentStatusEnum.CANCELLED` says THAT it was; this says when, and `cancellationReason`
* says why. A discovery surface has to strike a cancelled tournament through rather than drop it,
* because somebody entered it and needs to see what happened.
*
* Unrelated to `PracticeRegistration.cancelledAt`, which is a different object's own lifecycle.
*/
cancelledAt?: Date | string;
cancellationReason?: string;
tournamentTier?: TierClassification;
updatedAt?: Date | string;
venues?: Venue[];
weekdays?: WeekdayUnion[];
}
Organisation
export interface Organisation {
onlineResources?: OnlineResource[];
organisationAbbreviation: string;
parentOrganisationId?: string;
extensions?: Extension[];
organisationName: string;
organisationId: string;
notes?: string;
/**
* What KIND of body this is — a national association, a school, a club.
*
* INTRINSIC to the organisation, and deliberately distinct from {@link SanctioningAuthority.role},
* which is POSITIONAL: what part a body plays in one particular approval chain. A school is a
* SCHOOL wherever it appears, and may occupy a CLUB-level rung in one federation's hierarchy and
* no rung at all in another's. Collapsing the two would make a body's identity depend on which
* chain you happened to read it from.
*
* Not a new vocabulary. `organisationType` has been declared in `tournament.schema.json` and
* documented on the public docs site all along; the TypeScript types were the only one of the
* three declarations missing it. This closes that gap rather than opening a fourth.
*
* Optional and never inferred: an organisation whose type nobody recorded is unknown, not a CLUB.
*/
organisationType?: OrganisationTypeUnion;
}
RegistrationProfile
export interface RegistrationProfile {
// temporal
createdAt?: Date | string;
entriesClose?: Date | string;
entriesOpen?: Date | string;
updatedAt?: Date | string;
withdrawalDeadline?: Date | string;
// entry & eligibility
eligibilityNotes?: string;
entryFees?: RegistrationEntryFee[];
entryMethod?: string;
entryUrl?: string;
// logistics (structured + HTML notes)
accommodation?: LogisticsSection;
hospitality?: LogisticsSection;
medicalInfo?: LogisticsSection;
transportation?: LogisticsSection;
// simple text
contingencyPlan?: string;
dressCode?: string;
// ceremony & social
awardsCeremonyDate?: string;
awardsDescription?: string;
drawCeremonyDate?: string;
socialEvents?: SocialEvent[];
// regulations & compliance
codeOfConduct?: DocumentLink;
regulations?: DocumentLink[];
// branding
sponsors?: Sponsor[];
extensions?: Extension[];
isMock?: boolean;
notes?: string;
timeItems?: TimeItem[];
}
participant
Participant
export interface Participant {
contacts?: Contact[];
/**
* Members who hold contact information for this grouping — "who do I call about this group".
* Only relevant when participantType is TEAM or GROUP, and entirely optional.
*
* A POINTER to members rather than contact details copied onto the grouping. A copy is a snapshot:
* rename the person or change their number and the group keeps advertising the old one. Entries must
* appear in `individualParticipantIds` — a pointer to a non-member is stale rather than authoritative,
* so it is rejected on write rather than tolerated on read.
*/
contactParticipantIds?: string[];
createdAt?: Date | string;
extensions?: Extension[];
homeVenueIds?: string[]; // only releveant when participantType is TEAM
individualParticipantIds?: string[];
isMock?: boolean;
notes?: string;
onlineResources?: OnlineResource[];
participantId: string;
participantName?: string;
// CODES first-class: this participant's identity in OTHER organisations' systems, one
// entry per organisation. Unlike `person.personOtherIds` this works for PAIR and TEAM
// participants, which carry no `person` at all.
participantOtherIds?: UnifiedParticipantID[];
participantOtherName?: string;
participantRole?: ParticipantRoleUnion;
participantRoleResponsibilities?: string[];
participantStatus?: ParticipantStatusUnion;
participantType?: ParticipantTypeUnion;
penalties?: Penalty[];
person?: Person;
personId?: string;
/**
* CODES first-class: this participant's tournament-scoped presence history — arrival and departure,
* previously stored as `SIGN_IN_STATUS` timeItems.
*
* A COLLECTION because the history is the value. Nothing signs anybody out at the end of a day, so
* "is this person here today" is answered by reading the log as of that date, not by taking a latest
* value — a volunteer who signed in on Thursday reads SIGNED_IN on Sunday otherwise. The derived
* latest-value answer is still hydrated as `participant.signedIn`.
*/
presence?: PresenceAttestation[];
representing?: CountryCodeUnion;
teamId?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
useOtherName?: boolean;
}
Contact
export interface Contact {
createdAt?: Date | string;
emailAddress?: string;
extensions?: Extension[];
fax?: string;
isMock?: boolean;
isPublic?: boolean;
mobileTelephone?: string;
name?: string;
notes?: string;
/** Whose contact this is — the person themselves, or a parent / guardian / chaperone acting for them. */
relationship?: ContactRelationshipUnion;
telephone?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
Penalty
export interface Penalty {
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
issuedAt?: string;
matchUpId?: string;
notes?: string;
penaltyCode?: string;
penaltyId: string;
penaltyType: PenaltyTypeUnion;
refereeParticipantId?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
Person
export interface Person {
addresses?: Address[];
biographicalInformation?: BiographicalInformation;
birthDate?: string;
/** Year-precision date of birth (CODES). Use when only the birth year is
* known (common in federation junior data). `birthDate` is authoritative when
* both are present; age/category eligibility falls back to this via the
* calendar-year convention (age-in-year = year − birthYear). */
birthYear?: number;
contacts?: Contact[];
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
nationalityCode?: string;
nativeFamilyName?: string;
nativeGivenName?: string;
notes?: string;
onlineResources?: OnlineResource[];
otherNames?: string[];
parentOrganisationId?: string;
passportFamilyName?: string;
passportGivenName?: string;
personId: string;
personOtherIds?: UnifiedPersonID[];
previousNames?: string[];
sectionId?: string;
sex?: SexUnion;
standardFamilyName?: string;
standardGivenName?: string;
status?: string;
tennisId?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
wheelchair?: boolean;
}
BiographicalInformation
export interface BiographicalInformation {
ageBeganTennis?: number;
ageTurnedPro?: number;
birthCountryCode?: CountryCodeUnion;
coachId?: string;
createdAt?: Date | string;
doublePlayingHand?: PlayingDoubleHandCodeUnion;
extensions?: Extension[];
height?: number;
heightUnit?: LengthUnitUnion;
isMock?: boolean;
notes?: string;
organisationIds?: string[];
placeOfResidence?: string;
playingHand?: PlayingHandCodeUnion;
residenceCountryCode?: CountryCodeUnion;
teamAttributes?: TeamAttribute[];
timeItems?: TimeItem[];
updatedAt?: Date | string;
weight?: number;
weightUnit?: WeightUnitEnum;
}
event
Event
export interface Event {
activeDates?: Date[] | string[]; // dates from startDate to endDate on which the tournament is active
allowedDrawTypes?: DrawTypeUnion[];
category?: Category;
competitionFormat?: competitionFormat;
createdAt?: Date | string;
discipline?: DisciplineUnion;
drawDefinitions?: DrawDefinition[];
endDate?: string;
entries?: Entry[];
eventAbbreviation?: string;
eventId: string;
eventLevel?: TournamentLevelUnion;
eventName?: string;
/**
* Prize money offered for this event.
*
* The counterpart to `Tournament.totalPrizeMoney`. Its absence was a real gap: `EventProposal`
* has carried `prizeMoney` all along, so an applicant could propose event-level prize money that
* activation then dropped for want of anywhere to put it — and the mocks documentation worked
* around it with an ad-hoc extension.
*/
prizeMoney?: PrizeMoney[];
/**
* What each finishing position in this event was worth — the ladder behind `prizeMoney`.
*
* Event grain because that is the grain ladders are published at. A main draw, its qualifying,
* and a smaller-draw event in the same tournament each carry their own — different rung counts
* at different draw sizes, none of them derivable from another.
*/
prizeMoneyAwards?: PrizeMoneyAward[];
/**
* Registration windows, fees and entry URL for THIS event, where they differ from the
* tournament's. Resolve with `getEffectiveRegistrationProfile`, which merges field by field —
* an event that overrides only `entriesClose` still inherits the tournament's `entryUrl`.
*/
registrationProfile?: EventRegistration;
/** how this event accepts entries, and how many — see {@link EventEntryProfile} */
entryProfile?: EventEntryProfile;
/**
* Conditions on WHO may enter this event, beyond what `category` and `gender` express.
*
* Where a governing body's level name encodes one — "Level 4 Closed" meaning closed to the
* section — the grade belongs in `sanction.classification` and the rule belongs here. Welding
* them into one string makes the grade unsortable and the rule unreadable.
*/
entryRestrictions?: EntryRestriction[];
/** when this event was cancelled — the *when* behind a CANCELLED status */
cancelledAt?: Date | string;
/** why this event was cancelled, for a consumer that must explain it to an entrant */
cancellationReason?: string;
/**
* Event-grain sanction. Grade is genuinely per-event in several federations — one tournament
* routinely carries different categories for different age groups — mirroring `eventTier`.
* Where absent, the tournament's `sanction` applies.
*/
sanction?: TournamentSanction;
eventOrder?: number;
// CODES first-class: this event's identity in OTHER organisations' systems, one entry
// per organisation. The entry flagged `isOrigin` is the sanctioning source the event
// came from — its `tournamentId` is that organisation's, NOT the carrying record's.
eventOtherIds?: UnifiedEventID[];
eventRank?: string;
eventTier?: TierClassification;
eventType?: EventTypeUnion;
extensions?: Extension[];
// CODES first-class: previously stored as `flightProfile` extension. Lives on the EVENT only: a draw's flight is
// the `flights[]` entry whose `drawId` matches it, and the draw carries its `flightNumber`.
flightProfile?: FlightProfile;
gender?: GenderUnion;
indoorOutdoor?: IndoorOutdoorUnion;
isMock?: boolean;
links?: DrawLink[];
matchUpFormat?: string;
notes?: string;
processCodes?: string[];
startDate?: string;
surfaceCategory?: SurfaceCategoryUnion;
tennisOfficialIds?: string[];
tieFormat?: TieFormat;
tieFormatId?: string;
tieFormats?: TieFormat[];
timeItems?: TimeItem[];
updatedAt?: Date | string;
weekdays?: WeekdayUnion[];
wheelchairClass?: WheelchairClassUnion;
}
drawDefinition
DrawDefinition
export interface DrawDefinition {
/** Versioned format configuration; competition lifecycle state is stored separately. */
competitionProfile?: CompetitionProfile;
competitionRoster?: string[];
competitionRounds?: RotatingPartnerRoundRecord[];
competitionSettlements?: RotatingPartnerSettlement[];
activeDates?: Date[] | string[]; // dates from startDate to endDate on which the tournament is active
automated?: boolean;
competitionFormat?: competitionFormat;
createdAt?: Date | string;
drawId: string;
drawName?: string;
drawOrder?: number;
// CODES first-class: this draw's identity in OTHER organisations' systems. The entry
// flagged `isOrigin` is the system the draw came from. See {@link UnifiedDrawID}.
drawOtherIds?: UnifiedDrawID[];
drawRepresentativeIds?: string[];
drawStatus?: DrawStatusUnion;
drawType?: DrawTypeUnion;
endDate?: string;
entries?: Entry[];
extensions?: Extension[];
// CODES first-class: previously stored as `competitionState` extension
competitionState?: any;
// CODES first-class: previously stored as `draftState` extension
draftState?: any;
// Per-flight ordering number, sourced from the parent event's flightProfile
// (`flightProfile.flights[].flightNumber`) when a draw was generated as part
// of a multi-flight event. Optional because legacy + single-flight draws
// don't carry it. Consumers (TMX draws table/grid) use it for display
// ordering.
flightNumber?: number;
isMock?: boolean;
// CODES first-class: previously stored as `lineUps` extension
lineUps?: any;
links?: DrawLink[];
matchUpFormat?: string;
matchUps?: MatchUp[];
matchUpType?: EventTypeUnion;
notes?: string;
processCodes?: string[];
startDate?: string;
structures?: Structure[];
tieFormat?: TieFormat;
tieFormatId?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
Structure
/**
* Discriminated by `structureType`: test `structureType === CONTAINER` to reach `structures`, anything else to
* reach `matchUps` and `positionAssignments`. This is the soft form; the 8.0.0 form drops the `never` fields so
* that a read must narrow first (Mentat/planning/FACTORY_STRUCTURE_UNIONS_8_0_0.md).
*/
export type Structure = ItemStructure | ContainerStructure;
StructureBase
/** The fields every structure carries, whether it holds matchUps or other structures. */
export interface StructureBase {
competitionFormat?: competitionFormat;
createdAt?: Date | string;
extensions?: Extension[];
finishingPosition?: FinishingPositionUnion;
isMock?: boolean;
matchUpFormat?: string;
matchUpType?: EventTypeUnion;
notes?: string;
processCodes?: string[];
qualifyingRoundNumber?: number;
roundLimit?: number;
roundOffset?: number;
// CODES first-class: previously stored as `roundTarget` extension (qualifying routing)
roundTarget?: number;
seedAssignments?: SeedAssignment[];
seedingProfile?: SeedingProfileUnion;
seedLimit?: number;
stage?: StageTypeUnion;
stageSequence?: number;
structureAbbreviation?: string;
structureId: string;
structureName?: string;
structureOrder?: number;
tieFormat?: TieFormat;
tieFormatId?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
ItemStructure
/**
* A structure that holds matchUps: an elimination bracket, a round robin group, an AD_HOC or Swiss round set.
*
* `structureType` is optional because stored records mostly omit it: only the groups nested in a round robin
* container say `ITEM`. The CONTAINER fields are typed `never`, so reading them on an un-narrowed `Structure`
* still compiles (as `undefined`) while writing them onto an ITEM does not.
*/
export interface ItemStructure extends StructureBase {
structureType?: `${StructureTypeEnum.ITEM}`;
matchUps?: MatchUp[];
positionAssignments?: PositionAssignment[];
structures?: never;
}
ContainerStructure
/** A structure that holds other structures: a round robin's groups. It carries no matchUps or assignments itself. */
export interface ContainerStructure extends StructureBase {
structureType: `${StructureTypeEnum.CONTAINER}`;
structures: Structure[];
matchUps?: never;
positionAssignments?: never;
}
PositionAssignment
export interface PositionAssignment {
bye?: boolean;
createdAt?: Date | string;
// CODES first-class: previously stored as `disableLinks` extension
disableLinks?: boolean;
drawPosition: number;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
participantId?: string;
/**
* Marks a qualifier: a seat reserved for one, and the qualifier once placed there (the flag stays, e.g. for a 'Q'
* beside the name). Cleared with the position; travels with the qualifier when positions are swapped.
*/
qualifier?: boolean;
// CODES first-class: previously stored as `tally` extension
tally?: TallyResult;
// CODES first-class: previously stored as `subOrder` extension
subOrder?: number;
/**
* true when `bye` on this assignment was placed by an exit-propagation cascade rather than by
* draw generation or by hand. `removeDoubleExit` consults it instead of inferring provenance
* from topology, and every site that clears `bye` clears this in the same statement.
*
* Named to stay clear of the `doubleExitPropagateBye` progression POLICY, which decides whether
* a cascade should place a BYE at all. This records that one did.
*
* First-class rather than an extension because `removeExtensions: true` is a supported option
* on getState/getTournament — an extension marker is destroyed by a routine deep copy, and its
* loss would be silent, reverting removal to the inference this replaces.
*/
byeFromPropagation?: boolean;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
SeedAssignment
export interface SeedAssignment {
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
participantId?: string;
seedNumber: number;
/**
* Why this seed was awarded. See {@link SeedingBasisEnum}. Absent means `RANKING`.
*
* First-class rather than an extension for the same reason `byeFromPropagation` is:
* `removeExtensions: true` is a supported option on getState/getTournament, so an extension
* marker is destroyed by a routine deep copy and its loss is silent.
*/
seedingBasis?: SeedingBasisUnion;
seedValue: number | string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
DrawLink
/**
* A round link names a source `roundNumber`; a position link names `finishingPositions`. `WINNER` labels both, so the
* label alone does not discriminate: the source structure does (linkTraversal.ts).
*/
export type DrawLink = RoundDrawLink | PositionDrawLink;
DrawLinkBase
/** The fields every draw link carries, whatever it feeds. */
export interface DrawLinkBase {
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
linkCondition?: string;
notes?: string;
target: DrawLinkTarget;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
RoundDrawLink
/** A link that moves the winners or losers of one round of an ELIMINATION source structure, match by match. */
export interface RoundDrawLink extends DrawLinkBase {
linkType: `${LinkTypeEnum.LOSER}` | `${LinkTypeEnum.WINNER}`;
source: DrawLinkSource & { roundNumber: number; finishingPositions?: never };
}
PositionDrawLink
/**
* A link that moves participants by their FINISHING POSITION in a round robin: `finishingPositions` names the group
* places that cross it (`[1]`: every group's winner), decided by tally. Labelled `POSITION`, or `WINNER` (valid,
* permanently: CA, 2026-10-23); a reader asks `traversesByFinishingPosition`, never the label alone. A `roundNumber` is
* allowed and decides nothing (the factory records the groups' final round on a qualifying link).
*/
export interface PositionDrawLink extends DrawLinkBase {
linkType: `${LinkTypeEnum.POSITION}` | `${LinkTypeEnum.WINNER}`;
source: DrawLinkSource & { finishingPositions: number[]; roundNumber?: number };
}
DrawLinkSource
export interface DrawLinkSource {
bestOf?: number;
createdAt?: Date | string;
drawId?: string;
extensions?: Extension[];
finishingPositions?: number[];
isMock?: boolean;
notes?: string;
rankBy?: string;
qualifyingPositions?: number;
remainder?: boolean;
roundNumber?: number;
structureId: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
DrawLinkTarget
export interface DrawLinkTarget {
createdAt?: Date | string;
drawId?: string;
extensions?: Extension[];
feedProfile: PositioningProfileUnion;
groupedOrder?: number[];
isMock?: boolean;
notes?: string;
positionInterleave?: Interleave;
roundNumber: number;
structureId: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
Interleave
export interface Interleave {
interleave: number;
offset: number;
}
matchUp
MatchUp
export interface MatchUp {
/**
* CODES first-class: who has presented themselves at the desk for THIS matchUp, previously stored as
* `CHECK_IN` / `CHECK_OUT` timeItems carrying the participantId as `itemValue`.
*
* A COLLECTION, not a scalar — presence is folded from an ordered log and the timeItem representation
* could carry neither attribution nor a distinction between when a check-in happened and when it was
* written. Read it through `getCheckedInParticipantIds`, which folds either surface; it is also
* hydrated onto every in-context matchUp as `checkedInParticipantIds` / `allParticipantsCheckedIn`.
*/
checkIns?: PresenceAttestation[];
collectionId?: string;
collectionPosition?: number;
createdAt?: Date | string;
// CODES first-class: previously stored as `delegatedOutcome` extension
delegatedOutcome?: any;
// CODES first-class: previously stored as `disableAutoCalc` extension (tie matchUp)
disableAutoCalc?: boolean;
drawPositions?: number[];
endDate?: string;
extensions?: Extension[];
finishingPositionRange?: MatchUpFinishingPositionRange;
finishingRound?: number;
indoorOutdoor?: IndoorOutdoorUnion;
isMock?: boolean;
loserMatchUpId?: string;
matchUpDuration?: string;
matchUpFormat?: string;
matchUpId: string;
matchUpStatus?: MatchUpStatusUnion;
/**
* The scoring policy's codes, one per side, positionally — the display contract.
*
* Do not index it to decide which side did something. For "which side exited, and what produced
* it" read {@link MatchUp.sideExitProvenance}; for "which reason belongs to which side" read
* {@link MatchUp.sideStatusCodes}.
*/
matchUpStatusCodes?: MatchUpStatusCodeElement[];
/**
* The scoring policy's reason code for a result the MATCH did not produce — `OA` abandoned, `OC`
* cancelled, `OI` incomplete. Nobody is attributed, because nobody won.
*
* These are the `nonDirectingMatchUpStatuses`, and components states the rule plainly: they *"resolve
* nobody… asking who won an abandoned match"* is the wrong question. So a match-level reason gets a
* match-level home, rather than an index in an array whose index means a side.
*
* Side-attributed reasons live in {@link MatchUp.sideStatusCodes}. The two are disjoint by status, and
* {@link MatchUp.matchUpStatusCodes} is the positional projection of both, kept for display.
*/
matchUpStatusCode?: string;
/**
* The scoring policy's reason code per SIDE — `{ 2: 'DM' }` for "side 2 defaulted, misconduct".
*
* Keyed by `sideNumber`, never positional. A reason is side-attributed whenever a side did the thing:
* `DEFAULTED`, `WALKOVER`, `RETIRED`, and the `WITHDRAWN` label group, which is not a matchUpStatus at
* all but arrives as a walkover reason. A double exit attributes BOTH sides.
*
* **Why this exists.** The reason used to be readable only by indexing `matchUpStatusCodes`, where the
* index is the side — the assumption the exit tenant's eviction removed from everything else on this
* surface. It was also read at index 0 when carried into a connected structure, so a reason recorded
* against side 2 was silently dropped on the way.
*
* **Why not on `sideExitProvenance`.** Provenance says where a side CAME FROM, and only the cascade
* stamps it — `isPropagatedExit` is that presence, so widening it to played results would report every
* played exit as propagated. Three of the seven statuses with reason codes have no exit provenance to
* hang from in any case.
*
* A scalar per side: the policy models each status as a menu to choose from, the scoring dialog keeps a
* single reason, and no stored record has ever held two for one side.
*/
sideStatusCodes?: Record<number, string>;
matchUpType?: EventTypeUnion;
notes?: string;
/** a recording, stream or stats page of THIS matchUp — resources belong to every element, matchUps included */
onlineResources?: OnlineResource[];
orderOfFinish?: number;
processCodes?: string[];
roundName?: string;
roundNumber?: number;
roundPosition?: number;
// CODES first-class: previously stored as schedule-related timeItems
schedule?: MatchUpSchedule;
score?: Score;
// CODES first-class: per-side provenance for an exit PRODUCED by propagation.
// Keyed by sideNumber, so there is no positional padding and no polymorphism.
sideExitProvenance?: SideExitProvenance;
sides?: Side[];
startDate?: string;
surfaceCategory?: SurfaceCategoryUnion;
tieFormat?: TieFormat;
tieFormatId?: string;
tieMatchUps?: MatchUp[];
timeItems?: TimeItem[];
updatedAt?: Date | string;
winnerMatchUpId?: string;
winningSide?: number;
}
MatchUpSchedule
/**
* CODES first-class schedule attributes on a matchUp. Each field was
* historically stored as a `timeItem` of the corresponding `itemType`
* (SCHEDULED_DATE, ASSIGN_COURT, etc.). In CODES the canonical surface is
* this object; `timeItems[]` remains the home of `START_TIME / STOP_TIME /
* RESUME_TIME / END_TIME` because `matchUpDuration()` walks the full
* ordered history. Derived fields like `startTime`, `endTime`,
* `milliseconds`, `time`, `venueName`, `courtName`, `isoDateString`, and
* the recovery-time calculations remain hydration-time outputs and are
* NOT first-class writable.
*/
export interface MatchUpSchedule {
allocatedCourts?: any[];
// CODES 5.0.0 first-class: ISO timestamp captured when the matchUp is
// deliberately placed on the TMX active strip ("calling the match to court").
// Distinct from scheduledTime (plan) and START_TIME timeItem (actual start).
// Cleared only by explicit removal; persists past START_TIME as history.
calledAt?: string;
courtAnnotation?: string;
courtId?: string;
courtOrder?: number;
homeParticipantId?: string;
// CODES 5.0.0 first-class: pins this placement against bulk clears and
// automated scheduling. See {@link ScheduleLock}. No legacy timeItem mirror.
lock?: ScheduleLock;
official?: any;
// participantId of a nominated scorekeeper for this matchUp (crowd-scoring Phase D).
// SCHEDULE.ASSIGNMENT.SCOREKEEPER first-class value; not cleared by rescheduling.
scorekeeper?: any;
// participantId of an assigned timekeeper (timed matchUpFormats).
// SCHEDULE.ASSIGNMENT.TIMEKEEPER first-class value; not cleared by rescheduling.
timekeeper?: any;
scheduledDate?: string;
// sparse: the calendar day (scheduledDate + 1) on which a matchUp's END_TIME
// falls when the match crossed midnight. Absent ⇒ the end is on scheduledDate.
endDate?: string;
scheduledTime?: string;
// CODES 5.0.0 first-class: ISO timestamp auto-captured the first time a
// matchUp receives a meaningful score/winningSide. A lightweight proxy for
// "when did this match actually finish" — useful for analytics on tournament
// director behavior when no explicit END_TIME timeItem was recorded.
// Cleared automatically when the score is removed; an actual endTime, when
// present, supersedes it. No legacy timeItem mirror.
scoredTime?: string;
timeModifiers?: string[];
venueId?: string;
// derived/hydrated read-only fields (populated by getMatchUpScheduleDetails)
[key: string]: any;
}
SideExitProvenance
/** Keyed by sideNumber (1 | 2). Serialises as `{ "1": {...}, "2": {...} }`. */
export type SideExitProvenance = Record<number, SideExitProvenanceEntry>;
SideExitProvenanceEntry
export type SideExitProvenanceEntry = {
/** the status this side was given as a result of the upstream exit, e.g. WALKOVER / DEFAULTED */
matchUpStatus?: MatchUpStatusUnion;
/** the upstream status that produced it, e.g. DOUBLE_WALKOVER / DOUBLE_DEFAULT / BYE */
previousMatchUpStatus?: MatchUpStatusUnion;
/** the matchUp whose exit produced this entry; the identity the unwind lacks today */
sourceMatchUpId?: string;
};
MatchUpStatusCodeRecord
/**
* Why an exit sits on one side of a matchUp, recorded per side.
*
* `sideNumber` is a KEY rather than an array index, the element shape is fixed, and
* `sourceMatchUpId` makes an entry attributable to the exit that produced it.
*/
/**
* One element of `MatchUp.matchUpStatusCodes`.
*
* The engine persists STRINGS: the scoring policy's code for a side, at that side's position
* (`['', 'DM']`). That is the display contract.
*
* The record shape is accepted so that a tournament record stored by an earlier version still
* types. Nothing writes it and nothing reads it to decide behaviour.
*/
export type MatchUpStatusCodeRecord = {
code?: string | number;
sideNumber?: number;
matchUpStatus?: MatchUpStatusUnion;
previousMatchUpStatus?: MatchUpStatusUnion;
};
MatchUpFinishingPositionRange
export interface MatchUpFinishingPositionRange {
loser: number[];
winner: number[];
}
Score
export interface Score {
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
scoreStringSide1?: string;
scoreStringSide2?: string;
/** The current game's points on an in-progress matchUp (e.g. '30'), as live scoring records them. */
side1PointScore?: number | string;
side2PointScore?: number | string;
sets?: Set[];
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
Set
export interface Set {
createdAt?: Date | string;
extensions?: Extension[];
games?: Game[];
isMock?: boolean;
notes?: string;
setDuration?: string;
setFormat?: string;
setNumber?: number;
side1PointScore?: number | string;
side1Score?: number;
side1TiebreakScore?: number;
side2PointScore?: number | string;
side2Score?: number;
side2TiebreakScore?: number;
timeItems?: TimeItem[];
updatedAt?: Date | string;
winningSide?: number;
}
Game
export interface Game {
createdAt?: Date | string;
extensions?: Extension[];
gameDuration?: string;
gameFormat?: string;
gameNumber?: number;
isMock?: boolean;
notes?: string;
points?: Point[];
timeItems?: TimeItem[];
updatedAt?: Date | string;
winningSide?: number;
winReason?: WinReasonUnion;
}
Point
export interface Point {
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
pointDuration?: string;
pointNumber?: number;
shots?: Shot[];
side1Score?: string;
side2Score?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
winningSide?: number;
winReason?: WinReasonUnion;
}
Side
export interface Side {
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
lineUp?: TeamCompetitor[];
notes?: string;
participantId?: string;
participant?: Participant;
sideNumber?: number;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
Team
export interface Team {
gender?: GenderUnion;
homeVenueIds?: string[];
nativeTeamName?: string;
otherTeamNames?: string[];
parentOrganisationId?: string;
personIds?: string[];
previousTeamNames?: string[];
teamId: string;
teamName?: string;
}
TeamCompetitor
export interface TeamCompetitor {
collectionAssignments?: CollectionAssignment[];
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
participantId: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
CollectionAssignment
export interface CollectionAssignment {
collectionId: string;
collectionPosition: number;
previousParticipantId?: string;
substitutionOrder?: number;
}
tieFormat
TieFormat
export interface TieFormat {
collectionDefinitions: CollectionDefinition[];
collectionGroups?: CollectionGroup[];
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
scoreSource?: TieScoreSourceUnion;
tieFormatId?: string;
tieFormatName?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
winCriteria: WinCriteria;
}
CollectionDefinition
export interface CollectionDefinition {
collectionValueProfiles?: CollectionValueProfile[];
collectionGroupNumber?: number;
category?: Category;
collectionId: string;
collectionName?: string;
collectionOrder?: number;
collectionValue?: number;
createdAt?: Date | string;
extensions?: Extension[];
gender?: GenderUnion;
isMock?: boolean;
matchUpCount?: number;
matchUpFormat?: string;
matchUpType?: EventTypeUnion;
matchUpValue?: number;
notes?: string;
processCodes?: string[];
scoreValue?: number;
setValue?: number;
timeItems?: TimeItem[];
updatedAt?: Date | string;
winCriteria?: WinCriteria;
}
CollectionGroup
export interface CollectionGroup {
createdAt?: Date | string;
extensions?: Extension[];
groupName?: string;
groupNumber: number;
groupValue?: number;
isMock?: boolean;
notes?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
winCriteria?: WinCriteria;
}
CollectionValueProfile
export interface CollectionValueProfile {
collectionPosition: number;
createdAt?: Date | string;
updatedAt?: Date | string;
extensions?: Extension[];
timeItems?: TimeItem[];
matchUpValue: number;
isMock?: boolean;
notes?: string;
}
WinCriteria
export interface WinCriteria {
aggregateValue?: boolean;
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
valueGoal: number;
}
venue
Venue
export interface Venue {
addresses?: Address[];
contacts?: Contact[];
courts?: Court[];
createdAt?: Date | string;
dateAvailability?: Availability[];
defaultEndTime?: string;
defaultStartTime?: string;
// CODES first-class: previously stored as `disabled` extension
disabled?: boolean | { dates?: string[] };
extensions?: Extension[];
// Canonical cross-tournament facility identity (courthive-facilities). Defaults
// to venueId — a venue is its own facility unless several venues dedupe to one
// physical facility. Read into the read-model `facility_id` by cast().
facilityId?: string;
isMock?: boolean;
isPrimary?: boolean;
notes?: string;
onlineResources?: OnlineResource[];
parentOrganisationId?: string;
roles?: string[];
subVenues?: Venue[];
timeItems?: TimeItem[];
updatedAt?: Date | string;
venueAbbreviation?: string;
venueId: string;
venueName?: string;
venueOtherIds?: UnifiedVenueID[];
venueType?: string;
}
Court
export interface Court {
altitude?: number;
courtDimensions?: string;
courtId: string;
courtName?: string;
createdAt?: Date | string;
dateAvailability?: Availability[];
// CODES first-class: previously stored as `disabled` extension
disabled?: boolean | { dates?: string[] };
/**
* The sport this court is for. Absent means unspecified, NOT tennis — a court nobody has
* declared a discipline for is not evidence that it is a tennis court, and defaulting it
* would invent data.
*
* A physical surface can serve more than one sport (a tennis court with pickleball lines, or a
* portable net dropped on it). That is a court CAPABILITY and is deliberately NOT modelled
* here: this field says what the court IS, so a dedicated pickleball court is distinguishable
* from a tennis court. Do not repurpose it to mean "can also host" — that turns one physical
* slab into two schedulable courts.
*
* Open vocabulary, like `Event.discipline`: normalize with `normalizeDiscipline` and constrain
* with the `allowedDisciplines` policy where a fixed list is required.
*/
discipline?: DisciplineUnion;
extensions?: Extension[];
floodlit?: boolean;
indoorOutdoor?: IndoorOutdoorUnion;
isMock?: boolean;
latitude?: string;
longitude?: string;
notes?: string;
onlineResources?: OnlineResource[];
pace?: string;
surfaceCategory?: SurfaceCategoryUnion;
surfacedDate?: Date | string;
surfaceType?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
// Written by addCourt on every court (the parent venue's id). Declared so the type and
// tournament.schema.json agree with the records the engine actually writes.
venueId?: string;
}
Availability
export interface Availability {
bookings?: Booking[];
createdAt?: Date | string;
date?: string;
endTime?: string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
startTime?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
Booking
export interface Booking {
bookingId?: string;
// Widened with `(string & {})` so unrecognised values from external sources
// still type-check while known members keep autocompleting. The engine maps
// anything unrecognised to BLOCK_TYPES.BLOCKED — see BOOKING_TYPE_MAP.
bookingType?: BookingTypeUnion | (string & {});
capacity?: number | null;
createdAt?: Date | string;
endTime?: string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
registrations?: PracticeRegistration[];
startTime?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
Supporting declarations
TournamentStatusUnion
export type TournamentStatusUnion = `${TournamentStatusEnum}`;
OrganisationTypeUnion
export type OrganisationTypeUnion = `${OrganisationTypeEnum}`;
DrawTypeUnion
export type DrawTypeUnion = keyof typeof DrawTypeEnum;
BallTypeUnion
export type BallTypeUnion = `${BallTypeEnum}`;
DisciplineUnion
export type DisciplineUnion = `${DisciplineEnum}` | (string & {});
CategoryUnion
export type CategoryUnion = `${CategoryEnum}`;
DrawStatusUnion
export type DrawStatusUnion = `${DrawStatusEnum}`;
StageTypeUnion
export type StageTypeUnion = `${StageTypeEnum}`;
EntryStatusUnion
export type EntryStatusUnion = `${EntryStatusEnum}`;
LinkTypeUnion
export type LinkTypeUnion = `${LinkTypeEnum}`;
PositioningProfileUnion
export type PositioningProfileUnion = `${PositioningProfileEnum}`;
PayloadProfileUnion
export type PayloadProfileUnion = `${PayloadProfileEnum}`;
SeedingProfileUnion
export type SeedingProfileUnion = `${SeedingProfileEnum}`;
EventTypeUnion
export type EventTypeUnion = `${EventTypeEnum}`;
ScheduleLockAttribute
/** Placement attributes a {@link ScheduleLock} can pin. */
export type ScheduleLockAttribute =
'allocatedCourts' | 'courtId' | 'courtOrder' | 'scheduledDate' | 'scheduledTime' | 'venueId';
ScheduleLock
/**
* CODES first-class: a director's declaration that a matchUp's placement is
* deliberate and must not be moved by bulk or automated scheduling — the
* marquee match promised centre court at 19:00 surviving a Clear or a
* re-schedule of the rest of the day.
*
* The lock guards PLACEMENT only. `startTime` / `stopTime` / `resumeTime` /
* `endTime` record actual play and stay writable, or a locked matchUp could
* never be played. It is also inert once the matchUp reaches a completed
* status, where completed-status protection already applies.
*
* Presence of the object is the lock; `attributes` narrows it to specific
* placement fields (absent ⇒ the whole placement). Timestamps are
* caller-supplied — the factory never stamps wall-clock.
*
* Unrelated to `tournamentRecord.mutationLocks`, which gate whole methods at
* tournament grain.
*/
export interface ScheduleLock {
attributes?: ScheduleLockAttribute[];
lockedAt?: string;
lockedBy?: string;
reason?: string;
}
ScenarioPlacement
/**
* One proposed matchUp placement inside a {@link ScheduleScenario}. The shape
* is deliberately identical to a `bulkScheduleMatchUps` `matchUpDetails` entry
* so that committing a scenario is a direct hand-off to that mutation. An
* empty `schedule` (with `removePriorValues`) encodes "pull this matchUp off
* the grid" when the scenario is applied.
*/
export interface ScenarioPlacement {
tournamentId: string;
matchUpId: string;
schedule: MatchUpSchedule;
}
ScheduleScenario
/**
* A named alternate scheduling plan stored on `tournamentRecord.scheduling.scenarios`.
* Placements hold resolved matchUp positions only (courts / times / order) — a
* scenario never re-runs the round-level scheduler. `createdAt` / `updatedAt`
* are caller-provided ISO strings (factory does not stamp wall-clock, matching
* the `calledAt` idiom). `basedOnHash` fingerprints the official schedule at
* authoring time so drift / rebase can be detected before a commit.
*/
export interface ScheduleScenario {
scenarioId: string;
scenarioName: string;
scheduledDates?: string[];
createdAt?: string;
updatedAt?: string;
notes?: string;
basedOnHash?: string;
placements: ScenarioPlacement[];
}
MatchUpStatusCodeElement
/** A scoring policy code, or the record shape an earlier version stored. */
export type MatchUpStatusCodeElement = string | number | MatchUpStatusCodeRecord;
IndoorOutdoorUnion
export type IndoorOutdoorUnion = `${IndoorOutdoorEnum}`;
MatchUpStatusUnion
export type MatchUpStatusUnion = `${MatchUpStatusEnum}`;
Shot
export interface Shot {
bounceAt?: CourtPosition;
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
participantId: string;
shotDetail?: ShotDetailUnion;
shotMadeFrom?: CourtPosition;
shotNumber?: number;
shotOutcome?: ShotOutcomeUnion;
shotType?: ShotTypeUnion;
sideNumber?: number;
speed?: number;
spin?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
CourtPosition
export interface CourtPosition {
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
positionName?: CourtPositionUnion;
timeAtPosition?: Date | string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
x?: number;
y?: number;
}
CourtPositionUnion
export type CourtPositionUnion = `${CourtPositionEnum}`;
ShotDetailUnion
export type ShotDetailUnion = `${ShotDetailEnum}`;
ShotOutcomeUnion
export type ShotOutcomeUnion = `${ShotOutcomeEnum}`;
ShotTypeUnion
export type ShotTypeUnion = `${ShotTypeEnum}`;
WinReasonUnion
export type WinReasonUnion = `${WinReasonEnum}`;
SurfaceCategoryUnion
export type SurfaceCategoryUnion = `${SurfaceCategoryEnum}`;
TieScoreSourceUnion
export type TieScoreSourceUnion = `${TieScoreSourceEnum}`;
GenderUnion
export type GenderUnion = `${GenderEnum}`;
FinishingPositionUnion
export type FinishingPositionUnion = `${FinishingPositionEnum}`;
TallyResult
export interface TallyResult {
GEMscore?: number;
allDefaults?: number;
defaults?: number;
defeats?: any[];
gamesLost?: number;
gamesPct?: number;
gamesWon?: number;
groupOrder?: number;
matchUpsCancelled?: number;
matchUpsDrawn?: number;
matchUpsLost?: number;
matchUpsPct?: number;
matchUpsWon?: number;
pointsLost?: number;
pointsPct?: number;
pointsWon?: number;
pressureOrder?: number;
pressureScores?: number[];
provisionalOrder?: number;
rankOrder?: number;
retirements?: number;
setsLost?: number;
setsPct?: number;
setsWon?: number;
subOrder?: number;
standingsPoints?: number;
tieDoublesLost?: number;
tieDoublesWon?: number;
tieMatchUpsLost?: number;
tieMatchUpsWon?: number;
tieSinglesLost?: number;
tieSinglesWon?: number;
ties?: number;
victories?: any[];
walkovers?: number;
[key: string]: any;
}
SeedingBasisUnion
export type SeedingBasisUnion = `${SeedingBasisEnum}`;
StructureTypeUnion
export type StructureTypeUnion = `${StructureTypeEnum}`;
TierMovementUnion
export type TierMovementUnion = `${TierMovementEnum}`;
TierClassification
export interface TierClassification {
/** Federation/governing body tier system (e.g. 'ITF_JUNIOR', 'ATP', 'PPA', 'BWF') */
system: string;
/** Tier value within the system (e.g. '3', '1000', 'Gold', 'Super 500') */
value: string;
/** Optional sortable prestige rank within the system (lower = more prestigious) */
numericRank?: number;
}
TournamentLevelUnion
export type TournamentLevelUnion = `${TournamentLevelEnum}`;
WheelchairClassUnion
export type WheelchairClassUnion = `${WheelchairClassEnum}`;
CountryCodeUnion
export type CountryCodeUnion = `${CountryCodeEnum}`;
OnlineResourceTypeUnion
export type OnlineResourceTypeUnion = `${OnlineResourceTypeEnum}`;
ContactRelationshipUnion
export type ContactRelationshipUnion = `${ContactRelationshipEnum}`;
ParticipantRoleUnion
export type ParticipantRoleUnion = `${ParticipantRoleEnum}`;
ParticipantStatusUnion
export type ParticipantStatusUnion = `${ParticipantStatusEnum}`;
ParticipantTypeUnion
export type ParticipantTypeUnion = `${ParticipantTypeEnum}`;
PenaltyTypeUnion
export type PenaltyTypeUnion = `${PenaltyTypeEnum}`;
AddressTypeUnion
export type AddressTypeUnion = `${AddressTypeEnum}`;
TeamAttribute
export interface TeamAttribute {
teamId?: string;
teamName?: string;
jerseyNumber?: string;
jerseyName?: string;
position?: string;
captain?: boolean;
}
PlayingDoubleHandCodeUnion
export type PlayingDoubleHandCodeUnion = `${PlayingDoubleHandCodeEnum}`;
LengthUnitUnion
export type LengthUnitUnion = `${LengthUnitEnum}`;
PlayingHandCodeUnion
export type PlayingHandCodeUnion = `${PlayingHandCodeEnum}`;
WeightUnitUnion
export type WeightUnitUnion = `${WeightUnitEnum}`;
UnifiedPersonID
export interface UnifiedPersonID {
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
organisationId: string;
personId: string;
timeItems?: TimeItem[];
uniqueOrganisationName?: string;
updatedAt?: Date | string;
}
SexUnion
export type SexUnion = `${SexEnum}`;
EventRegistration
/**
* Registration facts that genuinely differ between events of the same tournament.
*
* Deliberately NOT the full {@link RegistrationProfile}. That type carries `accommodation`,
* `transportation`, `hospitality`, `sponsors`, `dressCode`, `socialEvents` and `codeOfConduct`,
* which are tournament-wide by nature — a tournament does not run one hotel block for the over-35s
* and another for the juniors. Putting the full profile on `Event` would ASSERT that all of them
* are overridable per event, and would make the field-level cascade unenforceable because most of
* its fields can never legitimately differ. Six fields, so the wrong data is inexpressible rather
* than merely discouraged.
*
* Resolve with `getEffectiveRegistrationProfile` — never by reading this directly. See that
* function for why the obvious fallback is wrong.
*/
export interface EventRegistration {
entriesOpen?: Date | string;
entriesClose?: Date | string;
withdrawalDeadline?: Date | string;
/** fees for THIS event; each entry still states its own `unit` — see {@link RegistrationEntryFee} */
entryFees?: RegistrationEntryFee[];
entryUrl?: string;
eligibilityNotes?: string;
extensions?: Extension[];
}
EventEntryProfile
/**
* How an event accepts entries, and how many.
*
* Event-grain for a structural reason rather than a presentational one: events within a single
* `tournamentRecord` can carry DIFFERENT SANCTIONING BODIES (`Event.sanction`, and
* `eventOtherIds[].isOrigin` at the identity grain), so a limit set by body A must not be expressed
* at a grain body B shares.
*
* There is a second, different question this does NOT answer: what the whole competition can
* physically schedule and play, given courts, days and daily match limits. The sum of individually
* valid `entriesLimit` values can exceed that. It is an acceptance rule here; feasibility is the
* scheduler's to report, and is deliberately not stored — two statements of one fact drift.
*/
export interface EventEntryProfile {
/** how many entries the event will accept */
entriesLimit?: number;
/** intended draw size before a `drawDefinition` exists to carry one */
targetDrawSize?: number;
selectionProcess?: SelectionProcessUnion;
/**
* WHICH scale orders acceptance — 'WTN', 'NTRP', a federation ranking list.
*
* `TOP_DOWN_BY_RANKING` alone is under-specified: two events sorting by different scales accept
* different players. Organisers currently state this in the tournament TITLE, after an asterisk
* ("*top down by ranking", "*top down by WTN"), which is the evidence that it needs a field.
*/
selectionScaleName?: string;
wildcardCount?: number;
waitlistEnabled?: boolean;
extensions?: Extension[];
}
EntryRestriction
/**
* A condition on WHO may enter, beyond the age / gender / rating that {@link Category} already
* carries.
*
* Evidenced by two federations independently. One gates on residency — a level whose name ends
* "Closed" is open only to competitors of the sanctioning section, which is why its grade and its
* eligibility rule are currently welded into one facet string. The other gates on membership:
* "required to compete in all Summer Series tournaments", where the membership is bought from the
* organisation running the event.
*
* **Advisory in v1, and that is deliberate.** CODES does not hold the section rosters or membership
* registers these resolve against, so most restrictions cannot be evaluated here — see `evaluable`.
* A restriction that ANNOUNCES itself is strictly more useful than one that stays silent, because
* silence is indistinguishable from "no restriction". That is the same reasoning `#4711` applied to
* a validator that cannot evaluate a rule.
*/
export interface EntryRestriction {
type: EntryRestrictionUnion;
/**
* Whose territory, roster or register the restriction resolves against.
*
* Required in practice for MEMBERSHIP and RESIDENCY and meaningless without it: "members only" is
* not a rule until it says whose members. A membership of the organisation running the event and
* a membership of its national body are different gates.
*/
organisationId?: string;
/**
* WHICH membership, where an organisation sells more than one that confers different rights.
*
* One federation's player membership gates entry while its team membership does not, so naming
* the organisation alone is not enough to decide whether a given competitor satisfies this.
*/
membershipCategory?: string;
/** free text for what a consumer should tell a competitor — not parsed */
description?: string;
/**
* Whether this can be decided from the record alone.
*
* Defaults to FALSE in effect: a restriction with no explicit `evaluable: true` is treated as
* undecidable by {@link getParticipantEligibility}, which reports `indeterminate` rather than
* guessing. Optimism here would be a confident wrong answer about whether someone may play.
*/
evaluable?: boolean;
extensions?: Extension[];
}
EntryRestrictionUnion
export type EntryRestrictionUnion = `${EntryRestrictionEnum}`;
SelectionProcessUnion
export type SelectionProcessUnion = `${SelectionProcessEnum}`;
LogisticsSection
export interface LogisticsSection {
notes?: string;
options?: LogisticsOption[];
}
LogisticsOption
export interface LogisticsOption {
address?: string;
description?: string;
email?: string;
extensions?: Extension[];
name: string;
notes?: string;
phone?: string;
priceRange?: string;
url?: string;
}
SocialEvent
export interface SocialEvent {
date?: string;
description?: string;
location?: string;
name: string;
time?: string;
}
Sponsor
export interface Sponsor {
logoUrl?: string;
name: string;
tier?: string;
websiteUrl?: string;
}
DocumentLink
export interface DocumentLink {
description?: string;
name: string;
url?: string;
}
RegistrationEntryFee
/**
* What a competitor pays to enter.
*
* Extends {@link MonetaryAmount}, so `unit` is required and the amount cannot be read at the wrong
* scale. It previously carried a bare `amount` + `currencyCode`, which is the exact ambiguity
* `MonetaryAmount` was introduced to remove: a federation stating a `6000` entry fee means $60.00,
* and a reader assuming whole units is out by 100× with nothing in the record to signal it. That
* was not hypothetical — two captured federation surfaces state this same field at different
* scales, one in minor units and one in major.
*
* NOT a {@link SanctionFee}. A sanction fee is a levy the governing body collects from the
* organiser; this is the organiser's price to a competitor. Different payer, different setter,
* different lifecycle. `SanctionFeeKindEnum.ENTRY` exists for an authority-collected per-entry levy
* and is not a home for this.
*
* ## Which events a fee applies to
*
* Three selectors, most specific first: `eventId` > `category` > `eventType`. All are optional, and
* a fee with none applies to the whole tournament. `eventType` alone cannot distinguish a $75
* over-35 doubles from a $95 open doubles when both are on the same tournament, which is why
* `eventId` exists; `eventType` and `category` are kept because organisers genuinely do price "all
* doubles" once without enumerating events.
*/
export interface RegistrationEntryFee extends MonetaryAmount {
/** the specific event this fee buys entry to — the most specific selector */
eventId?: string;
category?: string;
eventType?: EventTypeUnion;
/**
* Window in which this price is the one charged, for early-bird and late-entry pricing.
*
* Without it, an early rate and a late rate are two records with no stated relationship, and a
* consumer computing "what does this cost" has no way to exclude the one that has expired. Both
* ends optional: an early-bird rate with no `appliesFrom` has simply always been available.
*/
appliesFrom?: string;
appliesTo?: string;
extensions?: Extension[];
}
PrizeMoney
/**
* An award of prize money — a {@link MonetaryAmount} with provenance.
*
* Extends `MonetaryAmount` rather than redeclaring `amount`/`currencyCode` so there is exactly ONE
* money shape in CODES. That also means prize money now states its `unit`, which it previously did
* not: `{ amount: 4000, currencyCode: 'USD' }` was readable as either $40.00 or $4,000 with nothing
* to choose between them.
*
* A `PrizeMoney[]` may legitimately mix currencies (an event offering awards in two markets), so
* consumers MUST NOT sum `amount` across the array without first grouping by `currencyCode` and
* `unit` — the sum is otherwise meaningless.
*/
export interface PrizeMoney extends MonetaryAmount {
/**
* Never set. Declared as `never` so a {@link PrizeMoneyAward}`[]` — a ladder, where every entry
* carries a `finishingPosition` — cannot be passed where a list of TOTALS is expected. Without
* it TypeScript's structural typing accepts the substitution silently, and `sumAgainstBound`
* returns a confident, correctly-denominated, wrong number. See `PrizeMoneyAward`.
*/
finishingPosition?: never;
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
PrizeMoneyAward
/**
* What ONE finishing position was worth.
*
* `PrizeMoney` states a total; a `PrizeMoneyAward[]` states the ladder that produced it.
* Competitions commonly award money the way they award ranking points — by how far you got — and
* CODES could previously record only the sum, or drop the ladder entirely. A single tournament
* routinely carries several ladders that are not derivable from one another: a main draw, its
* qualifying, and smaller-draw events each publish their own.
*
* `finishingPosition` carries the SAME semantics as `finishingPositionRanges` in the
* ranking-points policies (`AwardProfile`, `types/rankingTypes.ts`): the MAXIMUM finishing position
* of the range the award covers. In a 128 draw the first-round losers finish 65th-128th, so their
* rung is `128`; semi-finalists finish 3rd-4th, so theirs is `4`. A Grand Slam singles ladder
* therefore lands on 1, 2, 4, 8, 16, 32, 64, 128 — the exact keys `grandSlamSingles` already uses
* in `POLICY_RANKING_POINTS_ATP`, because they describe the same eight outcomes. That is the point
* of reusing the key: "R16 was worth 200 points and 480,000" becomes one join rather than two
* vocabularies.
*
* Position rather than round, for the reason the points policies chose it: round-robins,
* consolations and playoffs have no clean "round you lost in", so a round-keyed model cannot
* express them at all.
*
* ## `roundCode` is NOT `finishingPosition`, and they run in opposite directions
*
* A "Round 1" rung is what a first-round LOSER received — the LAST finishing position, not the
* first. Code that maps round one to position one inverts the whole ladder and pays the champion
* the qualifier's cheque, with nothing in the data to signal it. Nor is the mapping fixed: the same
* round code is position 128 in a 128 draw and position 16 in a 16 draw, so a round number means
* nothing without the draw size. That is precisely why the position is what gets stored — and why
* `roundName` / `roundCode` are kept beside it, so the conversion stays auditable instead of lost.
*
* The top rung is not always position 1, either. A qualifying ladder's last rung is what the losers
* of the final qualifying round received; the players who came through are paid from the MAIN
* draw's ladder, so a qualifying ladder may carry no rung at position 1 at all.
*
* ## A ladder is not a list of totals — never sum one
*
* Each rung is what ONE competitor at that position received. A tournament's outlay is the sum of
* `amount x (competitors finishing in that range)`. For a 128 draw whose eight rungs run 140,000 /
* 190,000 / 290,000 / 480,000 / 780,000 / 1,450,000 / 2,800,000 / 5,500,000, adding the rungs gives
* 11,630,000 while the actual outlay is 37,840,000 — the naive figure is not the purse, nor
* anything else. This is why awards get their own field instead of being folded into `prizeMoney`.
*
* That is enforced rather than merely documented. `PrizeMoney` declares `finishingPosition?: never`
* for this single purpose, so passing a ladder where a list of totals is expected is a compile
* error — structural typing would otherwise accept the substitution in silence, and
* `sumAgainstBound` would return a confident, wrong, correctly-denominated number.
*
* As with `PrizeMoney`, `unit` is required and load-bearing. Publishers disagree on scale and on
* formatting: whole units as a comma-formatted STRING (which `Number()`s to `NaN`), whole units as
* an integer, and minor units as an integer all occur. Parsing is the producer's job; declaring the
* scale is this type's.
*/
export interface PrizeMoneyAward extends MonetaryAmount {
/**
* Maximum finishing position of the range this award covers — `1` winner, `2` finalist,
* `4` semi-finalist, `128` first-round loser in a 128 draw. A position, never a round number.
*/
finishingPosition: number;
/** the authority's own label for the rung, where it publishes one (`'Quarter-Finals'`) */
roundName?: string;
/** the authority's own code for the rung, where it publishes one (`'Q'`, `'W'`, `'1'`) */
roundCode?: string;
extensions?: Extension[];
notes?: string;
}
UnifiedTournamentID
export interface UnifiedTournamentID {
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
/** marks this entry as the source the RECORD originated from — the ingest or
* sanctioning system the whole tournament was acquired from. At most one entry per
* tournament should carry it; every other entry is a system the tournament is merely
* also known to (typically acquired by copy-back).
*
* Mirrors {@link UnifiedEventID.isOrigin} one grain up. The two are independent: a
* record acquired wholesale from one organisation can still carry events sanctioned
* by others, so neither flag can be inferred from the other.
*
* Unlike `UnifiedEventID`, `tournamentId` here stays REQUIRED. `eventId` is optional
* at event grain because the origin may not hold the event yet; at tournament grain
* the id is the entire payload, and an entry without one carries no identity at all. */
isOrigin?: boolean;
notes?: string;
organisationId: string;
timeItems?: TimeItem[];
tournamentId: string;
uniqueOrganisationName?: string;
updatedAt?: Date | string;
}
UnifiedDrawID
/**
* CODES first-class: a DRAW's identity in another organisation's system — the
* draw-grain member of the `Unified*ID` family ({@link UnifiedTournamentID},
* {@link UnifiedEventID}, {@link UnifiedParticipantID}, {@link UnifiedPersonID},
* {@link UnifiedVenueID}).
*
* Carried on `DrawDefinition.drawOtherIds[]`. It exists because an outside organisation's
* draw-grain object is frequently the ONLY grain that carries the identity and the
* metadata worth addressing. UTR is the motivating case: a UTR "flight" is a real remote
* object with its own GUID, its own `drawSize`, and its own UTR-band bounds, and it maps
* 1:1 to a CODES `drawDefinition` — while UTR has no event-grain object at all, so the
* CODES event above it is a synthetic gender × matchUpType grouping with no counterpart
* to record.
*
* Every id attribute is OPTIONAL and belongs to `organisationId`, never to the carrying
* record. Populate only the ones the origin system actually has: UTR supplies
* `tournamentId` (its event id) + `drawId` (the flight GUID) and no `eventId`, and an
* origin that models events supplies `eventId` too. `drawId` is absent until the draw
* exists there, which is the copy-back case — see {@link UnifiedEventID}.
*/
export interface UnifiedDrawID {
createdAt?: Date | string;
/** that organisation's id for this draw */
drawId?: string;
/** that organisation's id for the event carrying this draw, when it models events */
eventId?: string;
extensions?: Extension[];
isMock?: boolean;
/** marks this entry as the source the draw originated from. At most one entry per draw
* should carry it. */
isOrigin?: boolean;
notes?: string;
organisationId: string;
timeItems?: TimeItem[];
/** **that organisation's** tournamentId — never the carrying record's */
tournamentId?: string;
uniqueOrganisationName?: string;
updatedAt?: Date | string;
}
UnifiedParticipantID
/**
* CODES first-class: an EVENT's identity in another organisation's system —
* the event-grain member of the `Unified*ID` family
* ({@link UnifiedTournamentID}, {@link UnifiedPersonID}, {@link UnifiedVenueID}).
*
* Carried on `Event.eventOtherIds[]`. It exists because a single tournamentRecord
* can hold events sanctioned by SEVERAL organisations, each of which has its own
* internal `tournamentId`. The record has exactly one `tournamentId`; an event's
* sanctioning tournament is independent of it, and the sanctioning body may hold no
* record that carries the event at all.
*
* `tournamentId` is therefore OPTIONAL and deliberately *not* the carrying record's:
* it is the id of the tournament in `organisationId`'s system. `eventId` is likewise
* that system's id for this event, which is absent until the event exists there —
* an event may be copied back to its origin after the fact, or through an external
* API integration, at which point the returned id is written here.
*/
/**
* CODES first-class: a PARTICIPANT's identity in another organisation's system — the
* participant-grain member of the `Unified*ID` family ({@link UnifiedTournamentID},
* {@link UnifiedEventID}, {@link UnifiedPersonID}, {@link UnifiedVenueID}).
*
* Carried on `Participant.participantOtherIds[]`. It exists because
* {@link UnifiedPersonID} cannot cover every competitor: `personOtherIds` hangs off
* `participant.person`, and a **PAIR or TEAM participant has no `person`** — only
* `individualParticipantIds`. So a pair or team registered with an outside body had
* nowhere to record that body's id for it.
*
* Sitting on the participant rather than the person makes it uniform across INDIVIDUAL,
* PAIR and TEAM, and it is what lets an integration layer address results back to the
* sanctioning system that registered them.
*/
export interface UnifiedParticipantID {
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
organisationId: string;
/** that organisation's id for this participant — its registration/entry identity, which
* need not resemble any id of ours */
participantId: string;
timeItems?: TimeItem[];
uniqueOrganisationName?: string;
updatedAt?: Date | string;
}
UnifiedEventID
export interface UnifiedEventID {
createdAt?: Date | string;
eventId?: string;
extensions?: Extension[];
/** marks this entry as the SANCTIONING SOURCE the event originated from. At most one
* entry per event should carry it; every other entry is a system the event is merely
* also known to (typically acquired by copy-back). */
isOrigin?: boolean;
isMock?: boolean;
notes?: string;
organisationId: string;
timeItems?: TimeItem[];
tournamentId?: string;
uniqueOrganisationName?: string;
updatedAt?: Date | string;
}
BookingTypeUnion
export type BookingTypeUnion = `${BookingTypeEnum}`;
PracticeRegistrationStatusUnion
export type PracticeRegistrationStatusUnion = `${PracticeRegistrationStatusEnum}`;
PracticeRegistration
export interface PracticeRegistration {
cancelledAt?: Date | string;
createdAt?: Date | string;
endTime: string;
extensions?: Extension[];
notes?: string;
participantId: string;
registeredAt?: Date | string;
registrationId: string;
startTime: string;
status?: PracticeRegistrationStatusUnion;
timeItems?: TimeItem[];
updatedAt?: Date | string;
}
UnifiedVenueID
export interface UnifiedVenueID {
createdAt?: Date | string;
extensions?: Extension[];
isMock?: boolean;
notes?: string;
organisationId: string;
timeItems?: TimeItem[];
uniqueOrganisationName?: string;
updatedAt?: Date | string;
venueId: string;
}
WeekdayUnion
export type WeekdayUnion = `${WeekdayEnum}`;
SanctionDecisionUnion
export type SanctionDecisionUnion = `${SanctionDecisionEnum}`;
RecognitionUnion
export type RecognitionUnion = `${RecognitionEnum}`;
AuthorityRoleUnion
export type AuthorityRoleUnion = `${AuthorityRoleEnum}`;
SanctionEnforcementUnion
export type SanctionEnforcementUnion = `${SanctionEnforcementEnum}`;
CurrencyUnitUnion
export type CurrencyUnitUnion = `${CurrencyUnitEnum}`;
MonetaryAmount
/**
* An amount of money that cannot be stated ambiguously.
*
* All three fields are required together by construction. That is the point: an optional `unit`
* would reintroduce exactly the ambiguity this type exists to remove, since the omitted case is
* indistinguishable from the unconsidered one.
*
* `PrizeMoney` extends this, so prize money states its unit too. Every monetary field in CODES
* should be built on this type rather than redeclaring an amount and a currency.
*/
export interface MonetaryAmount {
amount: number;
currencyCode: string;
unit: CurrencyUnitUnion;
}
SanctionFeeKindUnion
export type SanctionFeeKindUnion = `${SanctionFeeKindEnum}`;
SanctioningAuthority
/** A body in the approval chain. Only `organisationId` OR `organisationName` need be known. */
export interface SanctioningAuthority {
organisationAbbreviation?: string;
organisationName?: string;
organisationId?: string;
role?: AuthorityRoleUnion;
/** the authority's own code for its territory, where it publishes one (USTA section '070') */
regionCode?: string;
extensions?: Extension[];
}
SanctionFee
/**
* A levy attached to the sanction.
*
* `amount` and `percentage` are not exclusive: a head tax is commonly `{fixedFee, percentageFee}`,
* and at least one federation charges per-entry with an absolute cap, which needs all three of
* `amount`, `perParticipant` and `maximumAmount`.
*/
export interface SanctionFee {
feeKind?: SanctionFeeKindUnion;
/** the levy itself — currency and unit travel with the amount, so it cannot be misread */
fee?: MonetaryAmount;
/** proportional levy, where a body charges a percentage instead of or alongside a fixed amount */
percentage?: number;
/** `fee` is charged per competitor rather than per competition */
perParticipant?: boolean;
/** cap on the total when `perParticipant` is set */
maximum?: MonetaryAmount;
/** draw stage or event the fee applies to, where a federation prices them separately */
appliesTo?: string;
note?: string;
}
SanctionConferral
/** What the sanction actually buys. None of these is derivable from the classification. */
export interface SanctionConferral {
/** results count toward the authority's ranking lists (USTA `isRanking`, ITA `isConferenceMatch`) */
rankingEligible?: boolean;
/** which points profile applies, when the authority runs more than one */
rankingPointsProfile?: string;
/** results may set records */
recordEligible?: boolean;
/** times/results are "official" for qualification purposes even if not ranked */
officialResults?: boolean;
/** the sanction carries the authority's liability cover — the reason many organisers apply */
insured?: boolean;
extensions?: Extension[];
}
SanctionRuleset
/**
* The rulebook in force, pinned to an edition.
*
* Pinning is not optional. Governing-body rulebooks are annual, and a sanction granted under one
* edition must not silently re-validate against the next. `appliedRules` exists because a body may
* enforce only an enumerated SUBSET of its rules at lower recognition levels.
*/
export interface SanctionRuleset {
rulesetId?: string;
/** e.g. '2026' — the edition in force when the decision was made */
edition?: string;
enforcement?: SanctionEnforcementUnion;
/** identifiers of the specific rules enforced, when only a subset applies */
appliedRules?: string[];
extensions?: Extension[];
}
SanctionDecisionRecord
/** Provenance of the decision — who decided, when, and whether it can be appealed. */
export interface SanctionDecisionRecord {
decidedAt?: string;
decidedByPersonId?: string;
decidedByName?: string;
/** false where the denial rested on a rule the appeal body cannot overturn */
appealable?: boolean;
reason?: string;
note?: string;
}
SanctionIdentifier
/** An identifier the authority issues for the competition (USTA `identificationCode`, '26-80173'). */
export interface SanctionIdentifier {
organisationId?: string;
identifier: string;
/** what kind of identifier this is, where an authority issues more than one */
identifierType?: string;
}
TournamentSanction
/**
* A governing body's decision about a competition, as an attribute of that competition.
*
* Attach to `Tournament.sanction` for the common case. `Event.sanction` exists because grade is
* genuinely event-grain in several federations — one tournament routinely carries different
* categories for different age groups — mirroring the existing `tournamentTier`/`eventTier` pair.
* Where an event carries no sanction of its own, the tournament's applies.
*
* Multiple sanctions are expressed as multiple entries in `Tournament.sanctions`; `sanction` is the
* primary. Dual sanctioning is real (an international-grade event usually also carries national
* approval, and one federation requires two co-equal approvals), so consumers must not assume a
* single authority.
*/
export interface TournamentSanction {
/** the workflow outcome */
decision?: SanctionDecisionUnion;
/** what the authority asserts about the competition */
recognition?: RecognitionUnion;
/** the grade conferred — reuses the CODES tier shape, so 'Unsanctioned' is expressible */
classification?: TierClassification;
/** the body whose decision this is — the terminal approver */
authority?: SanctioningAuthority;
/** the resolved chain, ordered broadest → narrowest. Not necessarily a containment ladder. */
approvalChain?: SanctioningAuthority[];
decisionRecord?: SanctionDecisionRecord;
confers?: SanctionConferral;
ruleset?: SanctionRuleset;
/** link back to the application this decision came from, where one is held */
sanctioningId?: string;
identifiers?: SanctionIdentifier[];
fees?: SanctionFee[];
/** the window in which the application was accepted, where the authority publishes one */
submissionWindow?: { from?: string; to?: string; timeZone?: string };
extensions?: Extension[];
timeItems?: TimeItem[];
}
VenueTransitEntry
/**
* Minutes to get between two venues (scheduling-pipeline.md § 3.4). Symmetric unless `oneWay`, when it runs from
* `venueIds[0]` to `venueIds[1]` only. `sanctioned` records a sanctioning body's value, a floor `minutes` never goes below.
*/
export interface VenueTransitEntry {
venueIds: [string, string];
minutes: number;
oneWay?: boolean;
sanctioned?: { minutes: number; sanctioningId?: string };
}