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