@claudinho/core 0.10.1 → 0.11.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.
- package/README.md +15 -12
- package/dist/index.d.ts +2604 -389
- package/dist/index.js +3408 -773
- package/package.json +23 -9
package/dist/index.d.ts
CHANGED
|
@@ -1,22 +1,154 @@
|
|
|
1
|
+
/** One row of a group table. */
|
|
2
|
+
interface StandingRow {
|
|
3
|
+
team: Team;
|
|
4
|
+
played: number;
|
|
5
|
+
won: number;
|
|
6
|
+
drawn: number;
|
|
7
|
+
lost: number;
|
|
8
|
+
goalsFor: number;
|
|
9
|
+
goalsAgainst: number;
|
|
10
|
+
goalDiff: number;
|
|
11
|
+
points: number;
|
|
12
|
+
/**
|
|
13
|
+
* The provider's own rank for this row. Present on every live row (the
|
|
14
|
+
* standings feed states it); absent on a computed or roster-at-zero table,
|
|
15
|
+
* which has no authority to rank anyone. Renderers print THIS, never the
|
|
16
|
+
* array position — on a partial table the two differ (audit A01).
|
|
17
|
+
*/
|
|
18
|
+
rank?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A table: its KEY and its rows in standings order.
|
|
22
|
+
*
|
|
23
|
+
* `group` is the key a surface selects the table by: a group letter (`A`), a
|
|
24
|
+
* numbered group (`A1`), a group under a league (`A-B`), or `LEAGUE` for a
|
|
25
|
+
* league's one table. `label` is the provider's name for the table, sanitized,
|
|
26
|
+
* present for every table EXCEPT a lettered group (which keeps its localized
|
|
27
|
+
* title, "Group A" / "Grupo A").
|
|
28
|
+
*/
|
|
29
|
+
interface GroupStandings {
|
|
30
|
+
group: string;
|
|
31
|
+
label?: string;
|
|
32
|
+
rows: StandingRow[];
|
|
33
|
+
/**
|
|
34
|
+
* Present when the provider's table could not be read in full: `omitted` rows
|
|
35
|
+
* were refused (malformed, duplicate, or beyond the row cap). The readable rows
|
|
36
|
+
* stay usable, but a partial table carries no authority to confirm or project
|
|
37
|
+
* qualification, and every surface says it is partial (audit A01).
|
|
38
|
+
*/
|
|
39
|
+
partial?: {
|
|
40
|
+
omitted: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** A table as every surface's structured output states it. */
|
|
44
|
+
interface TableData {
|
|
45
|
+
group: string;
|
|
46
|
+
/** The provider's name for the table; absent for a lettered group. */
|
|
47
|
+
label?: string;
|
|
48
|
+
standings: StandingRow[];
|
|
49
|
+
partial?: {
|
|
50
|
+
omitted: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A table key as a surface accepts it as an ARGUMENT (`table`, `get_standings`,
|
|
55
|
+
* `standings://`, `share table`): 1 to 12 letters, digits or `-`, in any case.
|
|
56
|
+
* It says what may be ASKED FOR, and is wider than what a provider's table can
|
|
57
|
+
* be keyed as: an unknown key is answered "no such group", a string that is
|
|
58
|
+
* not a key at all is refused before anything is fetched.
|
|
59
|
+
*/
|
|
60
|
+
declare const TABLE_KEY_ARG: RegExp;
|
|
61
|
+
/** The key an argument asks for, upper-cased; undefined when it is not a key. */
|
|
62
|
+
declare function tableKeyArg(raw: unknown): string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* A table's title on a surface that does not localize it (MCP text, the
|
|
65
|
+
* resource, a share card): `Group A` for a lettered group, and for any other
|
|
66
|
+
* table the provider's label followed by the key that selects it,
|
|
67
|
+
* `League A, Group B (A-B)`.
|
|
68
|
+
*/
|
|
69
|
+
declare function tableTitle(table: {
|
|
70
|
+
group: string;
|
|
71
|
+
label?: string;
|
|
72
|
+
}): string;
|
|
73
|
+
/**
|
|
74
|
+
* A table's structured form: `--json`, MCP `data` and both share cards. ONE
|
|
75
|
+
* mapping, because it carries a verdict (`partial`: rows were left out, so the
|
|
76
|
+
* table confirms nothing). It was written out at four emit sites, and the two
|
|
77
|
+
* share ones had already dropped `partial` once (review of #126).
|
|
78
|
+
*/
|
|
79
|
+
declare function tableData(table: GroupStandings): TableData;
|
|
80
|
+
/**
|
|
81
|
+
* Compute a group table from a set of matches. Teams are seeded from every
|
|
82
|
+
* match (so a pre-tournament table still lists all four teams at 0), and only
|
|
83
|
+
* finished matches with a score contribute to the tally.
|
|
84
|
+
*
|
|
85
|
+
* Sort: points, then goal difference, then goals for, then name. (Real FIFA
|
|
86
|
+
* tiebreakers add head-to-head and fair-play; this is the standard simplified
|
|
87
|
+
* ordering used for display.)
|
|
88
|
+
*/
|
|
89
|
+
declare function computeStandings(matches: Match[]): StandingRow[];
|
|
90
|
+
|
|
1
91
|
/**
|
|
2
92
|
* Claudinho domain model — the canonical, provider-agnostic shapes.
|
|
3
93
|
* Every data vendor maps INTO these types via a ProviderAdapter.
|
|
4
94
|
*/
|
|
5
|
-
/** A
|
|
95
|
+
/** A team. `flag` is an emoji (no image assets, no copyright), and a nation's only. */
|
|
6
96
|
interface Team {
|
|
7
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* Short code as the provider abbreviates it (e.g. "MEX", "ARS", "O&M"). A
|
|
99
|
+
* DISPLAY LABEL, not an identity: two clubs in one competition can share one
|
|
100
|
+
* (`CAR` is both Carabobo and Always Ready in the Libertadores).
|
|
101
|
+
*/
|
|
8
102
|
code: string;
|
|
9
103
|
/** Human-readable name (e.g. "Mexico"). */
|
|
10
104
|
name: string;
|
|
11
|
-
/**
|
|
12
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Emoji flag (e.g. "🇲🇽"), GENERATED from a NATION's name (`sealTeam`); the
|
|
107
|
+
* neutral 🏳️ for a nation competition's side that is no nation yet (a
|
|
108
|
+
* bundle placeholder). A club has none: the key is ABSENT, never `''` and
|
|
109
|
+
* never 🏳️, and nothing is printed in its place. Which teams are nations is
|
|
110
|
+
* a written fact of the competition (`TEAM_KIND`).
|
|
111
|
+
*/
|
|
112
|
+
flag?: string;
|
|
113
|
+
/**
|
|
114
|
+
* The provider's stable id for this team, namespaced by provider
|
|
115
|
+
* (`espn:359`). The same for a club in every competition it plays. Present
|
|
116
|
+
* on a team read from a live feed (and on that team read back from the
|
|
117
|
+
* cache); ABSENT on the bundled schedule's teams and on a feed record that
|
|
118
|
+
* carried none. Two teams with the same id are one team, whatever they are
|
|
119
|
+
* called; the id never makes two teams of one code AND name (see `sameTeam`).
|
|
120
|
+
*/
|
|
121
|
+
id?: string;
|
|
13
122
|
}
|
|
14
123
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
124
|
+
* What a provider reported about the season ONE response belongs to. Metadata
|
|
125
|
+
* of that response, never an input: a request selects a competition, and the
|
|
126
|
+
* provider says which season it answered for.
|
|
18
127
|
*/
|
|
19
|
-
|
|
128
|
+
interface SeasonInfo {
|
|
129
|
+
/** The season's year as the provider keys it (2026 for "2026-27"). */
|
|
130
|
+
year: number;
|
|
131
|
+
/** Human label (e.g. "2026-27 English Premier League"); may be empty. */
|
|
132
|
+
label: string;
|
|
133
|
+
/** ISO 8601 UTC, when the provider states it. */
|
|
134
|
+
startDate?: string;
|
|
135
|
+
/** ISO 8601 UTC, when the provider states it. */
|
|
136
|
+
endDate?: string;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The stage of a fixture, from a written grammar over the provider's season
|
|
140
|
+
* slug and the competition's kind (`stageFromSlug` in the trust layer).
|
|
141
|
+
* - `GROUP`, `R32`, `R16`, `QF`, `SF`, `3P` (the third-place play-off), `F`:
|
|
142
|
+
* a tournament's group stage and knockout rounds;
|
|
143
|
+
* - `REGULAR`: a league's regular season ("League");
|
|
144
|
+
* - `LEAGUE`: a cup's league phase ("League phase");
|
|
145
|
+
* - `PO`: play-offs ("Play-offs"), a knockout stage;
|
|
146
|
+
* - `FRIENDLY`: a friendly, under the friendly competition only;
|
|
147
|
+
* - `OTHER`: a phase the grammar does not know. It carries the provider's own
|
|
148
|
+
* words in `Match.stageLabel` when there are any, and prints nothing when
|
|
149
|
+
* there are none (an absent or unreadable slug asserts no phase).
|
|
150
|
+
*/
|
|
151
|
+
type Stage = 'GROUP' | 'R32' | 'R16' | 'QF' | 'SF' | '3P' | 'F' | 'FRIENDLY' | 'REGULAR' | 'LEAGUE' | 'PO' | 'OTHER';
|
|
20
152
|
/** Normalized match status across all providers. */
|
|
21
153
|
type Status = 'SCHEDULED' | 'LIVE' | 'HT' | 'FT' | 'POSTPONED' | 'CANCELLED';
|
|
22
154
|
/** Match outcome from the home team's perspective. */
|
|
@@ -33,6 +165,13 @@ interface Match {
|
|
|
33
165
|
/** Stable id (provider id for now; cross-provider keying comes later). */
|
|
34
166
|
id: string;
|
|
35
167
|
stage: Stage;
|
|
168
|
+
/**
|
|
169
|
+
* The provider's own words for a phase the grammar does not know (the slug
|
|
170
|
+
* humanized: `qualifying-final` → "Qualifying final"), a human label of at
|
|
171
|
+
* most 40 columns. Present only on an `OTHER` stage, and only when there are
|
|
172
|
+
* words; printed untranslated.
|
|
173
|
+
*/
|
|
174
|
+
stageLabel?: string;
|
|
36
175
|
/** Group letter "A".."L" for the group stage; undefined for knockouts. */
|
|
37
176
|
group?: string;
|
|
38
177
|
/** Kickoff time, ISO 8601 in UTC (always ends in "Z"). */
|
|
@@ -140,8 +279,15 @@ type Lang = 'en' | 'es' | 'pt' | 'fr';
|
|
|
140
279
|
declare function normalizeLang(lang?: string): Lang;
|
|
141
280
|
/** Translate a message key with optional `{placeholder}` interpolation. */
|
|
142
281
|
declare function t(lang: string | undefined, key: string, vars?: Record<string, string>): string;
|
|
143
|
-
/**
|
|
144
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Localized stage label, from the match (its stage, its group letter under the
|
|
284
|
+
* group stage, and the words an OTHER stage carries). An `OTHER` stage prints
|
|
285
|
+
* the provider's own words untranslated, or NOTHING when it carries none: a
|
|
286
|
+
* caller that joins a stage into a line drops an empty one with its separator.
|
|
287
|
+
* A bare stage (a bracket round, a `bracket <stage>` filter) is passed as
|
|
288
|
+
* `{ stage }`.
|
|
289
|
+
*/
|
|
290
|
+
declare function stageLabelI18n(lang: string | undefined, m: Pick<Match, 'stage' | 'group' | 'stageLabel'>): string;
|
|
145
291
|
|
|
146
292
|
/**
|
|
147
293
|
* Resolve the effective timezone: explicit arg > CLAUDINHO_TZ env > system.
|
|
@@ -165,8 +311,18 @@ declare function formatKickoff(iso: string, opts?: FormatOpts): string;
|
|
|
165
311
|
declare function formatDate(iso: string, opts?: FormatOpts): string;
|
|
166
312
|
/** Time of day like "19:00" (24-hour) in the target timezone/locale. */
|
|
167
313
|
declare function formatTime(iso: string, opts?: FormatOpts): string;
|
|
168
|
-
/**
|
|
314
|
+
/** What {@link countdown} says of a kickoff that has passed (the statusline prints it as it is: English, by its rule). */
|
|
315
|
+
declare const COUNTDOWN_NOW = "now";
|
|
316
|
+
/** Compact human countdown until kickoff: "3d4h", "2h10m", "45m", or {@link COUNTDOWN_NOW}. */
|
|
169
317
|
declare function countdown(iso: string, from?: Date): string;
|
|
318
|
+
/**
|
|
319
|
+
* The countdown as the reader's phrase, ONE rule for every localized line that
|
|
320
|
+
* says it (CLI `next`, the MCP match line): "in 3d4h" (`next.in`), or, for a
|
|
321
|
+
* kickoff that has passed (a stale record, the bundle's skeleton during an
|
|
322
|
+
* outage), the language's own word for now (`countdown.now`), never the
|
|
323
|
+
* English word inside a translated "in".
|
|
324
|
+
*/
|
|
325
|
+
declare function countdownPhrase(lang: string | undefined, iso: string, from?: Date): string;
|
|
170
326
|
/**
|
|
171
327
|
* The calendar date (YYYY-MM-DD) of a kickoff in the target timezone, or ''
|
|
172
328
|
* when the input can't be parsed — an unfileable fixture matches no date rather
|
|
@@ -213,6 +369,15 @@ declare function truncateVisible(s: string, maxColumns: number, marker?: string)
|
|
|
213
369
|
* too-long value overflows its column rather than being cut mid-name).
|
|
214
370
|
*/
|
|
215
371
|
declare function padVisible(s: string, width: number): string;
|
|
372
|
+
/**
|
|
373
|
+
* A team's text with its flag beside it, or the text alone: the one rule every
|
|
374
|
+
* renderer applies where a flag goes. The flag prints only when it is a
|
|
375
|
+
* non-empty string, with its one adjoining space (`🇲🇽 Mexico` on the home side,
|
|
376
|
+
* `Mexico 🇲🇽` on the away side); a side with no flag (a club, by the
|
|
377
|
+
* competition's written kind) prints its text alone. Nothing is put in the
|
|
378
|
+
* flag's place: no placeholder, no double space, no code.
|
|
379
|
+
*/
|
|
380
|
+
declare function withFlag(text: string, flag: string | undefined, side: 'home' | 'away'): string;
|
|
216
381
|
|
|
217
382
|
/** Outcome (home perspective) from a final/looking scoreline. */
|
|
218
383
|
declare function outcomeFromScore(home: number, away: number): Outcome;
|
|
@@ -236,17 +401,258 @@ declare function scoreline(match: Match): string;
|
|
|
236
401
|
declare function matchLocation(match: Match): string;
|
|
237
402
|
/** Sort comparator by kickoff time, ascending. */
|
|
238
403
|
declare function byKickoff(a: Match, b: Match): number;
|
|
239
|
-
/**
|
|
240
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Human-readable stage for display, in English (the group letter under the
|
|
406
|
+
* group stage). An `OTHER` stage prints the provider's own words it carries,
|
|
407
|
+
* untranslated, or NOTHING when it carries none: a caller that joins a stage
|
|
408
|
+
* into a line drops an empty one with its separator.
|
|
409
|
+
*/
|
|
410
|
+
declare function stageLabel(m: Pick<Match, 'stage' | 'group' | 'stageLabel'>): string;
|
|
411
|
+
/**
|
|
412
|
+
* The segments of a line that are joined with ` · ` (a stage, a location, a
|
|
413
|
+
* time), with the EMPTY ones dropped: an OTHER stage with no words, or a
|
|
414
|
+
* record with no venue, prints no segment and no separator, never a dangling
|
|
415
|
+
* ` ·` or a `· ·`.
|
|
416
|
+
*/
|
|
417
|
+
declare function joinSegments(segments: readonly (string | undefined)[]): string;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The supported set, as ONE table: the sixteen competitions the product
|
|
421
|
+
* answers for, one row each, and every written fact about a competition that
|
|
422
|
+
* the rest of the code reads (its teams' kind, its own kind, a league's season
|
|
423
|
+
* name, its standings shape, what `bracket` and the market sidecar offer, how
|
|
424
|
+
* often it has an edition). Every other table derives from this one
|
|
425
|
+
* ({@link deriveTables}): `TEAM_KIND`, `COMPETITION_KIND`, `SEASON_SLUG`,
|
|
426
|
+
* `STANDINGS_SHAPE` (`kinds.ts`), `NO_BRACKET` (`competition.ts`),
|
|
427
|
+
* `MARKET_COMPETITIONS` (`markets/provider.ts`); `bracketCapability`, the
|
|
428
|
+
* canary's competitions and cadences and the README matrix read the rows.
|
|
429
|
+
* Adding a competition is one row here plus its fixtures; nothing else in the
|
|
430
|
+
* source changes.
|
|
431
|
+
*
|
|
432
|
+
* A LEAF module: it imports nothing. The trust layer reads the derived kinds
|
|
433
|
+
* at parse time (through `kinds.ts`), and `competition.ts` imports the adapter,
|
|
434
|
+
* which imports the trust layer; a table with imports of its own could be read
|
|
435
|
+
* uninitialized on one of those paths.
|
|
436
|
+
*
|
|
437
|
+
* Every lookup is by own property, or by equality on a row's slug: the
|
|
438
|
+
* competition reaches the code from a flag, an environment variable or a tool
|
|
439
|
+
* argument, and `constructor` is not one.
|
|
440
|
+
*/
|
|
441
|
+
/** Whether a competition fields nations (a flag is generated from the name) or clubs (no flag). */
|
|
442
|
+
type TeamKind = 'nation' | 'club';
|
|
443
|
+
/**
|
|
444
|
+
* What a surface does for a competition: `offered`; `not-offered-yet` (the
|
|
445
|
+
* competition may have it, the product does not read it yet); `not-applicable`
|
|
446
|
+
* (the competition has no such thing: a league season with no knockout tie
|
|
447
|
+
* has no bracket, a knockout-only cup has no table).
|
|
448
|
+
*/
|
|
449
|
+
type Capability = 'offered' | 'not-offered-yet' | 'not-applicable';
|
|
450
|
+
/** One supported competition, as written down. */
|
|
451
|
+
interface CompetitionEntry {
|
|
452
|
+
/** The provider's slug (`eng.1`): what a request asks for. */
|
|
453
|
+
readonly slug: string;
|
|
454
|
+
/** What a person types (`premier-league`): lower case, digits and hyphens, never a dot. */
|
|
455
|
+
readonly alias: string;
|
|
456
|
+
/** What a surface names it (`Premier League`): the provider's name, shortened where it says the country. */
|
|
457
|
+
readonly name: string;
|
|
458
|
+
/** The teams it fields. */
|
|
459
|
+
readonly teams: TeamKind;
|
|
460
|
+
/** A league (its season is the regular stage) or a cup (rounds and phases). */
|
|
461
|
+
readonly kind: 'league' | 'cup';
|
|
462
|
+
/**
|
|
463
|
+
* The season NAME a league's regular season is filed under, as measured on
|
|
464
|
+
* the real feed (`2026-27-english-premier-league`: the year in front varies,
|
|
465
|
+
* the name is this one, matched whole); absent where there is none.
|
|
466
|
+
*/
|
|
467
|
+
readonly seasonSlug?: string;
|
|
468
|
+
/**
|
|
469
|
+
* How its standings are read: `league` (authorised to serve exactly ONE
|
|
470
|
+
* table: a fact of the competition, never inferred from a payload having one
|
|
471
|
+
* child), `groups` (lettered or numbered groups), `none` (no table at all:
|
|
472
|
+
* knockout from the first round).
|
|
473
|
+
*/
|
|
474
|
+
readonly standings: 'league' | 'groups' | 'none';
|
|
475
|
+
/** What `bracket` is for it. */
|
|
476
|
+
readonly bracket: Capability;
|
|
477
|
+
/** What the market sidecar is for it (Claudinho's slug derivation, not the market provider's coverage). */
|
|
478
|
+
readonly markets: Capability;
|
|
479
|
+
/**
|
|
480
|
+
* How often it has an edition, in years: a season turn steps up by at most
|
|
481
|
+
* this (at most, not exactly: the Copa America went 2021, 2024, 2028).
|
|
482
|
+
*/
|
|
483
|
+
readonly cadenceYears: 1 | 2 | 4;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* The sixteen, in the order they are listed.
|
|
487
|
+
* - Nations: the World Cup, the Euro, the Copa America (four-yearly), the
|
|
488
|
+
* UEFA Nations League, the Concacaf Nations League (its next editions are
|
|
489
|
+
* 2026/27 and 2028/29, by Concacaf's published 2026 to 2030 calendar) and
|
|
490
|
+
* the Gold Cup (two-yearly).
|
|
491
|
+
* - The six leagues, yearly. A league's season name as measured on the real
|
|
492
|
+
* feed (Oct 3, 2026); `mex.1` and `uru.1` have none (their slugs are
|
|
493
|
+
* `torneo-apertura`, `torneo-clausura` and, for `mex.1`, its play-off
|
|
494
|
+
* rounds).
|
|
495
|
+
* - The four club cups. The Champions League's league phase reads as one
|
|
496
|
+
* table; the Concacaf Champions Cup has none (knockout from the first
|
|
497
|
+
* round); the Club World Cup is four-yearly.
|
|
498
|
+
* `bracket`: offered on the World Cup alone (its topology ships bundled);
|
|
499
|
+
* not applicable for the Premier League and LaLiga, 38-round leagues with no
|
|
500
|
+
* play-off; not offered yet elsewhere (`ita.1` has had championship and
|
|
501
|
+
* relegation play-offs since the FIGC's May 2026 decision, `ger.1` a
|
|
502
|
+
* relegation play-off, `mex.1` the Liguilla, `uefa.champions` a knockout after
|
|
503
|
+
* the league phase). A competition joins "not applicable" by a written fact.
|
|
504
|
+
* `markets`: offered on the World Cup alone: the sidecar derives its event
|
|
505
|
+
* slugs from the World Cup series only.
|
|
506
|
+
*/
|
|
507
|
+
declare const SUPPORTED: readonly CompetitionEntry[];
|
|
508
|
+
/** Every written fact a consumer reads, as derived from one table. */
|
|
509
|
+
interface DerivedTables {
|
|
510
|
+
/** The teams each competition fields. */
|
|
511
|
+
readonly teamKind: Readonly<Record<string, TeamKind>>;
|
|
512
|
+
/** Each competition's kind. */
|
|
513
|
+
readonly competitionKind: Readonly<Record<string, 'league' | 'cup'>>;
|
|
514
|
+
/** A league's season name, where it has one. */
|
|
515
|
+
readonly seasonSlug: Readonly<Record<string, string>>;
|
|
516
|
+
/** The standings shape where it is not groups (the default). */
|
|
517
|
+
readonly standingsShape: Readonly<Record<string, 'league' | 'none'>>;
|
|
518
|
+
/** The competitions with no bracket (`not-applicable`). */
|
|
519
|
+
readonly noBracket: ReadonlySet<string>;
|
|
520
|
+
/** The competitions the market sidecar reads (`offered`). */
|
|
521
|
+
readonly marketCompetitions: ReadonlySet<string>;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* The views every consumer reads, derived from a table. A function of the
|
|
525
|
+
* table, so a test can run it on one with a row past the table's end and
|
|
526
|
+
* see every view carry it. Every view is frozen.
|
|
527
|
+
*
|
|
528
|
+
* A row that offers a bracket for any competition but the bundled one is
|
|
529
|
+
* REFUSED here, at load, naming it: an offered bracket needs a knockout
|
|
530
|
+
* topology the clients ship, and only the bundled competition has one (the
|
|
531
|
+
* same question as `bundleApplies(slug)` with no season, asked of the leaf's
|
|
532
|
+
* own constant), so a wrong row never reaches a user.
|
|
533
|
+
*/
|
|
534
|
+
declare function deriveTables(entries: readonly CompetitionEntry[]): DerivedTables;
|
|
535
|
+
/** A competition's row, by its slug; undefined for a slug the table does not hold. */
|
|
536
|
+
declare function entryOf(slug: string | undefined, table?: readonly CompetitionEntry[]): CompetitionEntry | undefined;
|
|
537
|
+
/**
|
|
538
|
+
* What ONE value names, by the grammar every source of a selection shares
|
|
539
|
+
* (a flag, the environment, the saved choice): the row whose alias or slug it
|
|
540
|
+
* is; `raw` for a well-formed slug the table does not hold (experimental);
|
|
541
|
+
* undefined for anything else (`foo`, `ENG.1`, a space, an empty string). The
|
|
542
|
+
* resolver (`competition.ts`) and the config file's reader (`userConfig.ts`)
|
|
543
|
+
* ask this one question, so the file can never hold a value the resolver
|
|
544
|
+
* would refuse.
|
|
545
|
+
*/
|
|
546
|
+
declare function competitionValue(value: unknown, table?: readonly CompetitionEntry[]): {
|
|
547
|
+
row: CompetitionEntry;
|
|
548
|
+
} | {
|
|
549
|
+
raw: string;
|
|
550
|
+
} | undefined;
|
|
551
|
+
/** What each surface is for a competition. */
|
|
552
|
+
interface Capabilities {
|
|
553
|
+
readonly scores: Capability;
|
|
554
|
+
readonly next: Capability;
|
|
555
|
+
readonly standings: Capability;
|
|
556
|
+
readonly bracket: Capability;
|
|
557
|
+
readonly markets: Capability;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* A competition's capability row. Scores and the next fixture are the generic
|
|
561
|
+
* reads, offered on every competition; standings are offered unless the
|
|
562
|
+
* competition has no table; bracket and markets are the row's. A slug the
|
|
563
|
+
* table does not hold (a raw slug) gets the generic reads and nothing else.
|
|
564
|
+
*/
|
|
565
|
+
declare function capabilitiesOf(slug: string, table?: readonly CompetitionEntry[]): Capabilities;
|
|
566
|
+
/** What a surface names a competition: its row's name, or a raw slug as it is. */
|
|
567
|
+
declare function competitionLabel(slug: string, table?: readonly CompetitionEntry[]): string;
|
|
568
|
+
/** One competition as a listing states it. */
|
|
569
|
+
interface ListedCompetition {
|
|
570
|
+
readonly slug: string;
|
|
571
|
+
readonly alias: string;
|
|
572
|
+
readonly name: string;
|
|
573
|
+
readonly teams: TeamKind;
|
|
574
|
+
readonly kind: 'league' | 'cup';
|
|
575
|
+
readonly capabilities: Capabilities;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* The table as a listing: every row with its capabilities, in the table's
|
|
579
|
+
* order, and the current selection as the caller states it (`null` when
|
|
580
|
+
* there is none). No edition state: whether an edition is current is a fact
|
|
581
|
+
* of a READ (`betweenEditions`), never of the table.
|
|
582
|
+
*/
|
|
583
|
+
declare function listCompetitions<C>(table: readonly CompetitionEntry[], current: C | null): {
|
|
584
|
+
competitions: ListedCompetition[];
|
|
585
|
+
current: C | null;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* The team rally cries: a team's own cry takes the flair slot of its match
|
|
590
|
+
* line instead of the moment's phrase, in every language (a fan's cry is not
|
|
591
|
+
* translated). Mexico's "¿Y si sí?" was the first, and was hard-coded; it is
|
|
592
|
+
* now one row of the table.
|
|
593
|
+
*
|
|
594
|
+
* A side carries a cry by IDENTITY, in a competition whose teams are of the
|
|
595
|
+
* entry's kind (a nation's cry in a nations competition, a club's in a club
|
|
596
|
+
* one): by the provider's id when the side carries one (the same id for a club
|
|
597
|
+
* in every competition; a present id that is not the entry's is never that
|
|
598
|
+
* team, whatever its labels say), or, for an id-less side in a nations
|
|
599
|
+
* competition (the bundle's nations), by its code AND its name. A club's side
|
|
600
|
+
* with no id carries none: a club's code is a label, not an identity. When
|
|
601
|
+
* both sides carry one, the pinned side's cry when the pin is one of them (the
|
|
602
|
+
* one pin predicate, `isPinnedSide`), else the home side's.
|
|
603
|
+
*
|
|
604
|
+
* The content is pinned to the approved fixture
|
|
605
|
+
* `test/fixtures/rally-cries.approved.json`: a cry is changed there, on
|
|
606
|
+
* purpose, and here to match (`flavor-bank.test.ts` compares them).
|
|
607
|
+
*/
|
|
608
|
+
|
|
609
|
+
/** One team's cry: the provider's id (the key), its code and name (labels), its kind, the cry. */
|
|
610
|
+
interface RallyCry {
|
|
611
|
+
readonly id: string;
|
|
612
|
+
readonly code: string;
|
|
613
|
+
readonly name: string;
|
|
614
|
+
readonly kind: TeamKind;
|
|
615
|
+
readonly cry: string;
|
|
616
|
+
}
|
|
617
|
+
/** A pinned team, as far as the cries need it (core's `Pin` is one). */
|
|
618
|
+
interface RallyPin {
|
|
619
|
+
readonly id?: string;
|
|
620
|
+
readonly code: string;
|
|
621
|
+
readonly name: string;
|
|
622
|
+
}
|
|
623
|
+
/** The approved cries, keyed by the provider's id. Frozen. */
|
|
624
|
+
declare const RALLY_CRIES: readonly RallyCry[];
|
|
625
|
+
/**
|
|
626
|
+
* The table's entry for this side, in a competition whose teams are of
|
|
627
|
+
* `kind`: by its id when it carries one, else (a nations competition only) by
|
|
628
|
+
* its code and its name; undefined when it is no team of the table.
|
|
629
|
+
*/
|
|
630
|
+
declare function rallyEntryFor(team: Team, kind: TeamKind): RallyCry | undefined;
|
|
631
|
+
/**
|
|
632
|
+
* The rally cry a match's line carries, or undefined when neither side has
|
|
633
|
+
* one: the side's that has one; when both have one, the pinned side's when
|
|
634
|
+
* the pin is one of them, else the home side's.
|
|
635
|
+
*/
|
|
636
|
+
declare function rallyCryFor(m: Match, kind: TeamKind, pin?: RallyPin): string | undefined;
|
|
241
637
|
|
|
242
638
|
/**
|
|
243
|
-
* Commentary "flavor"
|
|
244
|
-
*
|
|
639
|
+
* Commentary "flavor": a small, localized layer of football-broadcast energy,
|
|
640
|
+
* one short exclamation for the moment a match is in, printed in the flair
|
|
641
|
+
* slot of a match line (CLI and MCP text; never in structured output).
|
|
245
642
|
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
643
|
+
* The rule for a phrase (Arturo, Oct 5, 2026): generic genre lines AND the
|
|
644
|
+
* popular catchphrases, song lines and slogans fans know are in. What stays
|
|
645
|
+
* out: a real person's NAME inside a phrase (nothing impersonates anyone by
|
|
646
|
+
* name), any betting word, and anything false for some score of its moment
|
|
647
|
+
* (a `draw` phrase is said of a level result only, a `late` one from the 80th
|
|
648
|
+
* minute, a `ht` one at the break whatever the score). The banks below are
|
|
649
|
+
* the approved content, pinned to the fixture
|
|
650
|
+
* `test/fixtures/flavor-bank.approved.json`: a phrase is changed there, on
|
|
651
|
+
* purpose, and here to match (`flavor-bank.test.ts` compares them).
|
|
652
|
+
*
|
|
653
|
+
* A team's rally cry is not a phrase of the bank: it takes the slot instead
|
|
654
|
+
* of the moment's phrase (`rally.ts`, through {@link matchFlairs} for a list
|
|
655
|
+
* and {@link matchFlair} for one line).
|
|
250
656
|
*/
|
|
251
657
|
|
|
252
658
|
/** Commentary-flair intensity. */
|
|
@@ -258,13 +664,75 @@ declare function isFlavorLevel(s: string): s is FlavorLevel;
|
|
|
258
664
|
/** Coerce arbitrary input (flag/env) to a level, defaulting to `full`. */
|
|
259
665
|
declare function asFlavorLevel(s: string | undefined | null): FlavorLevel;
|
|
260
666
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
667
|
+
* The moments a phrase is said of, in the banks' order: before kickoff, in
|
|
668
|
+
* play with no goal yet, in play with a goal on the board, at the break, from
|
|
669
|
+
* the 80th minute, a decided full time, a level one.
|
|
670
|
+
*/
|
|
671
|
+
declare const FLAVOR_MOMENTS: readonly ["scheduled", "live", "goal", "ht", "late", "ft", "draw"];
|
|
672
|
+
type Moment = (typeof FLAVOR_MOMENTS)[number];
|
|
673
|
+
/**
|
|
674
|
+
* One language's bank: a list of phrases per moment. Indexable by any string
|
|
675
|
+
* too (a moment read from elsewhere), which yields `undefined` for one that is
|
|
676
|
+
* not a moment.
|
|
263
677
|
*/
|
|
264
|
-
|
|
678
|
+
type FlavorBank = Readonly<Record<Moment, readonly string[]>> & Readonly<Partial<Record<string, readonly string[]>>>;
|
|
679
|
+
/** What a phrase is chosen with: the level (default `full`) and the language (default `en`, any unknown one `en`). */
|
|
680
|
+
interface FlavorOpts {
|
|
265
681
|
level?: FlavorLevel;
|
|
266
682
|
locale?: string;
|
|
267
|
-
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* A short, localized exclamation for a match's moment, or '' when the level
|
|
686
|
+
* or moment calls for restraint. Deterministic per match id: the phrase its
|
|
687
|
+
* id hashes to. A match in a list may get another one ({@link flavorsFor}).
|
|
688
|
+
*/
|
|
689
|
+
declare function matchFlavor(m: Match, opts?: FlavorOpts): string;
|
|
690
|
+
/**
|
|
691
|
+
* A phrase per match of a list, in order, none twice while the bank allows:
|
|
692
|
+
* each match takes its own phrase ({@link matchFlavor}) unless a previous row
|
|
693
|
+
* of THIS list already took it, and then the next phrase of its bank not yet
|
|
694
|
+
* taken, cyclically from its own; once every phrase of that bank is taken,
|
|
695
|
+
* its own phrase again (repeats resume). A match the level or its moment
|
|
696
|
+
* leaves silent gets ''. Deterministic for the same list; the first row's
|
|
697
|
+
* phrase is always its own, so a list of one is `matchFlavor`.
|
|
698
|
+
*/
|
|
699
|
+
declare function flavorsFor(matches: readonly Match[], opts?: FlavorOpts): string[];
|
|
700
|
+
/** What a flair slot holds: the text (empty for none), and whether it is a team's rally cry (the CLI prints one green). */
|
|
701
|
+
interface Flair {
|
|
702
|
+
text: string;
|
|
703
|
+
rally: boolean;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* What a flair is chosen with: the level and the language, the competition's
|
|
707
|
+
* team kind (without it no side carries a cry: a kind nobody stated vouches
|
|
708
|
+
* for nothing), and the pinned team, which decides when both sides carry one.
|
|
709
|
+
*/
|
|
710
|
+
type FlairOpts = FlavorOpts & {
|
|
711
|
+
kind?: TeamKind;
|
|
712
|
+
pin?: RallyPin;
|
|
713
|
+
};
|
|
714
|
+
/**
|
|
715
|
+
* The flair slots of a list of match lines, in order: ONE rule for every list
|
|
716
|
+
* (CLI `today` and `live`, MCP `get_today` and `get_live`). A row whose side
|
|
717
|
+
* carries a cry ({@link cryOf}) prints the cry (`rally: true`, in every
|
|
718
|
+
* language: a fan's cry is not translated) and reserves no phrase; the other
|
|
719
|
+
* rows get the phrases {@link flavorsFor} gives them, as a list of their own,
|
|
720
|
+
* so they stay distinct while the bank allows whatever the cries around them.
|
|
721
|
+
* With no team kind the list is `flavorsFor`'s.
|
|
722
|
+
*/
|
|
723
|
+
declare function matchFlairs(matches: readonly Match[], opts?: FlairOpts): Flair[];
|
|
724
|
+
/**
|
|
725
|
+
* What ONE match line's flair slot holds (CLI `next` and `match`, MCP
|
|
726
|
+
* `get_match` and `get_next_fixture`): the list of one ({@link matchFlairs}),
|
|
727
|
+
* so the match's cry, else the phrase the caller handed in, else the match's
|
|
728
|
+
* own phrase; `off` is silent whatever was handed in.
|
|
729
|
+
*/
|
|
730
|
+
declare function matchFlair(m: Match, opts: FlairOpts, phrase?: string): Flair;
|
|
731
|
+
/**
|
|
732
|
+
* The approved banks, per language and moment, in the approved order. Frozen:
|
|
733
|
+
* the content is pinned to `test/fixtures/flavor-bank.approved.json`.
|
|
734
|
+
*/
|
|
735
|
+
declare const FLAVOR_BANKS: Readonly<Record<string, FlavorBank>>;
|
|
268
736
|
|
|
269
737
|
/** A participant slot in the bundled bracket topology (pre-resolution). */
|
|
270
738
|
type SlotRef = {
|
|
@@ -292,7 +760,7 @@ type SlotRef = {
|
|
|
292
760
|
interface BracketMatchNode {
|
|
293
761
|
matchId: string;
|
|
294
762
|
stage: Stage;
|
|
295
|
-
/** 1-based index within the stage, ordered by
|
|
763
|
+
/** 1-based index within the stage, ordered by ascending numeric ESPN event id. */
|
|
296
764
|
index: number;
|
|
297
765
|
home: SlotRef;
|
|
298
766
|
away: SlotRef;
|
|
@@ -308,7 +776,8 @@ type SlotStatus = 'confirmed' | 'projected' | 'tbd';
|
|
|
308
776
|
/** A resolved bracket participant for display. */
|
|
309
777
|
interface ResolvedParticipant {
|
|
310
778
|
label: string;
|
|
311
|
-
flag
|
|
779
|
+
/** A nation's flag, or the neutral 🏳️ of a slot not yet known; absent for a side with none. */
|
|
780
|
+
flag?: string;
|
|
312
781
|
code?: string;
|
|
313
782
|
status: SlotStatus;
|
|
314
783
|
}
|
|
@@ -333,16 +802,43 @@ interface BracketView {
|
|
|
333
802
|
source?: string;
|
|
334
803
|
/** The bracket is a World Cup feature; off the bundle there is none (audit A03). */
|
|
335
804
|
unsupported?: true;
|
|
805
|
+
/** This competition has no bracket at all (a league season with no knockout tie of its own). */
|
|
806
|
+
inapplicable?: true;
|
|
336
807
|
}
|
|
337
808
|
interface BracketResult {
|
|
338
809
|
view: BracketView;
|
|
339
810
|
/** Off the bundle there is no bracket (audit A03); `view.stages` is empty. */
|
|
340
811
|
unsupported?: true;
|
|
812
|
+
/**
|
|
813
|
+
* This competition has no bracket at all (`bracketCapability`: a league
|
|
814
|
+
* season with no knockout tie of its own); `view.stages` is empty.
|
|
815
|
+
*/
|
|
816
|
+
inapplicable?: true;
|
|
341
817
|
degraded: boolean;
|
|
342
818
|
standingsDegraded: boolean;
|
|
343
819
|
source?: string;
|
|
820
|
+
/**
|
|
821
|
+
* The knockout window said its answer was not whole (see
|
|
822
|
+
* `VerdictSource.partial`): the view holds the ties that were read, and a
|
|
823
|
+
* tie whose record was left out shows its bundled slot. Absent when the
|
|
824
|
+
* window was whole, failed, or said nothing.
|
|
825
|
+
*/
|
|
826
|
+
partial?: {
|
|
827
|
+
omitted?: number;
|
|
828
|
+
};
|
|
344
829
|
}
|
|
345
830
|
|
|
831
|
+
/**
|
|
832
|
+
* Is this a knockout stage: a tie that is played until it is decided (extra
|
|
833
|
+
* time, penalties)? The rounds (`R32` to `F`, the third-place play-off) and the
|
|
834
|
+
* play-offs (`PO`). Not a group, a league's season, a cup's league phase, a
|
|
835
|
+
* friendly, or a phase the grammar does not know (`OTHER`). ONE predicate for
|
|
836
|
+
* every reader that asks "is this a knockout tie" (the bundle's knockout
|
|
837
|
+
* readers, the live window's length); on the bundle, whose stages are GROUP and
|
|
838
|
+
* the rounds, it says what `stage !== 'GROUP' && stage !== 'FRIENDLY'` said.
|
|
839
|
+
* Not the shootout rule: `sealMatch` refuses penalties on GROUP only.
|
|
840
|
+
*/
|
|
841
|
+
declare function isKnockoutStage(stage: Stage): boolean;
|
|
346
842
|
/**
|
|
347
843
|
* Strip live/final state from a fixture for the bundled skeleton schedule.
|
|
348
844
|
* The JSON ships kickoffs, teams, venues, and bracket structure only — never
|
|
@@ -397,7 +893,11 @@ declare const LIVE_WINDOW_MS: number;
|
|
|
397
893
|
* Numerically matches the slack `marketFixtureForTeam` already applies (live.ts).
|
|
398
894
|
*/
|
|
399
895
|
declare const KNOCKOUT_EXTRA_TIME_MS: number;
|
|
400
|
-
/**
|
|
896
|
+
/**
|
|
897
|
+
* Live-window length for a fixture — longer for a knockout stage (extra time +
|
|
898
|
+
* penalties), the short one for every other (a group, a league's season, a
|
|
899
|
+
* cup's league phase, a friendly, a phase the grammar does not know).
|
|
900
|
+
*/
|
|
401
901
|
declare function liveWindowMsFor(m: Match): number;
|
|
402
902
|
/**
|
|
403
903
|
* True once EVERY bundled fixture's (stage-aware) live window has ELAPSED —
|
|
@@ -440,74 +940,6 @@ declare function currentOrNextFixtureForTeam(code: string, opts?: {
|
|
|
440
940
|
/** Sorted list of distinct group letters present in the schedule. */
|
|
441
941
|
declare function groups(fixtures?: Match[]): string[];
|
|
442
942
|
|
|
443
|
-
/** A roster team plus its group letter (A–L). */
|
|
444
|
-
interface TeamInfo extends Team {
|
|
445
|
-
group?: string;
|
|
446
|
-
}
|
|
447
|
-
/**
|
|
448
|
-
* The 48-team roster from the bundled group-stage fixtures, sorted by name.
|
|
449
|
-
* Placeholder/knockout slots (non-3-letter code or 🏳️) are skipped.
|
|
450
|
-
*/
|
|
451
|
-
declare function allTeams(fixtures?: Match[]): TeamInfo[];
|
|
452
|
-
interface TeamLookup {
|
|
453
|
-
query: string;
|
|
454
|
-
/** The single confident match, or null when the query is ambiguous or unknown. */
|
|
455
|
-
team: TeamInfo | null;
|
|
456
|
-
/** All candidate teams, best-first (1+ whenever anything matched). */
|
|
457
|
-
matches: TeamInfo[];
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Resolve a free-text team name or code to a roster team. Precedence: exact code,
|
|
461
|
-
* exact name, alias, then prefix/substring fuzzy on the name (fuzzy only for
|
|
462
|
-
* queries of 3+ letters, so a 1–2 char query can't substring-match noise). A
|
|
463
|
-
* unique fuzzy hit resolves; multiple hits return as candidates with `team: null`.
|
|
464
|
-
*/
|
|
465
|
-
declare function lookupTeam(query: string, fixtures?: Match[]): TeamLookup;
|
|
466
|
-
|
|
467
|
-
/** One row of a group table. */
|
|
468
|
-
interface StandingRow {
|
|
469
|
-
team: Team;
|
|
470
|
-
played: number;
|
|
471
|
-
won: number;
|
|
472
|
-
drawn: number;
|
|
473
|
-
lost: number;
|
|
474
|
-
goalsFor: number;
|
|
475
|
-
goalsAgainst: number;
|
|
476
|
-
goalDiff: number;
|
|
477
|
-
points: number;
|
|
478
|
-
/**
|
|
479
|
-
* The provider's own rank for this row. Present on every live row (the
|
|
480
|
-
* standings feed states it); absent on a computed or roster-at-zero table,
|
|
481
|
-
* which has no authority to rank anyone. Renderers print THIS, never the
|
|
482
|
-
* array position — on a partial table the two differ (audit A01).
|
|
483
|
-
*/
|
|
484
|
-
rank?: number;
|
|
485
|
-
}
|
|
486
|
-
/** A group's table: the group letter ("A".."L") and its rows in standings order. */
|
|
487
|
-
interface GroupStandings {
|
|
488
|
-
group: string;
|
|
489
|
-
rows: StandingRow[];
|
|
490
|
-
/**
|
|
491
|
-
* Present when the provider's table could not be read in full: `omitted` rows
|
|
492
|
-
* were refused (malformed, duplicate, or beyond the row cap). The readable rows
|
|
493
|
-
* stay usable, but a partial table carries no authority to confirm or project
|
|
494
|
-
* qualification, and every surface says it is partial (audit A01).
|
|
495
|
-
*/
|
|
496
|
-
partial?: {
|
|
497
|
-
omitted: number;
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Compute a group table from a set of matches. Teams are seeded from every
|
|
502
|
-
* match (so a pre-tournament table still lists all four teams at 0), and only
|
|
503
|
-
* finished matches with a score contribute to the tally.
|
|
504
|
-
*
|
|
505
|
-
* Sort: points, then goal difference, then goals for, then name. (Real FIFA
|
|
506
|
-
* tiebreakers add head-to-head and fair-play; this is the standard simplified
|
|
507
|
-
* ordering used for display.)
|
|
508
|
-
*/
|
|
509
|
-
declare function computeStandings(matches: Match[]): StandingRow[];
|
|
510
|
-
|
|
511
943
|
/** Capabilities a provider advertises so callers can pick a strategy. */
|
|
512
944
|
interface ProviderCapabilities {
|
|
513
945
|
/** True if the provider can push events (websocket/SSE) vs poll-only. */
|
|
@@ -515,6 +947,14 @@ interface ProviderCapabilities {
|
|
|
515
947
|
/** Rough event->feed latency hint, in seconds (for poll-cadence tuning). */
|
|
516
948
|
latencyHintSec: number;
|
|
517
949
|
}
|
|
950
|
+
/** How a window is asked for (see {@link ProviderAdapter.fetchWindow}). */
|
|
951
|
+
interface FetchWindowOptions {
|
|
952
|
+
/**
|
|
953
|
+
* Compose parts that state two seasons instead of refusing them. Only for a
|
|
954
|
+
* caller that applies no bundled schedule and publishes no cached slice.
|
|
955
|
+
*/
|
|
956
|
+
readonly acrossSeasons?: boolean;
|
|
957
|
+
}
|
|
518
958
|
/**
|
|
519
959
|
* The single swap-point for data vendors. Every provider (ESPN, API-Football,
|
|
520
960
|
* Goalserve, …) implements this and maps INTO the canonical Match model, so the
|
|
@@ -522,6 +962,15 @@ interface ProviderCapabilities {
|
|
|
522
962
|
*/
|
|
523
963
|
interface ProviderAdapter {
|
|
524
964
|
readonly name: string;
|
|
965
|
+
/**
|
|
966
|
+
* The competition this adapter fetches (a provider slug, e.g. `fifa.world`,
|
|
967
|
+
* `eng.1`). An adapter serves exactly one, decided when it is built, and
|
|
968
|
+
* everything below the edge asks the ADAPTER — never the environment — which
|
|
969
|
+
* competition a request is for. That is what keeps a request from changing
|
|
970
|
+
* its mind halfway through, and what lets "does the bundled schedule apply?"
|
|
971
|
+
* be a question about a value.
|
|
972
|
+
*/
|
|
973
|
+
readonly competition: string;
|
|
525
974
|
readonly capabilities: ProviderCapabilities;
|
|
526
975
|
/**
|
|
527
976
|
* Expected group-table scope for omission checks. Omit when the competition's
|
|
@@ -534,12 +983,40 @@ interface ProviderAdapter {
|
|
|
534
983
|
* can have known groups without sharing the bundled World Cup teams.
|
|
535
984
|
*/
|
|
536
985
|
readonly standingsFallbackGroups?: readonly string[];
|
|
537
|
-
/**
|
|
986
|
+
/**
|
|
987
|
+
* All fixtures/results for a single calendar date (provider's timezone
|
|
988
|
+
* semantics). What a provider knows about a response — the season it
|
|
989
|
+
* belongs to, whether every record in it could be read — it attaches to the
|
|
990
|
+
* returned array (`attachFetchMeta`); callers read it with `fetchMeta`. The
|
|
991
|
+
* same holds for every fetch below.
|
|
992
|
+
*/
|
|
538
993
|
fetchByDate(dateISO: string): Promise<Match[]>;
|
|
539
994
|
/** Currently in-progress matches (poll path). */
|
|
540
995
|
fetchLive(): Promise<Match[]>;
|
|
541
|
-
/**
|
|
542
|
-
|
|
996
|
+
/**
|
|
997
|
+
* Optional inclusive date-range fetch. By default (strictly) a window whose
|
|
998
|
+
* parts state two seasons is refused: an absent season would let the bundled
|
|
999
|
+
* schedule apply and a cached slice stand. `acrossSeasons` asks it to compose
|
|
1000
|
+
* them instead (the result then states no `season`, and `seasons` lists
|
|
1001
|
+
* both): only for a caller that merges nothing and keeps no slice: the live
|
|
1002
|
+
* read, and off the bundled competition the dated read and the match read.
|
|
1003
|
+
* An adapter that ignores the option behaves as without it.
|
|
1004
|
+
*/
|
|
1005
|
+
fetchWindow?(startDate: string, endDate: string, opts?: FetchWindowOptions): Promise<Match[]>;
|
|
1006
|
+
/**
|
|
1007
|
+
* Optional: the provider's calendar day (`YYYY-MM-DD`) for an instant, i.e.
|
|
1008
|
+
* the day a fixture kicking off then is filed under (ESPN: US/Eastern). A
|
|
1009
|
+
* caller that counts a span in days counts THESE days when the adapter states
|
|
1010
|
+
* them, and UTC days otherwise.
|
|
1011
|
+
*/
|
|
1012
|
+
bucketDay?(instant: Date): string;
|
|
1013
|
+
/**
|
|
1014
|
+
* Optional: the clock this adapter counts time by (epoch ms), the one its
|
|
1015
|
+
* cooldown window is armed on. A read whose caller passed no clock asks this
|
|
1016
|
+
* one, so the adapter and the reads it serves agree on "now" (and a test
|
|
1017
|
+
* that injects the adapter's clock injects the read's).
|
|
1018
|
+
*/
|
|
1019
|
+
now?(): number;
|
|
543
1020
|
/**
|
|
544
1021
|
* Optional authoritative group tables (cumulative across the group stage).
|
|
545
1022
|
* Returned in standings order per group. Providers that can't supply a real
|
|
@@ -577,36 +1054,885 @@ interface ProviderAdapter {
|
|
|
577
1054
|
}
|
|
578
1055
|
|
|
579
1056
|
/**
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
* simply run out of budget. Those are the same bug three ways: the shape of the
|
|
585
|
-
* data did not carry what the reader needed to interpret it. A `BoundedList`
|
|
586
|
-
* cannot be constructed without stating all four facts.
|
|
1057
|
+
* A pinned team, as `follow --team` resolved it: the provider's id (every
|
|
1058
|
+
* club, and required for one: a pin is believed without an id on the bundled
|
|
1059
|
+
* competition alone, whose nations the bundle names by code), and its code and
|
|
1060
|
+
* name as labels.
|
|
587
1061
|
*/
|
|
588
|
-
interface
|
|
589
|
-
|
|
590
|
-
readonly
|
|
1062
|
+
interface Pin {
|
|
1063
|
+
readonly id?: string;
|
|
1064
|
+
readonly code: string;
|
|
1065
|
+
readonly name: string;
|
|
1066
|
+
}
|
|
1067
|
+
/** The config file, as believed. */
|
|
1068
|
+
interface UserConfig {
|
|
1069
|
+
readonly version: 1;
|
|
1070
|
+
/** An alias, a slug in the table or a raw slug, AS WRITTEN: the resolver maps it. */
|
|
1071
|
+
readonly competition: string;
|
|
1072
|
+
readonly team?: Pin;
|
|
1073
|
+
}
|
|
1074
|
+
/** Why there is no saved choice. */
|
|
1075
|
+
type NoConfigReason = 'absent' | 'symlink' | 'unreadable' | 'malformed' | 'version' | 'competition';
|
|
1076
|
+
/** What one read of the config file found. */
|
|
1077
|
+
type UserConfigRead = {
|
|
1078
|
+
kind: 'read';
|
|
1079
|
+
config: UserConfig;
|
|
1080
|
+
} | {
|
|
1081
|
+
kind: 'none';
|
|
1082
|
+
reason: NoConfigReason;
|
|
1083
|
+
};
|
|
1084
|
+
/** The bound on the config file, in bytes: a real one is under 200. */
|
|
1085
|
+
declare const MAX_CONFIG_BYTES = 4096;
|
|
1086
|
+
/** The environment a path rule reads: only the variables named below. */
|
|
1087
|
+
type PathEnv = Readonly<Record<string, string | undefined>>;
|
|
1088
|
+
/**
|
|
1089
|
+
* Where the config file lives: `$XDG_CONFIG_HOME/claudinho/config.json` when
|
|
1090
|
+
* that is set to an absolute path (on every platform: tests and smokes set it;
|
|
1091
|
+
* a relative value is invalid and ignored, see {@link baseDir}); else on
|
|
1092
|
+
* Windows `%APPDATA%\claudinho\config.json` when `APPDATA` is set to an
|
|
1093
|
+
* absolute path (a relative one is ignored the same way); else
|
|
1094
|
+
* `<home>/.config/claudinho/config.json`. An empty variable is absent.
|
|
1095
|
+
*/
|
|
1096
|
+
declare function configPath(env: PathEnv, platform: string, home: string): string;
|
|
1097
|
+
/**
|
|
1098
|
+
* Where the CLI's cache lives, by the same rule: `$XDG_CACHE_HOME/claudinho`
|
|
1099
|
+
* when set to an absolute path (a relative value is invalid and ignored, see
|
|
1100
|
+
* {@link baseDir}); else on Windows `%LOCALAPPDATA%\claudinho` when set to an
|
|
1101
|
+
* absolute path (a relative one is ignored the same way); else
|
|
1102
|
+
* `<home>/.cache/claudinho`. (A Windows install that used `~/.cache` moves
|
|
1103
|
+
* once: it is a cache, refilled by one cold refresh, and the throttle note
|
|
1104
|
+
* lives beside the snapshot, so it moves with it.)
|
|
1105
|
+
*/
|
|
1106
|
+
declare function cacheDirFor(env: PathEnv, platform: string, home: string): string;
|
|
1107
|
+
/**
|
|
1108
|
+
* Read the config file at `path`, once: through the one bounded reader, NOT
|
|
1109
|
+
* following a symbolic link (`lookAtOwnFile`: a link is `symlink`, whatever it
|
|
1110
|
+
* points at), bounded at {@link MAX_CONFIG_BYTES} (a directory, a device, a
|
|
1111
|
+
* file past the bound or one without permission is `unreadable`), never
|
|
1112
|
+
* waiting. Then parsed: bytes that are not JSON, or a root that is not an
|
|
1113
|
+
* object, are `malformed`; a `version` that is not 1 (or none) is `version`; a
|
|
1114
|
+
* `competition` that is no alias, no slug in the table and no well-formed raw
|
|
1115
|
+
* slug (the resolver's grammar) is `competition`. The `team` is believed only
|
|
1116
|
+
* as a team (see {@link believedPin}: without an id only when the competition
|
|
1117
|
+
* resolves to the bundled one, an alias through the table); otherwise it is
|
|
1118
|
+
* dropped and the competition kept. Never throws.
|
|
1119
|
+
*/
|
|
1120
|
+
declare function readUserConfig(path: string): UserConfigRead;
|
|
1121
|
+
/**
|
|
1122
|
+
* The slug a saved competition value names (`competitionValue`: an alias maps
|
|
1123
|
+
* through the table, a slug is itself, a raw slug is itself), or undefined for
|
|
1124
|
+
* a value the resolver's grammar refuses.
|
|
1125
|
+
*/
|
|
1126
|
+
declare function savedSlug(value: string): string | undefined;
|
|
1127
|
+
/**
|
|
1128
|
+
* The saved pin that applies to a request for `selected` (the slug the edge
|
|
1129
|
+
* resolved, undefined when nothing or a refused value was): the file's team
|
|
1130
|
+
* when the file's competition IS that competition (an alias and its slug are
|
|
1131
|
+
* one), whatever chose it: the flag, the environment or the file itself. A
|
|
1132
|
+
* pin belongs to its competition, not to the source that chose it; another
|
|
1133
|
+
* competition does not apply it (a club's id is the same in every
|
|
1134
|
+
* competition, so a pin never carries over by accident). The ONE rule both
|
|
1135
|
+
* edges ask.
|
|
1136
|
+
*/
|
|
1137
|
+
declare function pinUnder(selected: string | undefined, config: UserConfig | undefined): Pin | undefined;
|
|
1138
|
+
|
|
1139
|
+
/** The edition a read's season says has ended (see `betweenEditions`). */
|
|
1140
|
+
interface BetweenEditions {
|
|
591
1141
|
/**
|
|
592
|
-
*
|
|
593
|
-
*
|
|
1142
|
+
* The provider's calendar day (`YYYY-MM-DD`) of the season's STATED end:
|
|
1143
|
+
* the day the rule decides on, printed as it is. An administrative date the
|
|
1144
|
+
* provider states for its season, not the day of the last match.
|
|
594
1145
|
*/
|
|
595
|
-
readonly
|
|
596
|
-
/**
|
|
597
|
-
readonly
|
|
598
|
-
|
|
599
|
-
|
|
1146
|
+
readonly ended: string;
|
|
1147
|
+
/** The season's label (e.g. "2026 Concacaf Champions Cup"); absent when the provider gave none. */
|
|
1148
|
+
readonly label?: string;
|
|
1149
|
+
}
|
|
1150
|
+
/** Any result that may state a verdict. Results state more than this; these are the verdicts. */
|
|
1151
|
+
interface VerdictSource {
|
|
600
1152
|
/**
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
1153
|
+
* No competition is chosen (no flag, no environment, no saved choice), so
|
|
1154
|
+
* nothing was asked of any provider. Stated by a selection, never by a read
|
|
1155
|
+
* (core `selectionVerdict`).
|
|
604
1156
|
*/
|
|
605
|
-
readonly
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
1157
|
+
readonly noCompetition?: boolean;
|
|
1158
|
+
/** The feature does not exist for this competition yet (off the bundled schedule). */
|
|
1159
|
+
readonly unsupported?: boolean;
|
|
1160
|
+
/**
|
|
1161
|
+
* The competition has no such thing at all (a league season with no knockout
|
|
1162
|
+
* tie of its own has no bracket). Not "not yet": a capability that does not
|
|
1163
|
+
* apply.
|
|
1164
|
+
*/
|
|
1165
|
+
readonly inapplicable?: boolean;
|
|
1166
|
+
/**
|
|
1167
|
+
* The competition's table, read WHOLE, and its fixtures over discovery's
|
|
1168
|
+
* span hold no team by the name asked for. A claim about that evidence, not
|
|
1169
|
+
* the competition (a club out in a qualifying round is in no table). Never
|
|
1170
|
+
* stated from a roster that is not whole (a refused row, a missing table, a
|
|
1171
|
+
* row with no id): that is not knowing (`rosterIncomplete`).
|
|
1172
|
+
*/
|
|
1173
|
+
readonly unknownTeam?: boolean;
|
|
1174
|
+
/**
|
|
1175
|
+
* The evidence an `unknownTeam` rests on, which its sentence names: the
|
|
1176
|
+
* competition's table read whole and its fixtures over discovery's span
|
|
1177
|
+
* (`table`), or the bundled roster of the World Cup's nations (`bundle`).
|
|
1178
|
+
* Forwarded with the key; not a verdict of its own.
|
|
1179
|
+
*/
|
|
1180
|
+
readonly rosterEvidence?: 'table' | 'bundle';
|
|
1181
|
+
/**
|
|
1182
|
+
* The competition's roster was asked for and could not be read whole (a
|
|
1183
|
+
* table missing or partial, a row with no id, no answer), and the name could
|
|
1184
|
+
* not be answered without it: no club matched, or only by a code or a fuzzy
|
|
1185
|
+
* name the unread rest may share. Not "no such team", and not an outage.
|
|
1186
|
+
*/
|
|
1187
|
+
readonly rosterIncomplete?: boolean;
|
|
1188
|
+
/**
|
|
1189
|
+
* The name a team-scoped verdict names (`unknownTeam`, `rosterIncomplete`):
|
|
1190
|
+
* the query as the reader typed it, bounded as a label. Not a verdict.
|
|
1191
|
+
*/
|
|
1192
|
+
readonly query?: string;
|
|
1193
|
+
/**
|
|
1194
|
+
* The read's edition ended before the day asked and nothing in it is
|
|
1195
|
+
* scheduled or in play (see `live.ts`, `betweenEditionsOf`).
|
|
1196
|
+
*/
|
|
1197
|
+
readonly betweenEditions?: BetweenEditions;
|
|
1198
|
+
/**
|
|
1199
|
+
* The result holds what could be read and is not the whole answer: the
|
|
1200
|
+
* provider sent a table that did not become one (an all-tables standings
|
|
1201
|
+
* read). Unlike `unsupported`, the result is not empty: the sentence goes
|
|
1202
|
+
* BESIDE what is shown, not instead of it.
|
|
1203
|
+
*/
|
|
1204
|
+
readonly incomplete?: boolean;
|
|
1205
|
+
/**
|
|
1206
|
+
* The read behind the result said it was not whole: the provider sent
|
|
1207
|
+
* records the result does not hold (unreadable, a second copy of one
|
|
1208
|
+
* fixture, beyond a bound). `omitted` is how many, when that is known. The
|
|
1209
|
+
* result holds what was read, and the sentence goes beside it. Stated only
|
|
1210
|
+
* when the adapter SAID its answer was not whole; an adapter that says
|
|
1211
|
+
* nothing about its answer states no verdict.
|
|
1212
|
+
*/
|
|
1213
|
+
readonly partial?: {
|
|
1214
|
+
readonly omitted?: number;
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
/** The structured keys of the verdicts a result states. Empty when it states none. */
|
|
1218
|
+
interface VerdictExtras {
|
|
1219
|
+
noCompetition?: true;
|
|
1220
|
+
unsupported?: true;
|
|
1221
|
+
inapplicable?: true;
|
|
1222
|
+
unknownTeam?: true;
|
|
1223
|
+
rosterEvidence?: 'table' | 'bundle';
|
|
1224
|
+
rosterIncomplete?: true;
|
|
1225
|
+
betweenEditions?: {
|
|
1226
|
+
ended: string;
|
|
1227
|
+
label?: string;
|
|
1228
|
+
};
|
|
1229
|
+
incomplete?: true;
|
|
1230
|
+
/** `omitted` present only when the count is known (a positive integer). */
|
|
1231
|
+
partial?: {
|
|
1232
|
+
omitted?: number;
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Whether a result states `partial` (an object; anything else states nothing).
|
|
1237
|
+
* The one test of it, for the sentences built beside the verdict's (an empty
|
|
1238
|
+
* body after a read that was not whole says nothing was READ).
|
|
1239
|
+
*/
|
|
1240
|
+
declare function statesPartial<T extends VerdictSource>(result: T): result is T & {
|
|
1241
|
+
readonly partial: {
|
|
1242
|
+
readonly omitted?: number;
|
|
1243
|
+
};
|
|
1244
|
+
};
|
|
1245
|
+
/**
|
|
1246
|
+
* The verdict keys to spread into `--json` and MCP `data`, in a stable order.
|
|
1247
|
+
* Only verdicts the result actually states: a healthy result adds no key.
|
|
1248
|
+
*/
|
|
1249
|
+
declare function verdictExtras(result: VerdictSource): VerdictExtras;
|
|
1250
|
+
/**
|
|
1251
|
+
* `served` for a structured twin: the SHOWN fixtures whose record the read's
|
|
1252
|
+
* window held, in the order shown (a shown fixture not among them is the
|
|
1253
|
+
* bundled schedule's row, its live state unconfirmed). Bounded like the rows
|
|
1254
|
+
* it interprets: `shown` is what the surface finally displays (the day's rows,
|
|
1255
|
+
* the bounded list, the card's matches, the one match), never the window's
|
|
1256
|
+
* every id, which can hold other dates. A plain field, not a verdict, carried
|
|
1257
|
+
* only BESIDE the `partial` verdict it interprets: empty on a whole read,
|
|
1258
|
+
* whose structured output keeps its shape, and when the result states none.
|
|
1259
|
+
* The results state the window's ids on every successful read (the read's
|
|
1260
|
+
* fact); this is the one rule for what a surface carries of them.
|
|
1261
|
+
*/
|
|
1262
|
+
declare function servedExtras(result: VerdictSource & {
|
|
1263
|
+
readonly served?: readonly string[];
|
|
1264
|
+
}, shown: ReadonlyArray<{
|
|
1265
|
+
readonly id: string;
|
|
1266
|
+
}>): {
|
|
1267
|
+
served?: readonly string[];
|
|
1268
|
+
};
|
|
1269
|
+
/**
|
|
1270
|
+
* The sentence that REPLACES the body, in the reader's language — or
|
|
1271
|
+
* undefined when the result states none, and the surface prints its body (or
|
|
1272
|
+
* says what it would have said anyway: "no fixture found", "couldn't reach the
|
|
1273
|
+
* provider"). A qualifying verdict is never returned here: see
|
|
1274
|
+
* {@link verdictQualifiers}.
|
|
1275
|
+
*/
|
|
1276
|
+
declare function verdictNotice(result: VerdictSource, lang?: string): string | undefined;
|
|
1277
|
+
/**
|
|
1278
|
+
* The sentences that QUALIFY the body, printed beside it, in a fixed order
|
|
1279
|
+
* (`incomplete`, then `partial`), in the reader's language. Empty when the
|
|
1280
|
+
* result states none, and when it states a replacement (which stands alone).
|
|
1281
|
+
*
|
|
1282
|
+
* Any result object may be handed over, also one whose type declares none of
|
|
1283
|
+
* the verdict fields (a degraded result: it simply states none); the fields it
|
|
1284
|
+
* does declare are still checked against `VerdictSource`.
|
|
1285
|
+
*/
|
|
1286
|
+
declare function verdictQualifiers(result: VerdictSource & object, lang?: string): string[];
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Discovery's span, in the provider's calendar days around the day asked. Its
|
|
1290
|
+
* own module because the read (`live.ts`) and the sentences that name it
|
|
1291
|
+
* (`verdict.ts`: "no team called X in the table or in its fixtures over the
|
|
1292
|
+
* next 14 days") both need the number, and the verdict module is below the
|
|
1293
|
+
* read.
|
|
1294
|
+
*/
|
|
1295
|
+
/** Discovery looks this many provider days back (a match that kicked off late yesterday can still be in play). */
|
|
1296
|
+
declare const SCHEDULE_LOOKBACK_DAYS = 1;
|
|
1297
|
+
/** And this many ahead. */
|
|
1298
|
+
declare const SCHEDULE_AHEAD_DAYS = 14;
|
|
1299
|
+
|
|
1300
|
+
/** Provider names {@link makeAdapter} can construct (the CLI validates against this). */
|
|
1301
|
+
declare const KNOWN_SOURCES: readonly ["espn"];
|
|
1302
|
+
/**
|
|
1303
|
+
* Whether a source names a provider {@link makeAdapter} can construct: THE
|
|
1304
|
+
* question for every caller that must not ask a provider it does not name (a
|
|
1305
|
+
* command's precheck, the refresher, the trigger that starts one).
|
|
1306
|
+
*/
|
|
1307
|
+
declare function isKnownSource(source: string): boolean;
|
|
1308
|
+
interface AdapterOptions {
|
|
1309
|
+
/**
|
|
1310
|
+
* The competition the adapter will fetch. REQUIRED: the caller is the edge
|
|
1311
|
+
* that resolved it (see `resolveCompetition`), and an adapter that picked a
|
|
1312
|
+
* default here would be a second place where the competition is decided.
|
|
1313
|
+
*/
|
|
1314
|
+
competition: string;
|
|
1315
|
+
/**
|
|
1316
|
+
* Enrich group-stage fixtures with their group letter (one extra standings
|
|
1317
|
+
* request per poll). Default on; the statusline refresher turns it off because
|
|
1318
|
+
* it never renders group letters.
|
|
1319
|
+
*/
|
|
1320
|
+
enrichGroups?: boolean;
|
|
1321
|
+
/** Clock, injectable for tests; drives the provider cooldown window. */
|
|
1322
|
+
now?: () => number;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Construct a provider adapter for a `--source` name (default: espn). An
|
|
1326
|
+
* unknown source FAILS LOUD instead of silently running ESPN — `--source foo`
|
|
1327
|
+
* previously no-op'd, which lied about what the flag did (attribution stayed
|
|
1328
|
+
* honest, but the advertised knob did nothing).
|
|
1329
|
+
*/
|
|
1330
|
+
declare function makeAdapter(source: string, opts: AdapterOptions): ProviderAdapter;
|
|
1331
|
+
/**
|
|
1332
|
+
* Merge live matches over a base set by id. Live entries replace base entries
|
|
1333
|
+
* with the same id; unknown ids are appended.
|
|
1334
|
+
*/
|
|
1335
|
+
declare function mergeLive(base: Match[], live: Match[]): Match[];
|
|
1336
|
+
interface LiveResult {
|
|
1337
|
+
matches: Match[];
|
|
1338
|
+
/** True when the provider call failed and we fell back to static data. */
|
|
1339
|
+
degraded: boolean;
|
|
1340
|
+
/**
|
|
1341
|
+
* The live-data provider that served this result (e.g. "espn"), for
|
|
1342
|
+
* attribution. Absent when `degraded` — the bundled static schedule, served
|
|
1343
|
+
* by no live provider, must not be attributed to one.
|
|
1344
|
+
*/
|
|
1345
|
+
source?: string;
|
|
1346
|
+
/**
|
|
1347
|
+
* The season the provider reported for the response behind this result.
|
|
1348
|
+
* Absent when the provider stated none or the fetch failed, and for a live
|
|
1349
|
+
* read at a season turn, whose days state two. A fact about THIS result:
|
|
1350
|
+
* `today 2024-06-14` reports the season of that day.
|
|
1351
|
+
*/
|
|
1352
|
+
season?: SeasonInfo;
|
|
1353
|
+
/**
|
|
1354
|
+
* The read says its competition is between editions (see
|
|
1355
|
+
* {@link betweenEditionsOf}): a replacement verdict. The live read's
|
|
1356
|
+
* `matches` is then empty (it keeps the matches in play); the dated read
|
|
1357
|
+
* keeps the window's records, and states it only when the asked local date
|
|
1358
|
+
* holds none of them. Never stated on the bundled competition.
|
|
1359
|
+
*/
|
|
1360
|
+
betweenEditions?: BetweenEditions;
|
|
1361
|
+
/**
|
|
1362
|
+
* The read behind the result said it was not whole (see
|
|
1363
|
+
* `VerdictSource.partial`): the provider sent records the result does not
|
|
1364
|
+
* hold. `omitted` is how many, when the read knew it. Absent for a whole
|
|
1365
|
+
* read, for an adapter that said nothing about its answer, and for a failed
|
|
1366
|
+
* read (which is `degraded`).
|
|
1367
|
+
*/
|
|
1368
|
+
partial?: {
|
|
1369
|
+
omitted?: number;
|
|
1370
|
+
};
|
|
1371
|
+
/**
|
|
1372
|
+
* The dated read only: the bundled skeleton was merged under the provider's
|
|
1373
|
+
* records (the bundled competition, and no season stated or the bundle's
|
|
1374
|
+
* own). The day's list is then the bundle's, whole, and a fixture whose
|
|
1375
|
+
* record was left out shows without its live state; without it, a record
|
|
1376
|
+
* left out is simply absent. Request-local: never written to a cache.
|
|
1377
|
+
*/
|
|
1378
|
+
skeleton?: true;
|
|
1379
|
+
/**
|
|
1380
|
+
* The dated read only, when it succeeded: the ids of the records the
|
|
1381
|
+
* provider's window held (what the overlay served). A surface compares it
|
|
1382
|
+
* with the fixtures it displays (see `dayAttribution`). Request-local: never
|
|
1383
|
+
* written to a cache.
|
|
1384
|
+
*/
|
|
1385
|
+
served?: readonly string[];
|
|
1386
|
+
}
|
|
1387
|
+
/** Human label for a live-data provider name (attribution). Text only. */
|
|
1388
|
+
declare function liveSourceLabel(source: string): string;
|
|
1389
|
+
/**
|
|
1390
|
+
* Matches for a date, preferring live provider data, falling back to the static
|
|
1391
|
+
* schedule on any provider/network error (graceful degradation).
|
|
1392
|
+
*
|
|
1393
|
+
* Off the bundled competition there is no skeleton to merge, so the window is
|
|
1394
|
+
* asked ACROSS a season turn, as the live read asks it: on a turn day its
|
|
1395
|
+
* three days state two seasons, the records are served as they are, each on
|
|
1396
|
+
* its own day, and the result states no season. On the bundle it is asked
|
|
1397
|
+
* strictly: two seasons there would let the skeleton merge over another
|
|
1398
|
+
* edition's day, and the refusal protects it. Off the bundle a read whose
|
|
1399
|
+
* edition ended before the asked date, with nothing current in it and NOTHING
|
|
1400
|
+
* on the asked local date, says "between editions" ({@link betweenEditionsOf});
|
|
1401
|
+
* a date on or before the end day is a historical question, answered as asked.
|
|
1402
|
+
* `matches` is the window's records in every case: the surface files them by
|
|
1403
|
+
* the viewer's date.
|
|
1404
|
+
*
|
|
1405
|
+
* The result also says what it is made of: `partial` when the window said it
|
|
1406
|
+
* was not whole (the count when it knew it), `skeleton` when the bundled
|
|
1407
|
+
* schedule was merged under the records, and `served`, the ids the window
|
|
1408
|
+
* held. The last two are request-local: a surface reads them to attribute the
|
|
1409
|
+
* day it displays (`dayAttribution`) and to say what an empty day means.
|
|
1410
|
+
*
|
|
1411
|
+
* `tz` is the viewer's zone, the one the surface files the records by (pass
|
|
1412
|
+
* the same effective zone: `resolveTz`). A caller that names none asks in the
|
|
1413
|
+
* provider's zone, the days the window is counted in.
|
|
1414
|
+
*/
|
|
1415
|
+
declare function getMatchesForDate(adapter: ProviderAdapter, dateISO: string, tz?: string): Promise<LiveResult>;
|
|
1416
|
+
interface StandingsResult {
|
|
1417
|
+
/** The tables, sorted by key (`A`, `A1`, `A-B`, `LEAGUE`); each table's rows in standings order. */
|
|
1418
|
+
tables: GroupStandings[];
|
|
1419
|
+
/** True when no authoritative table was available; tables may be a static roster or empty. */
|
|
1420
|
+
degraded: boolean;
|
|
1421
|
+
/** Provider that served the authoritative result, including an empty one (absent when degraded). */
|
|
1422
|
+
source?: string;
|
|
1423
|
+
/**
|
|
1424
|
+
* The verdict of an ALL-TABLES read whose provider sent a table that could
|
|
1425
|
+
* not be read: `tables` holds the ones that were, and is not the whole
|
|
1426
|
+
* competition. Never set on a keyed read: a table that was found was read
|
|
1427
|
+
* (it says `partial` itself if rows are missing), and one that was not found
|
|
1428
|
+
* while a table may be missing is `degraded`, not "no such group".
|
|
1429
|
+
*/
|
|
1430
|
+
incomplete?: true;
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Authoritative group tables, preferring the provider's cumulative standings and
|
|
1434
|
+
* FAILING CLOSED when none is available. An adapter with explicit bundled-
|
|
1435
|
+
* roster compatibility may use the roster-at-zero; otherwise it returns an
|
|
1436
|
+
* empty degraded result because expected group letters alone do not prove the
|
|
1437
|
+
* bundle belongs to the same competition.
|
|
1438
|
+
*
|
|
1439
|
+
* Deliberately does NOT compute a table from a live-match window: that silently
|
|
1440
|
+
* drops earlier matchdays and reports a wrong, partial table (e.g. all-zeros for
|
|
1441
|
+
* a group not playing today) — the bug this replaced. A degraded roster is
|
|
1442
|
+
* explicitly zeroed; a confidently-wrong table is the failure mode we refuse.
|
|
1443
|
+
*
|
|
1444
|
+
* An empty `tables` with `degraded: false` means either the fetch succeeded but
|
|
1445
|
+
* the asked-for group wasn't in it, or the group is definitively outside the
|
|
1446
|
+
* adapter's declared scope (caller renders "no such group"). A group declared
|
|
1447
|
+
* in that expected scope but omitted from a partial
|
|
1448
|
+
* provider result takes the degraded fallback instead of rendering as an
|
|
1449
|
+
* authoritative empty table. An aggregate read also falls back when any group
|
|
1450
|
+
* in that scope is absent; one result-level verdict cannot honestly describe a
|
|
1451
|
+
* mix of live tables and static roster tables. Transport failure without
|
|
1452
|
+
* explicit bundled-roster compatibility stays empty and degraded; it must
|
|
1453
|
+
* never borrow the bundled World Cup roster.
|
|
1454
|
+
*
|
|
1455
|
+
* `group` is a table KEY (`A`, `A1`, `A-B`, `LEAGUE`), matched in any case.
|
|
1456
|
+
*
|
|
1457
|
+
* Two different things can be "not whole". A table whose rows were refused is
|
|
1458
|
+
* `partial` (on the table). A provider result from which a whole TABLE is
|
|
1459
|
+
* missing has an incomplete inventory (`fetchMeta`), and what that means
|
|
1460
|
+
* depends on what was asked:
|
|
1461
|
+
* - with an expected scope, the scope decides, as above (and a table outside
|
|
1462
|
+
* it is not shown);
|
|
1463
|
+
* - without one, an all-tables read returns the tables that were read and
|
|
1464
|
+
* says `incomplete` (when NO table was read it is `degraded`: there is
|
|
1465
|
+
* nothing to qualify); a keyed read returns the table if it was read, and
|
|
1466
|
+
* is `degraded` if it was not: the missing table may be the one asked for.
|
|
1467
|
+
*/
|
|
1468
|
+
declare function getStandings(adapter: ProviderAdapter, group?: string): Promise<StandingsResult>;
|
|
1469
|
+
declare function knockoutWindow(): {
|
|
1470
|
+
start: string;
|
|
1471
|
+
end: string;
|
|
1472
|
+
} | null;
|
|
1473
|
+
/**
|
|
1474
|
+
* Knockout bracket with hybrid slot resolution: confirmed FT winners/losers from
|
|
1475
|
+
* the live overlay; group slots project from live standings once a group has started.
|
|
1476
|
+
*/
|
|
1477
|
+
declare function getBracket(adapter: ProviderAdapter, opts?: {
|
|
1478
|
+
stage?: Stage;
|
|
1479
|
+
lang?: string;
|
|
1480
|
+
}): Promise<BracketResult>;
|
|
1481
|
+
interface MatchByIdResult {
|
|
1482
|
+
match?: Match;
|
|
1483
|
+
degraded: boolean;
|
|
1484
|
+
source?: string;
|
|
1485
|
+
/** Off the bundle a fixture list to look an id up in does not exist yet (audit A03). */
|
|
1486
|
+
unsupported?: true;
|
|
1487
|
+
/**
|
|
1488
|
+
* Off the bundle: the span searched for an id that a WHOLE read did not
|
|
1489
|
+
* hold, in the provider's calendar days (`YYYY-MM-DD`, discovery's span:
|
|
1490
|
+
* yesterday to 14 days ahead). "Not found in this window" is not "no such
|
|
1491
|
+
* match": a plain field of the answer, not a verdict.
|
|
1492
|
+
*/
|
|
1493
|
+
window?: {
|
|
1494
|
+
from: string;
|
|
1495
|
+
to: string;
|
|
1496
|
+
};
|
|
1497
|
+
/**
|
|
1498
|
+
* The read behind the answer was not whole (see `VerdictSource.partial`):
|
|
1499
|
+
* an absent id is then not known to be absent, and a found record's refresh
|
|
1500
|
+
* left a record out.
|
|
1501
|
+
*/
|
|
1502
|
+
partial?: {
|
|
1503
|
+
omitted?: number;
|
|
1504
|
+
};
|
|
1505
|
+
/** The read's edition ended and nothing in it is current (a replacement verdict). */
|
|
1506
|
+
betweenEditions?: BetweenEditions;
|
|
1507
|
+
/**
|
|
1508
|
+
* On the bundle, when the read succeeded: the ids of the records its window
|
|
1509
|
+
* held (what the overlay served), as the dated read states them. A shown
|
|
1510
|
+
* match not among them is the bundled schedule's row, its live state
|
|
1511
|
+
* unconfirmed: the surfaces say so through the day's rule
|
|
1512
|
+
* (`dayAttribution`). Request-local: never written to a cache. Not a verdict.
|
|
1513
|
+
*/
|
|
1514
|
+
served?: readonly string[];
|
|
1515
|
+
/**
|
|
1516
|
+
* Off the bundle, with `degraded`: `match` is the provider's own EARLIER
|
|
1517
|
+
* record (read by discovery in this command), whose state could not be
|
|
1518
|
+
* refreshed: the refresh failed, or did not hold it. Not the bundled
|
|
1519
|
+
* schedule, which is what a degraded match is on the bundle. Says which
|
|
1520
|
+
* not-live sentence a surface prints; not a verdict.
|
|
1521
|
+
*/
|
|
1522
|
+
earlierRecord?: true;
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* The not-live sentence for a match that is the provider's earlier record
|
|
1526
|
+
* (`earlierRecord`), as the share card prints it: English (the card's copy),
|
|
1527
|
+
* the catalog's sentence (`feed.earlierRecord`, which the CLI and the MCP text
|
|
1528
|
+
* print in the reader's language) in parentheses.
|
|
1529
|
+
*/
|
|
1530
|
+
declare const EARLIER_RECORD_NOTE: string;
|
|
1531
|
+
/**
|
|
1532
|
+
* The fixture a team-scoped MARKET query should be about, live-confirmed.
|
|
1533
|
+
* Static window math alone fails twice at the edges: a match in extra time
|
|
1534
|
+
* (now > kickoff + 140min, still LIVE) would be skipped for next week's
|
|
1535
|
+
* fixture, and a just-finished match (inside the window, already FT) would be
|
|
1536
|
+
* selected over the next one. So: pick the in-window candidate using a widened
|
|
1537
|
+
* window, overlay live state, and fall through to the next fixture when the
|
|
1538
|
+
* overlay says the candidate is finished. Degraded fetches keep the static
|
|
1539
|
+
* candidate (fail-closed: the market-relevance gate then errs toward showing
|
|
1540
|
+
* nothing rather than something wrong).
|
|
1541
|
+
*/
|
|
1542
|
+
declare function marketFixtureForTeam(adapter: ProviderAdapter, code: string, now?: Date): Promise<MatchByIdResult>;
|
|
1543
|
+
interface NextFixtureResult {
|
|
1544
|
+
fixture?: Match;
|
|
1545
|
+
/** True when the live overlay fetch failed and only the static skeleton was used. */
|
|
1546
|
+
degraded: boolean;
|
|
1547
|
+
/** The provider that served the live overlay (absent when degraded). */
|
|
1548
|
+
source?: string;
|
|
1549
|
+
/** Off the bundle "next" is built on a schedule we do not have yet (audit A03). */
|
|
1550
|
+
unsupported?: true;
|
|
1551
|
+
/**
|
|
1552
|
+
* Off the bundle: the query as asked, bounded as a label. What the answer's
|
|
1553
|
+
* sentences name when no team was resolved.
|
|
1554
|
+
*/
|
|
1555
|
+
query?: string;
|
|
1556
|
+
/** Off the bundle: the club the query resolved to (by its provider id). */
|
|
1557
|
+
team?: Team;
|
|
1558
|
+
/** Off the bundle: two or more clubs matched the query; no fixture is picked. */
|
|
1559
|
+
candidates?: Team[];
|
|
1560
|
+
/**
|
|
1561
|
+
* The roster the competition has holds no team by that name (a replacement
|
|
1562
|
+
* verdict): off the bundle the table read whole and a whole read of the
|
|
1563
|
+
* span; on the World Cup the bundled nations (`nationArg`).
|
|
1564
|
+
*/
|
|
1565
|
+
unknownTeam?: true;
|
|
1566
|
+
/** What `unknownTeam` rests on, which its sentence names (see `VerdictSource.rosterEvidence`). */
|
|
1567
|
+
rosterEvidence?: 'table' | 'bundle';
|
|
1568
|
+
/**
|
|
1569
|
+
* Off the bundle: the roster was asked for and could not be read whole (a
|
|
1570
|
+
* table missing or partial, a row with no id, or no answer), and the query
|
|
1571
|
+
* matched no club, or only by a code or a fuzzy name that the unread rest
|
|
1572
|
+
* may share. A replacement verdict, not an outage: the provider answered.
|
|
1573
|
+
*/
|
|
1574
|
+
rosterIncomplete?: true;
|
|
1575
|
+
/**
|
|
1576
|
+
* Off the bundle: a WHOLE read of the span held no fixture for the club. The
|
|
1577
|
+
* span in provider days ahead (discovery's): a plain field of the answer,
|
|
1578
|
+
* whose sentence is the empty body's own text, never a verdict.
|
|
1579
|
+
*/
|
|
1580
|
+
horizon?: {
|
|
1581
|
+
days: number;
|
|
1582
|
+
};
|
|
1583
|
+
/** Off the bundle: the read's edition ended and nothing in it is current (a replacement verdict). */
|
|
1584
|
+
betweenEditions?: BetweenEditions;
|
|
1585
|
+
/** Off the bundle: the season the discovery stated, when it stated one. */
|
|
1586
|
+
season?: SeasonInfo;
|
|
1587
|
+
/**
|
|
1588
|
+
* The knockout window said its answer was not whole: a record it was sent
|
|
1589
|
+
* is not in it (see `VerdictSource.partial`). `fixture` is still what was
|
|
1590
|
+
* read; a team whose tie was the record left out has none, and that is not
|
|
1591
|
+
* "eliminated". Absent when the window was whole, failed, or said nothing.
|
|
1592
|
+
*/
|
|
1593
|
+
partial?: {
|
|
1594
|
+
omitted?: number;
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* A team's next UPCOMING fixture, LIVE-RESOLVED across the knockout phase.
|
|
1599
|
+
*
|
|
1600
|
+
* The bundled schedule's knockout slots are resultless placeholders (home/away
|
|
1601
|
+
* are slot refs like "Group A winner", never a nation — see
|
|
1602
|
+
* {@link sanitizeBundledFixture}), so a purely static lookup goes blind the
|
|
1603
|
+
* moment a team's last GROUP game passes: `next MEX` answers "no upcoming
|
|
1604
|
+
* fixture" even after ESPN has confirmed Mexico's Round-of-32 tie. Overlay the
|
|
1605
|
+
* live knockout window (the SAME window {@link getBracket} asks for) so the
|
|
1606
|
+
* merged set carries the resolved nations, then pick the team's next fixture
|
|
1607
|
+
* with kickoff ≥ now. (Strictly upcoming — the in-play match is `getLiveMatches`'
|
|
1608
|
+
* job, preserving the pre-overlay `next` semantics.)
|
|
1609
|
+
*
|
|
1610
|
+
* Fails closed: on any provider error it falls back to the static result
|
|
1611
|
+
* (`degraded: true`), so a feed outage degrades to "no fixture" rather than
|
|
1612
|
+
* inventing one — advancement is never read from the static skeleton.
|
|
1613
|
+
*/
|
|
1614
|
+
declare function getNextFixtureForTeam(adapter: ProviderAdapter, code: string, now?: Date): Promise<NextFixtureResult>;
|
|
1615
|
+
/**
|
|
1616
|
+
* The next fixture of a SAVED team (`claudinho follow <alias> --team <name>`):
|
|
1617
|
+
* a team already resolved when it was saved, so it is never resolved again.
|
|
1618
|
+
*
|
|
1619
|
+
* Off the bundle: discovery's span as `next <club>` reads it (the same month
|
|
1620
|
+
* requests, the same between-editions question first), and the fixture
|
|
1621
|
+
* selected by `isTeam` with the pin (an id on both decides: a side carrying
|
|
1622
|
+
* another club's id is never the pin's, whatever its labels). No roster is
|
|
1623
|
+
* read: the pin is the identity. `team` is the pin, on every answer; the
|
|
1624
|
+
* verdicts are `next`'s (`partial`, `betweenEditions`; never `unknownTeam` or
|
|
1625
|
+
* `rosterIncomplete`, which are about resolving a name). The team is built by
|
|
1626
|
+
* the one constructor (`sealTeam` with the competition's written kind). On the
|
|
1627
|
+
* bundle the nations carry no id: by code, exactly as `next <code>` answers.
|
|
1628
|
+
*/
|
|
1629
|
+
declare function nextFixtureForPin(adapter: ProviderAdapter, pin: Pin, now?: Date): Promise<NextFixtureResult>;
|
|
1630
|
+
interface KnockoutFixturesResult {
|
|
1631
|
+
/** Resolved (both nations known) upcoming knockout fixtures, sorted by kickoff. */
|
|
1632
|
+
fixtures: Match[];
|
|
1633
|
+
/** True when the overlay fetch failed — caller must NOT cache this as "none". */
|
|
1634
|
+
degraded: boolean;
|
|
1635
|
+
/** Off the bundle there is no knockout window to fetch (audit A03). */
|
|
1636
|
+
unsupported?: true;
|
|
1637
|
+
/**
|
|
1638
|
+
* The season the provider reported for THIS response (absent when it stated
|
|
1639
|
+
* none, or on failure). The fixtures belong to it — which need not be the
|
|
1640
|
+
* season a live read made in the same breath answered for.
|
|
1641
|
+
*/
|
|
1642
|
+
season?: SeasonInfo;
|
|
1643
|
+
/**
|
|
1644
|
+
* False when the provider sent records this result does not hold (one it
|
|
1645
|
+
* could not read, or two that contradict each other). A fixture that is
|
|
1646
|
+
* absent from such a result is not known to be gone: a caller that keeps a
|
|
1647
|
+
* previous answer must not let this one erase it. Absent or true otherwise.
|
|
1648
|
+
*/
|
|
1649
|
+
complete?: boolean;
|
|
1650
|
+
/**
|
|
1651
|
+
* Stated only with `complete: false`: the id of every fixture this answer
|
|
1652
|
+
* DID read, whatever became of it. `fixtures` holds the ties that are
|
|
1653
|
+
* resolved and still to be played; a tie the provider postponed, cancelled,
|
|
1654
|
+
* started, un-resolved or moved outside the span was read and set aside (the
|
|
1655
|
+
* last by the window itself: see `FetchMeta.mentioned`). A caller keeping a
|
|
1656
|
+
* previous answer must ask "was it read?" of this list, not of `fixtures`,
|
|
1657
|
+
* or it puts back what the provider just took away.
|
|
1658
|
+
*/
|
|
1659
|
+
mentioned?: readonly string[];
|
|
1660
|
+
/**
|
|
1661
|
+
* The verdict a surface prints for `complete: false` (see
|
|
1662
|
+
* `VerdictSource.partial`), with the count of records left out when the
|
|
1663
|
+
* window knew it. Stated exactly when `complete` is false.
|
|
1664
|
+
*/
|
|
1665
|
+
partial?: {
|
|
1666
|
+
omitted?: number;
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* The RESOLVED upcoming knockout fixtures from the live overlay — the data the
|
|
1671
|
+
* hot-path statusline can't fetch itself but needs to show a real next-match
|
|
1672
|
+
* countdown (e.g. "🇲🇽 vs 🇪🇨 in 2d") instead of a 🏳️ placeholder. The cold-path
|
|
1673
|
+
* refresher calls this and caches the result; the statusline reads the cache.
|
|
1674
|
+
*
|
|
1675
|
+
* Returns only fixtures where BOTH sides are resolved (neither is the 🏳️ placeholder) and kickoff
|
|
1676
|
+
* ≥ now, so a slot ESPN hasn't filled yet is simply absent (the statusline then
|
|
1677
|
+
* fails closed to "⚽ —", never a placeholder). **Fails closed with
|
|
1678
|
+
* `degraded: true` on a provider error** — the caller must keep its prior cached
|
|
1679
|
+
* fixtures rather than cache an empty list as a real "no knockouts" (a transient
|
|
1680
|
+
* outage must never read as "your team is out").
|
|
1681
|
+
*/
|
|
1682
|
+
declare function getKnockoutFixtures(adapter: ProviderAdapter, now?: Date): Promise<KnockoutFixturesResult>;
|
|
1683
|
+
/**
|
|
1684
|
+
* A single match by id, with live overlay. The provider's scoreboard buckets
|
|
1685
|
+
* days in its own zone (ESPN: US/Eastern), so a fixture's UTC date can differ
|
|
1686
|
+
* from the scoreboard day it's filed under — a 02:00Z kickoff belongs to the
|
|
1687
|
+
* previous ET evening, and fetching only the UTC date silently misses its live
|
|
1688
|
+
* state (the match then renders from the static schedule as if scheduled).
|
|
1689
|
+
* Fetch the ±1-day window around the fixture's UTC date instead — the same
|
|
1690
|
+
* trick `getMatchesForDate` uses — and fall back to the static fixture on any
|
|
1691
|
+
* provider error.
|
|
1692
|
+
*/
|
|
1693
|
+
declare function getMatchById(adapter: ProviderAdapter, id: string, now?: Date): Promise<MatchByIdResult>;
|
|
1694
|
+
|
|
1695
|
+
interface ScheduleAheadResult {
|
|
1696
|
+
/** Every fixture read in the span, in ANY status, by kickoff. */
|
|
1697
|
+
fixtures: Match[];
|
|
1698
|
+
/** The schedule could not be had (a failed request, a refused window): the caller keeps what it has. */
|
|
1699
|
+
degraded: boolean;
|
|
1700
|
+
/**
|
|
1701
|
+
* The season every month's response stated, when they all stated the SAME
|
|
1702
|
+
* one (by year); absent when one stated none, when two stated different
|
|
1703
|
+
* ones, or on failure. A month whose list held no readable record states what
|
|
1704
|
+
* its response stated. Two months can state two seasons (the provider's
|
|
1705
|
+
* season turns on June 1): the fixtures of both are returned, and the answer
|
|
1706
|
+
* states none, because no one season describes all of them.
|
|
1707
|
+
*/
|
|
1708
|
+
season?: SeasonInfo;
|
|
1709
|
+
/**
|
|
1710
|
+
* False when the provider sent records this result does not hold (one it
|
|
1711
|
+
* could not read, a month whose list held no readable record, one fixture in
|
|
1712
|
+
* both months), or when the adapter says nothing about its answer. A fixture
|
|
1713
|
+
* absent from such a result is not known to be gone. Stated on every answer
|
|
1714
|
+
* that is not `degraded`.
|
|
1715
|
+
*/
|
|
1716
|
+
complete?: boolean;
|
|
1717
|
+
/**
|
|
1718
|
+
* Stated with `complete: false`: the id of every fixture either month's
|
|
1719
|
+
* response held, BEFORE the months are narrowed to the span. A caller keeping
|
|
1720
|
+
* a previous answer asks "was it read?" of this list, not of `fixtures` (a
|
|
1721
|
+
* fixture moved beyond the span was read, and must not be put back).
|
|
1722
|
+
*/
|
|
1723
|
+
mentioned?: readonly string[];
|
|
1724
|
+
/**
|
|
1725
|
+
* Stated with `complete: false` when the count is known: the provider
|
|
1726
|
+
* records the answer left out (each month's own count, and each second copy
|
|
1727
|
+
* across the months). Absent when a month's count is not known (a month that
|
|
1728
|
+
* read nothing states none). What `partialOfRead` reads beside `complete`.
|
|
1729
|
+
*/
|
|
1730
|
+
omitted?: number;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Discovery's span for a moment, in the PROVIDER's calendar days: from
|
|
1734
|
+
* `SCHEDULE_LOOKBACK_DAYS` back to `SCHEDULE_AHEAD_DAYS` ahead, added as dates
|
|
1735
|
+
* (a daylight-saving change does not move it). One computation, so the span
|
|
1736
|
+
* discovery reads and the span `match <id>` says it searched are the same.
|
|
1737
|
+
*/
|
|
1738
|
+
declare function scheduleSpan(adapter: ProviderAdapter, now: Date): {
|
|
1739
|
+
start: string;
|
|
1740
|
+
end: string;
|
|
1741
|
+
};
|
|
1742
|
+
/**
|
|
1743
|
+
* The schedule ahead: every fixture from `SCHEDULE_LOOKBACK_DAYS` back to
|
|
1744
|
+
* `SCHEDULE_AHEAD_DAYS` ahead, counted in the PROVIDER's calendar days and
|
|
1745
|
+
* added as dates (a daylight-saving change does not move the span). What the
|
|
1746
|
+
* cold-path refresher reads, about once an hour, to know when a match can be
|
|
1747
|
+
* in play in a competition the bundled schedule does not describe.
|
|
1748
|
+
*
|
|
1749
|
+
* Each calendar month the span touches is its OWN window, asked whole (first
|
|
1750
|
+
* day to last), so it is exactly one month request whatever part of the month
|
|
1751
|
+
* is in the span (a two- or three-day fragment passed as itself would be asked
|
|
1752
|
+
* a day at a time). One request, or two, sent together and both settled before
|
|
1753
|
+
* this answers (a throttle on one is retained by the adapter whatever the
|
|
1754
|
+
* other did). Two windows, not one window of two months: a response states the
|
|
1755
|
+
* season of the dates asked, and a composed window asked strictly (as these
|
|
1756
|
+
* are) refuses two seasons. So the answer states a season only when every
|
|
1757
|
+
* month stated the same one: discovery's own rule, not the window's (a window
|
|
1758
|
+
* states the season its stating parts agree on).
|
|
1759
|
+
*
|
|
1760
|
+
* "A list that is not empty and holds no readable record is a failure" is
|
|
1761
|
+
* asked of the WHOLE discovery, as a window asks it of all its parts: a month
|
|
1762
|
+
* refused that way (`ProviderError.noReadableRecord`) read nothing, so the
|
|
1763
|
+
* answer is not whole and nothing of that month is in `mentioned`, while the
|
|
1764
|
+
* other month's fixtures are returned. If no month read a record, the
|
|
1765
|
+
* discovery failed. Any other failure of either window fails it:
|
|
1766
|
+
* `degraded`, nothing else.
|
|
1767
|
+
*/
|
|
1768
|
+
declare function getScheduleAhead(adapter: ProviderAdapter, now?: Date): Promise<ScheduleAheadResult>;
|
|
1769
|
+
/**
|
|
1770
|
+
* Currently-live matches; empty + degraded on error.
|
|
1771
|
+
*
|
|
1772
|
+
* The provider buckets its scoreboard by its own day (ESPN: US/Eastern), so a
|
|
1773
|
+
* late kickoff that crossed the day boundary lands in an ADJACENT bucket — a
|
|
1774
|
+
* 04:00Z match still live at 76' while the provider's default "today" bucket
|
|
1775
|
+
* already shows the prior, all-FT day. A bare `adapter.fetchLive()` reads only
|
|
1776
|
+
* that single default bucket, so it silently MISSES such a match and `live` /
|
|
1777
|
+
* the statusline / the hook show "nothing live" mid-match. Fetch the ±1-day UTC
|
|
1778
|
+
* window around `now` instead — the same trick {@link getMatchesForDate} and
|
|
1779
|
+
* {@link getMatchById} use — and filter to in-play, so no live match can hide in
|
|
1780
|
+
* an adjacent bucket. Adapters without a window fetch fall back to `fetchLive()`.
|
|
1781
|
+
*/
|
|
1782
|
+
declare function getLiveMatches(adapter: ProviderAdapter, now?: Date): Promise<LiveResult>;
|
|
1783
|
+
/** A live read with the answer's own account of itself: what the refresher decides from. */
|
|
1784
|
+
interface LiveReadResult extends LiveResult {
|
|
1785
|
+
/**
|
|
1786
|
+
* True only when the provider's answer said every record in it was read.
|
|
1787
|
+
* Absent is not true, and a failed read is not whole: a read that is not
|
|
1788
|
+
* whole and holds no match in play does not prove none is.
|
|
1789
|
+
*/
|
|
1790
|
+
complete: boolean;
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* {@link getLiveMatches}, keeping the window's `complete` verdict (read from
|
|
1794
|
+
* the window's result BEFORE the in-play filter makes a new array). For the
|
|
1795
|
+
* cold-path refresher, which keeps polling after a match was seen in play
|
|
1796
|
+
* until a WHOLE read holds none.
|
|
1797
|
+
*/
|
|
1798
|
+
declare function getLiveRead(adapter: ProviderAdapter, now?: Date): Promise<LiveReadResult>;
|
|
1799
|
+
|
|
1800
|
+
/** A roster team plus its group letter (A–L). */
|
|
1801
|
+
interface TeamInfo extends Team {
|
|
1802
|
+
group?: string;
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* The 48-team roster from the bundled group-stage fixtures, sorted by name.
|
|
1806
|
+
* Placeholder/knockout slots (non-3-letter code or 🏳️) are skipped.
|
|
1807
|
+
*/
|
|
1808
|
+
declare function allTeams(fixtures?: Match[]): TeamInfo[];
|
|
1809
|
+
interface TeamLookup {
|
|
1810
|
+
query: string;
|
|
1811
|
+
/** The single confident match, or null when the query is ambiguous or unknown. */
|
|
1812
|
+
team: TeamInfo | null;
|
|
1813
|
+
/** All candidate teams, best-first (1+ whenever anything matched). */
|
|
1814
|
+
matches: TeamInfo[];
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Resolve a free-text team name or code to a roster team. Precedence: exact code,
|
|
1818
|
+
* exact name, alias, then prefix/substring fuzzy on the name (fuzzy only for
|
|
1819
|
+
* queries of 3+ letters, so a 1–2 char query can't substring-match noise). A
|
|
1820
|
+
* unique fuzzy hit resolves; multiple hits return as candidates with `team: null`.
|
|
1821
|
+
*/
|
|
1822
|
+
declare function lookupTeam(query: string, fixtures?: Match[]): TeamLookup;
|
|
1823
|
+
/**
|
|
1824
|
+
* A competition's roster, read from its standings, and what that read can
|
|
1825
|
+
* PROVE. Off the bundled competition there is no static roster: the clubs are
|
|
1826
|
+
* whoever the provider's tables hold.
|
|
1827
|
+
*/
|
|
1828
|
+
interface Roster {
|
|
1829
|
+
/** The tables' teams, one per id (a row with no id is kept, once per code and name). */
|
|
1830
|
+
teams: Team[];
|
|
1831
|
+
/**
|
|
1832
|
+
* True only when the roster can prove a name is NOT in the competition: a
|
|
1833
|
+
* table was asked for, the read was not degraded, no table is missing from
|
|
1834
|
+
* it, none is partial, and every row's team carries an id. Anything less is
|
|
1835
|
+
* "not known", never "no such team": the club behind a refused row cannot be
|
|
1836
|
+
* ruled out, and a row with no id cannot be resolved by identity.
|
|
1837
|
+
*/
|
|
1838
|
+
complete: boolean;
|
|
1839
|
+
/**
|
|
1840
|
+
* Whether a table exists to be asked for: false for a competition written
|
|
1841
|
+
* down as having none (`STANDINGS_SHAPE` `none`), whose empty document is a
|
|
1842
|
+
* complete inventory of nobody. The bundle's groups count as asked.
|
|
1843
|
+
*/
|
|
1844
|
+
tableAsked: boolean;
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* The roster of the adapter's competition, from ONE standings read (all
|
|
1848
|
+
* tables). Not asked at all for a competition with no table. The adapter
|
|
1849
|
+
* shares its standings read for a short while, so a command whose fixture read
|
|
1850
|
+
* already enriched from the standings asks once (a failed read is retried).
|
|
1851
|
+
*/
|
|
1852
|
+
declare function rosterFor(adapter: ProviderAdapter): Promise<Roster>;
|
|
1853
|
+
/** What a club query resolves to (see {@link resolveClub}). */
|
|
1854
|
+
type ClubResolution = {
|
|
1855
|
+
outcome: 'resolved';
|
|
1856
|
+
team: Team;
|
|
1857
|
+
} | {
|
|
1858
|
+
outcome: 'ambiguous';
|
|
1859
|
+
candidates: Team[];
|
|
1860
|
+
} | {
|
|
1861
|
+
outcome: 'unknown';
|
|
1862
|
+
} | {
|
|
1863
|
+
outcome: 'unresolved';
|
|
1864
|
+
/**
|
|
1865
|
+
* The teams the query DID match that carry no id (they cannot be
|
|
1866
|
+
* resolved by identity). Empty when it matched nothing.
|
|
1867
|
+
*/
|
|
1868
|
+
idless: Team[];
|
|
1869
|
+
};
|
|
1870
|
+
/**
|
|
1871
|
+
* Resolve a club's name or code against a competition's roster, with the
|
|
1872
|
+
* schedule ahead as POSITIVE evidence: a club found there is known, absence
|
|
1873
|
+
* from fourteen days proves nothing. One function for the CLI and MCP.
|
|
1874
|
+
*
|
|
1875
|
+
* The candidates are `Team`s, never `allTeams` (which keeps three-letter codes
|
|
1876
|
+
* only and one team per code: a real club is `O&M`, and two Libertadores clubs
|
|
1877
|
+
* are both `CAR`). One per id; a team with no id is a candidate only while no
|
|
1878
|
+
* team with an id has its code and name (`sameTeam` as written: the same code
|
|
1879
|
+
* and name are the same team), and it can never be the answer: identity needs
|
|
1880
|
+
* an id. Precedence as {@link lookupTeam}, with no alias table for clubs:
|
|
1881
|
+
* exact code (any case), exact name (normalized), then the name by prefix or
|
|
1882
|
+
* substring for a query of 3+ letters. One hit with an id resolves (one hit
|
|
1883
|
+
* by CODE or by a FUZZY name only when the roster was read whole, or no table
|
|
1884
|
+
* exists: it is not known to be unique otherwise; an exact name resolves
|
|
1885
|
+
* anyway); two or more are `ambiguous` (never a silent pick); none is
|
|
1886
|
+
* `unknown` only when the roster can prove it (`complete`), and `unresolved`
|
|
1887
|
+
* otherwise.
|
|
1888
|
+
*/
|
|
1889
|
+
declare function resolveClub(query: string, roster: Roster, schedule: readonly Match[]): ClubResolution;
|
|
1890
|
+
/**
|
|
1891
|
+
* The World Cup's team argument where a surface also takes club names (the MCP
|
|
1892
|
+
* tools): a 3-letter token is a code, uppercased, as it always was (an unknown
|
|
1893
|
+
* code still passes through: the escape hatch for other feeds); a NAME
|
|
1894
|
+
* resolves against the bundled roster. A name that resolves to no single
|
|
1895
|
+
* nation is answered here, with no request: two or more nations are the
|
|
1896
|
+
* `candidates`, none is `unknownTeam` (the bundled roster is the competition's
|
|
1897
|
+
* whole roster). The same answer for every tool that takes a team.
|
|
1898
|
+
*/
|
|
1899
|
+
declare function nationArg(query: string): {
|
|
1900
|
+
code: string;
|
|
1901
|
+
} | {
|
|
1902
|
+
answer: NextFixtureResult;
|
|
1903
|
+
};
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Bounded collections — the type that carries its own honesty.
|
|
1907
|
+
*
|
|
1908
|
+
* Every round of review found a list that was capped without saying so, or that
|
|
1909
|
+
* reported a post-cap count as the total, or that said "none found" when it had
|
|
1910
|
+
* simply run out of budget. Those are the same bug three ways: the shape of the
|
|
1911
|
+
* data did not carry what the reader needed to interpret it. A `BoundedList`
|
|
1912
|
+
* cannot be constructed without stating all four facts.
|
|
1913
|
+
*/
|
|
1914
|
+
interface BoundedList<T> {
|
|
1915
|
+
/** What survived the cap. */
|
|
1916
|
+
readonly items: readonly T[];
|
|
1917
|
+
/**
|
|
1918
|
+
* How many were established before capping. Exact when `complete` is true;
|
|
1919
|
+
* otherwise it may only be a lower bound over the records examined.
|
|
1920
|
+
*/
|
|
1921
|
+
readonly total: number;
|
|
1922
|
+
/** `items.length` — kept explicit so a serialized payload is self-describing. */
|
|
1923
|
+
readonly shown: number;
|
|
1924
|
+
/** Did the cap drop anything? */
|
|
1925
|
+
readonly truncated: boolean;
|
|
1926
|
+
/**
|
|
1927
|
+
* Did we finish looking? False when a deadline, error, or partial fetch cut
|
|
1928
|
+
* the work short — so a caller can tell "there are none" from "we don't know".
|
|
1929
|
+
* An empty list with `complete: false` must NEVER render as "nothing found".
|
|
1930
|
+
*/
|
|
1931
|
+
readonly complete: boolean;
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Bound a list, recording what that cost.
|
|
1935
|
+
*
|
|
610
1936
|
* SLICE BEFORE MAP is the caller's job — this records the outcome, it does not
|
|
611
1937
|
* make an unbounded traversal safe. `takeBounded` is the one that bounds work.
|
|
612
1938
|
*/
|
|
@@ -684,8 +2010,39 @@ declare function parsedValue<T>(r: ParseResult<T>): T | undefined;
|
|
|
684
2010
|
*/
|
|
685
2011
|
declare function isCacheable<T>(r: ParseResult<T>): boolean;
|
|
686
2012
|
|
|
2013
|
+
/**
|
|
2014
|
+
* How a competition's standings payload is read. `groups`: every table is
|
|
2015
|
+
* named by one of the group grammars. `league`: the competition is authorised
|
|
2016
|
+
* to serve exactly ONE table, whatever its name (a season, a phase). `none`:
|
|
2017
|
+
* the competition has no table (knockout from the first round), and the
|
|
2018
|
+
* provider answers with its own document and no table list at all; that
|
|
2019
|
+
* answer is then an empty one, not an unreadable one. If such a competition
|
|
2020
|
+
* ever serves a child or a table, nothing is read: it is not what was written
|
|
2021
|
+
* down.
|
|
2022
|
+
*/
|
|
2023
|
+
type StandingsShape = 'groups' | 'league' | 'none';
|
|
687
2024
|
interface MapContext {
|
|
2025
|
+
/**
|
|
2026
|
+
* Group letter by team CODE. Used for a team the feed gave NO id, and for
|
|
2027
|
+
* every team when there is no id map at all (standings rows that carried
|
|
2028
|
+
* none). Never for a team that has an id while an id map exists.
|
|
2029
|
+
*/
|
|
688
2030
|
groupByTeam?: Record<string, string>;
|
|
2031
|
+
/**
|
|
2032
|
+
* Group letter by team ID. When present it is the ONLY source for a team
|
|
2033
|
+
* that has an id: two teams can share a code (`CAR` is two clubs in the
|
|
2034
|
+
* Libertadores), so a code that happens to match is another team's row, and
|
|
2035
|
+
* a missing row means "no group known", not "try the letters".
|
|
2036
|
+
*/
|
|
2037
|
+
groupByTeamId?: Record<string, string>;
|
|
2038
|
+
/**
|
|
2039
|
+
* The competition the payload was asked for (the adapter states its own).
|
|
2040
|
+
* Its WRITTEN kinds decide two things here: whether a team is a nation (a
|
|
2041
|
+
* flag is generated from its name) or a club (none), and which slugs the
|
|
2042
|
+
* stage grammar reads as a league's season (see `stageFromSlug`). Absent:
|
|
2043
|
+
* clubs and a cup, the reading that asserts nothing.
|
|
2044
|
+
*/
|
|
2045
|
+
competition?: string;
|
|
689
2046
|
}
|
|
690
2047
|
|
|
691
2048
|
/**
|
|
@@ -700,8 +2057,17 @@ interface MapContext {
|
|
|
700
2057
|
* should be re-verified during an actual live match.
|
|
701
2058
|
*/
|
|
702
2059
|
|
|
703
|
-
/** Default competition slug (the 2026 World Cup). */
|
|
2060
|
+
/** Default competition slug (the 2026 World Cup): the bundled one, written once in the supported table's module. */
|
|
704
2061
|
declare const DEFAULT_COMPETITION = "fifa.world";
|
|
2062
|
+
/**
|
|
2063
|
+
* Reject declared response bodies past this before JSON.parse — a hijacked
|
|
2064
|
+
* endpoint must not be able to balloon the refresher's memory every ~15s.
|
|
2065
|
+
* (A day's scoreboard is tens of KB; a month's, the largest thing asked for,
|
|
2066
|
+
* was 845 KB for the World Cup's June.) Shared by the Polymarket provider. The
|
|
2067
|
+
* bytes actually read are counted too (`readJsonBounded`), so a body with no
|
|
2068
|
+
* declared length is cut at the same cap.
|
|
2069
|
+
*/
|
|
2070
|
+
declare const MAX_RESPONSE_BYTES: number;
|
|
705
2071
|
/**
|
|
706
2072
|
* Provider cooldown after a 429/403 (audit A12): the window a throttled
|
|
707
2073
|
* adapter refuses to fetch in. `Retry-After` is honoured in both RFC 9110 forms
|
|
@@ -726,15 +2092,41 @@ declare class ProviderError extends Error {
|
|
|
726
2092
|
readonly status?: number;
|
|
727
2093
|
/** For a throttle: how long the adapter will refuse to fetch (bounded). */
|
|
728
2094
|
retryAfterMs?: number;
|
|
729
|
-
|
|
2095
|
+
/**
|
|
2096
|
+
* Set on ONE refusal: a response whose envelope was read and whose list was
|
|
2097
|
+
* not empty, but held no record that could be read. It is not a kind of its
|
|
2098
|
+
* own: to every reader of `kind` it is the `parse` failure it always was (for
|
|
2099
|
+
* a single read it is one). A caller that composes several answers into one
|
|
2100
|
+
* (discovery: a window per month) asks this to tell such a list from an
|
|
2101
|
+
* envelope nobody could read, which is refused with the same kind and
|
|
2102
|
+
* without it. `season` is the one that response stated, if it stated one.
|
|
2103
|
+
*/
|
|
2104
|
+
readonly noReadableRecord?: {
|
|
2105
|
+
readonly season?: SeasonInfo;
|
|
2106
|
+
};
|
|
2107
|
+
constructor(message: string, kind: ProviderErrorKind, status?: number, detail?: {
|
|
2108
|
+
noReadableRecord?: {
|
|
2109
|
+
readonly season?: SeasonInfo;
|
|
2110
|
+
};
|
|
2111
|
+
});
|
|
730
2112
|
/** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
|
|
731
2113
|
get throttled(): boolean;
|
|
732
2114
|
}
|
|
733
2115
|
/** Map a single ESPN event into the canonical Match model. Exported for tests. */
|
|
734
2116
|
declare function mapEspnEvent(ev: unknown, ctx?: MapContext): Match | undefined;
|
|
735
|
-
/**
|
|
736
|
-
|
|
2117
|
+
/**
|
|
2118
|
+
* Project an ESPN standings payload onto group tables. Exported for tests.
|
|
2119
|
+
* `competition` states whose rows these are (its written kind decides whether
|
|
2120
|
+
* a row's team is flagged); absent, the teams are clubs.
|
|
2121
|
+
*/
|
|
2122
|
+
declare function parseStandings(data: unknown, competition?: string): GroupStandings[];
|
|
737
2123
|
interface EspnAdapterOptions {
|
|
2124
|
+
/**
|
|
2125
|
+
* The competition to fetch (an ESPN slug). Default: the 2026 World Cup. The
|
|
2126
|
+
* base URL is derived from it unless `baseUrl` overrides where to fetch.
|
|
2127
|
+
*/
|
|
2128
|
+
competition?: string;
|
|
2129
|
+
/** Override the base URL (tests, a custom host). Implies a non-default competition. */
|
|
738
2130
|
baseUrl?: string;
|
|
739
2131
|
fetchImpl?: typeof fetch;
|
|
740
2132
|
timeoutMs?: number;
|
|
@@ -757,10 +2149,15 @@ interface EspnAdapterOptions {
|
|
|
757
2149
|
declare class EspnAdapter implements ProviderAdapter {
|
|
758
2150
|
private readonly opts;
|
|
759
2151
|
readonly name = "espn";
|
|
2152
|
+
readonly competition: string;
|
|
2153
|
+
/** Where this adapter fetches; fixed at construction, like the competition. */
|
|
2154
|
+
private readonly base;
|
|
760
2155
|
readonly capabilities: ProviderCapabilities;
|
|
761
2156
|
readonly expectedStandingsGroups?: readonly string[];
|
|
762
2157
|
readonly standingsFallbackGroups?: readonly string[];
|
|
763
|
-
/**
|
|
2158
|
+
/** How this competition's standings payload is read (`standingsShapeOf`, from `STANDINGS_SHAPE`). */
|
|
2159
|
+
private readonly standingsShape;
|
|
2160
|
+
/** Short-lived group-letter maps, by team code and by team id (built lazily from standings). */
|
|
764
2161
|
private groupMap?;
|
|
765
2162
|
/**
|
|
766
2163
|
* One in-flight/recent standings fetch shared by fetchStandings and
|
|
@@ -787,6 +2184,8 @@ declare class EspnAdapter implements ProviderAdapter {
|
|
|
787
2184
|
private readonly cooldownListeners;
|
|
788
2185
|
private readonly clock;
|
|
789
2186
|
constructor(opts?: EspnAdapterOptions);
|
|
2187
|
+
/** The clock this adapter counts by (see `ProviderAdapter.now`). */
|
|
2188
|
+
now(): number;
|
|
790
2189
|
/** Epoch ms until which requests are refused, when a cooldown is armed. */
|
|
791
2190
|
get cooldownUntil(): number | undefined;
|
|
792
2191
|
/**
|
|
@@ -808,7 +2207,67 @@ declare class EspnAdapter implements ProviderAdapter {
|
|
|
808
2207
|
*/
|
|
809
2208
|
private arm;
|
|
810
2209
|
fetchByDate(dateISO: string): Promise<Match[]>;
|
|
811
|
-
|
|
2210
|
+
/**
|
|
2211
|
+
* The provider's calendar day (`YYYY-MM-DD`) for an instant: the day a
|
|
2212
|
+
* fixture kicking off then is filed under. See {@link PROVIDER_ZONE}. Used
|
|
2213
|
+
* to keep, from a month's response, the fixtures a window asked for; the
|
|
2214
|
+
* canary checks the rule against the real feed.
|
|
2215
|
+
*/
|
|
2216
|
+
bucketDay(instant: Date): string;
|
|
2217
|
+
/**
|
|
2218
|
+
* Every fixture whose provider day is in `[startDate, endDate]`, composed
|
|
2219
|
+
* from the request forms the provider accepts (see `WINDOW_DAY_REQUESTS`):
|
|
2220
|
+
* never a range. The parts are sent together, so the wait is one timeout,
|
|
2221
|
+
* not one per part, and EVERY part is settled before the window answers: a
|
|
2222
|
+
* throttle that arrives after a sibling's quick failure has armed the
|
|
2223
|
+
* cooldown by the time a caller looks at it.
|
|
2224
|
+
*
|
|
2225
|
+
* ONE result with ONE account, built in the order the parts were asked for
|
|
2226
|
+
* (never the order they arrived in):
|
|
2227
|
+
* - any part's failure fails the window. A response whose envelope cannot
|
|
2228
|
+
* be read is a failed part (see `readScoreboard`), whatever its siblings
|
|
2229
|
+
* hold. If a part was throttled, the window's error is the throttle the
|
|
2230
|
+
* adapter retains (the one whose deadline is latest, counted from when
|
|
2231
|
+
* each was received); otherwise it is the first failed part's.
|
|
2232
|
+
* - a part that filled the request's limit fails the window: its tail is
|
|
2233
|
+
* unknown.
|
|
2234
|
+
* - the season. A response states the season of the DATE asked, and a
|
|
2235
|
+
* competition turns on its own date (June 1, July 1, January 1, ...), so
|
|
2236
|
+
* the parts of a window can state two. The window states the one its
|
|
2237
|
+
* stating parts agree on; a part that states none does not veto them.
|
|
2238
|
+
* Its start and end date are kept only when every stating part stated
|
|
2239
|
+
* the same one (`agreedSeason`): agreeing on the year is not agreeing on
|
|
2240
|
+
* when the edition ends. `seasons` lists every distinct season the parts
|
|
2241
|
+
* stated, in the order asked (empty: none stated). Two seasons are then
|
|
2242
|
+
* one of two answers:
|
|
2243
|
+
* strictly (the default): a failure. "Unknown" would be the wrong
|
|
2244
|
+
* account of a known disagreement: an absent season lets the bundled
|
|
2245
|
+
* schedule apply, and lets a cached slice from another season stand.
|
|
2246
|
+
* Every caller that merges the bundle or keeps a slice asks this way.
|
|
2247
|
+
* `acrossSeasons`: the window is composed, states no `season`, and
|
|
2248
|
+
* `seasons` names both. A window composes across seasons only when
|
|
2249
|
+
* its caller merges nothing and keeps no slice, where a refusal would
|
|
2250
|
+
* spend its requests on a verdict nobody can act on. Three callers
|
|
2251
|
+
* do: the live read (it keeps only the matches in play); the dated
|
|
2252
|
+
* read OFF the bundle (there is no skeleton to merge); and the match
|
|
2253
|
+
* read off the bundle (it refreshes one record it found itself).
|
|
2254
|
+
* (Discovery states a season by a different rule, every month the same;
|
|
2255
|
+
* see `getScheduleAhead`. The two stay two.)
|
|
2256
|
+
* - it is complete only if every part is. `omitted` is the parts' counts
|
|
2257
|
+
* summed (see `parseEspnEvents`), plus each second copy across parts;
|
|
2258
|
+
* 0 exactly when the window is complete.
|
|
2259
|
+
* - a fixture is filed under exactly one day, so two parts cannot hold the
|
|
2260
|
+
* same one. If they ever do, it is the parser's rule for a duplicate:
|
|
2261
|
+
* the first copy, in the order asked, is the one that counts, the second
|
|
2262
|
+
* is counted as omitted, and the window says it is not complete. Copies
|
|
2263
|
+
* are compared BEFORE a month is narrowed to the window, so a second copy
|
|
2264
|
+
* that falls outside it (the same id, another kickoff) is still a
|
|
2265
|
+
* contradiction.
|
|
2266
|
+
* Everything but the season is the same in both modes. It is a union of
|
|
2267
|
+
* responses taken moments apart, not one snapshot: a fixture moved between
|
|
2268
|
+
* two parts being answered can be in neither.
|
|
2269
|
+
*/
|
|
2270
|
+
fetchWindow(startDate: string, endDate: string, opts?: FetchWindowOptions): Promise<Match[]>;
|
|
812
2271
|
/**
|
|
813
2272
|
* In-play matches from ESPN's DEFAULT scoreboard bucket only (no `dates`). This
|
|
814
2273
|
* single bucket misses a late kickoff ESPN files under an adjacent day, so it
|
|
@@ -821,10 +2280,12 @@ declare class EspnAdapter implements ProviderAdapter {
|
|
|
821
2280
|
/** The shared standings fetch (see {@link standingsShared}). */
|
|
822
2281
|
private sharedStandings;
|
|
823
2282
|
/**
|
|
824
|
-
* Authoritative, cumulative
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
* readable siblings
|
|
2283
|
+
* Authoritative, cumulative tables from the standings endpoint, read for the
|
|
2284
|
+
* shape this competition is written down as (`STANDINGS_SHAPE`). Throws on
|
|
2285
|
+
* fetch failure, and when nothing in the payload could be read. A malformed
|
|
2286
|
+
* row is omitted without hiding its readable siblings (its table says
|
|
2287
|
+
* `partial`); a child that did not become a table is omitted too, and the
|
|
2288
|
+
* result's metadata then says the inventory is not complete.
|
|
828
2289
|
*/
|
|
829
2290
|
fetchStandings(): Promise<GroupStandings[]>;
|
|
830
2291
|
/**
|
|
@@ -837,229 +2298,43 @@ declare class EspnAdapter implements ProviderAdapter {
|
|
|
837
2298
|
* drift and one command never fetches standings twice.
|
|
838
2299
|
*/
|
|
839
2300
|
fetchGroupMap(force?: boolean): Promise<Record<string, string>>;
|
|
840
|
-
private fetchScoreboard;
|
|
841
|
-
private get;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
/**
|
|
845
|
-
* The ONE bounded JSON reader for provider responses (audit A11).
|
|
846
|
-
*
|
|
847
|
-
* A declared Content-Length is the cheap early exit; an undeclared (chunked)
|
|
848
|
-
* body used to be materialised in full before any cap applied, so a hijacked
|
|
849
|
-
* or misbehaving upstream could balloon the refresher's memory every cycle.
|
|
850
|
-
* Now the bytes actually consumed from the stream are counted, the read stops
|
|
851
|
-
* and cancels at the cap, and parsing happens only after the whole body has
|
|
852
|
-
* passed the bound. Test doubles without a body stream (`{ ok, json }`) still
|
|
853
|
-
* work: they carry no bytes to bound.
|
|
854
|
-
*/
|
|
855
|
-
declare class ResponseTooLargeError extends Error {
|
|
856
|
-
readonly bytes: number;
|
|
857
|
-
readonly limit: number;
|
|
858
|
-
constructor(bytes: number, limit: number);
|
|
859
|
-
}
|
|
860
|
-
/** Parse a JSON body of at most `maxBytes`, counting what is actually read. */
|
|
861
|
-
declare function readJsonBounded(res: Response, maxBytes: number): Promise<unknown>;
|
|
862
|
-
|
|
863
|
-
/**
|
|
864
|
-
* The ESPN competition slug to fetch live state from. Defaults to the 2026
|
|
865
|
-
* World Cup (`fifa.world`); override with CLAUDINHO_COMPETITION (e.g.
|
|
866
|
-
* `eng.1`, `uefa.champions`, `fifa.friendly`). Only affects the *live* fetch —
|
|
867
|
-
* the bundled static schedule is always the World Cup.
|
|
868
|
-
*
|
|
869
|
-
* Lives in its own module so that both the live layer and the market sidecar
|
|
870
|
-
* read the ONE resolver without importing each other.
|
|
871
|
-
*/
|
|
872
|
-
declare function resolveCompetition(explicit?: string): string;
|
|
873
|
-
/** The competition whose schedule ships bundled in the clients: the 2026 World Cup. */
|
|
874
|
-
declare const BUNDLE_COMPETITION = "fifa.world";
|
|
875
|
-
/**
|
|
876
|
-
* True when the bundled World Cup schedule applies to the active competition.
|
|
877
|
-
* Off the bundle, every path built on the skeleton (date merge, `match <id>`,
|
|
878
|
-
* `next`, the bracket, the knockout-fixture cache, the market fixture) must
|
|
879
|
-
* not read it: an empty foreign day showed 104 World Cup fixtures with foreign
|
|
880
|
-
* attribution (audit A03). Real per-competition support is 0.11 (2.1).
|
|
881
|
-
*/
|
|
882
|
-
declare function bundleApplies(competition?: string): boolean;
|
|
883
|
-
|
|
884
|
-
/** Provider names {@link makeAdapter} can construct (the CLI validates against this). */
|
|
885
|
-
declare const KNOWN_SOURCES: readonly ["espn"];
|
|
886
|
-
interface AdapterOptions {
|
|
887
|
-
/**
|
|
888
|
-
* Enrich group-stage fixtures with their group letter (one extra standings
|
|
889
|
-
* request per poll). Default on; the statusline refresher turns it off because
|
|
890
|
-
* it never renders group letters.
|
|
891
|
-
*/
|
|
892
|
-
enrichGroups?: boolean;
|
|
893
|
-
/** Clock, injectable for tests; drives the provider cooldown window. */
|
|
894
|
-
now?: () => number;
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* Construct a provider adapter for a `--source` name (default: espn). An
|
|
898
|
-
* unknown source FAILS LOUD instead of silently running ESPN — `--source foo`
|
|
899
|
-
* previously no-op'd, which lied about what the flag did (attribution stayed
|
|
900
|
-
* honest, but the advertised knob did nothing).
|
|
901
|
-
*/
|
|
902
|
-
declare function makeAdapter(source?: string, opts?: AdapterOptions): ProviderAdapter;
|
|
903
|
-
/**
|
|
904
|
-
* Merge live matches over a base set by id. Live entries replace base entries
|
|
905
|
-
* with the same id; unknown ids are appended.
|
|
906
|
-
*/
|
|
907
|
-
declare function mergeLive(base: Match[], live: Match[]): Match[];
|
|
908
|
-
interface LiveResult {
|
|
909
|
-
matches: Match[];
|
|
910
|
-
/** True when the provider call failed and we fell back to static data. */
|
|
911
|
-
degraded: boolean;
|
|
912
2301
|
/**
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
2302
|
+
* Both group maps from one standings read. A fixture's team that has an id
|
|
2303
|
+
* is enriched from the id map alone; the code map serves a team with no id,
|
|
2304
|
+
* and every team when the standings rows carried no ids (see `MapContext`).
|
|
916
2305
|
*/
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
* Deliberately does NOT compute a table from a live-match window: that silently
|
|
942
|
-
* drops earlier matchdays and reports a wrong, partial table (e.g. all-zeros for
|
|
943
|
-
* a group not playing today) — the bug this replaced. A degraded roster is
|
|
944
|
-
* explicitly zeroed; a confidently-wrong table is the failure mode we refuse.
|
|
945
|
-
*
|
|
946
|
-
* An empty `tables` with `degraded: false` means either the fetch succeeded but
|
|
947
|
-
* the asked-for group wasn't in it, or the group is definitively outside the
|
|
948
|
-
* adapter's declared scope (caller renders "no such group"). A group declared
|
|
949
|
-
* in that expected scope but omitted from a partial
|
|
950
|
-
* provider result takes the degraded fallback instead of rendering as an
|
|
951
|
-
* authoritative empty table. An aggregate read also falls back when any group
|
|
952
|
-
* in that scope is absent; one result-level verdict cannot honestly describe a
|
|
953
|
-
* mix of live tables and static roster tables. Transport failure without
|
|
954
|
-
* explicit bundled-roster compatibility stays empty and degraded; it must
|
|
955
|
-
* never borrow the bundled World Cup roster.
|
|
956
|
-
*/
|
|
957
|
-
declare function getStandings(adapter: ProviderAdapter, group?: string): Promise<StandingsResult>;
|
|
958
|
-
declare function knockoutWindow(): {
|
|
959
|
-
start: string;
|
|
960
|
-
end: string;
|
|
961
|
-
} | null;
|
|
962
|
-
/**
|
|
963
|
-
* Knockout bracket with hybrid slot resolution: confirmed FT winners/losers from
|
|
964
|
-
* the live overlay; group slots project from live standings once a group has started.
|
|
965
|
-
*/
|
|
966
|
-
declare function getBracket(adapter: ProviderAdapter, opts?: {
|
|
967
|
-
stage?: Stage;
|
|
968
|
-
lang?: string;
|
|
969
|
-
}): Promise<BracketResult>;
|
|
970
|
-
interface MatchByIdResult {
|
|
971
|
-
match?: Match;
|
|
972
|
-
degraded: boolean;
|
|
973
|
-
source?: string;
|
|
974
|
-
/** Off the bundle a fixture list to look an id up in does not exist yet (audit A03). */
|
|
975
|
-
unsupported?: true;
|
|
976
|
-
}
|
|
977
|
-
/**
|
|
978
|
-
* The fixture a team-scoped MARKET query should be about, live-confirmed.
|
|
979
|
-
* Static window math alone fails twice at the edges: a match in extra time
|
|
980
|
-
* (now > kickoff + 140min, still LIVE) would be skipped for next week's
|
|
981
|
-
* fixture, and a just-finished match (inside the window, already FT) would be
|
|
982
|
-
* selected over the next one. So: pick the in-window candidate using a widened
|
|
983
|
-
* window, overlay live state, and fall through to the next fixture when the
|
|
984
|
-
* overlay says the candidate is finished. Degraded fetches keep the static
|
|
985
|
-
* candidate (fail-closed: the market-relevance gate then errs toward showing
|
|
986
|
-
* nothing rather than something wrong).
|
|
987
|
-
*/
|
|
988
|
-
declare function marketFixtureForTeam(adapter: ProviderAdapter, code: string, now?: Date): Promise<MatchByIdResult>;
|
|
989
|
-
interface NextFixtureResult {
|
|
990
|
-
fixture?: Match;
|
|
991
|
-
/** True when the live overlay fetch failed and only the static skeleton was used. */
|
|
992
|
-
degraded: boolean;
|
|
993
|
-
/** The provider that served the live overlay (absent when degraded). */
|
|
994
|
-
source?: string;
|
|
995
|
-
/** Off the bundle "next" is built on a schedule we do not have yet (audit A03). */
|
|
996
|
-
unsupported?: true;
|
|
997
|
-
}
|
|
998
|
-
/**
|
|
999
|
-
* A team's next UPCOMING fixture, LIVE-RESOLVED across the knockout phase.
|
|
1000
|
-
*
|
|
1001
|
-
* The bundled schedule's knockout slots are resultless placeholders (home/away
|
|
1002
|
-
* are slot refs like "Group A winner", never a nation — see
|
|
1003
|
-
* {@link sanitizeBundledFixture}), so a purely static lookup goes blind the
|
|
1004
|
-
* moment a team's last GROUP game passes: `next MEX` answers "no upcoming
|
|
1005
|
-
* fixture" even after ESPN has confirmed Mexico's Round-of-32 tie. Overlay the
|
|
1006
|
-
* live knockout window (the SAME single fetch {@link getBracket} uses) so the
|
|
1007
|
-
* merged set carries the resolved nations, then pick the team's next fixture
|
|
1008
|
-
* with kickoff ≥ now. (Strictly upcoming — the in-play match is `getLiveMatches`'
|
|
1009
|
-
* job, preserving the pre-overlay `next` semantics.)
|
|
1010
|
-
*
|
|
1011
|
-
* Fails closed: on any provider error it falls back to the static result
|
|
1012
|
-
* (`degraded: true`), so a feed outage degrades to "no fixture" rather than
|
|
1013
|
-
* inventing one — advancement is never read from the static skeleton.
|
|
1014
|
-
*/
|
|
1015
|
-
declare function getNextFixtureForTeam(adapter: ProviderAdapter, code: string, now?: Date): Promise<NextFixtureResult>;
|
|
1016
|
-
interface KnockoutFixturesResult {
|
|
1017
|
-
/** Resolved (both nations known) upcoming knockout fixtures, sorted by kickoff. */
|
|
1018
|
-
fixtures: Match[];
|
|
1019
|
-
/** True when the overlay fetch failed — caller must NOT cache this as "none". */
|
|
1020
|
-
degraded: boolean;
|
|
1021
|
-
/** Off the bundle there is no knockout window to fetch (audit A03). */
|
|
1022
|
-
unsupported?: true;
|
|
1023
|
-
}
|
|
1024
|
-
/**
|
|
1025
|
-
* The RESOLVED upcoming knockout fixtures from the live overlay — the data the
|
|
1026
|
-
* hot-path statusline can't fetch itself but needs to show a real next-match
|
|
1027
|
-
* countdown (e.g. "🇲🇽 vs 🇪🇨 in 2d") instead of a 🏳️ placeholder. The cold-path
|
|
1028
|
-
* refresher calls this and caches the result; the statusline reads the cache.
|
|
1029
|
-
*
|
|
1030
|
-
* Returns only fixtures where BOTH nations are resolved (flag ≠ 🏳️) and kickoff
|
|
1031
|
-
* ≥ now, so a slot ESPN hasn't filled yet is simply absent (the statusline then
|
|
1032
|
-
* fails closed to "⚽ —", never a placeholder). **Fails closed with
|
|
1033
|
-
* `degraded: true` on a provider error** — the caller must keep its prior cached
|
|
1034
|
-
* fixtures rather than cache an empty list as a real "no knockouts" (a transient
|
|
1035
|
-
* outage must never read as "your team is out").
|
|
1036
|
-
*/
|
|
1037
|
-
declare function getKnockoutFixtures(adapter: ProviderAdapter, now?: Date): Promise<KnockoutFixturesResult>;
|
|
1038
|
-
/**
|
|
1039
|
-
* A single match by id, with live overlay. The provider's scoreboard buckets
|
|
1040
|
-
* days in its own zone (ESPN: US/Eastern), so a fixture's UTC date can differ
|
|
1041
|
-
* from the scoreboard day it's filed under — a 02:00Z kickoff belongs to the
|
|
1042
|
-
* previous ET evening, and fetching only the UTC date silently misses its live
|
|
1043
|
-
* state (the match then renders from the static schedule as if scheduled).
|
|
1044
|
-
* Fetch the ±1-day window around the fixture's UTC date instead — the same
|
|
1045
|
-
* trick `getMatchesForDate` uses — and fall back to the static fixture on any
|
|
1046
|
-
* provider error.
|
|
1047
|
-
*/
|
|
1048
|
-
declare function getMatchById(adapter: ProviderAdapter, id: string): Promise<MatchByIdResult>;
|
|
1049
|
-
/**
|
|
1050
|
-
* Currently-live matches; empty + degraded on error.
|
|
1051
|
-
*
|
|
1052
|
-
* The provider buckets its scoreboard by its own day (ESPN: US/Eastern), so a
|
|
1053
|
-
* late kickoff that crossed the day boundary lands in an ADJACENT bucket — a
|
|
1054
|
-
* 04:00Z match still live at 76' while the provider's default "today" bucket
|
|
1055
|
-
* already shows the prior, all-FT day. A bare `adapter.fetchLive()` reads only
|
|
1056
|
-
* that single default bucket, so it silently MISSES such a match and `live` /
|
|
1057
|
-
* the statusline / the hook show "nothing live" mid-match. Fetch the ±1-day UTC
|
|
1058
|
-
* window around `now` instead — the same trick {@link getMatchesForDate} and
|
|
1059
|
-
* {@link getMatchById} use — and filter to in-play, so no live match can hide in
|
|
1060
|
-
* an adjacent bucket. Adapters without a window fetch fall back to `fetchLive()`.
|
|
2306
|
+
private groupMaps;
|
|
2307
|
+
private fetchScoreboard;
|
|
2308
|
+
/**
|
|
2309
|
+
* One scoreboard response, parsed, with the parser's account of it. The
|
|
2310
|
+
* "a non-empty payload with no readable record is a failure" rule is NOT
|
|
2311
|
+
* applied here: it belongs to the whole answer, and a window is one answer
|
|
2312
|
+
* made of several of these. An unreadable ENVELOPE is refused here, for
|
|
2313
|
+
* every caller: it says nothing about its day, so no sibling can stand in
|
|
2314
|
+
* for it (and a single read of one always was a failure).
|
|
2315
|
+
*/
|
|
2316
|
+
private readScoreboard;
|
|
2317
|
+
private get;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/**
|
|
2321
|
+
* The ONE bounded JSON reader for provider responses (audit A11).
|
|
2322
|
+
*
|
|
2323
|
+
* A declared Content-Length is the cheap early exit; an undeclared (chunked)
|
|
2324
|
+
* body used to be materialised in full before any cap applied, so a hijacked
|
|
2325
|
+
* or misbehaving upstream could balloon the refresher's memory every cycle.
|
|
2326
|
+
* Now the bytes actually consumed from the stream are counted, the read stops
|
|
2327
|
+
* and cancels at the cap, and parsing happens only after the whole body has
|
|
2328
|
+
* passed the bound. Test doubles without a body stream (`{ ok, json }`) still
|
|
2329
|
+
* work: they carry no bytes to bound.
|
|
1061
2330
|
*/
|
|
1062
|
-
declare
|
|
2331
|
+
declare class ResponseTooLargeError extends Error {
|
|
2332
|
+
readonly bytes: number;
|
|
2333
|
+
readonly limit: number;
|
|
2334
|
+
constructor(bytes: number, limit: number);
|
|
2335
|
+
}
|
|
2336
|
+
/** Parse a JSON body of at most `maxBytes`, counting what is actually read. */
|
|
2337
|
+
declare function readJsonBounded(res: Response, maxBytes: number): Promise<unknown>;
|
|
1063
2338
|
|
|
1064
2339
|
/**
|
|
1065
2340
|
* Choosing one of several, and reporting on a batch.
|
|
@@ -1257,6 +2532,17 @@ declare function mapsCleanly(match: Match, outcomes: MarketOutcome[]): boolean;
|
|
|
1257
2532
|
declare function marketSignalRendersFor(match: Match, signal: MarketSignal): boolean;
|
|
1258
2533
|
/** Sanity check: ≥2 priced outcomes whose probabilities sum to ~1. */
|
|
1259
2534
|
declare function hasSaneDistribution(outcomes: MarketOutcome[]): boolean;
|
|
2535
|
+
/**
|
|
2536
|
+
* May this signal be SHOWN beside this fixture? Only if it still belongs to the
|
|
2537
|
+
* fixture being rendered ({@link marketSignalRendersFor} — display labels come
|
|
2538
|
+
* from the Match, so a signal cached for a pairing that has since degraded to
|
|
2539
|
+
* a placeholder must not print), is not ambiguous, has a determinable
|
|
2540
|
+
* favorite, and its distribution is sane.
|
|
2541
|
+
*
|
|
2542
|
+
* THE display rule, for every surface. The CLI and the MCP server each carried
|
|
2543
|
+
* a copy of it; a rule that exists twice is a rule one copy will forget.
|
|
2544
|
+
*/
|
|
2545
|
+
declare function marketDisplayable(match: Match, signal: MarketSignal): boolean;
|
|
1260
2546
|
/**
|
|
1261
2547
|
* Is the signal outside the freshness window? Unparseable timestamps are stale.
|
|
1262
2548
|
*
|
|
@@ -1302,6 +2588,21 @@ declare function buildMarketSignal(input: BuildSignalInput): MarketSignal;
|
|
|
1302
2588
|
* percent on purpose, to avoid implying false precision.
|
|
1303
2589
|
*/
|
|
1304
2590
|
|
|
2591
|
+
/**
|
|
2592
|
+
* What a market read says about a competition outside the sidecar's scope
|
|
2593
|
+
* (see `MARKET_COMPETITIONS`): there the sidecar is a network-free no-op, and
|
|
2594
|
+
* the copy says so instead of reporting a "no signal" it never looked for.
|
|
2595
|
+
* One sentence, in the copy bank: the CLI and the MCP server each had it. Its
|
|
2596
|
+
* structured twin is `marketScopeVerdict`.
|
|
2597
|
+
*/
|
|
2598
|
+
declare const MARKETS_SCOPE_NOTE = "Market signals cover the World Cup only; none are read for this competition.";
|
|
2599
|
+
/**
|
|
2600
|
+
* The empty body of a dated market answer whose FIXTURE read was not whole:
|
|
2601
|
+
* no signal among the fixtures that were read, which is not "none for the
|
|
2602
|
+
* date". Market copy, so English on every locale like the rest of the bank;
|
|
2603
|
+
* the card builder's `marketsNoneRead` says when it applies.
|
|
2604
|
+
*/
|
|
2605
|
+
declare function marketsNoneReadNote(date: string): string;
|
|
1305
2606
|
/** Human label for the data source (text only — never a logo). */
|
|
1306
2607
|
declare function marketSourceLabel(source: string): string;
|
|
1307
2608
|
/** One-line favorite read, drawn only from the approved copy bank. */
|
|
@@ -1329,10 +2630,28 @@ declare function marketBlock(signal: MarketSignal, match: Match): string[];
|
|
|
1329
2630
|
* slug that cannot exist, so the sidecar is switched off by construction
|
|
1330
2631
|
* instead of issuing doomed requests. Supporting a league is slug-derivation,
|
|
1331
2632
|
* mapping and validation work per competition — not widening this set.
|
|
2633
|
+
* Derived from the supported table (`markets: 'offered'`), like every written
|
|
2634
|
+
* fact of a competition.
|
|
1332
2635
|
*/
|
|
1333
2636
|
declare const MARKET_COMPETITIONS: ReadonlySet<string>;
|
|
1334
|
-
/** Whether the market sidecar can say anything about
|
|
1335
|
-
declare function marketsCoverCompetition(competition
|
|
2637
|
+
/** Whether the market sidecar can say anything about a competition. */
|
|
2638
|
+
declare function marketsCoverCompetition(competition: string): boolean;
|
|
2639
|
+
/**
|
|
2640
|
+
* The verdict a market read with NOTHING TO SHOW states about its competition:
|
|
2641
|
+
* outside the sidecar's scope, "none" means "not read for this competition",
|
|
2642
|
+
* which is not "none found". A surface passes this to `verdictExtras` so a
|
|
2643
|
+
* day's read off scope says so in its structured output, as the by-team and
|
|
2644
|
+
* by-id reads do through their fixture lookup.
|
|
2645
|
+
*
|
|
2646
|
+
* `shown` is how many signals the read is about to display. A read that shows
|
|
2647
|
+
* some states no such verdict, whatever the competition: the demo source
|
|
2648
|
+
* (`CLAUDINHO_MARKETS_SOURCE=fake`) synthesizes signals for any competition,
|
|
2649
|
+
* and the text beside them says nothing about scope. Text and structured
|
|
2650
|
+
* output decide on the same two facts.
|
|
2651
|
+
*/
|
|
2652
|
+
declare function marketScopeVerdict(competition: string, shown: number): {
|
|
2653
|
+
unsupported?: true;
|
|
2654
|
+
};
|
|
1336
2655
|
/**
|
|
1337
2656
|
* Resolve the market-data source: explicit arg > CLAUDINHO_MARKETS_SOURCE env >
|
|
1338
2657
|
* 'polymarket' (mirrors resolveCompetition). Set CLAUDINHO_MARKETS_SOURCE=fake
|
|
@@ -1340,11 +2659,15 @@ declare function marketsCoverCompetition(competition?: string): boolean;
|
|
|
1340
2659
|
*/
|
|
1341
2660
|
declare function resolveMarketSource(explicit?: string): string;
|
|
1342
2661
|
/**
|
|
1343
|
-
* Construct a market-signal provider. Defaults to the
|
|
1344
|
-
* adapter; honors CLAUDINHO_MARKETS_SOURCE ('fake' =
|
|
1345
|
-
* demo data; 'none'/'off' = network-free no-op). Tests
|
|
2662
|
+
* Construct a market-signal provider for one competition. Defaults to the
|
|
2663
|
+
* Polymarket public-data adapter; honors CLAUDINHO_MARKETS_SOURCE ('fake' =
|
|
2664
|
+
* network-free synthetic demo data; 'none'/'off' = network-free no-op). Tests
|
|
2665
|
+
* usually inject directly.
|
|
2666
|
+
*
|
|
2667
|
+
* The competition is the REQUEST'S (its adapter's), passed in — the sidecar
|
|
2668
|
+
* never asks the environment which competition it is serving.
|
|
1346
2669
|
*/
|
|
1347
|
-
declare function makeMarketProvider(source
|
|
2670
|
+
declare function makeMarketProvider(source: string | undefined, competition: string): MarketProvider;
|
|
1348
2671
|
/** Fetch one match's signal; never throws — undefined on any error. */
|
|
1349
2672
|
declare function getMarketSignal(provider: MarketProvider, match: Match, options?: MarketSignalOptions): Promise<MarketSignal | undefined>;
|
|
1350
2673
|
/** Batch fetch; never throws — an empty, INCOMPLETE batch on any error. */
|
|
@@ -1387,6 +2710,19 @@ declare const MAX_LABEL_COLUMNS = 100;
|
|
|
1387
2710
|
* Total: never throws, whatever the input.
|
|
1388
2711
|
*/
|
|
1389
2712
|
declare function humanLabel(value: unknown, maxColumns?: number): string;
|
|
2713
|
+
/**
|
|
2714
|
+
* Is this value ALREADY a human label, as a reader typed it: the role leaves
|
|
2715
|
+
* it as it is, up to NFC and ordinary spaces at its ends (`' Arsenal '` is a
|
|
2716
|
+
* label). For INPUT that must be refused rather than repaired (a tool
|
|
2717
|
+
* argument): the same grammar `humanLabel` applies, asked as a question, of
|
|
2718
|
+
* the RAW value. So a code point the role forbids is refused ANYWHERE, ends
|
|
2719
|
+
* included (a control such as a tab or a newline, a format character such as
|
|
2720
|
+
* a byte-order mark, a line or paragraph separator, anything invisible):
|
|
2721
|
+
* comparing after a trim would hide one at an end, and a newline at the end
|
|
2722
|
+
* of an argument splits the run cue it is pasted into. Then an emoji, or a
|
|
2723
|
+
* tail past the column bound, which the role would drop, is refused too.
|
|
2724
|
+
*/
|
|
2725
|
+
declare function isHumanLabel(value: unknown, maxColumns?: number): boolean;
|
|
1390
2726
|
/**
|
|
1391
2727
|
* A flag, GENERATED from the bundled map — never accepted from anywhere.
|
|
1392
2728
|
*
|
|
@@ -1399,6 +2735,69 @@ declare function humanLabel(value: unknown, maxColumns?: number): string;
|
|
|
1399
2735
|
*/
|
|
1400
2736
|
declare function productFlag(nameOrCode: string | undefined): string;
|
|
1401
2737
|
|
|
2738
|
+
/** A league (its season is the regular stage), a cup (rounds and phases), or the friendly competition. */
|
|
2739
|
+
type CompetitionKind = 'league' | 'cup' | 'friendly';
|
|
2740
|
+
/**
|
|
2741
|
+
* The teams each competition fields: the supported table's (`nation` for the
|
|
2742
|
+
* World Cup, the Euro, the Copa America, both Nations Leagues and the Gold
|
|
2743
|
+
* Cup; `club` for the six leagues and the four club cups), and the friendly
|
|
2744
|
+
* competition's (`nation`). An unlisted competition fields clubs: a nations
|
|
2745
|
+
* competition the set does not list renders its teams by name until it is
|
|
2746
|
+
* written down.
|
|
2747
|
+
*/
|
|
2748
|
+
declare const TEAM_KIND: Readonly<Record<string, TeamKind>>;
|
|
2749
|
+
/** The teams a competition fields; an unlisted (or absent) competition fields clubs. */
|
|
2750
|
+
declare function teamKind(competition: string | undefined): TeamKind;
|
|
2751
|
+
/**
|
|
2752
|
+
* Each competition's kind: the supported table's (`league`: the six leagues,
|
|
2753
|
+
* whose regular season the stage grammar reads as `REGULAR`; `cup`: the other
|
|
2754
|
+
* ten), and `friendly` for `fifa.friendly`, the one competition whose
|
|
2755
|
+
* `friendly` slug is the `FRIENDLY` stage.
|
|
2756
|
+
*/
|
|
2757
|
+
declare const COMPETITION_KIND: Readonly<Record<string, CompetitionKind>>;
|
|
2758
|
+
/** A competition's kind; an unlisted (or absent) competition is a cup, the kind that asserts nothing. */
|
|
2759
|
+
declare function competitionKind(competition: string | undefined): CompetitionKind;
|
|
2760
|
+
/**
|
|
2761
|
+
* The season NAME a league's regular season is filed under (the table's
|
|
2762
|
+
* `seasonSlug`): `2026-27-english-premier-league`, `2026-27-laliga`,
|
|
2763
|
+
* `2026-27-italian-serie-a`, `2026-27-german-bundesliga` on the real feed. The
|
|
2764
|
+
* year in front varies; the name is this one, matched whole. `mex.1` has none.
|
|
2765
|
+
*/
|
|
2766
|
+
declare const SEASON_SLUG: Readonly<Record<string, string>>;
|
|
2767
|
+
/**
|
|
2768
|
+
* What a competition's standings are, where it is not lettered or numbered
|
|
2769
|
+
* GROUPS (the default, also for a competition that is not listed): the
|
|
2770
|
+
* table's `standings`.
|
|
2771
|
+
* - `league`: the competition is authorised to serve exactly ONE table (a
|
|
2772
|
+
* season, a league phase). That is a fact about the competition, written
|
|
2773
|
+
* down; it is never inferred from a payload having one child, because a
|
|
2774
|
+
* grouped competition whose payload shrinks to one group is not a league.
|
|
2775
|
+
* - `none`: the competition has no table (knockout from the first round).
|
|
2776
|
+
* The provider then answers with no table list at all, and for such a
|
|
2777
|
+
* competition, and only for one, that is an empty answer rather than an
|
|
2778
|
+
* unreadable one. The canary checks the feed against it: the day it serves
|
|
2779
|
+
* a table, that is a changed shape.
|
|
2780
|
+
* Read through {@link standingsShapeOf}, never by a bare index.
|
|
2781
|
+
*/
|
|
2782
|
+
declare const STANDINGS_SHAPE: Readonly<Record<string, 'league' | 'none'>>;
|
|
2783
|
+
/**
|
|
2784
|
+
* How a competition's standings payload is read: its written shape, or
|
|
2785
|
+
* `groups` for one not written down. By own property: a bare index on the
|
|
2786
|
+
* table gave a competition named like a prototype key (`constructor`) the
|
|
2787
|
+
* `Object` function as its shape.
|
|
2788
|
+
*/
|
|
2789
|
+
declare function standingsShapeOf(competition: string | undefined): StandingsShape;
|
|
2790
|
+
/**
|
|
2791
|
+
* Is this side Mexico's national team: the team the rally cry ("¿Y si sí?")
|
|
2792
|
+
* belongs to? The Mexico case of the rally table's rule (`rally.ts`): by
|
|
2793
|
+
* IDENTITY, in a competition that fields nations: the provider's id when the
|
|
2794
|
+
* side carries one (a present id that is not Mexico's is never Mexico,
|
|
2795
|
+
* whatever its labels say), or, for the bundle's id-less Mexico, its code AND
|
|
2796
|
+
* its name. A club abbreviated `MEX` never is; neither is Mexico's id read
|
|
2797
|
+
* under a club competition.
|
|
2798
|
+
*/
|
|
2799
|
+
declare function isMexicoNationalTeam(team: Team, kind: TeamKind): boolean;
|
|
2800
|
+
|
|
1402
2801
|
/**
|
|
1403
2802
|
* The single place a `Match` is sealed — whichever path it arrived by.
|
|
1404
2803
|
*
|
|
@@ -1428,6 +2827,7 @@ declare function productFlag(nameOrCode: string | undefined): string;
|
|
|
1428
2827
|
interface MatchParts {
|
|
1429
2828
|
id?: unknown;
|
|
1430
2829
|
stage?: unknown;
|
|
2830
|
+
stageLabel?: unknown;
|
|
1431
2831
|
group?: unknown;
|
|
1432
2832
|
kickoff?: unknown;
|
|
1433
2833
|
venue?: unknown;
|
|
@@ -1443,6 +2843,35 @@ interface MatchParts {
|
|
|
1443
2843
|
updatedAt?: unknown;
|
|
1444
2844
|
events?: unknown;
|
|
1445
2845
|
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Is this side THE team a reader asked about — for SELECTING a team's fixture
|
|
2848
|
+
* (`next`, the next card's label), never for the pairing question
|
|
2849
|
+
* {@link sameTeam} answers.
|
|
2850
|
+
*
|
|
2851
|
+
* When both carry a provider id, the ids decide, alone: a side carrying
|
|
2852
|
+
* another club's id is that other club, whatever its labels say (contradictory
|
|
2853
|
+
* data is not this team's fixture). When either has none, the same code AND
|
|
2854
|
+
* name (a side the provider sent without an id). `sameTeam` is the generous
|
|
2855
|
+
* one, by design: for "can these two play each other" a shared label is
|
|
2856
|
+
* already a refusal.
|
|
2857
|
+
*/
|
|
2858
|
+
declare function isTeam(side: Team, team: Team): boolean;
|
|
2859
|
+
/**
|
|
2860
|
+
* Is this side the PINNED team (`claudinho follow --team`): the one rule for
|
|
2861
|
+
* every line or list that prefers the pin (the statusline's and the hook's
|
|
2862
|
+
* pick, the rally cry between two sides that both carry one). A pin with no
|
|
2863
|
+
* id (the World Cup's nations, which the config reader believes on the
|
|
2864
|
+
* bundled competition alone) by its CODE, the bundle's contract: a nation's
|
|
2865
|
+
* FIFA code is unique there, and the feed's name need not be the bundle's
|
|
2866
|
+
* ("United States of America" for the saved "United States"). A pin with an
|
|
2867
|
+
* id by {@link isTeam}: equal ids decide when the side carries one too, the
|
|
2868
|
+
* code and the name when it carries none.
|
|
2869
|
+
*/
|
|
2870
|
+
declare function isPinnedSide(side: Team, pin: {
|
|
2871
|
+
readonly id?: string;
|
|
2872
|
+
readonly code: string;
|
|
2873
|
+
readonly name: string;
|
|
2874
|
+
}): boolean;
|
|
1446
2875
|
/**
|
|
1447
2876
|
* Validate candidate parts into a Match, or say why not.
|
|
1448
2877
|
*
|
|
@@ -1464,6 +2893,14 @@ interface SealOptions {
|
|
|
1464
2893
|
* needs ZERO. Cheapest work is work not done.
|
|
1465
2894
|
*/
|
|
1466
2895
|
readonly events?: boolean;
|
|
2896
|
+
/**
|
|
2897
|
+
* The teams' kind, a written fact of the competition the record belongs to
|
|
2898
|
+
* (`teamKind(competition)`): a `nation` side's flag is generated from its
|
|
2899
|
+
* name, a `club` side has none. Default `club`: a kind nobody stated vouches
|
|
2900
|
+
* for nothing, so no flag is generated from a name that merely looks like a
|
|
2901
|
+
* region. Every reader of a cached match states its competition's kind.
|
|
2902
|
+
*/
|
|
2903
|
+
readonly teamKind?: TeamKind;
|
|
1467
2904
|
}
|
|
1468
2905
|
declare function sealMatch(parts: MatchParts, opts?: SealOptions): ParseResult<Match>;
|
|
1469
2906
|
/**
|
|
@@ -1478,6 +2915,66 @@ declare function parseCachedMatch(raw: unknown, opts?: SealOptions): ParseResult
|
|
|
1478
2915
|
/** Cached fixtures, bounded BEFORE the per-record work. */
|
|
1479
2916
|
declare function parseCachedMatches(raw: unknown, max: number, opts?: SealOptions): BoundedList<Match>;
|
|
1480
2917
|
|
|
2918
|
+
/**
|
|
2919
|
+
* The season a provider reported for one response — sealed here, whether it
|
|
2920
|
+
* arrived in a feed payload or was read back from our own cache file.
|
|
2921
|
+
*
|
|
2922
|
+
* A season is METADATA of a result. It is never part of what a request selects
|
|
2923
|
+
* (a request selects a competition), and nothing may assume one before a
|
|
2924
|
+
* provider has said it: the statusline, which reads only the cache, cannot know
|
|
2925
|
+
* that a newer season exists and must not claim to.
|
|
2926
|
+
*/
|
|
2927
|
+
|
|
2928
|
+
/**
|
|
2929
|
+
* THE constructor of a `SeasonInfo`. The year is the fact; without a readable
|
|
2930
|
+
* one there is no season to report. Label and dates are optional decorations:
|
|
2931
|
+
* an unreadable one is dropped, never substituted.
|
|
2932
|
+
*/
|
|
2933
|
+
declare function sealSeason(raw: unknown): SeasonInfo | undefined;
|
|
2934
|
+
|
|
2935
|
+
/**
|
|
2936
|
+
* The schedule INDEX: one small record per fixture the refresher knows about,
|
|
2937
|
+
* sealed here whether it was just read from a provider or read back from our
|
|
2938
|
+
* own cache file. It is what decides, off the bundled competition, whether a
|
|
2939
|
+
* match can be in play — so a record carries exactly what that decision needs
|
|
2940
|
+
* and nothing a file could use to stretch it: a window is always the kickoff
|
|
2941
|
+
* plus a fixed length, never a stored end.
|
|
2942
|
+
*/
|
|
2943
|
+
|
|
2944
|
+
/** One fixture the schedule knows: its id, its kickoff, and whether it has a live window. */
|
|
2945
|
+
interface ScheduleEntry {
|
|
2946
|
+
readonly id: string;
|
|
2947
|
+
/** A canonical instant. */
|
|
2948
|
+
readonly kickoff: string;
|
|
2949
|
+
/** False for a postponed, cancelled or finished fixture: known, and opening no window. */
|
|
2950
|
+
readonly on: boolean;
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Whether a fixture in this status has a live window: not postponed, not
|
|
2954
|
+
* cancelled, and not finished (nothing is left to poll for, and nothing to call
|
|
2955
|
+
* live). THE rule, for the index (`scheduleEntryOf`) and for the statusline
|
|
2956
|
+
* reading the slice's display records. A match seen IN PLAY is held by the
|
|
2957
|
+
* refresher's continuation, not by its entry.
|
|
2958
|
+
*/
|
|
2959
|
+
declare function hasLiveWindow(status: Status): boolean;
|
|
2960
|
+
/**
|
|
2961
|
+
* The most records an index holds. A 16-day span of the busiest competition
|
|
2962
|
+
* measured holds about 80. An index is whole or it does not exist: a longer
|
|
2963
|
+
* one is not cut somewhere, it is no schedule.
|
|
2964
|
+
*/
|
|
2965
|
+
declare const MAX_SCHEDULE_INDEX = 256;
|
|
2966
|
+
/** THE constructor of a schedule entry. */
|
|
2967
|
+
declare function sealScheduleEntry(raw: unknown): ScheduleEntry | undefined;
|
|
2968
|
+
/** A fixture's entry; none if the fixture has no id or no instant. */
|
|
2969
|
+
declare function scheduleEntryOf(m: Match): ScheduleEntry | undefined;
|
|
2970
|
+
/**
|
|
2971
|
+
* A stored index, read back. Not a list, or longer than the bound: no schedule
|
|
2972
|
+
* at all (decided before a record is looked at; never read as a prefix). A
|
|
2973
|
+
* record that cannot be sealed does not count; an id held twice counts once,
|
|
2974
|
+
* the first. In kickoff order.
|
|
2975
|
+
*/
|
|
2976
|
+
declare function parseCachedScheduleIndex(raw: unknown): ScheduleEntry[] | undefined;
|
|
2977
|
+
|
|
1481
2978
|
/**
|
|
1482
2979
|
* Validate a market signal, or say why not.
|
|
1483
2980
|
*
|
|
@@ -1539,6 +3036,24 @@ declare class PolymarketProvider implements MarketProvider {
|
|
|
1539
3036
|
private toSignal;
|
|
1540
3037
|
}
|
|
1541
3038
|
|
|
3039
|
+
/**
|
|
3040
|
+
* The one non-affiliation sentence (0.11, ledger D5 option B). A leaf: every
|
|
3041
|
+
* runtime surface imports it (the share card footer, the CLI's help footer and
|
|
3042
|
+
* its English localized footer, the MCP server's footer), and the static copies
|
|
3043
|
+
* that cannot import it (the listing JSON files, the public Markdown) are pinned
|
|
3044
|
+
* to it by `test/disclaimer.test.ts`. A listing host is added by
|
|
3045
|
+
* `disclaimerLine(host)`, the only composition: never a second spelling.
|
|
3046
|
+
*/
|
|
3047
|
+
/** Not affiliated with FIFA, any confederation, league or club, or Anthropic. */
|
|
3048
|
+
declare const DISCLAIMER = "Not affiliated with FIFA, any confederation, league or club, or Anthropic.";
|
|
3049
|
+
/** The fan line every footer pairs with the disclaimer. */
|
|
3050
|
+
declare const FAN_PROJECT = "Independent fan project";
|
|
3051
|
+
/**
|
|
3052
|
+
* The disclaimer, with `, nor with <host>` before its final period when a host
|
|
3053
|
+
* is given (a listing's own host: "Smithery", "Cursor").
|
|
3054
|
+
*/
|
|
3055
|
+
declare function disclaimerLine(host?: string): string;
|
|
3056
|
+
|
|
1542
3057
|
/** The two v1 snippet shapes. `social` is the rich card; `compact` is one terse line per match. */
|
|
1543
3058
|
type ShareStyle = 'compact' | 'social';
|
|
1544
3059
|
/** The project social tag — a distribution unit, default-on but removable. */
|
|
@@ -1547,7 +3062,7 @@ declare const SHARE_HASHTAG = "#VibingLaVidaLoca";
|
|
|
1547
3062
|
* The non-affiliation line. Non-optional in every snippet: a shared artifact is
|
|
1548
3063
|
* decontextualized, so the legal disclaimer must travel with every paste.
|
|
1549
3064
|
*/
|
|
1550
|
-
declare const SHARE_DISCLAIMER = "Independent fan project \u00B7
|
|
3065
|
+
declare const SHARE_DISCLAIMER = "Independent fan project \u00B7 Not affiliated with FIFA, any confederation, league or club, or Anthropic.";
|
|
1551
3066
|
interface ShareSnippetOptions {
|
|
1552
3067
|
/** Snippet shape; defaults to `social`. */
|
|
1553
3068
|
style?: ShareStyle;
|
|
@@ -1578,16 +3093,31 @@ interface ShareSnippetInput {
|
|
|
1578
3093
|
* ZZZ."), so an unknown/empty target yields a clear card instead of a void.
|
|
1579
3094
|
*/
|
|
1580
3095
|
emptyNote?: string;
|
|
3096
|
+
/**
|
|
3097
|
+
* A sentence that QUALIFIES the card (a verdict's, e.g. "Fixture data may
|
|
3098
|
+
* be incomplete."): printed whenever it is set, right after the title and
|
|
3099
|
+
* BEFORE the body, on a populated card and an empty one alike. A card is
|
|
3100
|
+
* pasted where nobody can ask, so what it holds must not look like all
|
|
3101
|
+
* there was. (A verdict that REPLACES the body is the `emptyNote`.)
|
|
3102
|
+
*/
|
|
3103
|
+
note?: string;
|
|
1581
3104
|
/** Exact run cue to advertise, e.g. "npx @claudinho/cli next MEX". */
|
|
1582
3105
|
installLine?: string;
|
|
1583
3106
|
/**
|
|
1584
3107
|
* True when the live fetch failed and these are static fixtures (no live
|
|
1585
3108
|
* scores). A pasted card must say so — otherwise a "no matches" / scheduled
|
|
1586
3109
|
* card reads as authoritative when the feed is actually down. When set with
|
|
1587
|
-
* matches present, a not-live notice is
|
|
1588
|
-
* caller picks a feed-down `emptyNote`.
|
|
3110
|
+
* matches present, a not-live notice is printed before the matches (after
|
|
3111
|
+
* the `note`); for the empty case the caller picks a feed-down `emptyNote`.
|
|
1589
3112
|
*/
|
|
1590
3113
|
degraded?: boolean;
|
|
3114
|
+
/**
|
|
3115
|
+
* The not-live sentence a degraded card with records prints, when its
|
|
3116
|
+
* builder knows better than the default (which says the records are the
|
|
3117
|
+
* bundled schedule's): off the bundled competition a match card's record is
|
|
3118
|
+
* the provider's own earlier one.
|
|
3119
|
+
*/
|
|
3120
|
+
degradedNote?: string;
|
|
1591
3121
|
/** Timezone for kickoff date/time (date/time only — copy stays English). */
|
|
1592
3122
|
tz?: string;
|
|
1593
3123
|
/** Locale for kickoff date/time. */
|
|
@@ -1600,9 +3130,17 @@ interface ShareSnippetInput {
|
|
|
1600
3130
|
*/
|
|
1601
3131
|
declare function formatShareSnippet(input: ShareSnippetInput, options?: ShareSnippetOptions): string;
|
|
1602
3132
|
interface ShareTableInput {
|
|
3133
|
+
/**
|
|
3134
|
+
* The card's first line, naming the competition (`Premier League ·
|
|
3135
|
+
* standings`), before the tables (each keeps its own title); absent on a
|
|
3136
|
+
* card built with no competition.
|
|
3137
|
+
*/
|
|
3138
|
+
heading?: string;
|
|
1603
3139
|
/** Group tables to render (1..n); each in standings order. */
|
|
1604
3140
|
tables: readonly {
|
|
1605
3141
|
group: string;
|
|
3142
|
+
/** The provider's name for the table; absent for a lettered group. */
|
|
3143
|
+
label?: string;
|
|
1606
3144
|
rows: readonly StandingRow[];
|
|
1607
3145
|
/** Rows the provider served that could not be read (see GroupStandings). */
|
|
1608
3146
|
partial?: {
|
|
@@ -1615,6 +3153,12 @@ interface ShareTableInput {
|
|
|
1615
3153
|
installLine?: string;
|
|
1616
3154
|
/** Body line when there are no tables (e.g. "No group Z."). */
|
|
1617
3155
|
emptyNote?: string;
|
|
3156
|
+
/**
|
|
3157
|
+
* Stated before the tables when they are not the whole competition (a table
|
|
3158
|
+
* the provider sent could not be read). A card is pasted where nobody can
|
|
3159
|
+
* ask: the tables that were read must not look like all of them.
|
|
3160
|
+
*/
|
|
3161
|
+
incompleteNote?: string;
|
|
1618
3162
|
/**
|
|
1619
3163
|
* True when no authoritative table was available. Non-empty rows are a
|
|
1620
3164
|
* static roster, not live results; an empty open-scope outage is described by
|
|
@@ -1631,33 +3175,6 @@ interface ShareTableInput {
|
|
|
1631
3175
|
*/
|
|
1632
3176
|
declare function formatShareTable(input: ShareTableInput, options?: ShareSnippetOptions): string;
|
|
1633
3177
|
|
|
1634
|
-
declare function matchKey(stage: Stage, index: number): string;
|
|
1635
|
-
/**
|
|
1636
|
-
* Build and validate bracket topology from knockout fixtures.
|
|
1637
|
-
* Throws when ESPN introduces an unparsed placeholder or the graph is inconsistent.
|
|
1638
|
-
*/
|
|
1639
|
-
declare function buildBracketTopology(matches: Match[], generatedAt: string): BracketTopology;
|
|
1640
|
-
|
|
1641
|
-
/**
|
|
1642
|
-
* Parse an ESPN home/away label into a bracket slot reference (name patterns only).
|
|
1643
|
-
* Real nation flags are mapped to `seed` — never `team` — so the bundled schedule
|
|
1644
|
-
* cannot treat ESPN pre-draw labels as confirmed participants.
|
|
1645
|
-
*/
|
|
1646
|
-
declare function parseTeamSlot(team: Team): SlotRef | null;
|
|
1647
|
-
|
|
1648
|
-
/**
|
|
1649
|
-
* Resolve the bundled topology against merged knockout matches and standings.
|
|
1650
|
-
* Group slots project from live standings once a group has started; confirmed when
|
|
1651
|
-
* the group is fully played. Winner/loser slots require a confirmed FT result.
|
|
1652
|
-
*/
|
|
1653
|
-
declare function buildBracketView(topology: BracketTopology, matches: Match[], tables: GroupStandings[], standingsDegraded: boolean, liveDegraded: boolean, filterStage?: string, lang?: string): BracketView;
|
|
1654
|
-
|
|
1655
|
-
/** True when a team carries a real nation flag (not a bracket placeholder). */
|
|
1656
|
-
declare function isResolvedNation(team: Team): boolean;
|
|
1657
|
-
|
|
1658
|
-
/** The bundled knockout bracket topology (generated by gen:schedule). */
|
|
1659
|
-
declare function loadBracketTopology(): BracketTopology;
|
|
1660
|
-
|
|
1661
3178
|
interface BracketFormatOpts {
|
|
1662
3179
|
flags?: boolean;
|
|
1663
3180
|
tz?: string;
|
|
@@ -1678,10 +3195,24 @@ declare function formatBracketTree(view: BracketView, opts?: BracketFormatOpts &
|
|
|
1678
3195
|
footer?: boolean;
|
|
1679
3196
|
}): string | null;
|
|
1680
3197
|
interface ShareBracketInput {
|
|
3198
|
+
/**
|
|
3199
|
+
* The card's first line, as its builder states it in the reader's language
|
|
3200
|
+
* (`Knockout bracket · World Cup`: the title, then the competition's name;
|
|
3201
|
+
* no year, the ties carry their dates). Absent on an input built by hand,
|
|
3202
|
+
* whose first line is the title in the options' locale.
|
|
3203
|
+
*/
|
|
3204
|
+
title?: string;
|
|
1681
3205
|
view: BracketView;
|
|
1682
3206
|
source?: string;
|
|
1683
3207
|
installLine?: string;
|
|
1684
3208
|
emptyNote?: string;
|
|
3209
|
+
/**
|
|
3210
|
+
* A sentence that QUALIFIES the card (a verdict's, e.g. "Fixture data may
|
|
3211
|
+
* be incomplete."): printed whenever it is set, after the title and BEFORE
|
|
3212
|
+
* the tree, on a populated card and an empty one alike. (A verdict that
|
|
3213
|
+
* REPLACES the tree is the `emptyNote`.)
|
|
3214
|
+
*/
|
|
3215
|
+
note?: string;
|
|
1685
3216
|
}
|
|
1686
3217
|
interface ShareBracketOptions {
|
|
1687
3218
|
includeHashtag?: boolean;
|
|
@@ -1696,4 +3227,688 @@ declare function formatShareBracket(input: ShareBracketInput, options?: ShareBra
|
|
|
1696
3227
|
/** Compact one-line-per-match bracket for narrow share contexts. */
|
|
1697
3228
|
declare function formatBracketCompactLine(mv: BracketMatchView, opts?: BracketFormatOpts): string;
|
|
1698
3229
|
|
|
1699
|
-
|
|
3230
|
+
/**
|
|
3231
|
+
* Share cards, assembled once.
|
|
3232
|
+
*
|
|
3233
|
+
* `claudinho share` and the MCP `get_share_snippet` tool hand out the same
|
|
3234
|
+
* artifact, and each used to build it for itself: the title, the note an empty
|
|
3235
|
+
* card shows, the run cue, which provider (if any) is attributed. Two copies of
|
|
3236
|
+
* that assembly is two places for a verdict to be dropped and for the copy to
|
|
3237
|
+
* drift. A surface now gets the card from here and only decides how to
|
|
3238
|
+
* serialize it — and, for the MCP server, how many records it may hold: the
|
|
3239
|
+
* builders take the caller's list, so a surface that bounds its payload passes
|
|
3240
|
+
* the bounded one.
|
|
3241
|
+
*
|
|
3242
|
+
* Copy stays English (see AGENTS.md: share snippets are English-only in v1,
|
|
3243
|
+
* except the localized bracket card); the sentences that ARE localized here
|
|
3244
|
+
* are a verdict's, which are the same sentences on every surface. A verdict
|
|
3245
|
+
* that replaces the body becomes the card's `emptyNote`; the ones that qualify
|
|
3246
|
+
* it become its `note`, printed beside a populated body and an empty one.
|
|
3247
|
+
*
|
|
3248
|
+
* The empty bodies off the bundled competition say what was searched: `next`
|
|
3249
|
+
* ("no fixture for X within the next 14 days") and `match <id>` ("not found
|
|
3250
|
+
* between A and B") after a whole read, and "none read" after one that was not
|
|
3251
|
+
* whole ("no fixture for X was read in this span", beside the partial
|
|
3252
|
+
* sentence). They are the BODY's own text, not verdicts, and they are built
|
|
3253
|
+
* here ({@link nextHorizonSentence}, {@link matchWindowSentence},
|
|
3254
|
+
* {@link nextNoneReadSentence}, {@link matchNoneReadSentence}) for every
|
|
3255
|
+
* surface: the CLI and the MCP tools print the same sentence, from this one
|
|
3256
|
+
* place, and a card carries its span as a plain field (`span`).
|
|
3257
|
+
*
|
|
3258
|
+
* The same holds for the live and dated reads (0.11 2.1d): an empty body after
|
|
3259
|
+
* a read that was not whole says nothing was READ ({@link liveNoneRead},
|
|
3260
|
+
* {@link dateNoneRead}, {@link marketsNoneRead}), and a day's attribution on
|
|
3261
|
+
* such a read is decided over the fixtures a surface displays
|
|
3262
|
+
* ({@link dayAttribution}: the "Live data" line, and the sentence counting
|
|
3263
|
+
* the bundled rows the read did not serve).
|
|
3264
|
+
*/
|
|
3265
|
+
|
|
3266
|
+
/**
|
|
3267
|
+
* The sentence for a `next` whose WHOLE read of the span held no fixture for
|
|
3268
|
+
* the club (`horizon` stated), in the reader's language; undefined otherwise.
|
|
3269
|
+
* It names the resolved club, else the query as asked, else `fallback`.
|
|
3270
|
+
*/
|
|
3271
|
+
declare function nextHorizonSentence(result: NextFixtureResult, fallback: string, lang: string | undefined): string | undefined;
|
|
3272
|
+
/**
|
|
3273
|
+
* The sentence for a `match <id>` that a WHOLE read of the span did not hold
|
|
3274
|
+
* (`window` stated), in the reader's language; undefined otherwise. Not "no
|
|
3275
|
+
* such match": the id was not in the provider days searched.
|
|
3276
|
+
*/
|
|
3277
|
+
declare function matchWindowSentence(result: MatchByIdResult, id: string, lang: string | undefined): string | undefined;
|
|
3278
|
+
/**
|
|
3279
|
+
* The sentence for an EMPTY `next` off the bundled competition whose read was
|
|
3280
|
+
* not whole (`partial` stated, no fixture, not degraded): no fixture for the
|
|
3281
|
+
* club was READ in the span, which is not "none exists". Undefined otherwise.
|
|
3282
|
+
* (On the bundle the partial empty card keeps its own sentence.)
|
|
3283
|
+
*/
|
|
3284
|
+
declare function nextNoneReadSentence(result: NextFixtureResult, fallback: string, lang: string | undefined): string | undefined;
|
|
3285
|
+
/**
|
|
3286
|
+
* The sentence for an EMPTY `match <id>` whose read was not whole (`partial`
|
|
3287
|
+
* stated, no record, not degraded): no match with that id was READ in the
|
|
3288
|
+
* span, which is not "no such match". Undefined otherwise.
|
|
3289
|
+
*/
|
|
3290
|
+
declare function matchNoneReadSentence(result: MatchByIdResult, id: string, lang: string | undefined): string | undefined;
|
|
3291
|
+
/**
|
|
3292
|
+
* "No match in play was read", in the reader's language: an EMPTY live body
|
|
3293
|
+
* after a read that was not whole, which is not "nothing is in play". (When
|
|
3294
|
+
* it applies: {@link liveNoneRead}.)
|
|
3295
|
+
*/
|
|
3296
|
+
declare function liveNoneReadSentence(lang?: string): string;
|
|
3297
|
+
/**
|
|
3298
|
+
* "No fixture was read for {date}", in the reader's language: an EMPTY day
|
|
3299
|
+
* after a read that was not whole and merged no bundled schedule, which is not
|
|
3300
|
+
* "no matches scheduled". `date` is printed as given: the card passes its
|
|
3301
|
+
* human label, the CLI and MCP the date they print. (When it applies:
|
|
3302
|
+
* {@link dateNoneRead}.)
|
|
3303
|
+
*/
|
|
3304
|
+
declare function dateNoneReadSentence(date: string, lang?: string): string;
|
|
3305
|
+
/**
|
|
3306
|
+
* "Couldn't reach the data provider — no fixtures confirmed for {date}", in
|
|
3307
|
+
* the reader's language: the EMPTY day of a read that FAILED and merged no
|
|
3308
|
+
* bundled schedule (off the bundle), where nothing but the provider knows the
|
|
3309
|
+
* fixtures, so an outage must not read as "no matches scheduled". `date` is
|
|
3310
|
+
* printed as given: the card passes its human label, the CLI and MCP the date
|
|
3311
|
+
* they print. (When it applies: {@link dateUnreached}.)
|
|
3312
|
+
*/
|
|
3313
|
+
declare function dateUnreachedSentence(date: string, lang?: string): string;
|
|
3314
|
+
/**
|
|
3315
|
+
* The sentence beside a day's partial verdict that counts the displayed
|
|
3316
|
+
* fixtures the read did not serve: they are the bundled schedule's rows, and
|
|
3317
|
+
* their live state is unconfirmed. Singular and plural, in the reader's
|
|
3318
|
+
* language. (When it applies: {@link dayAttribution}.)
|
|
3319
|
+
*/
|
|
3320
|
+
declare function unservedSentence(n: number, lang?: string): string;
|
|
3321
|
+
/**
|
|
3322
|
+
* "No market signal among the fixtures read for {date}": an EMPTY dated
|
|
3323
|
+
* market answer whose fixture read was not whole, which is not "none for the
|
|
3324
|
+
* date". Market copy from the approved bank (`marketsNoneReadNote`), English
|
|
3325
|
+
* on every locale. (When it applies: {@link marketsNoneRead}.)
|
|
3326
|
+
*/
|
|
3327
|
+
declare function marketsNoneReadSentence(date: string): string;
|
|
3328
|
+
/**
|
|
3329
|
+
* What a live or dated read says about itself, as the empty bodies and the
|
|
3330
|
+
* day's attribution read it: its verdicts, whether it failed, whether it
|
|
3331
|
+
* merged the bundled schedule, and what the provider's window held.
|
|
3332
|
+
*/
|
|
3333
|
+
type ReadAccount = VerdictSource & {
|
|
3334
|
+
readonly degraded?: boolean;
|
|
3335
|
+
readonly skeleton?: true;
|
|
3336
|
+
readonly served?: readonly string[];
|
|
3337
|
+
};
|
|
3338
|
+
/**
|
|
3339
|
+
* The empty body of a live read that was not whole (`partial` stated, not
|
|
3340
|
+
* degraded): {@link liveNoneReadSentence}. Undefined otherwise: a whole read
|
|
3341
|
+
* with nothing in play says so in the surface's own words.
|
|
3342
|
+
*/
|
|
3343
|
+
declare function liveNoneRead(result: ReadAccount, lang?: string): string | undefined;
|
|
3344
|
+
/**
|
|
3345
|
+
* The empty body of a dated read that was not whole and merged NO bundled
|
|
3346
|
+
* schedule (off the bundle, or the bundle's slug answering for another
|
|
3347
|
+
* edition): {@link dateNoneReadSentence}. Undefined otherwise: where the
|
|
3348
|
+
* skeleton was merged the day's list is the bundle's, whole, and its empty
|
|
3349
|
+
* body is the bundle's answer.
|
|
3350
|
+
*/
|
|
3351
|
+
declare function dateNoneRead(result: ReadAccount, date: string, lang?: string): string | undefined;
|
|
3352
|
+
/**
|
|
3353
|
+
* The empty body of a dated read that FAILED and merged no bundled schedule:
|
|
3354
|
+
* {@link dateUnreachedSentence}. Undefined otherwise: a failed read on the
|
|
3355
|
+
* bundle shows the bundle's day (the schedule is known without the provider),
|
|
3356
|
+
* and a read that answered says what it read.
|
|
3357
|
+
*/
|
|
3358
|
+
declare function dateUnreached(result: ReadAccount, date: string, lang?: string): string | undefined;
|
|
3359
|
+
/**
|
|
3360
|
+
* The empty body of a dated MARKET answer whose fixture read was not whole:
|
|
3361
|
+
* {@link marketsNoneReadSentence} (market copy, English). Undefined otherwise.
|
|
3362
|
+
* Separate from the market requests' own completeness (`complete`), which
|
|
3363
|
+
* says something else.
|
|
3364
|
+
*/
|
|
3365
|
+
declare function marketsNoneRead(result: ReadAccount, date: string): string | undefined;
|
|
3366
|
+
/** What a day's surface says about the provider, for the fixtures it displays. */
|
|
3367
|
+
interface DayAttribution {
|
|
3368
|
+
/** Whether the "Live data" line is printed (the structured `source` keeps the provider either way). */
|
|
3369
|
+
attributed: boolean;
|
|
3370
|
+
/** The sentence counting the displayed fixtures the read did not serve, when there are any. */
|
|
3371
|
+
unserved?: string;
|
|
3372
|
+
}
|
|
3373
|
+
/**
|
|
3374
|
+
* The day's attribution, ONE rule for the CLI, the MCP tool and the date card,
|
|
3375
|
+
* computed over the fixtures the surface finally DISPLAYS (after the date
|
|
3376
|
+
* filter and any bound). A whole read, a degraded one, and a read that does not
|
|
3377
|
+
* say what it served change nothing: the line is printed as it always was. On
|
|
3378
|
+
* a read that was not whole:
|
|
3379
|
+
* - when some displayed fixtures are not among the ids the window held
|
|
3380
|
+
* (`served`), they are the bundled schedule's rows, whose live state is
|
|
3381
|
+
* unconfirmed: the sentence says how many;
|
|
3382
|
+
* - when NONE of the displayed fixtures was served, the "Live data" line is
|
|
3383
|
+
* not printed: a provider is never named for a day it served nothing of.
|
|
3384
|
+
* (An empty day attributes no fixture, and keeps the line.)
|
|
3385
|
+
*/
|
|
3386
|
+
declare function dayAttribution(read: ReadAccount, shown: readonly Match[], lang?: string): DayAttribution;
|
|
3387
|
+
/** Where and in which language a card's dates are rendered. */
|
|
3388
|
+
interface ShareCardContext {
|
|
3389
|
+
tz?: string;
|
|
3390
|
+
locale?: string;
|
|
3391
|
+
/**
|
|
3392
|
+
* The competition the card is about (its slug). Its title names it and its
|
|
3393
|
+
* run cue selects it (`npx @claudinho/cli --competition premier-league next
|
|
3394
|
+
* Arsenal`), the World Cup's too: a pasted card must answer the same thing
|
|
3395
|
+
* whatever the recipient follows. A card built with none (a test) keeps the
|
|
3396
|
+
* bare title and cue.
|
|
3397
|
+
*/
|
|
3398
|
+
competition?: string;
|
|
3399
|
+
}
|
|
3400
|
+
/** Display-ready market signals for a card, and whether every fixture was checked. */
|
|
3401
|
+
interface ShareCardMarket {
|
|
3402
|
+
signals: Map<string, MarketSignal>;
|
|
3403
|
+
complete: boolean;
|
|
3404
|
+
}
|
|
3405
|
+
/** What a surface adds when it shows fewer records than exist. */
|
|
3406
|
+
interface ShareCardView {
|
|
3407
|
+
/** The records the surface will show (default: all of the result's). */
|
|
3408
|
+
matches?: readonly Match[];
|
|
3409
|
+
/**
|
|
3410
|
+
* The sentence a surface that bounds the list states, e.g. "(showing 20 of
|
|
3411
|
+
* 31 — list truncated)". It QUALIFIES the body, so it is printed in the
|
|
3412
|
+
* card's `note`, after the verdict's sentences and before the body, like a
|
|
3413
|
+
* tool's; the title stays bare.
|
|
3414
|
+
*/
|
|
3415
|
+
cap?: string;
|
|
3416
|
+
}
|
|
3417
|
+
/** A match card: what to format, what it is, and the verdict it carries. */
|
|
3418
|
+
interface MatchShareCard {
|
|
3419
|
+
kind: 'today' | 'live' | 'next' | 'match';
|
|
3420
|
+
target: string;
|
|
3421
|
+
/**
|
|
3422
|
+
* A next card's team: the club the query resolved to (its provider id,
|
|
3423
|
+
* code and name) when one was, the query (a nation's code on the World Cup)
|
|
3424
|
+
* otherwise.
|
|
3425
|
+
*/
|
|
3426
|
+
team?: string | Team;
|
|
3427
|
+
/** A next card for a name two or more teams match: them, and no fixture. */
|
|
3428
|
+
candidates?: Team[];
|
|
3429
|
+
input: ShareSnippetInput;
|
|
3430
|
+
/** The structured verdict keys the card's note stands for (see `verdictExtras`). */
|
|
3431
|
+
verdict: VerdictExtras;
|
|
3432
|
+
/**
|
|
3433
|
+
* The span an empty card says was searched, as plain fields for the
|
|
3434
|
+
* structured twin (`horizon` for `next`, `window` for a match id); empty
|
|
3435
|
+
* when the card states none. Not a verdict.
|
|
3436
|
+
*/
|
|
3437
|
+
span: {
|
|
3438
|
+
horizon?: {
|
|
3439
|
+
days: number;
|
|
3440
|
+
};
|
|
3441
|
+
window?: {
|
|
3442
|
+
from: string;
|
|
3443
|
+
to: string;
|
|
3444
|
+
};
|
|
3445
|
+
};
|
|
3446
|
+
/**
|
|
3447
|
+
* Plain fields of the card's read for its structured twin, spread beside
|
|
3448
|
+
* `source`: the card's shown fixtures whose record the window held, beside
|
|
3449
|
+
* `partial` only (`servedExtras`); empty otherwise. Not a verdict.
|
|
3450
|
+
*/
|
|
3451
|
+
readFields: {
|
|
3452
|
+
served?: readonly string[];
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
/** Matches in play right now. No market lines: a live card stays lean. */
|
|
3456
|
+
declare function liveShareCard(result: LiveResult, ctx: ShareCardContext, view?: ShareCardView): MatchShareCard;
|
|
3457
|
+
/** A team's next fixture. */
|
|
3458
|
+
declare function nextShareCard(result: NextFixtureResult, code: string, market: ShareCardMarket, ctx: ShareCardContext): MatchShareCard;
|
|
3459
|
+
/** One match, by id. */
|
|
3460
|
+
declare function matchShareCard(result: MatchByIdResult, id: string, market: ShareCardMarket, ctx: ShareCardContext): MatchShareCard;
|
|
3461
|
+
/** A day's matches: today's, or an explicitly requested date's. */
|
|
3462
|
+
declare function dateShareCard(day: {
|
|
3463
|
+
/** The calendar date shown (YYYY-MM-DD). */
|
|
3464
|
+
date: string;
|
|
3465
|
+
/** True when the caller named the date; false for "today". */
|
|
3466
|
+
explicit: boolean;
|
|
3467
|
+
/** The day's fixtures the surface will show. */
|
|
3468
|
+
matches: readonly Match[];
|
|
3469
|
+
degraded: boolean;
|
|
3470
|
+
source?: string;
|
|
3471
|
+
/** The list's cap when the surface bounded it (see `ShareCardView.cap`): in the note, not the title. */
|
|
3472
|
+
cap?: string;
|
|
3473
|
+
/**
|
|
3474
|
+
* The dated read itself: the verdicts the card states (between editions,
|
|
3475
|
+
* partial), whether it merged the bundled schedule (`skeleton`: the ONE
|
|
3476
|
+
* input for what an empty day may say), and what it served (the card's
|
|
3477
|
+
* attribution, over the fixtures it shows). A card handed no read is a
|
|
3478
|
+
* whole read that merged no schedule.
|
|
3479
|
+
*/
|
|
3480
|
+
read?: ReadAccount;
|
|
3481
|
+
}, market: ShareCardMarket, ctx: ShareCardContext): MatchShareCard;
|
|
3482
|
+
/** A standings card: facts only, never a market line. */
|
|
3483
|
+
interface TableShareCard {
|
|
3484
|
+
group?: string;
|
|
3485
|
+
input: ShareTableInput;
|
|
3486
|
+
/** Null when degraded: a roster served by no provider is attributed to none. */
|
|
3487
|
+
source: string | undefined;
|
|
3488
|
+
degraded: boolean;
|
|
3489
|
+
/** The card's tables in structured form, each with the verdict it carries. */
|
|
3490
|
+
tables: TableData[];
|
|
3491
|
+
/** The structured verdict keys of the READ (see `verdictExtras`): `incomplete` when tables are missing. */
|
|
3492
|
+
verdict: VerdictExtras;
|
|
3493
|
+
}
|
|
3494
|
+
declare function tableShareCard(result: StandingsResult, group: string | undefined,
|
|
3495
|
+
/** The tables the surface will show (default: all of the result's). */
|
|
3496
|
+
tables?: readonly GroupStandings[],
|
|
3497
|
+
/** The reader's language, for the one localized sentence a card prints: a verdict's. */
|
|
3498
|
+
lang?: string,
|
|
3499
|
+
/** The competition the card is about: its first line names it, its run cue selects it (see `ShareCardContext`). */
|
|
3500
|
+
competition?: string): TableShareCard;
|
|
3501
|
+
/** A knockout bracket card. Its empty note is localized, like the card itself. */
|
|
3502
|
+
interface BracketShareCard {
|
|
3503
|
+
stage?: string;
|
|
3504
|
+
input: ShareBracketInput;
|
|
3505
|
+
source: string | undefined;
|
|
3506
|
+
degraded: boolean;
|
|
3507
|
+
verdict: VerdictExtras;
|
|
3508
|
+
}
|
|
3509
|
+
declare function bracketShareCard(result: BracketResult, stage: string | undefined, lang: string | undefined,
|
|
3510
|
+
/** The competition the card is about: its first line names it, its run cue selects it (see `ShareCardContext`). */
|
|
3511
|
+
competition?: string): BracketShareCard;
|
|
3512
|
+
|
|
3513
|
+
declare function matchKey(stage: Stage, index: number): string;
|
|
3514
|
+
/**
|
|
3515
|
+
* Build and validate bracket topology from knockout fixtures.
|
|
3516
|
+
* Throws when ESPN introduces an unparsed placeholder or the graph is inconsistent.
|
|
3517
|
+
*/
|
|
3518
|
+
declare function buildBracketTopology(matches: Match[], generatedAt: string): BracketTopology;
|
|
3519
|
+
|
|
3520
|
+
/**
|
|
3521
|
+
* Parse an ESPN home/away label into a bracket slot reference (name patterns only).
|
|
3522
|
+
* Real nation flags are mapped to `seed` — never `team` — so the bundled schedule
|
|
3523
|
+
* cannot treat ESPN pre-draw labels as confirmed participants.
|
|
3524
|
+
*/
|
|
3525
|
+
declare function parseTeamSlot(team: Team): SlotRef | null;
|
|
3526
|
+
|
|
3527
|
+
/**
|
|
3528
|
+
* Resolve the bundled topology against merged knockout matches and standings.
|
|
3529
|
+
* Group slots project from live standings once a group has started; confirmed when
|
|
3530
|
+
* the group is fully played. Winner/loser slots require a confirmed FT result.
|
|
3531
|
+
*/
|
|
3532
|
+
declare function buildBracketView(topology: BracketTopology, matches: Match[], tables: GroupStandings[], standingsDegraded: boolean, liveDegraded: boolean, filterStage?: string, lang?: string): BracketView;
|
|
3533
|
+
|
|
3534
|
+
/**
|
|
3535
|
+
* True exactly when a side is a placeholder: the neutral 🏳️ a nation
|
|
3536
|
+
* competition's side carries when its name is no nation (the bundle's
|
|
3537
|
+
* unresolved knockout slots, "Round of 32 1 Winner"). A side with NO flag is a
|
|
3538
|
+
* club (a club has none, by the competition's written kind) and is RESOLVED: it
|
|
3539
|
+
* is a real team, drawn by its name or code. "Not a placeholder" is not "has a
|
|
3540
|
+
* nation's flag".
|
|
3541
|
+
*/
|
|
3542
|
+
declare function isPlaceholderSide(team: Pick<Team, 'flag'>): boolean;
|
|
3543
|
+
|
|
3544
|
+
/** The bundled knockout bracket topology (generated by gen:schedule). */
|
|
3545
|
+
declare function loadBracketTopology(): BracketTopology;
|
|
3546
|
+
|
|
3547
|
+
/**
|
|
3548
|
+
* Where a selection came from: the command line's `--competition` or a tool's
|
|
3549
|
+
* `competition` argument (`flag`), `CLAUDINHO_COMPETITION` (`env`), or the
|
|
3550
|
+
* user's saved choice (`saved`: the config file `claudinho follow` writes).
|
|
3551
|
+
* Nothing else chooses: with none of the three there is no competition
|
|
3552
|
+
* (`{ kind: 'none' }`), never a default.
|
|
3553
|
+
*/
|
|
3554
|
+
type ChosenBy = 'flag' | 'env' | 'saved';
|
|
3555
|
+
/** A competition a request is for, and where that choice came from. */
|
|
3556
|
+
interface SelectedCompetition {
|
|
3557
|
+
readonly kind: 'selected';
|
|
3558
|
+
/** The provider's slug the request asks for. */
|
|
3559
|
+
readonly slug: string;
|
|
3560
|
+
/** The table's alias; absent for a raw slug. */
|
|
3561
|
+
readonly alias?: string;
|
|
3562
|
+
/** What a surface names it: the table's name, or a raw slug as it is. */
|
|
3563
|
+
readonly name: string;
|
|
3564
|
+
readonly chosenBy: ChosenBy;
|
|
3565
|
+
/** A raw slug the table does not hold: the escape hatch, labelled. */
|
|
3566
|
+
readonly experimental: boolean;
|
|
3567
|
+
}
|
|
3568
|
+
/**
|
|
3569
|
+
* What the edge resolved: a competition; a value that is neither an alias nor
|
|
3570
|
+
* a slug, REFUSED with the aliases (an unknown value is never a request); or
|
|
3571
|
+
* `none`: nothing was chosen (no flag, no environment, no saved choice). The
|
|
3572
|
+
* finished World Cup is not a default anyone falls into without choosing.
|
|
3573
|
+
*/
|
|
3574
|
+
type CompetitionSelection = SelectedCompetition | {
|
|
3575
|
+
readonly kind: 'refused';
|
|
3576
|
+
readonly value: string;
|
|
3577
|
+
readonly aliases: string[];
|
|
3578
|
+
/** The source that gave the refused value (a flag or the environment: the file's reader refuses its own). */
|
|
3579
|
+
readonly chosenBy: ChosenBy;
|
|
3580
|
+
} | {
|
|
3581
|
+
readonly kind: 'none';
|
|
3582
|
+
};
|
|
3583
|
+
/**
|
|
3584
|
+
* The ONE constructor of a selected competition: a slug and where it was
|
|
3585
|
+
* chosen, described by the table (its alias and name) or, for a slug the
|
|
3586
|
+
* table does not hold, as itself and experimental. The resolver builds every
|
|
3587
|
+
* selection through it; an edge that already holds a slug (a config built
|
|
3588
|
+
* without the resolver) describes it through it too, never by hand.
|
|
3589
|
+
*/
|
|
3590
|
+
declare function selectedCompetition(slug: string, chosenBy: ChosenBy): SelectedCompetition;
|
|
3591
|
+
/**
|
|
3592
|
+
* THE EDGE. The one function that decides which competition a request is for,
|
|
3593
|
+
* from the values its edge hands in: the flag (`--competition`, a tool's
|
|
3594
|
+
* `competition` argument), then the environment (`CLAUDINHO_COMPETITION`), then
|
|
3595
|
+
* the saved choice (the config file's `competition`, read at the edge through
|
|
3596
|
+
* core's one reader: `readUserConfig`). The FIRST present source decides (an
|
|
3597
|
+
* empty string is absent): a present value that is refused is refused, even
|
|
3598
|
+
* when a lower source holds a valid one (what was asked for is not answered
|
|
3599
|
+
* with something else). With no source present, `none`: nothing is chosen,
|
|
3600
|
+
* and every competition-answering surface says so instead of answering.
|
|
3601
|
+
*
|
|
3602
|
+
* Each value is an alias (`premier-league`), a slug in the table (`eng.1`), or
|
|
3603
|
+
* a raw ESPN slug the table does not hold (`fifa.friendly`, `esp.copa_del_rey`:
|
|
3604
|
+
* lower-case segments of letters, digits and underscores joined by dots, 64
|
|
3605
|
+
* units at most; experimental);
|
|
3606
|
+
* anything else (`foo`, `ENG.1`, a space) is refused with the aliases.
|
|
3607
|
+
*
|
|
3608
|
+
* Core reads NO environment and no file: the edges pass them. It is called
|
|
3609
|
+
* where a request ENTERS (the CLI's option resolution, the MCP server's
|
|
3610
|
+
* request) and nowhere else. From there the competition travels as a value
|
|
3611
|
+
* (on the config, on the adapter), so nothing further down can answer for a
|
|
3612
|
+
* different competition halfway through. `core/test/selection-identity.test.ts`
|
|
3613
|
+
* fails if a call appears anywhere else.
|
|
3614
|
+
*/
|
|
3615
|
+
declare function resolveCompetition(explicit?: string, env?: string, saved?: string): CompetitionSelection;
|
|
3616
|
+
/**
|
|
3617
|
+
* The line every competition-answering text answer prints after its header:
|
|
3618
|
+
* the competition's name, then where the choice came from when it was the
|
|
3619
|
+
* flag or the environment (`from the command line`; on MCP, `from the
|
|
3620
|
+
* request`; `from the environment`), then `experimental` for a raw slug. A
|
|
3621
|
+
* saved choice prints the name alone. The name is not localized; the rest is.
|
|
3622
|
+
* Empty for a selection that is not a competition.
|
|
3623
|
+
*/
|
|
3624
|
+
declare function modeLine(selection: CompetitionSelection, lang?: string,
|
|
3625
|
+
/** What a flag is on this surface: the command line's, or a tool request's argument. */
|
|
3626
|
+
flag?: 'command' | 'request'): string;
|
|
3627
|
+
/** The structured form of a selection: what `--json` and MCP `data` carry. */
|
|
3628
|
+
interface CompetitionKey {
|
|
3629
|
+
readonly slug: string;
|
|
3630
|
+
readonly alias?: string;
|
|
3631
|
+
readonly name: string;
|
|
3632
|
+
readonly chosenBy: ChosenBy;
|
|
3633
|
+
readonly experimental?: true;
|
|
3634
|
+
}
|
|
3635
|
+
/**
|
|
3636
|
+
* The selection as ONE structured key, `competition`, for every
|
|
3637
|
+
* competition-answering structured answer (CLI `--json`, MCP `data`): the
|
|
3638
|
+
* alias only when there is one, `experimental` only when true; `null` when
|
|
3639
|
+
* nothing is chosen (beside the `noCompetition` verdict: see
|
|
3640
|
+
* {@link selectionVerdict}). Nothing for a refused value (it is an error, never
|
|
3641
|
+
* an answer). Every emit site spreads this; none builds the key by hand.
|
|
3642
|
+
*/
|
|
3643
|
+
declare function selectionExtras(selection: CompetitionSelection): {
|
|
3644
|
+
competition?: CompetitionKey | null;
|
|
3645
|
+
};
|
|
3646
|
+
/**
|
|
3647
|
+
* The verdict a selection states about every answer made under it: with
|
|
3648
|
+
* nothing chosen, `noCompetition` (the first replacing verdict: its sentence
|
|
3649
|
+
* stands instead of any answer, and its key goes beside `competition: null`);
|
|
3650
|
+
* nothing otherwise. A surface hands THIS to `verdictNotice`/`verdictExtras`,
|
|
3651
|
+
* never an object it builds itself.
|
|
3652
|
+
*/
|
|
3653
|
+
declare function selectionVerdict(selection: CompetitionSelection): VerdictSource;
|
|
3654
|
+
/**
|
|
3655
|
+
* Why a selection is not a competition, in the reader's language; undefined
|
|
3656
|
+
* for a competition. A refused value names the value (bounded) and the
|
|
3657
|
+
* aliases; nothing chosen is the CLI's sentence (`claudinho follow <alias>`,
|
|
3658
|
+
* `follow --list`): MCP answers nothing chosen with the `noCompetition`
|
|
3659
|
+
* verdict's own sentence (`verdictNotice`), never this one. The CLI raises it
|
|
3660
|
+
* as an input error, MCP a refused value as a tool error, before any request.
|
|
3661
|
+
*/
|
|
3662
|
+
declare function selectionRefusal(selection: CompetitionSelection, lang?: string): string | undefined;
|
|
3663
|
+
/** The competition whose schedule ships bundled in the clients: the World Cup. */
|
|
3664
|
+
declare const BUNDLE_COMPETITION = "fifa.world";
|
|
3665
|
+
/**
|
|
3666
|
+
* The edition the bundled schedule describes, read from the schedule itself
|
|
3667
|
+
* (the year of its first kickoff) rather than written down a second time.
|
|
3668
|
+
*/
|
|
3669
|
+
declare function bundleSeasonYear(): number;
|
|
3670
|
+
/**
|
|
3671
|
+
* Does the bundled schedule describe what this request is about?
|
|
3672
|
+
*
|
|
3673
|
+
* The competition is an ARGUMENT — the caller's adapter states it — never a
|
|
3674
|
+
* default. Off the bundle, every path built on the skeleton (date merge,
|
|
3675
|
+
* `match <id>`, `next`, the bracket, the knockout-fixture cache, the market
|
|
3676
|
+
* fixture) must not read it: an empty foreign day once showed 104 World Cup
|
|
3677
|
+
* fixtures with foreign attribution (audit A03).
|
|
3678
|
+
*
|
|
3679
|
+
* The bundle is ONE EDITION of its competition. When the provider reported the
|
|
3680
|
+
* season a response belongs to and it is a different one, the bundle does not
|
|
3681
|
+
* describe that response. With no provider answer (offline, degraded) there is
|
|
3682
|
+
* nothing to compare, and the bundle stays what it is: the 2026 edition.
|
|
3683
|
+
*/
|
|
3684
|
+
declare function bundleApplies(competition: string, season?: SeasonInfo): boolean;
|
|
3685
|
+
/**
|
|
3686
|
+
* The competitions that HAVE NO BRACKET: league seasons with no knockout tie of
|
|
3687
|
+
* their own, where "no bracket" is the answer, not a gap. Written down in the
|
|
3688
|
+
* supported table (`bracket: 'not-applicable'`), one fact each, never inferred
|
|
3689
|
+
* from the standings shape (`STANDINGS_SHAPE` says
|
|
3690
|
+
* how a table is read, not whether a knockout follows it: `uefa.champions` and
|
|
3691
|
+
* `mex.1` read as one league table and both have one).
|
|
3692
|
+
* - `eng.1`: the Premier League; a 38-round league, no play-off.
|
|
3693
|
+
* - `esp.1`: LaLiga; a 38-round league, no play-off.
|
|
3694
|
+
* Deliberately NOT here: `ita.1` (championship and relegation play-offs since
|
|
3695
|
+
* the FIGC's May 2026 decision), `ger.1` (the relegation play-off), `mex.1`
|
|
3696
|
+
* (the Liguilla), `uefa.champions` (a knockout after the league phase). A
|
|
3697
|
+
* competition joins by a written fact. A derived view, kept for its readers (it
|
|
3698
|
+
* is exported); {@link bracketCapability} reads the row itself.
|
|
3699
|
+
*/
|
|
3700
|
+
declare const NO_BRACKET: ReadonlySet<string>;
|
|
3701
|
+
/**
|
|
3702
|
+
* What `bracket` is for a competition, one of three values, read from its ROW
|
|
3703
|
+
* (one row, one fact: a listing, the README matrix and the command cannot
|
|
3704
|
+
* disagree):
|
|
3705
|
+
* - `offered`: the row offers it (only the bundled competition can:
|
|
3706
|
+
* `deriveTables` refuses any other row that does);
|
|
3707
|
+
* - `inapplicable`: the row says the competition has none (`not-applicable`);
|
|
3708
|
+
* - `unsupported`: it may have one, and it is not offered yet; also a slug
|
|
3709
|
+
* the table does not hold.
|
|
3710
|
+
*/
|
|
3711
|
+
declare function bracketCapability(competition: string, table?: readonly CompetitionEntry[]): 'offered' | 'inapplicable' | 'unsupported';
|
|
3712
|
+
|
|
3713
|
+
/**
|
|
3714
|
+
* Metadata that belongs to ONE fetch result.
|
|
3715
|
+
*
|
|
3716
|
+
* A provider says things about a response that are not fixtures: which season
|
|
3717
|
+
* (or, for a window across a season turn, which seasons) it answered for and,
|
|
3718
|
+
* from 0.11's bundle decoupling on, whether every record in it could be read
|
|
3719
|
+
* and how many could not. Those facts decide what a caller may claim, so they
|
|
3720
|
+
* must belong to the particular result they describe.
|
|
3721
|
+
*
|
|
3722
|
+
* They are NOT kept on the adapter. A field there is shared by every call in
|
|
3723
|
+
* flight — `lastError` is, and documents itself as "best-effort under
|
|
3724
|
+
* concurrency" for exactly that reason: with two overlapping calls, the second
|
|
3725
|
+
* to finish overwrites what the first was told. Here the metadata is keyed on
|
|
3726
|
+
* the returned array itself, so it cannot outlive, precede or cross to another
|
|
3727
|
+
* result, and `ProviderAdapter` keeps its plain-array contract.
|
|
3728
|
+
*/
|
|
3729
|
+
|
|
3730
|
+
interface FetchMeta {
|
|
3731
|
+
/**
|
|
3732
|
+
* The season the provider reported for this response, when it stated one.
|
|
3733
|
+
* For a window, the one its stating parts agree on; absent when none stated
|
|
3734
|
+
* one, or when a window asked across seasons held two (see `seasons`).
|
|
3735
|
+
*/
|
|
3736
|
+
readonly season?: SeasonInfo;
|
|
3737
|
+
/**
|
|
3738
|
+
* Every distinct season the response(s) stated, by year, in the order the
|
|
3739
|
+
* parts were asked: zero or one for a single response, and for a window
|
|
3740
|
+
* asked strictly (two are refused there); two for a window asked across a
|
|
3741
|
+
* season turn. Empty is "none stated", which a reader must tell from "two".
|
|
3742
|
+
* Absent when the adapter does not say.
|
|
3743
|
+
*/
|
|
3744
|
+
readonly seasons?: readonly SeasonInfo[];
|
|
3745
|
+
/**
|
|
3746
|
+
* Whether the result accounts for every record the provider sent. False when
|
|
3747
|
+
* a record was left out: refused as unreadable, contradicting a sibling under
|
|
3748
|
+
* the same id, or beyond the bound. A record the provider itself marks as not
|
|
3749
|
+
* a fixture is not "left out". Absent when the adapter does not say.
|
|
3750
|
+
*/
|
|
3751
|
+
readonly complete?: boolean;
|
|
3752
|
+
/**
|
|
3753
|
+
* How many provider records made the result not whole: refused as
|
|
3754
|
+
* unreadable, a second record under an id already accepted (within a
|
|
3755
|
+
* response or across the parts of a window), or beyond the bound. A record
|
|
3756
|
+
* the provider marks as not a fixture is not counted. 0 exactly when
|
|
3757
|
+
* `complete` is true. Absent when the count is not known: a response that
|
|
3758
|
+
* filled the request's limit lost a tail nobody can count.
|
|
3759
|
+
*/
|
|
3760
|
+
readonly omitted?: number;
|
|
3761
|
+
/**
|
|
3762
|
+
* The id of every fixture the response(s) held and the parser read, when the
|
|
3763
|
+
* result can hold FEWER: a window narrowed from month responses sets aside
|
|
3764
|
+
* what falls outside it, and the copy of a fixture held twice. A caller that
|
|
3765
|
+
* keeps a previous answer asks "was this fixture read?" of this list, never
|
|
3766
|
+
* of the result: a fixture the provider moved out of the window was read,
|
|
3767
|
+
* and its old copy must not be put back. Absent when the result holds
|
|
3768
|
+
* everything that was read.
|
|
3769
|
+
*/
|
|
3770
|
+
readonly mentioned?: readonly string[];
|
|
3771
|
+
/**
|
|
3772
|
+
* Standings only: whether every table child the provider sent became a
|
|
3773
|
+
* table. A different fact from `complete`, which is also false for a refused
|
|
3774
|
+
* ROW of a table that was read. False means a table may be missing from the
|
|
3775
|
+
* result, so a key it does not hold is not known to be absent.
|
|
3776
|
+
*/
|
|
3777
|
+
readonly inventoryComplete?: boolean;
|
|
3778
|
+
}
|
|
3779
|
+
/** Attach metadata to a fetch result and return that same result. */
|
|
3780
|
+
declare function attachFetchMeta<T extends object>(result: T, meta: FetchMeta | undefined): T;
|
|
3781
|
+
/** The metadata of a fetch result, if the provider that produced it stated any. */
|
|
3782
|
+
declare function fetchMeta(result: object | undefined): FetchMeta | undefined;
|
|
3783
|
+
|
|
3784
|
+
/**
|
|
3785
|
+
* What one look at a small file found. Four answers, because the refresh lock
|
|
3786
|
+
* must tell them apart (the other kept files only want `read` or nothing):
|
|
3787
|
+
* - `read`: a regular file within the bound, its bytes and its mtime, both
|
|
3788
|
+
* from the one open file;
|
|
3789
|
+
* - `absent`: no directory entry at the path. The lock's claimer must not
|
|
3790
|
+
* remove what is not there (a lock created a moment later is someone's);
|
|
3791
|
+
* - `unreadable`: something is there that cannot be read as such a file (a
|
|
3792
|
+
* pipe, a device, a directory, a link to nothing, too large, no permission).
|
|
3793
|
+
* The lock's claimer takes it over: nobody can judge it;
|
|
3794
|
+
* - `grown`: a regular file within the bound that grew while it was read: its
|
|
3795
|
+
* writer is writing it in place NOW (a lock's owner writes its token just
|
|
3796
|
+
* after creating it). No bytes, because nobody may parse a file caught
|
|
3797
|
+
* mid-write; its mtime, so a lock being written is judged by its date.
|
|
3798
|
+
*/
|
|
3799
|
+
type SmallFile = {
|
|
3800
|
+
kind: 'read';
|
|
3801
|
+
bytes: Buffer;
|
|
3802
|
+
mtimeMs: number;
|
|
3803
|
+
} | {
|
|
3804
|
+
kind: 'absent';
|
|
3805
|
+
} | {
|
|
3806
|
+
kind: 'unreadable';
|
|
3807
|
+
} | {
|
|
3808
|
+
kind: 'grown';
|
|
3809
|
+
mtimeMs: number;
|
|
3810
|
+
};
|
|
3811
|
+
/** What a NO-FOLLOW look found: the four answers, or a symbolic link at the path (never followed). */
|
|
3812
|
+
type OwnFile = SmallFile | {
|
|
3813
|
+
kind: 'symlink';
|
|
3814
|
+
};
|
|
3815
|
+
/**
|
|
3816
|
+
* Read a small regular file through ONE descriptor, never more than
|
|
3817
|
+
* `maxBytes + 1` bytes of it, and say which of the four `SmallFile` answers it
|
|
3818
|
+
* is. Never throws, never waits. The ONE implementation for every file kept on
|
|
3819
|
+
* disk; `readSmallFile` is its bytes-or-nothing form, and {@link lookAtOwnFile}
|
|
3820
|
+
* its no-follow form.
|
|
3821
|
+
*
|
|
3822
|
+
* A check by path followed by a read by path is two looks at what may be two
|
|
3823
|
+
* files: between them the path can become a pipe (the read then blocks the
|
|
3824
|
+
* statusline for ever) or a file far larger than the check allowed (read
|
|
3825
|
+
* whole). Here the check and the read are of the same open file, and the read
|
|
3826
|
+
* stops one byte past the size the check saw (at most the bound): a file that
|
|
3827
|
+
* grew in place after it was checked is `grown`, and is not read further.
|
|
3828
|
+
*
|
|
3829
|
+
* `absent` means NO ENTRY. An open that follows a link to nothing also fails
|
|
3830
|
+
* with ENOENT, so that failure is followed by one look at the entry itself
|
|
3831
|
+
* (`lstat`, not followed): a link is there, so `unreadable`. Anything else that
|
|
3832
|
+
* look finds (nothing, or a file created between the two looks) stays
|
|
3833
|
+
* `absent`: answering `unreadable` for a file that appeared in between would
|
|
3834
|
+
* have the lock's claimer remove a fresh lock (the vanished-lock tests in
|
|
3835
|
+
* `cache-file-races.test.ts` pin it).
|
|
3836
|
+
*/
|
|
3837
|
+
declare function lookAtSmallFile(path: string, maxBytes: number): SmallFile;
|
|
3838
|
+
/**
|
|
3839
|
+
* The contents of a small regular file (see `lookAtSmallFile`), or `undefined`
|
|
3840
|
+
* for every other answer (absent, unreadable, grown while it was read: a file
|
|
3841
|
+
* caught mid-write is not parsed). Never throws, never waits.
|
|
3842
|
+
*/
|
|
3843
|
+
declare function readSmallFile(path: string, maxBytes: number): Buffer | undefined;
|
|
3844
|
+
/** What a rename onto a path would do (see {@link lookAtEntry}). */
|
|
3845
|
+
type FileEntry = 'absent' | 'file' | 'replaceable' | 'unhealable';
|
|
3846
|
+
/**
|
|
3847
|
+
* What a RENAME onto `path` would do, from one look at the entry (`lstat`,
|
|
3848
|
+
* never following a link), one open for a regular file whose owner-read bit is
|
|
3849
|
+
* set, and one probe where that bit is clear; nothing is read. The replacement
|
|
3850
|
+
* (an atomic write: a temporary file made in the same directory and renamed
|
|
3851
|
+
* over the path) is this process's own file and keeps the mode bits of
|
|
3852
|
+
* whatever it replaces but a link, and nothing else of the entry (no
|
|
3853
|
+
* access-control entry of its own, no other owner); what the DIRECTORY gives a
|
|
3854
|
+
* new file (an inherited access-control entry) it gets too. So the owner-read
|
|
3855
|
+
* bit decides nothing on its own where it is clear: there the look MEASURES
|
|
3856
|
+
* whether a file made beside the path with those bits reads back, before any
|
|
3857
|
+
* open of the entry (a probe, made with those bits, opened, and removed:
|
|
3858
|
+
* nothing is left behind but where a crash interrupts it or the directory
|
|
3859
|
+
* denies its deletion, at most one file per look). Where it is set the replacement is taken to read
|
|
3860
|
+
* back (steps 5 and 6; a directory whose inherited entries deny a new file a
|
|
3861
|
+
* read is not measured there). In this order:
|
|
3862
|
+
* 1. the `lstat` fails: no entry → `absent` (a rename creates a fresh file);
|
|
3863
|
+
* anything else → `unhealable` (nobody can look at it: the directory above
|
|
3864
|
+
* cannot be searched);
|
|
3865
|
+
* 2. a symbolic link to anything → `replaceable` (the rename replaces the link
|
|
3866
|
+
* itself and keeps nothing of it; its target is never followed, a link to
|
|
3867
|
+
* nothing included);
|
|
3868
|
+
* 3. a directory → `unhealable` (a rename cannot replace it);
|
|
3869
|
+
* 4. the owner-read bit clear (mode 000, 200, 044), whatever the kind (a
|
|
3870
|
+
* regular file refused, or one that opens through its other bits or an
|
|
3871
|
+
* access-control entry of its own; a pipe, a socket, a device): the
|
|
3872
|
+
* replacement keeps the bits and none of what let this process read the
|
|
3873
|
+
* entry, so whether it reads back is measured (`replacementReadsBack`): a
|
|
3874
|
+
* probe made there with those bits reads back (the directory's inherited
|
|
3875
|
+
* permissions let a new file be read) → `replaceable`; else → `unhealable`;
|
|
3876
|
+
* 5. a regular file → opened (read-only, non-blocking where the platform has
|
|
3877
|
+
* it, on one descriptor, closed before returning, nothing read): it opens →
|
|
3878
|
+
* `file`, whatever its size or content; refused → `replaceable` (an
|
|
3879
|
+
* access-control list, another owner's 0600, a lock: the replacement is
|
|
3880
|
+
* ours with the owner-read bit set, so it reads back);
|
|
3881
|
+
* 6. anything else with the bit set (a pipe, a socket, a device) →
|
|
3882
|
+
* `replaceable` (the rename replaces the entry with a regular file of ours
|
|
3883
|
+
* with those bits, which reads back).
|
|
3884
|
+
*
|
|
3885
|
+
* On Windows every mode reports the owner-read bit, so step 4 never runs there
|
|
3886
|
+
* (no probe is made) and a file that cannot be opened is `replaceable`; if the
|
|
3887
|
+
* rename itself then fails, the atomic write throws, and the publish that
|
|
3888
|
+
* called it is one that did not happen, as before. The look cannot see a flag
|
|
3889
|
+
* the system keeps beside the mode (an immutable or append-only flag: Node's
|
|
3890
|
+
* `lstat` reports none); under one the rename is refused whatever it answers,
|
|
3891
|
+
* with the same outcome.
|
|
3892
|
+
*
|
|
3893
|
+
* The bounded reader cannot answer this: `lookAtSmallFile` says `unreadable`
|
|
3894
|
+
* for a file it cannot open, for a regular file larger than its bound, and for
|
|
3895
|
+
* a link to nothing alike. For the CLI's refresher, which asks whether a
|
|
3896
|
+
* publish can heal a snapshot it could not use. Never throws, never waits.
|
|
3897
|
+
*/
|
|
3898
|
+
declare function lookAtEntry(path: string): FileEntry;
|
|
3899
|
+
/**
|
|
3900
|
+
* {@link lookAtSmallFile} for a file that must be the user's OWN regular file
|
|
3901
|
+
* (the config file): a symbolic link at the path is `symlink`, never followed.
|
|
3902
|
+
* Where the platform has `O_NOFOLLOW` (POSIX) the open itself refuses a link
|
|
3903
|
+
* (`ELOOP`, or `EMLINK` on some BSDs), and one look at the entry tells a link
|
|
3904
|
+
* from a path that loops elsewhere. Windows has no such flag: there the entry
|
|
3905
|
+
* is looked at (`lstat`: a link is `symlink`) BEFORE the open, and the file
|
|
3906
|
+
* the open gave must be THAT entry: the same device and inode by `fstat` as by
|
|
3907
|
+
* the `lstat`. A link (or any other file) put in place between the two looks
|
|
3908
|
+
* is then not what was inspected, and is answered `symlink`, its descriptor
|
|
3909
|
+
* closed and nothing read; so is a file found at the open where the look found
|
|
3910
|
+
* no entry. The two looks are kept apart by the check, not closed into one.
|
|
3911
|
+
*/
|
|
3912
|
+
declare function lookAtOwnFile(path: string, maxBytes: number): OwnFile;
|
|
3913
|
+
|
|
3914
|
+
export { BRACKET_STAGE_ORDER, BUNDLE_COMPETITION, type BatchResolution, type BetweenEditions, type BoundedList, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketShareCard, type BracketTopology, type BracketView, type BuildSignalInput, COMPETITION_KIND, COUNTDOWN_NOW, type Capabilities, type Capability, type ChosenBy, type ClubResolution, type CompetitionEntry, type CompetitionKey, type CompetitionKind, type CompetitionSelection, DEFAULT_COMPETITION, DEFAULT_COOLDOWN_MS, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, DISCLAIMER, type DayAttribution, type DerivedTables, EARLIER_RECORD_NOTE, EspnAdapter, type EspnAdapterOptions, FAN_PROJECT, FLAVOR_BANKS, FLAVOR_LEVELS, FLAVOR_MOMENTS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FetchMeta, type FetchWindowOptions, type FileEntry, type Flair, type FlairOpts, type FlavorBank, type FlavorLevel, type Moment as FlavorMoment, type FlavorOpts, type FormatOpts, type GroupStandings, KNOCKOUT_EXTRA_TIME_MS, KNOWN_SOURCES, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type ListedCompetition, type LiveReadResult, type LiveResult, MARKETS_SCOPE_NOTE, MARKET_COMPETITIONS, MAX_CONFIG_BYTES, MAX_COOLDOWN_MS, MAX_LABEL_COLUMNS, MAX_RESPONSE_BYTES, MAX_SCHEDULE_INDEX, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchByIdResult, type MatchEvent, type MatchShareCard, NO_BRACKET, type NextFixtureResult, type NoConfigReason, type Outcome, type OwnFile, type ParseResult, type Pin, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, ProviderError, type ProviderErrorKind, type PunditPick, RALLY_CRIES, type RallyCry, type RallyPin, type ReadAccount, type ResolvedParticipant, ResponseTooLargeError, type Roster, SCHEDULE_AHEAD_DAYS, SCHEDULE_LOOKBACK_DAYS, SEASON_SLUG, SHARE_DISCLAIMER, SHARE_HASHTAG, STANDINGS_SHAPE, SUPPORTED, type ScheduleAheadResult, type ScheduleEntry, type SeasonInfo, type SelectedCompetition, type Selection, type ShareBracketInput, type ShareBracketOptions, type ShareCardContext, type ShareCardMarket, type ShareCardView, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type SmallFile, type Stage, type StandingRow, type StandingsResult, type Status, TABLE_KEY_ARG, TEAM_KIND, type TableData, type TableShareCard, type Team, type TeamInfo, type TeamKind, type TeamLookup, type UserConfig, type UserConfigRead, type VerdictExtras, type VerdictSource, allFixtures, allTeams, ambiguous, asFlavorLevel, attachFetchMeta, bounded, bracketCapability, bracketShareCard, buildBracketTopology, buildBracketView, buildMarketSignal, bundleApplies, bundleSeasonYear, byKickoff, cacheDirFor, cacheableKeys, capabilitiesOf, competitionBase, competitionKind, competitionLabel, competitionValue, computeStandings, configPath, countdown, countdownPhrase, currentOrNextFixtureForTeam, dateNoneRead, dateNoneReadSentence, dateShareCard, dateUnreached, dateUnreachedSentence, dayAttribution, definitiveNone, deriveFavorite, deriveTables, disclaimerLine, displayWidth, emptyBatch, entryOf, favoriteStrength, fetchMeta, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, flavorsFor, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getLiveRead, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getScheduleAhead, getStandings, groups, hasLiveWindow, hasSaneDistribution, humanLabel, isCacheable, isFinished, isFlavorLevel, isHumanLabel, isKnockoutStage, isKnownSource, isLive, isMexicoNationalTeam, isPinnedSide, isPlaceholderSide, isReliableMarketSignal, isStaleSignal, isTeam, isTournamentWindowOver, isUpcoming, isValidDate, isValidTimeZone, joinSegments, knockoutWindow, listCompetitions, liveNoneRead, liveNoneReadSentence, liveShareCard, liveSourceLabel, liveWindowMsFor, loadBracketTopology, localDate, lookAtEntry, lookAtOwnFile, lookAtSmallFile, lookupTeam, makeAdapter, makeMarketProvider, malformed, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketDisplayable, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketScopeVerdict, marketSignalRendersFor, marketSourceLabel, marketsCoverCompetition, marketsNoneRead, marketsNoneReadNote, marketsNoneReadSentence, matchFlair, matchFlairs, matchFlavor, matchKey, matchLocation, matchNoneReadSentence, matchShareCard, matchWindowSentence, mergeLive, modeLine, nationArg, nationToFlag, nationToRegion, nextFixtureForPin, nextFixtureForTeam, nextHorizonSentence, nextNoneReadSentence, nextShareCard, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseCachedMarketSignal, parseCachedMatch, parseCachedMatches, parseCachedScheduleIndex, parseStandings, parseTeamSlot, parsedValue, pinUnder, productFlag, rallyCryFor, rallyEntryFor, readJsonBounded, readSmallFile, readUserConfig, resolveClub, resolveCompetition, resolveMarketSource, resolveTz, resolvedValues, retryAfterMs, rosterFor, sanitizeBundledFixture, savedSlug, scheduleEntryOf, scheduleSpan, scoreline, sealMarketSignal, sealMatch, sealScheduleEntry, sealSeason, selectOne, selectedCompetition, selectionExtras, selectionRefusal, selectionVerdict, servedExtras, stageLabel, stageLabelI18n, standingsShapeOf, statesPartial, t, tableData, tableKeyArg, tableShareCard, tableTitle, teamKind, truncateVisible, unresolved, unservedSentence, valid, verdictExtras, verdictNotice, verdictQualifiers, withFlag };
|