@claudinho/core 0.10.1 → 0.11.1

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