courthive-components 6.1.0 → 6.2.1

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.
@@ -11,7 +11,7 @@ export declare const MATCH_ROOTS: readonly ["SET", "HAL", "QTR", "PER", "INN", "
11
11
  export declare const MATCH_ROOT_LABELS: Record<string, string>;
12
12
  /**
13
13
  * Returns valid bestOf options for a given match root.
14
- * SET root: [1,3,5] (standard tennis)
14
+ * SET root: [1,3,5,7,9] — odd counts; the factory parses any best-of count (#5136)
15
15
  * Other roots: [1..12] (wide range for cross-sport)
16
16
  */
17
17
  export declare function getBestOfOptionsForRoot(matchRoot?: string): number[];
@@ -10,3 +10,8 @@ export type FormatOptions = {
10
10
  * Example: "6464" with matchUpFormat 'SET3-S:6/TB7' becomes "6-4 6-4"
11
11
  */
12
12
  export declare function formatScoreString(digits: string, options: FormatOptions): string;
13
+ /**
14
+ * The number of sets a format names. `matchUpFormatCode.parse` puts `bestOf` and `exactly` at the top level
15
+ * of the parsed format, never inside `setFormat`.
16
+ */
17
+ export declare function setCountOf(parsedFormat: any): number;
@@ -0,0 +1,24 @@
1
+ import { SideNumber } from './logic/scoreEntryState';
2
+ /**
3
+ * The LOWER participant's row — where entry begins, in every approach.
4
+ *
5
+ * CA, 2026-09-28: *"the dynamic sets when launched should always initially give focus to the lower
6
+ * row; that way e.g. 3 is lower row and shift+3 is upper row, which feels natural ... each subsequent
7
+ * entry column should start on the bottom cell."*
8
+ *
9
+ * Named rather than written as a bare `2` at each site: it is one decision, and the shifted case is
10
+ * derived from it (`otherSide`) so the two can never be set independently and disagree.
11
+ */
12
+ export declare const ENTRY_SIDE: SideNumber;
13
+ /** The side a SHIFTED keystroke writes to — the upper row, by construction. */
14
+ export declare const SHIFTED_SIDE: SideNumber;
15
+ export declare function otherSide(sideNumber: SideNumber): SideNumber;
16
+ /**
17
+ * The digit a key press means, or `undefined` for anything that is not a digit key.
18
+ *
19
+ * Both the number row and the numpad, because an operator entering scores at speed uses whichever is
20
+ * under their hand.
21
+ */
22
+ export declare function digitFromCode(code: string): number | undefined;
23
+ /** Whether a key event carries a modifier that means it belongs to the browser or the OS. */
24
+ export declare function hasCommandModifier(event: KeyboardEvent): boolean;
@@ -1,17 +1,37 @@
1
1
  import { SetScore } from '../types';
2
2
  /**
3
3
  * Set format information returned by matchUpFormatCode.parse()
4
+ *
5
+ * ── `NoAD`, and why the casing is load-bearing ──
6
+ *
7
+ * This type is a hand-written MIRROR of what `matchUpFormatCode.parse` emits, and it named the
8
+ * no-advantage flag `noAd` while the factory emits **`NoAD`**. Measured 2026-09-28:
9
+ * `parse('SET1-S:TB10NOAD').setFormat.tiebreakSet` is `{ tiebreakTo: 10, NoAD: true }`, so every read
10
+ * of `.noAd` in this module answered `undefined` for every format — and the TYPE is what made those
11
+ * reads look correct.
12
+ *
13
+ * Nothing threw and nothing logged; no-advantage scoring was simply ignored. Two measured
14
+ * consequences, both live in the shipping dialog: a low of 6 in a `TB7NOAD` completed to **8** instead
15
+ * of 7, and a low of 5 in `S:6NOAD` completed to **7** instead of 6 — a 7-5 in a format where first to
16
+ * six wins.
17
+ *
18
+ * Every OTHER reader of this flag in the package had it right — `freeScore.ts`, `matchUpFormat.ts`,
19
+ * `matchUpFormatLogic.ts` and the format picker all read `.NoAD`. This one type was the outlier, which
20
+ * is the shape the architectural standards call mock divergence: a hand-written mirror that renames a
21
+ * field and then certifies the rename.
4
22
  */
5
23
  export type SetFormat = {
6
24
  setTo?: number;
7
25
  tiebreakAt?: number;
26
+ /** No-advantage on the SET — with no tiebreak, the set ends at `setTo` without a two-game margin. */
27
+ NoAD?: boolean;
8
28
  tiebreakFormat?: {
9
29
  tiebreakTo?: number;
10
- noAd?: boolean;
30
+ NoAD?: boolean;
11
31
  };
12
32
  tiebreakSet?: {
13
33
  tiebreakTo?: number;
14
- noAd?: boolean;
34
+ NoAD?: boolean;
15
35
  };
16
36
  timed?: boolean;
17
37
  minutes?: number;
@@ -25,6 +45,7 @@ export type SetFormat = {
25
45
  export type MatchUpConfig = {
26
46
  bestOf: number;
27
47
  exactly?: number;
48
+ aggregate?: boolean;
28
49
  setFormat?: SetFormat;
29
50
  finalSetFormat?: SetFormat;
30
51
  };
@@ -37,24 +58,6 @@ export type SmartComplementResult = {
37
58
  shouldApply: boolean;
38
59
  reason?: string;
39
60
  };
40
- /**
41
- * Get the format for a specific set index
42
- * Uses finalSetFormat for deciding set if available
43
- */
44
- /**
45
- * The `MatchUpConfig` for a TODS matchUpFormat string.
46
- *
47
- * Hoisted because it takes TWO sources to assemble and `dynamicSetsApproach` already had its own copy
48
- * (`getMatchUpConfig`, line ~134): `bestOf` comes from `parseMatchUpFormat`, which resolves an
49
- * `exactly:N` format down to a set count, while `exactly`, `setFormat` and `finalSetFormat` come
50
- * straight off `matchUpFormatCode.parse`. A region building its own would be a second copy of a
51
- * derivation whose two halves must agree, which is the shape every other divergence in this module
52
- * family started as.
53
- *
54
- * Never parse a matchUpFormat with a regex — the factory owns that grammar, and an unparseable format
55
- * falls back to SET3 rather than throwing, because a dialog that will not open is worse than one that
56
- * opens on the wrong best-of.
57
- */
58
61
  export declare function matchUpConfigFor(matchUpFormat?: string): MatchUpConfig;
59
62
  export declare function getSetFormatForIndex(setIndex: number, config: MatchUpConfig): SetFormat | undefined;
60
63
  /**
@@ -95,12 +98,12 @@ export declare function getSetWinner(setIndex: number, scores: {
95
98
  /**
96
99
  * Determine if match is complete based on sets won
97
100
  */
98
- export declare function isMatchComplete(sets: SetScore[], bestOf: number, exactly?: number): boolean;
101
+ export declare function isMatchComplete(sets: SetScore[], config: MatchUpConfig): boolean;
99
102
  /**
100
103
  * Get the match winner based on sets won
101
104
  * Returns undefined if match is not complete
102
105
  */
103
- export declare function getMatchWinner(sets: SetScore[], bestOf: number, exactly?: number): 1 | 2 | undefined;
106
+ export declare function getMatchWinner(sets: SetScore[], config: MatchUpConfig): 1 | 2 | undefined;
104
107
  /**
105
108
  * Calculate the complement score for smart complement entry
106
109
  * Returns null if no predictable complement exists
@@ -0,0 +1,175 @@
1
+ import { hydrateScoreEntryState, ScoreEntryState, SideNumber } from './scoreEntryState';
2
+ import { MatchUpConfig } from './dynamicSetsLogic';
3
+ import { SetScore } from '../types';
4
+ /** The three ways of typing a score. The dialog's own union of the same three names is re-pointed here in S5. */
5
+ export type ScoreEntryApproach = 'dynamicSets' | 'freeScore' | 'dialPad';
6
+ /**
7
+ * One set as ENTERED, cell by cell. Any cell may be empty, which is how a half-entered set is kept.
8
+ *
9
+ * `side1` / `side2` are the set's principal score — games, or the POINTS of a tiebreak-only set, exactly
10
+ * as the regions' cells hold them. `tiebreak1` / `tiebreak2` are the points that decided an ordinary set
11
+ * that went to a tiebreak.
12
+ */
13
+ export type SetEntry = {
14
+ side1?: number;
15
+ side2?: number;
16
+ tiebreak1?: number;
17
+ tiebreak2?: number;
18
+ };
19
+ /** Which cell of which set an intent is aimed at. A tiebreak-only set has no `tiebreak` cells. */
20
+ export type CellRef = {
21
+ setIndex: number;
22
+ side: SideNumber;
23
+ kind: 'games' | 'tiebreak';
24
+ };
25
+ export type ScoreEntryModel = {
26
+ matchUpFormat: string;
27
+ approach: ScoreEntryApproach;
28
+ /** Exactly the set count the format plays, every entry possibly empty. */
29
+ sets: readonly SetEntry[];
30
+ /** The existing pure ending engine, folded in unchanged. */
31
+ ending: ScoreEntryState;
32
+ };
33
+ /** The ways an ending is chosen, one per transition in `scoreEntryState.ts`. */
34
+ export type EndingIntent = {
35
+ kind: 'side';
36
+ sideNumber: SideNumber;
37
+ status: string;
38
+ } | {
39
+ kind: 'match';
40
+ status: string;
41
+ } | {
42
+ kind: 'bothSidesOut';
43
+ } | {
44
+ kind: 'reasonCode';
45
+ code: string | undefined;
46
+ };
47
+ export type CreateScoreEntryModelParams = {
48
+ matchUpFormat?: string;
49
+ approach?: ScoreEntryApproach;
50
+ /** A saved score to open with, in factory shape. */
51
+ sets?: SetScore[];
52
+ /** A saved matchUp whose outcome hydrates the ending — see `hydrateScoreEntryState`. */
53
+ matchUp?: Parameters<typeof hydrateScoreEntryState>[0];
54
+ };
55
+ /** What a model with no format opens under. The same fallback `freeScoreRegion` and `matchUpConfigFor` use. */
56
+ export declare const DEFAULT_MATCH_UP_FORMAT = "SET3-S:6/TB7";
57
+ export declare function createScoreEntryModel(params?: CreateScoreEntryModelParams): ScoreEntryModel;
58
+ export type TypeDigitIntent = {
59
+ cell: CellRef;
60
+ digit: number;
61
+ /**
62
+ * Derive the OTHER side from what was typed, where the format makes that a single answer.
63
+ *
64
+ * For a games cell this is the smart complement — a `4` typed into an EMPTY cell writes `6` opposite,
65
+ * over whatever the other cell held: the inference is about the pair. Whether to offer it again for
66
+ * the same set is the caller's policy. For an ordinary tiebreak it derives the
67
+ * winner's points from the loser's, and fires only from the games LOSER's cell, because a winner's 7
68
+ * could have beaten anything. For a tiebreak-only set it derives the other side from the one typed —
69
+ * CA's rule for the Dial Pad, which always asks; Dynamic Sets asks while its Smart toggle is on.
70
+ */
71
+ complement?: boolean;
72
+ };
73
+ /**
74
+ * Append a digit to a cell, as a keystroke does.
75
+ *
76
+ * Refused — the same model comes back — when the digit could not become a legal score: past the
77
+ * ceiling for a games cell, or past two digits where no ceiling exists. Refusal is what lets a keypad
78
+ * move on to the next cell rather than the model guessing.
79
+ *
80
+ * The ceiling is `getMaxSetScore`'s, given the OPPONENT's games where they are in: 7 in a set to six,
81
+ * tightened to 6 for a side facing 3 — 7 is reachable only through a tiebreak at six-all — and left at
82
+ * 7 for a side facing 5, since 7-5 needs no tiebreak. That last answer is `#5043(factory)`, merged
83
+ * 2026-09-30; this module is written to the fixed factory, and a `7` typed against a `5` is pinned.
84
+ */
85
+ export declare function typeDigit(model: ScoreEntryModel, intent: TypeDigitIntent): ScoreEntryModel;
86
+ /**
87
+ * Write a whole value into a cell, as setting an input's value does. `undefined` empties it.
88
+ *
89
+ * Nothing is clamped or derived: the value recorded is the value given, and whether it is legal is
90
+ * `error`'s question. A keystroke goes through `typeDigit`, which is where the derivations live.
91
+ */
92
+ export declare function setCell(model: ScoreEntryModel, cell: CellRef, value: number | undefined): ScoreEntryModel;
93
+ export declare function clearCell(model: ScoreEntryModel, cell: CellRef): ScoreEntryModel;
94
+ /**
95
+ * Empty every set and keep the ending: what a score-clearing ending does to the cells.
96
+ *
97
+ * CA, 2026-09-30: *"when I enter set score(s) and then select (Walkover) the set score(s) should
98
+ * clear."* The card calls this when the chosen ending carries no score, having first read `scoreString`
99
+ * for the band. Final, like `[Clear]`: un-choosing the walkover does not bring the score back, because a
100
+ * remembered score that silently reappears on an approach switch was the worse bug.
101
+ */
102
+ export declare function clearScore(model: ScoreEntryModel): ScoreEntryModel;
103
+ /** Empty every set and un-choose the ending: the card's `[Clear]`. Format and approach stay. */
104
+ export declare function clearAll(model: ScoreEntryModel): ScoreEntryModel;
105
+ /**
106
+ * Choose (or un-choose) an ending, delegating to `scoreEntryState.ts` — every control there is a
107
+ * toggle, and that is preserved.
108
+ *
109
+ * The SETS are left exactly as they are, even for an ending that carries no score. The card today
110
+ * clears the region on a walkover and keeps the string aside for the band; here the selectors suppress
111
+ * the score while such an ending is chosen (`scoreString` answers nothing, `discardedScore` names what
112
+ * was typed) and it comes back if the ending is un-chosen. Keeping the keystrokes is the point of the
113
+ * model; whether the card should ALSO clear explicitly is S5's decision, and `clearAll` is there for it.
114
+ */
115
+ export declare function chooseEnding(model: ScoreEntryModel, intent: EndingIntent): ScoreEntryModel;
116
+ /**
117
+ * Move to another format, keeping what the new format has not invalidated.
118
+ *
119
+ * `retainScoreForFormat` decides, given both formats: a set whose position keeps the same rule survives
120
+ * finished or not, a set whose rule changed must stand as a complete legal set, and everything after the
121
+ * first casualty goes with it. It keeps a PREFIX, so the entries are cut to its length rather than
122
+ * rebuilt from its output — the operator's cells survive untouched. The array is then resized to the
123
+ * new format's set count.
124
+ */
125
+ export declare function changeFormat(model: ScoreEntryModel, matchUpFormat: string): ScoreEntryModel;
126
+ /** Change how the score is typed. A no-op on the score itself: the sets are the sets whichever region shows them. */
127
+ export declare function switchApproach(model: ScoreEntryModel, approach: ScoreEntryApproach): ScoreEntryModel;
128
+ /**
129
+ * The index of the set that decides the match, or `undefined` while nothing does.
130
+ *
131
+ * Asked of the factory prefix by prefix, so that a decision reached at set 2 is found even when a stale
132
+ * set 3 would make the whole array read as undecided — measured: `analyzeMatchUp` on `6-2 6-2 6-3` with
133
+ * three winning sides answers NO winner, because three is not `setsToWin`.
134
+ */
135
+ export declare function decidingIndex(sets: readonly SetEntry[], config: MatchUpConfig): number | undefined;
136
+ /** The sets with BOTH principal cells in, as the factory reads them, positioned by `setNumber`. */
137
+ export declare function enteredFactorySets(sets: readonly SetEntry[], config: MatchUpConfig): SetScore[];
138
+ /**
139
+ * One entry in the factory's shape.
140
+ *
141
+ * `buildSetScore` derives the winning side and the tiebreak pair from the loser's points, which is the
142
+ * contract this module keeps: the lower of two tiebreak cells is the loser's. A cell that is empty stays
143
+ * empty in the output — it is not read as 0, which is what let a half-entered set claim `6-0`.
144
+ *
145
+ * A tiebreak-only set is built here rather than by `buildSetScore`. When this was written that function's
146
+ * winner rule was a hand-rolled "both sides above zero and unequal" (a `3-1` won, a `10-0` not); it asks
147
+ * the factory now too, and this path simply never left. The set's winner is `analyzeSet`'s, given the
148
+ * points AS points — `getSetWinner` would hand them over as games, which a tiebreak-only format has no
149
+ * rule for.
150
+ */
151
+ export declare function toFactorySet(entry: SetEntry, index: number, config: MatchUpConfig): SetScore;
152
+ /**
153
+ * Replace every set from a factory-shaped array: the Free Score boundary, where TEXT becomes sets.
154
+ *
155
+ * Free Score keeps the text as the field's own value while it is being typed — a half-typed `6-4 re` is
156
+ * nothing the model can represent — and commits here at the points where the text parses to sets. The
157
+ * same seeding and the same invariants as opening a saved score: a set past the decider is trimmed.
158
+ * The same model comes back when nothing changed, so a keystroke inside a word is not a transition.
159
+ */
160
+ export declare function replaceSets(model: ScoreEntryModel, sets: SetScore[]): ScoreEntryModel;
161
+ export declare function readCell(entry: SetEntry, cell: CellRef): number | undefined;
162
+ /** The side that lost the set on games, or `undefined` while the games are level or not both in. */
163
+ export declare function gamesLoser(entry: SetEntry): SideNumber | undefined;
164
+ /** Whether both principal cells are in — the regions' `bothEntered`. */
165
+ export declare function isEntered(entry: SetEntry): boolean;
166
+ /** Whether the set holds anything at all, in any cell. */
167
+ export declare function isEmptyEntry(entry: SetEntry): boolean;
168
+ /**
169
+ * How many sets the format can hold: every set of an `exactly` format, otherwise the best-of. An
170
+ * aggregate format with a final tiebreak holds one more, its sudden-death decider, which is set N + 1
171
+ * and never one of the N (factory #5166; CA, 2026-10-04). It opens only on a level total.
172
+ */
173
+ export declare function setCountOf(config: MatchUpConfig): number;
174
+ /** The set number of an aggregate format's final-tiebreak decider, or `undefined` when it has none. */
175
+ export declare function deciderSetNumberOf(config: MatchUpConfig): number | undefined;
@@ -0,0 +1,88 @@
1
+ import { ScoreEntryResolution, SideNumber } from './scoreEntryState';
2
+ import { ScoreEntryModel } from './scoreEntryModel';
3
+ import { SetScore } from '../types';
4
+ /** One visible column: a set's principal cells, or the tiebreak cells beside a set that went to one. */
5
+ export type ScoreSlot = {
6
+ kind: 'games' | 'tiebreak';
7
+ setIndex: number;
8
+ };
9
+ export type ColumnOptions = {
10
+ /**
11
+ * A set the operator has clicked back into, whose tiebreak column stays open though its points are
12
+ * known. Presentation, held by the region — passed in rather than kept in the model.
13
+ */
14
+ editingSet?: number;
15
+ };
16
+ /** The ending, resolved. The one place `resolveScoreEntry` is asked about a model. */
17
+ export declare function resolveEnding(model: ScoreEntryModel): ScoreEntryResolution;
18
+ /** Sets with BOTH principal cells in, in factory shape, positioned by `setNumber`. A partial set is not here. */
19
+ export declare function enteredSets(model: ScoreEntryModel): SetScore[];
20
+ /** The entered sets that are SETTLED — see `isSettled`. A `4-2` is entered; it is not completed. */
21
+ export declare function completedSets(model: ScoreEntryModel): SetScore[];
22
+ /** Whether ANYTHING has been typed, complete or not — a single digit counts. */
23
+ export declare function hasEntry(model: ScoreEntryModel): boolean;
24
+ /** The score as it stands, in the factory's own line — or nothing while an ending has set it aside. */
25
+ export declare function scoreString(model: ScoreEntryModel): string | undefined;
26
+ /** What a score-clearing ending set aside, so the band can name it. Nothing unless one is chosen. */
27
+ export declare function discardedScore(model: ScoreEntryModel): string | undefined;
28
+ /**
29
+ * Whether the score is a FINISHED result: the factory names a winner, the last set is settled, and
30
+ * nothing is wrong with what came before it. An ending that discards the score makes this false,
31
+ * because there is no result to be finished.
32
+ */
33
+ export declare function isComplete(model: ScoreEntryModel): boolean;
34
+ /** The winner: the ending's when it names one, otherwise the finished score's. */
35
+ export declare function winningSide(model: ScoreEntryModel): SideNumber | undefined;
36
+ /** The ending's status when one is chosen; `COMPLETED` for a finished score; nothing otherwise. */
37
+ export declare function matchUpStatus(model: ScoreEntryModel): string | undefined;
38
+ /**
39
+ * The first thing wrong with the score, in words, or `undefined`.
40
+ *
41
+ * Three checks, in order:
42
+ *
43
+ * 1. a tiebreak that contradicts who won the set, with the participant NAMED — the factory refuses it
44
+ * too, but "side 1" is not a message an operator can act on;
45
+ * 2. the FACTORY's judgement of each entered set (`validateSetScore` with `allowIncomplete`), which
46
+ * forgives unfinished games but knows `3-7` cannot be. A set whose tiebreak points are still owed
47
+ * is skipped, because the card is asking for them and an error at that moment would answer its
48
+ * own question;
49
+ * 3. invariant 10A: a set before the last entered one that is empty or unfinished.
50
+ *
51
+ * Silent while a score-clearing ending is chosen: nothing typed will be submitted.
52
+ */
53
+ export type ErrorOptions = {
54
+ /** Names for the two sides, so a message can say who rather than "side 1". */
55
+ sideNames?: [string, string];
56
+ };
57
+ export declare function error(model: ScoreEntryModel, options?: ErrorOptions): string | undefined;
58
+ /**
59
+ * The visible columns, left to right.
60
+ *
61
+ * Every set that holds anything is shown, whatever came before it — this is the rule that ends the
62
+ * ghost set: a cleared set 1 beside a typed set 2 shows BOTH, one empty and one full, so the operator
63
+ * sees the score the model has. Then one empty set opens after the last holding one, and only when
64
+ * everything before it is settled and the match is not decided; a timed format plays every bolt, so
65
+ * for it the only question is whether one is left. A tiebreak column sits beside a set whose games call
66
+ * for points it does not yet have, or that the operator has reopened.
67
+ */
68
+ export declare function columns(model: ScoreEntryModel, options?: ColumnOptions): ScoreSlot[];
69
+ /**
70
+ * Whether a set is finished: both principal cells in, no tiebreak points owed, and the factory accepts
71
+ * it as a COMPLETE legal set. A `7-6` whose points are unknown is not settled, so the next set does not
72
+ * open under it.
73
+ *
74
+ * ── Strict `validateSetScore`, and not `analyzeSet`'s winning side ──
75
+ *
76
+ * Measured 2026-09-30, before `#5049(factory)`: `analyzeSet` named a winner for a `3-1` in a match
77
+ * tiebreak to ten, and for a `10-9` — any lead read as a win. Both answers are fixed in 7.4.0, and
78
+ * `dynamicSetsLogic` now asks the factory too. Strict validation stays the rule here all the same: it is
79
+ * the same answer `error` reports, so "settled" and "no error" cannot come apart.
80
+ *
81
+ * A timed set is the exception: any pair of scores ends it, a tie included, so both cells in is the
82
+ * whole test — strict validation refuses a `0-0` bolt, which is unusual but not impossible.
83
+ */
84
+ export declare function isSettled(model: ScoreEntryModel, index: number): boolean;
85
+ /** Whether the set's games call for a tiebreak whose points the operator has not supplied yet. */
86
+ export declare function tiebreakOutstanding(model: ScoreEntryModel, index: number): boolean;
87
+ /** The value in one cell. Exported for regions, which render cells from the model and never read them back. */
88
+ export declare function cellValue(model: ScoreEntryModel, setIndex: number, side: SideNumber, kind: ScoreSlot['kind']): number | undefined;
@@ -81,6 +81,39 @@ export declare function chooseReasonCode(state: ScoreEntryState, code: string |
81
81
  *
82
82
  * `undefined` when no ending is selected, because there is nothing to give a reason for.
83
83
  */
84
+ /**
85
+ * The card's state for a matchUp that ALREADY has an outcome.
86
+ *
87
+ * Without this the dialog opened blank on a scored matchUp: no recorded ending, no reason code. The
88
+ * shipping modal hydrates, so swapping the card in without this would lose the display of every outcome
89
+ * already entered — CA, 2026-09-28: *"we need to be able to open existing outcomes!"*
90
+ *
91
+ * ── The one inversion, again ──
92
+ *
93
+ * A stored matchUp names the WINNER. This card records the side the ending HAPPENED TO, and derives the
94
+ * winner from it. So hydration runs that mapping backwards: the exiting side is the one that did not
95
+ * win. `hydrateIrregularEnding` is reused for the status itself rather than re-deriving it, so the
96
+ * reverse lookup from `DOUBLE_WALKOVER` to the `WALKOVER` an operator actually clicks lives in exactly
97
+ * one place.
98
+ *
99
+ * Three cases and a refusal:
100
+ *
101
+ * - a DOUBLE exit — both sides out, so no side is distinguished. `sideNumber: 1` is arbitrary and only
102
+ * decides nothing: `bothSidesOut` makes the resolution `NEITHER_SIDE`, and the card draws the pill on
103
+ * both rows regardless.
104
+ * - an ending that resolves NOBODY — recorded at match level, because there is no side to attach it to.
105
+ * - a single exit — recorded against `3 - winningSide`.
106
+ * - a winner-requiring status with NO winning side is a corrupt record, and is refused: it returns the
107
+ * empty state rather than inventing a placement. `hydrateIrregularEnding` makes the same choice for
108
+ * an unmapped double exit, and for the same reason — a guess here silently advances a participant.
109
+ */
110
+ export declare function hydrateScoreEntryState(matchUp?: {
111
+ matchUpStatus?: string;
112
+ winningSide?: number;
113
+ sideStatusCodes?: Record<number, string>;
114
+ matchUpStatusCode?: string;
115
+ matchUpStatusCodes?: unknown[];
116
+ }): ScoreEntryState;
84
117
  export declare function reasonCodeStatus(state: ScoreEntryState): string | undefined;
85
118
  /**
86
119
  * Resolve the card into a submittable outcome.
@@ -0,0 +1,8 @@
1
+ import { CreateScoreEntryModelParams, ScoreEntryModel } from './scoreEntryModel';
2
+ export type ScoreEntryStore = {
3
+ /** The model as it stands. Never mutated; read it again after any transition. */
4
+ get: () => ScoreEntryModel;
5
+ /** Replace the model. The caller says what must be redrawn. */
6
+ set: (next: ScoreEntryModel) => void;
7
+ };
8
+ export declare function createScoreEntryStore(params?: CreateScoreEntryModelParams): ScoreEntryStore;
@@ -42,6 +42,40 @@ export declare function groupKeyForStatus(matchUpStatus: string | undefined): st
42
42
  *
43
43
  * Returns `undefined` for a provenance element, which carries no code at all.
44
44
  */
45
+ /**
46
+ * The reason code a matchUp actually recorded, for the control that has to re-display it.
47
+ *
48
+ * ── Why not `matchUpStatusCodes[0]` ──
49
+ *
50
+ * `matchUpStatusCodes` is POSITIONAL BY SIDE, so a reason belonging to side 2 sits at index 1 and
51
+ * index 0 is an empty string that `normalizeStatusCode` turns into `undefined`. The picker then opens
52
+ * empty, the operator's reason is missing from the control meant to show it, and re-saving loses it.
53
+ *
54
+ * Measured against published **7.1.0**, because the exposure is narrower than it first looks: a code
55
+ * submitted by this dialog is stored EXACTLY as submitted, so `['W1']` round-trips through index 0 and
56
+ * reads correctly today. The index-1 case arrives by PROPAGATION — a reason carried into a connected
57
+ * structure is placed at the arriving side's index — and factory #5016 both fixes that carry and gives
58
+ * the fact a home that states what it attributes to.
59
+ *
60
+ * ── The order, and why each step is needed ──
61
+ *
62
+ * 1. `sideStatusCodes[sideNumber]` — the side's own, once the factory writes it.
63
+ * 2. `matchUpStatusCode` — the MATCH's, for an ending that resolves nobody (ABANDONED, CANCELLED,
64
+ * INCOMPLETE). There is no side to ask.
65
+ * 3. `matchUpStatusCodes[0]` — records written before either field existed. Not optional: the peer
66
+ * range still admits a factory major that has neither, so dropping it would read nothing at all
67
+ * from every record stored to date.
68
+ *
69
+ * The fields are mirrored onto this package's own `MatchUp` rather than imported from the factory, for
70
+ * the reason `types.ts` already records: they do not exist in the older major the peer range admits.
71
+ */
72
+ export declare function recordedStatusCode(matchUp?: {
73
+ matchUpStatus?: string;
74
+ winningSide?: number;
75
+ sideStatusCodes?: Record<number, string>;
76
+ matchUpStatusCode?: string;
77
+ matchUpStatusCodes?: unknown[];
78
+ }): string | undefined;
45
79
  export declare function normalizeStatusCode(entry: unknown): string | undefined;
46
80
  /**
47
81
  * The codes a policy offers for this status, or an empty list when it offers none.
@@ -0,0 +1,14 @@
1
+ import { SetFormat } from './dynamicSetsLogic';
2
+ import { SideNumber } from './scoreEntryState';
3
+ /** The points a tiebreak-only set is played to, or `undefined` if this set is not one. */
4
+ export declare function tiebreakOnlyTarget(setFormat?: SetFormat): number | undefined;
5
+ /**
6
+ * Both sides' points, from the LOWER score and the side it was entered on.
7
+ *
8
+ * `undefined` when the set carries no target to complete against, or when the factory declines — never
9
+ * a guess. The caller leaves what was typed alone in that case rather than inventing the other half.
10
+ */
11
+ export declare function completeTiebreakOnly(lowValue: number, lowSide: SideNumber, setFormat?: SetFormat): {
12
+ side1: number;
13
+ side2: number;
14
+ } | undefined;
@@ -1,12 +1,17 @@
1
+ import { ScoreEntryStore } from '../logic/scoreEntryStore';
1
2
  import { ScoreRegion } from '../scoreEntryCard';
2
3
  import { SetScore } from '../types';
3
4
  export type DialPadRegionParams = {
5
+ /** The model the card holds. Omit it and the region makes its own from the params below. */
6
+ store?: ScoreEntryStore;
4
7
  matchUpFormat?: string;
5
- /** Sets already recorded, e.g. from a saved matchUp. */
8
+ /** Sets already recorded, e.g. from a saved matchUp. Read only when no `store` is given. */
6
9
  sets?: SetScore[];
7
10
  onChange?: () => void;
8
11
  };
9
12
  export type DialPadRegion = ScoreRegion & {
13
+ store: ScoreEntryStore;
14
+ /** The entered sets, for a host or a test reading the region directly. The card reads the model. */
10
15
  getSets: () => SetScore[];
11
16
  };
12
17
  export declare function createDialPadRegion(params: DialPadRegionParams): DialPadRegion;
@@ -1,7 +1,12 @@
1
1
  import { ScoreRegion } from '../scoreEntryCard';
2
+ import { ScoreEntryStore } from '../logic/scoreEntryStore';
2
3
  import { SetScore } from '../types';
3
4
  export type DynamicSetsRegionParams = {
5
+ /** The model the card holds. Omit it and the region makes its own from the params below. */
6
+ store?: ScoreEntryStore;
4
7
  matchUpFormat?: string;
8
+ /** Participant names, so an integrity message can say who rather than "side 1". */
9
+ sideNames?: [string, string];
5
10
  /** Sets already recorded, e.g. from a saved matchUp. */
6
11
  sets?: SetScore[];
7
12
  /** Whether smart complements starts enabled. Defaults to on. */
@@ -13,7 +18,11 @@ export type DynamicSetsRegionParams = {
13
18
  onStructureChange?: () => void;
14
19
  };
15
20
  export type DynamicSetsRegion = ScoreRegion & {
21
+ store: ScoreEntryStore;
22
+ /** The entered sets, for a host or a test reading the region directly. The card reads the model. */
16
23
  getSets: () => SetScore[];
24
+ /** The model's integrity message, named with this region's `sideNames`. The card computes its own. */
25
+ error: () => string | undefined;
17
26
  smartComplementsEnabled: () => boolean;
18
27
  };
19
28
  export declare function createDynamicSetsRegion(params: DynamicSetsRegionParams): DynamicSetsRegion;
@@ -1,8 +1,11 @@
1
+ import { ScoreEntryStore } from '../logic/scoreEntryStore';
1
2
  import { ScoreRegion } from '../scoreEntryCard';
2
3
  import { SetScore } from '../types';
3
4
  export type FreeScoreRegionParams = {
5
+ /** The model the card holds. Omit it and the region makes its own from the params below. */
6
+ store?: ScoreEntryStore;
4
7
  matchUpFormat?: string;
5
- /** The score to open with, already formatted (e.g. `'6-4 6-3'`). */
8
+ /** The score to open with, already formatted (e.g. `'6-4 6-3'`). With a `store`, the model's own line is used. */
6
9
  initialText?: string;
7
10
  /** Label above the field. */
8
11
  label?: string;
@@ -10,9 +13,12 @@ export type FreeScoreRegionParams = {
10
13
  onChange?: () => void;
11
14
  };
12
15
  export type FreeScoreRegion = ScoreRegion & {
16
+ store: ScoreEntryStore;
13
17
  /** What is currently in the field. */
14
18
  getText: () => string;
15
- /** The sets the parse produced, for submission. */
19
+ /** The entered sets, for a host or a test reading the region directly. The card reads the model. */
16
20
  getSets: () => SetScore[];
21
+ /** Text in the field counts as entry the model cannot see; required here. */
22
+ hasEntry: () => boolean;
17
23
  };
18
24
  export declare function createFreeScoreRegion(params: FreeScoreRegionParams): FreeScoreRegion;
@@ -0,0 +1,2 @@
1
+ import { SetScore } from '../types';
2
+ export declare function scoreLine(sets: SetScore[], matchUpFormat?: string): string | undefined;