courthive-components 3.15.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  export { InlineScoringManager } from './inlineScoringManager';
2
2
  export { renderInlineMatchUp } from './renderInlineMatchUp';
3
+ export { isScorable, sideParticipantName } from './isScorable';
3
4
  export { engineToMatchUp } from './engineToMatchUp';
4
5
  export { createInlineScoringFooter } from './inlineScoringFooter';
5
6
  export type { InlineScoringMode, InlineScoringConfig, InlineScoringCallbacks, InlineScoringEngineState } from './inlineScoringTypes';
@@ -0,0 +1,37 @@
1
+ import { MatchUp } from '../../types';
2
+ /**
3
+ * The name a side's participant is known by, or `undefined` when there isn't one.
4
+ *
5
+ * Returns `undefined` rather than a placeholder ON PURPOSE. `"Side 1"` reads like a legitimate name
6
+ * everywhere it is subsequently displayed or persisted, so a fallback here is indistinguishable from
7
+ * a real record. A refusal you can see beats a plausible value you cannot.
8
+ */
9
+ export declare function sideParticipantName(matchUp: MatchUp | undefined, sideNumber: number): string | undefined;
10
+ /**
11
+ * May this matchUp be offered for interactive scoring at all?
12
+ *
13
+ * FAIL CLOSED. A scoring affordance leads to stored results, and the two things that make a stored
14
+ * result trustworthy cannot be reconstructed afterwards:
15
+ *
16
+ * - **`matchUpFormat`** decides how a score is *interpreted*. It is determined by the tournamentRecord
17
+ * and delivered by the factory; there is no correct way to guess it. A score kept against an invented
18
+ * format is not slightly wrong, it is uninterpretable.
19
+ * - **Named participants.** A result attributed to "Side 1" is indistinguishable from a genuine one at
20
+ * every point downstream.
21
+ *
22
+ * So a matchUp missing either is not scored with a substitute — it is not offered for scoring. That is
23
+ * the only arrangement in which the substitute cannot exist.
24
+ *
25
+ * This lives in the LIBRARY rather than in each app deliberately. Both consumers had grown their own
26
+ * weaker version of this check (participant existence, no name check, no format check), and
27
+ * `renderInlineMatchUp` — the single place a scoring affordance is minted — defaulted the format for
28
+ * everyone. A guard beside the thing it guards is structural; a guard in each app is a rule each app
29
+ * has to remember.
30
+ *
31
+ * Costs nothing in practice: across a real production tournament (three events, 211 matchUps) every
32
+ * matchUp carried a `matchUpFormat`, and the set passing this gate was exactly the set with two
33
+ * hydrated, named sides.
34
+ *
35
+ * @param matchUpFormat optional override, matching `renderInlineMatchUp`'s parameter of the same name.
36
+ */
37
+ export declare function isScorable(matchUp: MatchUp | undefined, matchUpFormat?: string): boolean;
@@ -21,7 +21,9 @@ interface RenderInlineMatchUpParams {
21
21
  * The footer buttons (Undo/Redo/Clear/Submit) are rendered by `renderMatchUp` itself,
22
22
  * driven by the inlineScoring config state and eventHandlers passed down.
23
23
  *
24
- * Returns a container element that re-renders itself after each scoring action.
24
+ * Returns a container element that re-renders itself after each scoring action, or **`null`** when the
25
+ * matchUp must not be offered for scoring — see `isScorable`. Callers must handle the null rather than
26
+ * replacing a rendered cell with it; the return type is what forces them to.
25
27
  */
26
- export declare function renderInlineMatchUp(params: RenderInlineMatchUpParams): HTMLElement;
28
+ export declare function renderInlineMatchUp(params: RenderInlineMatchUpParams): HTMLElement | null;
27
29
  export {};
@@ -0,0 +1,21 @@
1
+ import { ProjectedPressureResult, PressureSeries } from './types';
2
+ import { GetProjectedPressureParams } from './getProjectedPressure';
3
+ export type BuildPressureSeriesParams = GetProjectedPressureParams;
4
+ export type PressureSeriesResult = {
5
+ series: PressureSeries[];
6
+ projection: ProjectedPressureResult;
7
+ /** The rating scale the values were read from, for axis labelling. */
8
+ scaleName?: string;
9
+ unratedCount: number;
10
+ };
11
+ export declare function buildPressureSeries(params: BuildPressureSeriesParams): PressureSeriesResult;
12
+ /**
13
+ * Participants ordered hardest SLOT first — the ranked "path difficulty" view,
14
+ * and the entry point of the all-players surface.
15
+ *
16
+ * Sorts on `slotDifficulty`, not `pathDifficulty`: the reach-weighted figure
17
+ * discounts the late rounds a weak player is unlikely to reach, so ranking on it
18
+ * would report the toughest draws in the field as the easiest. Unrated
19
+ * participants sort last rather than being dropped, so the field size stays honest.
20
+ */
21
+ export declare function byPathDifficulty(series: PressureSeries[]): PressureSeries[];
@@ -0,0 +1,41 @@
1
+ import { PressureUnsupportedReason } from './types';
2
+ export declare const BYE_STATUS = "BYE";
3
+ export type GraphSide = {
4
+ participantId?: string;
5
+ participant?: any;
6
+ drawPosition?: number;
7
+ bye: boolean;
8
+ };
9
+ export type GraphMatchUp = {
10
+ matchUpId?: string;
11
+ roundNumber: number;
12
+ roundPosition: number;
13
+ sides: [GraphSide, GraphSide];
14
+ winningSide?: number;
15
+ matchUpStatus?: string;
16
+ raw: any;
17
+ };
18
+ export type EliminationGraph = {
19
+ roundNumbers: number[];
20
+ /** roundNumber -> roundPosition -> matchUp */
21
+ byRound: Map<number, Map<number, GraphMatchUp>>;
22
+ firstRound: number;
23
+ };
24
+ /**
25
+ * Build the graph, or say why it could not be built.
26
+ *
27
+ * Rejects anything that is not a clean halving tree — round-robin containers,
28
+ * ad-hoc structures, and fed/consolation shapes whose round sizes do not halve.
29
+ * Refusing is deliberate: a wrong tree would produce a plausible-looking
30
+ * projection with silently wrong opponents, which is worse than no chart.
31
+ */
32
+ export declare function buildEliminationGraph(matchUps: any[]): {
33
+ graph?: EliminationGraph;
34
+ unsupported?: PressureUnsupportedReason;
35
+ };
36
+ /** The two feeder positions of `(roundNumber, roundPosition)` in the previous round. */
37
+ export declare function feederPositions(roundPosition: number): [number, number];
38
+ /** The position a winner of `roundPosition` advances into, in the next round. */
39
+ export declare function advancingPosition(roundPosition: number): number;
40
+ /** True when the matchUp is a bye for whichever side is present. */
41
+ export declare function isByeMatchUp(matchUp: GraphMatchUp): boolean;
@@ -0,0 +1,14 @@
1
+ import { ParticipantActualPressure } from './types';
2
+ export type GetActualPressureParams = {
3
+ matchUps: any[];
4
+ matchUpType?: string;
5
+ scaleName?: string;
6
+ };
7
+ /**
8
+ * Walk every matchUp a participant appears in and describe what they met.
9
+ *
10
+ * Only matchUps whose sides are hydrated contribute — an unplayed later round
11
+ * has empty sides, so it simply produces no entry. That is the correct
12
+ * behaviour: this series exists to show what *happened*.
13
+ */
14
+ export declare function getActualPressure(params: GetActualPressureParams): ParticipantActualPressure[];
@@ -0,0 +1,23 @@
1
+ import { WinProbabilityModel } from './winProbability';
2
+ import { ProjectedPressureResult } from './types';
3
+ /** Opponents below this arrival probability are excluded from the displayed range. */
4
+ export declare const DEFAULT_RANGE_THRESHOLD = 0.01;
5
+ export type GetProjectedPressureParams = {
6
+ /** Structure matchUps, hydrated with sides + participants. */
7
+ matchUps: any[];
8
+ /** SINGLES (default) or DOUBLES — selects which ratings block to read. */
9
+ matchUpType?: string;
10
+ /** Prefer this scale when a participant carries several. */
11
+ scaleName?: string;
12
+ /** Pairwise model knobs; see `winProbability`. */
13
+ model?: WinProbabilityModel;
14
+ /**
15
+ * When true, a played matchUp collapses to its actual winner, so the
16
+ * projection reads "from here". Default **false** — the untouched projection
17
+ * is what the draw said at the outset, which is the comparison the chart makes.
18
+ */
19
+ respectResults?: boolean;
20
+ /** Arrival-probability floor for inclusion in `opponentEloRange`. */
21
+ rangeThreshold?: number;
22
+ };
23
+ export declare function getProjectedPressure(params: GetProjectedPressureParams): ProjectedPressureResult;
@@ -0,0 +1,18 @@
1
+ export { buildPressureSmallMultiples, sharedYDomain } from './pressureSmallMultiples';
2
+ export { buildPressureSeries, byPathDifficulty } from './buildPressureSeries';
3
+ export { buildPressureChart, resolveYDomain, defaultRoundLabel } from './pressureChart';
4
+ export { getProjectedPressure, DEFAULT_RANGE_THRESHOLD } from './getProjectedPressure';
5
+ export { eloWinProbability, winProbability, DEFAULT_ELO_DIVISOR } from './winProbability';
6
+ export { resolveParticipantRating, ratingToElo, convertRange } from './ratingScale';
7
+ export { buildPathDifficultyBar } from './pathDifficultyBar';
8
+ export { getActualPressure } from './getActualPressure';
9
+ export { buildPressureTable } from './pressureTable';
10
+ export { PRESSURE_UNSUPPORTED } from './types';
11
+ export type { PressureSmallMultiplesOptions, PressureSmallMultiplesInstance } from './pressureSmallMultiples';
12
+ export type { PathDifficultyBarOptions, PathDifficultyBarInstance } from './pathDifficultyBar';
13
+ export type { BuildPressureSeriesParams, PressureSeriesResult } from './buildPressureSeries';
14
+ export type { PressureChartOptions, PressureChartInstance } from './pressureChart';
15
+ export type { GetProjectedPressureParams } from './getProjectedPressure';
16
+ export type { GetActualPressureParams } from './getActualPressure';
17
+ export type { WinProbabilityModel } from './winProbability';
18
+ export type { ParticipantPressureProjection, ParticipantActualPressure, ProjectedPressureResult, PressureUnsupportedReason, ProjectedRoundPressure, ActualRoundPressure, PressureSeriesPoint, PossibleOpponent, ResolvedRating, PressureSeries } from './types';
@@ -0,0 +1,17 @@
1
+ import { PressureSeries } from './types';
2
+ export type PathDifficultyBarOptions = {
3
+ width?: number;
4
+ rowHeight?: number;
5
+ labelWidth?: number;
6
+ /** Cap the rows rendered. When set, the count dropped is stated in the caption. */
7
+ limit?: number;
8
+ /** Overlay the difficulty actually faced so far, where a participant has played. */
9
+ showActual?: boolean;
10
+ onSelect?: (series: PressureSeries) => void;
11
+ emptyMessage?: string;
12
+ };
13
+ export type PathDifficultyBarInstance = {
14
+ element: HTMLElement;
15
+ update: (series: PressureSeries[], options?: PathDifficultyBarOptions) => void;
16
+ };
17
+ export declare function buildPathDifficultyBar(container: HTMLElement, series?: PressureSeries[], options?: PathDifficultyBarOptions): PathDifficultyBarInstance;
@@ -0,0 +1,40 @@
1
+ import { PressureSeries, PressureSeriesPoint } from './types';
2
+ export type PressureChartOptions = {
3
+ width?: number;
4
+ height?: number;
5
+ margin?: {
6
+ top: number;
7
+ right: number;
8
+ bottom: number;
9
+ left: number;
10
+ };
11
+ /** Compact mode for the small-multiples grid: no labels, no legend, thin marks. */
12
+ spark?: boolean;
13
+ /** Fix the y domain across a set of charts so small multiples are comparable. */
14
+ yDomain?: [number, number];
15
+ /** Label for the rating scale, e.g. 'WTN'. Shown on the axis. */
16
+ scaleName?: string;
17
+ showLegend?: boolean;
18
+ ariaLabel?: string;
19
+ roundLabels?: (roundNumber: number, index: number, total: number) => string;
20
+ emptyMessage?: string;
21
+ };
22
+ export type PressureChartInstance = {
23
+ element: HTMLElement;
24
+ update: (series: PressureSeries | undefined, options?: PressureChartOptions) => void;
25
+ destroy: () => void;
26
+ };
27
+ /** QF / SF / F for the last three rounds, R1.. before that — matches the round-nav convention. */
28
+ export declare function defaultRoundLabel(_roundNumber: number, index: number, total: number): string;
29
+ /** Symmetric domain around zero so "playing up" and "playing down" read equally. */
30
+ export declare function resolveYDomain(points: PressureSeriesPoint[], override?: [number, number]): [number, number];
31
+ /** Round a half-extent down to a readable 1 / 2 / 5 x 10^n step. */
32
+ export declare function niceStep(extent: number): number;
33
+ /**
34
+ * Build (or rebuild) the chart into a container.
35
+ *
36
+ * Returns a handle so a consumer can re-render on a live update without
37
+ * re-querying the DOM. `update(undefined)` renders the empty state — an unrated
38
+ * field is a legitimate outcome, not an error.
39
+ */
40
+ export declare function buildPressureChart(container: HTMLElement, series?: PressureSeries, options?: PressureChartOptions): PressureChartInstance;
@@ -0,0 +1,17 @@
1
+ import { PressureSeries } from './types';
2
+ export type PressureSmallMultiplesOptions = {
3
+ cellWidth?: number;
4
+ cellHeight?: number;
5
+ /** Cap the cells rendered; the number dropped is stated in the caption. */
6
+ limit?: number;
7
+ scaleName?: string;
8
+ onSelect?: (series: PressureSeries) => void;
9
+ emptyMessage?: string;
10
+ };
11
+ export type PressureSmallMultiplesInstance = {
12
+ element: HTMLElement;
13
+ update: (series: PressureSeries[], options?: PressureSmallMultiplesOptions) => void;
14
+ };
15
+ /** One y domain across every cell, so the grid can be read as a comparison. */
16
+ export declare function sharedYDomain(series: PressureSeries[]): [number, number];
17
+ export declare function buildPressureSmallMultiples(container: HTMLElement, series?: PressureSeries[], options?: PressureSmallMultiplesOptions): PressureSmallMultiplesInstance;
@@ -0,0 +1,2 @@
1
+ import { PressureSeries } from './types';
2
+ export declare function buildPressureTable(container: HTMLElement, series: PressureSeries): HTMLTableElement;
@@ -0,0 +1,44 @@
1
+ import { ResolvedRating } from './types';
2
+ /** Linear map of a value from one range onto another. Mirrors factory `convertRange`. */
3
+ export declare function convertRange({ value, sourceRange, targetRange }: {
4
+ value: number;
5
+ sourceRange?: number[];
6
+ targetRange?: number[];
7
+ }): number;
8
+ /**
9
+ * Convert a value on `scaleName` to its ELO equivalent.
10
+ *
11
+ * Scales where a LOWER number is better (WTN, declared `range: [40, 1]`) are
12
+ * declared high-to-low; the factory detects that as `range[0] > range[1]` and
13
+ * reflects the value before mapping, which is reproduced here.
14
+ */
15
+ export declare function ratingToElo({ scaleName, value }: {
16
+ scaleName?: string;
17
+ value?: number;
18
+ }): number | undefined;
19
+ /**
20
+ * Resolve a participant's rating onto the common scale.
21
+ *
22
+ * Expects the `participant.ratings` shape produced with
23
+ * `participantsProfile: { withScaleValues: true }` — the hydration CFS already
24
+ * requests in `getEventData`:
25
+ *
26
+ * ```
27
+ * ratings: { SINGLES: [{ scaleName: 'WTN', scaleValue: { wtnRating: 4.13 } }] }
28
+ * ```
29
+ *
30
+ * **Without `withScaleValues` the `ratings` key is present but empty** and the
31
+ * value lives in `participant.timeItems` instead; this returns `null` in that
32
+ * case rather than inventing a default. A blank chart is the correct outcome for
33
+ * an unrated field — see `PRESSURE_UNSUPPORTED.NO_RATINGS`.
34
+ */
35
+ export declare function resolveParticipantRating({ participant, matchUpType, preferredScaleName }: {
36
+ participant?: any;
37
+ matchUpType?: string;
38
+ preferredScaleName?: string;
39
+ }): ResolvedRating | null;
40
+ /**
41
+ * The scale name that appears most often across a set of resolved ratings.
42
+ * Reported so a caller can label the axis honestly when a field is mixed-scale.
43
+ */
44
+ export declare function dominantScaleName(ratings: (ResolvedRating | null)[]): string | undefined;
@@ -0,0 +1,153 @@
1
+ import { CompetitivenessBucket } from '../competitivenessBar/types';
2
+ /** Why a projection could not be produced. Never guess — say which. */
3
+ export declare const PRESSURE_UNSUPPORTED: {
4
+ readonly NO_MATCHUPS: "NO_MATCHUPS";
5
+ readonly NOT_ELIMINATION: "NOT_ELIMINATION";
6
+ readonly NO_RATINGS: "NO_RATINGS";
7
+ };
8
+ export type PressureUnsupportedReason = (typeof PRESSURE_UNSUPPORTED)[keyof typeof PRESSURE_UNSUPPORTED];
9
+ /** A rating resolved onto the common (ELO-equivalent) scale. */
10
+ export type ResolvedRating = {
11
+ /** ELO-equivalent value, produced by the same range conversion the factory uses. */
12
+ elo: number;
13
+ /** The scale the value was read from, e.g. 'WTN' / 'UTR' / 'ELO'. */
14
+ scaleName: string;
15
+ /** The raw value as published on that scale, for display. */
16
+ sourceValue: number;
17
+ };
18
+ /** A participant who could arrive in the opponent slot, with the probability they do. */
19
+ export type PossibleOpponent = {
20
+ participantId: string;
21
+ participantName?: string;
22
+ probability: number;
23
+ elo: number | null;
24
+ };
25
+ /** One round of a participant's projected path. */
26
+ export type ProjectedRoundPressure = {
27
+ roundNumber: number;
28
+ /** Probability the participant plays this round at all. Round 1 is 1. */
29
+ reachProbability: number;
30
+ /**
31
+ * Probability-weighted mean opponent rating (ELO-equivalent) over the
32
+ * participants who could arrive from the sibling sub-bracket. `null` when no
33
+ * opponent in that sub-bracket carries a rating.
34
+ */
35
+ expectedOpponentElo: number | null;
36
+ /** [min, max] over opponents whose arrival probability clears the threshold. */
37
+ opponentEloRange: [number, number] | null;
38
+ /** expectedOpponentElo - own rating. Positive = projected to play up. */
39
+ expectedSignedDelta: number | null;
40
+ /** How many distinct participants could arrive from the sibling sub-bracket. */
41
+ possibleOpponentCount: number;
42
+ /**
43
+ * Who could arrive, most-likely first. Drives the hover layer ("possible
44
+ * opponents"), and lets a test assert opponent *identity* rather than only a
45
+ * range — a range can be right by coincidence when sub-brackets are similar.
46
+ */
47
+ possibleOpponents: PossibleOpponent[];
48
+ /** True once exactly one opponent is possible — i.e. the uncertainty has burned off. */
49
+ resolved: boolean;
50
+ /** True when this round is a BYE for the participant: no opponent, no pressure. */
51
+ bye: boolean;
52
+ };
53
+ /** A participant's full projection across the structure. */
54
+ export type ParticipantPressureProjection = {
55
+ participantId: string;
56
+ participantName?: string;
57
+ drawPosition?: number;
58
+ /** The participant's own rating on the common scale, or `null` if unrated. */
59
+ rating: ResolvedRating | null;
60
+ rounds: ProjectedRoundPressure[];
61
+ /**
62
+ * **Experienced** difficulty: the reach-weighted mean of `expectedSignedDelta`.
63
+ * Answers "how hard are the matches this participant should expect to actually
64
+ * play?" — which is legitimately *lower* for a weak player, because the hard
65
+ * late rounds are discounted by their small chance of getting there.
66
+ *
67
+ * Do NOT rank a field on this. It systematically flatters weak players and so
68
+ * hides exactly the draw imbalance the ranked view exists to expose. Use
69
+ * `slotDifficulty` for that.
70
+ */
71
+ pathDifficulty: number | null;
72
+ /**
73
+ * **Slot** difficulty: the unweighted mean of `expectedSignedDelta` across all
74
+ * rounds carrying a rated opponent — i.e. how hard this bracket slot is if you
75
+ * had to walk the whole thing, independent of the occupant's own survival odds.
76
+ *
77
+ * This is the number the ranked "path difficulty" view sorts on.
78
+ *
79
+ * **Known limitation, measured on production data.** Because this is
80
+ * `mean(opponentElo) - ownElo` and the opponent mean varies far less across a
81
+ * field than the players' own ratings do, it is dominated by the participant's
82
+ * own rating: on the 2019 ITA DI Texas Men's Regional (99 rated entrants)
83
+ * Spearman rank correlation against own rating was **-0.986**. So ranking on it
84
+ * is close to ranking by rating inverted, and it does NOT by itself expose draw
85
+ * imbalance. Real draw-position signal is present but small — two entrants at
86
+ * UTR 10.42 and 10.55 in that draw differ by ~110 ELO of slot difficulty — and
87
+ * surfacing it needs the opponent-strength component on its own, or a residual
88
+ * against what a player of that rating typically faces. Not yet built.
89
+ */
90
+ slotDifficulty: number | null;
91
+ /** Sum of `reachProbability` across rounds — the expected number of matches played. */
92
+ expectedMatchesPlayed: number;
93
+ };
94
+ export type ProjectedPressureResult = {
95
+ projections: ParticipantPressureProjection[];
96
+ /** Present only when nothing could be produced. */
97
+ unsupported?: PressureUnsupportedReason;
98
+ /** The scale ratings were read from, when a single scale dominated. */
99
+ scaleName?: string;
100
+ /** Count of entrants with no usable rating — surfaced, never silently defaulted. */
101
+ unratedCount: number;
102
+ };
103
+ /** One round of a participant's realised path. */
104
+ export type ActualRoundPressure = {
105
+ roundNumber: number;
106
+ matchUpId?: string;
107
+ opponentParticipantId?: string;
108
+ opponentParticipantName?: string;
109
+ opponentElo: number | null;
110
+ /** opponentElo - own rating. Positive = played up. */
111
+ signedDelta: number | null;
112
+ /** How close the scoreline was. Undefined until the matchUp has a winner. */
113
+ competitiveness?: CompetitivenessBucket;
114
+ won?: boolean;
115
+ bye: boolean;
116
+ };
117
+ export type ParticipantActualPressure = {
118
+ participantId: string;
119
+ rating: ResolvedRating | null;
120
+ rounds: ActualRoundPressure[];
121
+ /** Mean `signedDelta` across played rounds carrying a rated opponent. */
122
+ facedDifficulty: number | null;
123
+ matchesPlayed: number;
124
+ };
125
+ /** Chart-ready merge of the projected and actual series for one participant. */
126
+ export type PressureSeries = {
127
+ participantId: string;
128
+ participantName?: string;
129
+ drawPosition?: number;
130
+ rating: ResolvedRating | null;
131
+ /** Reach-weighted expected delta. Do not rank on this — see the projection type. */
132
+ pathDifficulty: number | null;
133
+ /** Unweighted slot difficulty. This is what the ranked view sorts on. */
134
+ slotDifficulty: number | null;
135
+ facedDifficulty: number | null;
136
+ points: PressureSeriesPoint[];
137
+ };
138
+ export type PressureSeriesPoint = {
139
+ roundNumber: number;
140
+ /** Projected band, in signed-delta space. `null` where the round carries no rated opponent. */
141
+ projected: {
142
+ expected: number | null;
143
+ low: number | null;
144
+ high: number | null;
145
+ };
146
+ reachProbability: number;
147
+ /** Realised signed delta, present only once the round has been played. */
148
+ actual: number | null;
149
+ competitiveness?: CompetitivenessBucket;
150
+ won?: boolean;
151
+ bye: boolean;
152
+ resolved: boolean;
153
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Pairwise win probability on the common (ELO-equivalent) scale.
3
+ *
4
+ * The standard ELO logistic:
5
+ *
6
+ * P(A beats B) = 1 / (1 + 10 ^ ((eloB - eloA) / divisor))
7
+ *
8
+ * `divisor` is a policy knob rather than a constant so it can be calibrated per
9
+ * federation / discipline. 400 is the ELO convention and the default here.
10
+ *
11
+ * This is an ASSUMPTION until measured. The factory ships
12
+ * `getPredictiveAccuracy` for exactly this purpose — it scores how well a rating
13
+ * scale predicts real outcomes over a corpus. Anything presented to a user as a
14
+ * likelihood should be calibrated against that first; this module only ever
15
+ * feeds *relative* opponent-strength expectations, which are far less sensitive
16
+ * to the divisor than a displayed percentage would be.
17
+ */
18
+ export declare const DEFAULT_ELO_DIVISOR = 400;
19
+ export type WinProbabilityModel = {
20
+ divisor?: number;
21
+ };
22
+ /** Probability that the `elo`-rated side beats the `opponentElo`-rated side. */
23
+ export declare function eloWinProbability(elo: number, opponentElo: number, model?: WinProbabilityModel): number;
24
+ /**
25
+ * Win probability where either side may be unrated. An unrated participant is
26
+ * treated as a coin flip against anyone — a deliberate, visible choice: it keeps
27
+ * a single unrated entrant from erasing the whole sub-bracket's projection,
28
+ * while `unratedCount` on the result reports how much of the field it applied to.
29
+ */
30
+ export declare function winProbability(elo: number | null, opponentElo: number | null, model?: WinProbabilityModel): number;
@@ -24,6 +24,14 @@ export interface TierClassification {
24
24
  export interface TournamentEntryFee {
25
25
  amount: number;
26
26
  currencyCode?: string;
27
+ /**
28
+ * Whether `amount` is in the currency's smallest unit or in whole units.
29
+ *
30
+ * Optional here because a stored record written before the field existed still arrives without
31
+ * one — but a fee with no unit is NOT rendered as a number. See `feeFormatter`.
32
+ */
33
+ unit?: 'MINOR' | 'MAJOR';
34
+ eventId?: string;
27
35
  category?: string;
28
36
  eventType?: string;
29
37
  }