courthive-components 4.4.0 → 4.5.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.
@@ -24,6 +24,8 @@ export interface InlineScoringCallbacks {
24
24
  onEndMatch?: (params: {
25
25
  matchUpId: string;
26
26
  matchUpStatus: string;
27
+ /** Resolved from the exiting side; absent for a double exit and the non-directing statuses. */
28
+ winningSide?: number;
27
29
  sideNumber?: number;
28
30
  engine: any;
29
31
  }) => void;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Pure logic for resolving an irregular ending into a matchUpStatus.
3
+ *
4
+ * Every score-entry approach needs the same rule, and until now each one carried its own copy:
5
+ * "walkover with a winner is a WALKOVER, walkover without one is a DOUBLE_WALKOVER". Three copies
6
+ * of a rule is three chances to disagree about it, and they already disagreed about when Submit
7
+ * should be live.
8
+ *
9
+ * The rule this module encodes differs from those copies in one deliberate way. A missing winner
10
+ * used to mean "double exit" — so merely selecting Walkover produced a valid, submittable
11
+ * DOUBLE_WALKOVER before the operator had chosen anything. That is a fail-open default (Mentat
12
+ * architectural standard A3): the absence of an answer was read as a particular answer, and the
13
+ * particular answer it was read as is one of the most consequential outcomes in the draw, because a
14
+ * double exit advances nobody and propagates a walkover downstream.
15
+ *
16
+ * Here, absence is absence. `undefined` is "the operator has not answered yet" and is never valid;
17
+ * a double exit requires explicitly choosing NEITHER_SIDE. No DOM dependencies, no side effects.
18
+ */
19
+ /**
20
+ * The winner-selection value meaning "no side won this matchUp" — the explicit choice that produces
21
+ * a double exit. A sentinel rather than `null` so the three states (unanswered / a side / neither)
22
+ * stay distinguishable in a radio group's string values.
23
+ */
24
+ export declare const NEITHER_SIDE = "NEITHER";
25
+ /** Shown when an irregular ending has been chosen but the winner question is unanswered. */
26
+ export declare const WINNER_REQUIRED_ERROR = "Select a winner";
27
+ /** The statuses whose selection opens the winner question at all. */
28
+ export declare const WINNER_REQUIRING_STATUSES: Set<string>;
29
+ /**
30
+ * Endings that resolve nobody: the match did not produce a result, so there is no winner to name
31
+ * and no winner question to answer. The factory classifies these as non-directing
32
+ * (`nonDirectingMatchUpStatuses`) — nothing advances out of them.
33
+ *
34
+ * These are NOT a smaller version of the winner-requiring endings. Asking "who won an abandoned
35
+ * match" is not a question with a missing answer; it is not a question. So unlike a walkover with
36
+ * no winner selection, one of these is valid the moment it is chosen.
37
+ */
38
+ export declare const NON_DIRECTING_ENDINGS: Set<string>;
39
+ /**
40
+ * Every irregular ending a set-entry approach offers, in display order.
41
+ *
42
+ * These six are exactly the keys the factory's scoring policy can refine with `matchUpStatusCodes`
43
+ * (ABANDONED, CANCELLED, DEFAULTED, INCOMPLETE, RETIRED, WALKOVER), which is why the list is this
44
+ * list: a status with no code group cannot carry a reason, and a code group with no status cannot
45
+ * be reached. Ordered so the three an operator reaches for most sit first.
46
+ */
47
+ export declare const SELECTABLE_ENDINGS: string[];
48
+ /** Whether choosing this ending obliges the operator to answer the winner question. */
49
+ export declare function requiresWinner(selectedOutcome: string | undefined): boolean;
50
+ /**
51
+ * Statuses that carry NO score at all, so there is nothing to validate and nothing to submit.
52
+ *
53
+ * This mirrors the factory, which is the authority: `modifyMatchUpScore.ts:236` blanks the score
54
+ * outright for exactly this set — `Object.assign(matchUp, { ...toBePlayed })`, commented "a walkover
55
+ * has none". Note what is NOT here: `DEFAULTED` and `DOUBLE_DEFAULT` are absent in the factory too,
56
+ * because a player can default part-way through a match that was genuinely played, and that partial
57
+ * score is real. The same goes for `RETIRED` — a retirement keeps whatever was played, and needs no
58
+ * completed set to be a valid result.
59
+ *
60
+ * `CANCELLED` and `DEAD_RUBBER` are deliberately NOT here even though this repo's `validateScore`
61
+ * strips scores for them. They are unreachable from the set-entry approaches today (only freeScore
62
+ * parses them) and the factory does not blank scores for them, so adding them here would be a
63
+ * guess. Revisit when the status list widens — see Mentat SCORING_MODAL_STATUS_CODES.md M2.
64
+ */
65
+ export declare const NO_SCORE_STATUSES: Set<string>;
66
+ /**
67
+ * Whether this ending means "no score exists", so score validation must be skipped rather than run
68
+ * and then overridden.
69
+ */
70
+ export declare function carriesNoScore(selectedOutcome: string | undefined): boolean;
71
+ /**
72
+ * Unanswered (`undefined`), a side (`1` | `2`), or explicitly neither.
73
+ */
74
+ export type WinnerSelection = 1 | 2 | typeof NEITHER_SIDE | undefined;
75
+ /** The double-exit statuses themselves, as opposed to the endings that produce them. */
76
+ export declare const DOUBLE_EXIT_STATUSES: Set<string>;
77
+ /**
78
+ * Whether this status IS a double exit — no side advances.
79
+ *
80
+ * Distinct from `supportsNeitherSide`, which asks whether an *ending* can become one. Use this
81
+ * where a status arrives already resolved, as it does from the inline popover.
82
+ */
83
+ export declare function isDoubleExitStatus(matchUpStatus: string | undefined): boolean;
84
+ /**
85
+ * The winner implied by a per-side exit control, where clicking a side's pill means THAT side is the
86
+ * one exiting and the other therefore wins.
87
+ *
88
+ * Returns `undefined` for a double exit (nobody advances) and for the non-directing statuses —
89
+ * SUSPENDED, CANCELLED, ABANDONED — which resolve nothing. The inline popover previously listed the
90
+ * double exits among the statuses that "produce a winner", so had one ever been selectable it would
91
+ * have stamped a winningSide on an outcome that must carry none.
92
+ */
93
+ export declare function winnerFromExitingSide(matchUpStatus: string | undefined, exitingSideNumber?: number): number | undefined;
94
+ /**
95
+ * Whether "Neither side" is a legitimate answer for this irregular ending.
96
+ */
97
+ export declare function supportsNeitherSide(selectedOutcome: string | undefined): boolean;
98
+ /**
99
+ * Message shown beside a double-exit selection. A double exit is not a scoring detail — it advances
100
+ * nobody and propagates a walkover into the next round — so the modal says what it will do before it
101
+ * is submitted rather than after.
102
+ */
103
+ export declare function doubleExitWarning(selectedOutcome: string | undefined): string;
104
+ export type IrregularEndingResolution = {
105
+ /** The status to submit. `undefined` while the selection is incomplete. */
106
+ matchUpStatus?: string;
107
+ /** Present only when a side was named. A double exit deliberately carries none. */
108
+ winningSide?: number;
109
+ /** Whether this resolution may be submitted. */
110
+ isValid: boolean;
111
+ /** True when the operator still owes a winner answer — the fail-closed state. */
112
+ awaitingWinner: boolean;
113
+ /** True when the resolution is a double exit, so callers can warn before it is submitted. */
114
+ isDoubleExit: boolean;
115
+ };
116
+ /**
117
+ * Resolve an irregular ending plus a winner selection into the status to submit.
118
+ *
119
+ * `COMPLETED` is not an irregular ending — callers keep their own completion handling and this
120
+ * returns an inert resolution for it.
121
+ */
122
+ export declare function resolveIrregularEnding(params: {
123
+ selectedOutcome: string | undefined;
124
+ winnerSelection: WinnerSelection;
125
+ }): IrregularEndingResolution;
126
+ /**
127
+ * Apply a resolution onto a validation object in place, the shape the approaches already pass to
128
+ * `onScoreChange`.
129
+ *
130
+ * It also clears any `error` the score validator left behind, and the two cases that reach here are
131
+ * worth separating because only one of them is legitimate:
132
+ *
133
+ * - **RETIRED / DEFAULTED with a partial score.** The validator correctly reports "this is not a
134
+ * completed match"; the ending is what makes it a valid result anyway. Clearing the error is the
135
+ * right call — the validator answered a question that is no longer the one being asked.
136
+ * - **A walkover.** There is no score to validate, so the validator should never have run. Callers
137
+ * must check `carriesNoScore()` and skip validation entirely; the clear here is a backstop, not
138
+ * the fix. A walkover that reported `isValid: true` beside
139
+ * `error: 'At least one set is required'` was the symptom of exactly that missing check.
140
+ */
141
+ export declare function applyIrregularEndingToValidation(validation: any, selectedOutcome: string | undefined, winnerSelection: WinnerSelection): IrregularEndingResolution;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Reading a scoring policy's `matchUpStatusCodes` vocabulary.
3
+ *
4
+ * The policy refines a matchUpStatus with caller-defined codes — a retirement becomes "Ret [inj]",
5
+ * a default becomes "Def [dq]". The modal renders them; it never invents them. If a tournament's
6
+ * policy carries no codes for the chosen status, there is nothing to show and no control is drawn.
7
+ * That is the designed state, not a degraded one: CA, 2026-09-20 — TMX ships no built-in default,
8
+ * so the reason field exists exactly where a governing body's vocabulary applies and nowhere else.
9
+ *
10
+ * No DOM, no side effects.
11
+ */
12
+ /**
13
+ * One entry of a policy's code group, as the factory's fixtures ship it.
14
+ *
15
+ * CA settled the canonical shape on 2026-09-19: the OBJECT form is authoritative, because a bare
16
+ * string cannot carry the display text the picker exists to render.
17
+ */
18
+ export type StatusCodeEntry = {
19
+ matchUpStatusCode: string;
20
+ matchUpStatusCodeDisplay?: string;
21
+ label?: string;
22
+ description?: string;
23
+ };
24
+ /** A policy's groups, keyed by the matchUpStatus each refines. */
25
+ export type StatusCodeGroups = Record<string, StatusCodeEntry[]>;
26
+ /**
27
+ * The group key a status reads its codes from.
28
+ *
29
+ * A double exit has no group of its own — the policy files "Wo/Wo" inside WALKOVER and "Def/Def"
30
+ * inside DEFAULTED, because they are walkovers and defaults. Mapping them here is what keeps the
31
+ * double-exit choice and the code list from becoming two controls that can disagree.
32
+ */
33
+ export declare function groupKeyForStatus(matchUpStatus: string | undefined): string | undefined;
34
+ /**
35
+ * Normalise one element of a code group, or of a matchUp's `matchUpStatusCodes`, to its code string.
36
+ *
37
+ * Three shapes are legitimate and all three occur. `matchUpStatusCodes` on a matchUp is an `any[]`
38
+ * shared by three tenants (policy codes, propagation provenance, and `{ code }` wrappers), and
39
+ * `updateMatchUpStatusCodes` in the factory rewraps every string element as `{ code }` — so a code
40
+ * written as `'RJ'` reads back as `{ code: 'RJ' }` once propagation has touched the matchUp. A
41
+ * reader that assumes a string is wrong, and it is wrong only sometimes, which is worse.
42
+ *
43
+ * Returns `undefined` for a provenance element, which carries no code at all.
44
+ */
45
+ export declare function normalizeStatusCode(entry: unknown): string | undefined;
46
+ /**
47
+ * The codes a policy offers for this status, or an empty list when it offers none.
48
+ *
49
+ * Entries without a `matchUpStatusCode` are dropped rather than rendered as a blank option — an
50
+ * unpickable row in a picker is worse than a shorter picker.
51
+ */
52
+ export declare function codesForStatus(groups: StatusCodeGroups | undefined, matchUpStatus: string | undefined): StatusCodeEntry[];
53
+ /**
54
+ * What to show for a code: the policy's display form, falling back to its label, then to the code.
55
+ *
56
+ * The three fields are authored per code and mean different things — `matchUpStatusCodeDisplay` is
57
+ * the terse form an operator recognises ("Ret [inj]"), `label` the prose one ("Injury"). Never
58
+ * synthesise a display from the code: a policy that authored none is saying the code IS the label.
59
+ */
60
+ export declare function statusCodeDisplay(entry: StatusCodeEntry): string;
61
+ /** The secondary line beside the display form, when the policy authored one that adds something. */
62
+ export declare function statusCodeSubtext(entry: StatusCodeEntry): string | undefined;
@@ -0,0 +1,13 @@
1
+ import { StatusCodeGroups } from './logic/statusCodes';
2
+ import { ScoringModalLabels } from './types';
3
+ export type StatusCodePickerHandle = {
4
+ element: HTMLElement;
5
+ /** Re-render for a new status. Returns the code still selected, if any. */
6
+ update: (matchUpStatus: string | undefined) => string | undefined;
7
+ /** The chosen code, or undefined when none is chosen or none is offered. */
8
+ getSelectedCode: () => string | undefined;
9
+ };
10
+ export declare function buildStatusCodePicker(params: {
11
+ groups?: StatusCodeGroups;
12
+ labels?: ScoringModalLabels;
13
+ }): StatusCodePickerHandle;
@@ -1,6 +1,4 @@
1
- /**
2
- * Type definitions for scoring modal V2
3
- */
1
+ import { StatusCodeGroups } from './logic/statusCodes';
4
2
  export type SetScore = {
5
3
  setNumber?: number;
6
4
  side1Score?: number;
@@ -17,6 +15,8 @@ export type ScoreOutcome = {
17
15
  scoreObject?: any;
18
16
  winningSide?: number;
19
17
  matchUpStatus?: string;
18
+ /** Policy reason codes chosen for this outcome, as code strings — e.g. `['RJ']`. */
19
+ matchUpStatusCodes?: string[];
20
20
  error?: string;
21
21
  matchUpFormat?: string;
22
22
  score?: string;
@@ -30,13 +30,19 @@ export type ScoringModalLabels = {
30
30
  formatEditTitle?: string;
31
31
  irregularEnding?: string;
32
32
  winner?: string;
33
+ neitherSide?: string;
33
34
  retired?: string;
35
+ abandoned?: string;
36
+ cancelled?: string;
37
+ incomplete?: string;
34
38
  walkover?: string;
35
39
  defaulted?: string;
36
40
  validScore?: string;
37
41
  scoreIncomplete?: string;
38
42
  invalidScore?: string;
39
43
  scoreTips?: string;
44
+ reasonCode?: string;
45
+ noReasonCode?: string;
40
46
  setScores?: string;
41
47
  tiebreaks?: string;
42
48
  matchTiebreaks?: string;
@@ -50,6 +56,13 @@ export type ScoringModalParams = {
50
56
  callback: (outcome: any) => void;
51
57
  onClose?: () => void;
52
58
  labels?: ScoringModalLabels;
59
+ /**
60
+ * The scoring policy's `matchUpStatusCodes` groups, resolved by the CALLER — components never
61
+ * reaches into factory fixtures or the engine. Omit it (or pass groups with no entries for the
62
+ * chosen status) and no reason control is drawn, which is the designed state for a tournament
63
+ * with no governing-body policy attached.
64
+ */
65
+ matchUpStatusCodes?: StatusCodeGroups;
53
66
  };
54
67
  export type ScoreChangeHandler = (outcome: ScoreOutcome) => void;
55
68
  export type RenderScoreEntryParams = {