@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.
Files changed (4) hide show
  1. package/README.md +15 -12
  2. package/dist/index.d.ts +2690 -347
  3. package/dist/index.js +3618 -794
  4. 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 national team. `flag` is an emoji (no image assets, no copyright). */
95
+ /** A team. `flag` is an emoji (no image assets, no copyright), and a nation's only. */
6
96
  interface Team {
7
- /** Short code, typically the FIFA/IOC 3-letter abbreviation (e.g. "MEX"). */
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
- /** Emoji flag (e.g. "🇲🇽"). */
12
- flag: string;
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
- * Tournament stage. "3P" is the third-place play-off. "FRIENDLY" is used for
16
- * non-tournament fixtures (e.g. international friendlies) surfaced when the
17
- * adapter points at a non-World-Cup competition.
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
- type Stage = 'GROUP' | 'R32' | 'R16' | 'QF' | 'SF' | '3P' | 'F' | 'FRIENDLY';
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
- /** Localized knockout/group stage label for bracket rendering. */
144
- declare function stageLabelI18n(lang: string | undefined, stage: string, group?: string): string;
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
- /** Compact human countdown until kickoff: "3d4h", "2h10m", "45m", or "now". */
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
- /** Human-readable stage for display (group letter when applicable). */
240
- declare function stageLabel(m: Pick<Match, 'stage' | 'group'>): string;
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" — a small, localized layer of football-broadcast energy
244
- * that lets surfaces (and the model) narrate scores with some spice.
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
- * IMPORTANT (legal): these are GENERIC, unattributed exclamations that evoke the
247
- * *genre* of football commentary (with a Latin-American TV spirit for `es`).
248
- * They must NEVER quote, name, or impersonate a real commentator or reproduce a
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
- * A short, localized exclamation for a match's moment — or '' when the level or
262
- * moment calls for restraint. Deterministic per match id. Never quotes a person.
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
- declare function matchFlavor(m: Match, opts?: {
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
- }): string;
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 kickoff. */
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: string;
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
- /** Live-window length for a fixture — longer for knockout (extra time + penalties). */
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
- /** All fixtures/results for a single calendar date (provider's timezone semantics). */
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
- /** Optional inclusive date-range fetch (used for schedule generation). */
511
- fetchWindow?(startDate: string, endDate: string): Promise<Match[]>;
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
- * Bounded collections — the type that carries its own honesty.
538
- *
539
- * Every round of review found a list that was capped without saying so, or that
540
- * reported a post-cap count as the total, or that said "none found" when it had
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 BoundedList<T> {
546
- /** What survived the cap. */
547
- readonly items: readonly T[];
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
- * How many were established before capping. Exact when `complete` is true;
550
- * otherwise it may only be a lower bound over the records examined.
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 total: number;
553
- /** `items.length` — kept explicit so a serialized payload is self-describing. */
554
- readonly shown: number;
555
- /** Did the cap drop anything? */
556
- readonly truncated: boolean;
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
- * Did we finish looking? False when a deadline, error, or partial fetch cut
559
- * the work short — so a caller can tell "there are none" from "we don't know".
560
- * An empty list with `complete: false` must NEVER render as "nothing found".
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 complete: boolean;
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
- * Bound a list, recording what that cost.
566
- *
567
- * SLICE BEFORE MAP is the caller's job — this records the outcome, it does not
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 bounded<T>(items: readonly T[], max: number, complete?: boolean): BoundedList<T>;
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 vocabulary for "we could not use this".
574
- *
575
- * `undefined` was doing five jobs at once — absent, malformed, ambiguous, timed
576
- * out, and definitively none — and the caller could not tell them apart. That
577
- * ambiguity is what let a schema failure get negative-cached as the FACT "this
578
- * fixture has no market", and what made `checked` mean two different things on
579
- * two paths. Every rejection now says which kind it is.
580
- */
581
- /** A value we produced, or the reason we did not. */
582
- type ParseResult<T> = {
583
- readonly kind: 'valid';
584
- readonly value: T;
585
- }
586
- /** We read the payload fine, and the answer is genuinely "nothing here". Cacheable. */
587
- | {
588
- readonly kind: 'definitive-none';
589
- readonly reason: string;
590
- }
591
- /** We could not read the payload. A fact about US, not about the fixture. NOT cacheable. */
592
- | {
593
- readonly kind: 'malformed';
594
- readonly reason: string;
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
- * May this rejection be remembered for the length of a TTL?
632
- *
633
- * The line is whether we READ the payload, not whether we liked it. A
634
- * definitive none and an ambiguity are both conclusions drawn from bytes we
635
- * understood, and both are stable across a refetch — so remembering them is
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 isCacheable<T>(r: ParseResult<T>): boolean;
643
-
644
- interface MapContext {
645
- groupByTeam?: Record<string, string>;
646
- }
647
-
1275
+ declare function verdictNotice(result: VerdictSource, lang?: string): string | undefined;
648
1276
  /**
649
- * ESPN adapter — the free, keyless default/fallback source.
650
- *
651
- * Uses ESPN's unofficial public scoreboard endpoint:
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
- * Undocumented and unguaranteed — but confirmed to return real 2026 fixtures
655
- * (teams, ISO-UTC kickoffs, venues, status). The SCHEDULED/FT mapping is
656
- * verified against live data; the in-play minute/score path is best-effort and
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 class ProviderError extends Error {
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
- * The ESPN competition slug to fetch live state from. Defaults to the 2026
758
- * World Cup (`fifa.world`); override with CLAUDINHO_COMPETITION (e.g.
759
- * `eng.1`, `uefa.champions`, `fifa.friendly`). Only affects the *live* fetch —
760
- * the bundled static schedule is always the World Cup.
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
- declare function resolveCompetition(explicit?: string): string;
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?: string, opts?: AdapterOptions): ProviderAdapter;
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
- /** Group tables in group-letter order; each table's rows in standings order. */
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 single fetch {@link getBracket} uses) so the
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 nations are resolved (flag ≠ 🏳️) and kickoff
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
- * Currently-live matches; empty + degraded on error.
2320
+ * The ONE bounded JSON reader for provider responses (audit A11).
926
2321
  *
927
- * The provider buckets its scoreboard by its own day (ESPN: US/Eastern), so a
928
- * late kickoff that crossed the day boundary lands in an ADJACENT bucket — a
929
- * 04:00Z match still live at 76' while the provider's default "today" bucket
930
- * already shows the prior, all-FT day. A bare `adapter.fetchLive()` reads only
931
- * that single default bucket, so it silently MISSES such a match and `live` /
932
- * the statusline / the hook show "nothing live" mid-match. Fetch the ±1-day UTC
933
- * window around `now` instead — the same trick {@link getMatchesForDate} and
934
- * {@link getMatchById} use — and filter to in-play, so no live match can hide in
935
- * an adjacent bucket. Adapters without a window fetch fall back to `fetchLive()`.
936
- */
937
- declare function getLiveMatches(adapter: ProviderAdapter, now?: Date): Promise<LiveResult>;
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 the active competition. */
1210
- declare function marketsCoverCompetition(competition?: string): boolean;
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 Polymarket public-data
1219
- * adapter; honors CLAUDINHO_MARKETS_SOURCE ('fake' = network-free synthetic
1220
- * demo data; 'none'/'off' = network-free no-op). Tests usually inject directly.
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?: string): MarketProvider;
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 not affiliated with FIFA or Anthropic.";
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 appended; for the empty case the
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
- export { BRACKET_STAGE_ORDER, type BatchResolution, type BoundedList, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketTopology, type BracketView, type BuildSignalInput, DEFAULT_COMPETITION, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, EspnAdapter, type EspnAdapterOptions, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type GroupStandings, KNOCKOUT_EXTRA_TIME_MS, KNOWN_SOURCES, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type LiveResult, MARKET_COMPETITIONS, MAX_LABEL_COLUMNS, 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 NextFixtureResult, type Outcome, type ParseResult, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, ProviderError, type ProviderErrorKind, type PunditPick, type ResolvedParticipant, SHARE_DISCLAIMER, SHARE_HASHTAG, type Selection, type ShareBracketInput, type ShareBracketOptions, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type Stage, type StandingRow, type StandingsResult, type Status, type Team, type TeamInfo, type TeamLookup, allFixtures, allTeams, ambiguous, asFlavorLevel, bounded, buildBracketTopology, buildBracketView, buildMarketSignal, byKickoff, cacheableKeys, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, definitiveNone, deriveFavorite, displayWidth, emptyBatch, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getStandings, groups, hasSaneDistribution, humanLabel, isCacheable, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isResolvedNation, isStaleSignal, isTournamentWindowOver, isValidDate, isValidTimeZone, knockoutWindow, liveSourceLabel, liveWindowMsFor, loadBracketTopology, localDate, lookupTeam, makeAdapter, makeMarketProvider, malformed, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSignalRendersFor, marketSourceLabel, marketsCoverCompetition, matchFlavor, matchKey, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseCachedMarketSignal, parseCachedMatch, parseCachedMatches, parseStandings, parseTeamSlot, parsedValue, productFlag, resolveCompetition, resolveMarketSource, resolveTz, resolvedValues, sanitizeBundledFixture, scoreline, sealMarketSignal, sealMatch, selectOne, stageLabel, stageLabelI18n, t, truncateVisible, unresolved, valid };
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 };