@claudinho/core 0.9.4 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -331,9 +331,13 @@ interface BracketView {
331
331
  degraded: boolean;
332
332
  standingsDegraded: boolean;
333
333
  source?: string;
334
+ /** The bracket is a World Cup feature; off the bundle there is none (audit A03). */
335
+ unsupported?: true;
334
336
  }
335
337
  interface BracketResult {
336
338
  view: BracketView;
339
+ /** Off the bundle there is no bracket (audit A03); `view.stages` is empty. */
340
+ unsupported?: true;
337
341
  degraded: boolean;
338
342
  standingsDegraded: boolean;
339
343
  source?: string;
@@ -363,6 +367,17 @@ declare function fixturesByDate(dateISO: string, fixtures?: Match[], tz?: string
363
367
  declare function fixturesByTeam(code: string, fixtures?: Match[]): Match[];
364
368
  /** All fixtures in a group letter ("A".."L"). */
365
369
  declare function fixturesByGroup(group: string, fixtures?: Match[]): Match[];
370
+ /**
371
+ * Eligible to be "the next match": the kickoff is still ahead AND the match is
372
+ * still going to be played. ONE predicate for every selector (`next`, the
373
+ * statusline countdown, the knockout-fixture cache, the market fixture, the
374
+ * refresher cadence) — a cancelled or postponed fixture keeps its kickoff, so
375
+ * a time-only check advertised it as the next game (audit A07). Finished is
376
+ * deliberately NOT a clause: under a real clock a finished match never has a
377
+ * future kickoff, and excluding it only changed seeded-clock replays of an
378
+ * ended tournament (measured on the 62-surface parity corpus).
379
+ */
380
+ declare function isUpcoming(m: Match, now?: Date): boolean;
366
381
  /** The next upcoming fixture for a team at/after `from` (default now). */
367
382
  declare function nextFixtureForTeam(code: string, opts?: {
368
383
  from?: Date;
@@ -460,11 +475,27 @@ interface StandingRow {
460
475
  goalsAgainst: number;
461
476
  goalDiff: number;
462
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;
463
485
  }
464
486
  /** A group's table: the group letter ("A".."L") and its rows in standings order. */
465
487
  interface GroupStandings {
466
488
  group: string;
467
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
+ };
468
499
  }
469
500
  /**
470
501
  * Compute a group table from a set of matches. Teams are seeded from every
@@ -530,7 +561,19 @@ interface ProviderAdapter {
530
561
  readonly kind: string;
531
562
  readonly status?: number;
532
563
  readonly throttled?: boolean;
564
+ /** For a throttle: how long the adapter will refuse to fetch (bounded). */
565
+ readonly retryAfterMs?: number;
533
566
  };
567
+ /** Optional: epoch ms until which the adapter refuses requests (a retained throttle). */
568
+ readonly cooldownUntil?: number;
569
+ /**
570
+ * Optional: arm the throttle window from outside — a fresh process reading
571
+ * the backoff its refresher persisted. Inside the window every call fails as
572
+ * a throttle without a request.
573
+ */
574
+ armCooldown?(untilMs: number): void;
575
+ /** Optional: be told when the throttle window is armed or extended. Returns unsubscribe. */
576
+ onCooldown?(listener: (untilMs: number) => void): () => void;
534
577
  }
535
578
 
536
579
  /**
@@ -659,6 +702,17 @@ interface MapContext {
659
702
 
660
703
  /** Default competition slug (the 2026 World Cup). */
661
704
  declare const DEFAULT_COMPETITION = "fifa.world";
705
+ /**
706
+ * Provider cooldown after a 429/403 (audit A12): the window a throttled
707
+ * adapter refuses to fetch in. `Retry-After` is honoured in both RFC 9110 forms
708
+ * (delay-seconds and HTTP-date) up to MAX_COOLDOWN_MS — a longer request is
709
+ * capped, never silently shortened below the cap — and DEFAULT_COOLDOWN_MS
710
+ * applies when the header is absent or unreadable.
711
+ */
712
+ declare const DEFAULT_COOLDOWN_MS: number;
713
+ declare const MAX_COOLDOWN_MS: number;
714
+ /** The cooldown a `Retry-After` header asks for, as milliseconds from `nowMs`, bounded. */
715
+ declare function retryAfterMs(header: string | null | undefined, nowMs: number): number;
662
716
  /** Build an ESPN soccer base URL for a competition slug (e.g. "fifa.friendly"). */
663
717
  declare function competitionBase(slug: string): string;
664
718
  type ProviderErrorKind = 'http' | 'timeout' | 'parse';
@@ -670,6 +724,8 @@ type ProviderErrorKind = 'http' | 'timeout' | 'parse';
670
724
  declare class ProviderError extends Error {
671
725
  readonly kind: ProviderErrorKind;
672
726
  readonly status?: number;
727
+ /** For a throttle: how long the adapter will refuse to fetch (bounded). */
728
+ retryAfterMs?: number;
673
729
  constructor(message: string, kind: ProviderErrorKind, status?: number);
674
730
  /** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
675
731
  get throttled(): boolean;
@@ -695,6 +751,8 @@ interface EspnAdapterOptions {
695
751
  * path, where group letters aren't needed and the extra call is wasteful.
696
752
  */
697
753
  enrichGroups?: boolean;
754
+ /** Clock, injectable for tests. Drives the throttle cooldown window. */
755
+ now?: () => number;
698
756
  }
699
757
  declare class EspnAdapter implements ProviderAdapter {
700
758
  private readonly opts;
@@ -718,7 +776,37 @@ declare class EspnAdapter implements ProviderAdapter {
718
776
  * throttle (persist a backoff) from an ordinary blip.
719
777
  */
720
778
  lastError?: ProviderError;
779
+ /**
780
+ * A retained throttle (audit A12): after a 429/403 every call inside the
781
+ * window throws the provider's last answer WITHOUT a request. A server-
782
+ * lifetime MCP adapter is covered by this alone; the CLI pre-arms each
783
+ * process from its persisted cache via `armCooldown`.
784
+ */
785
+ private cooldownUntilMs?;
786
+ private cooldownError?;
787
+ private readonly cooldownListeners;
788
+ private readonly clock;
721
789
  constructor(opts?: EspnAdapterOptions);
790
+ /** Epoch ms until which requests are refused, when a cooldown is armed. */
791
+ get cooldownUntil(): number | undefined;
792
+ /**
793
+ * Arm the cooldown from outside — a fresh CLI process reading the backoff
794
+ * its refresher persisted. The retained error reads as a throttle so every
795
+ * caller's `degraded` path and the refresher's persistence treat it as one.
796
+ */
797
+ armCooldown(untilMs: number, reason?: ProviderError): void;
798
+ /**
799
+ * Be told whenever the cooldown window is armed or EXTENDED — the way a
800
+ * caller persists a throttle that arrives from a still-running request after
801
+ * its own call already returned (review P2 on #128). Returns unsubscribe.
802
+ */
803
+ onCooldown(listener: (untilMs: number) => void): () => void;
804
+ /**
805
+ * The ONE place a window is set. Concurrent requests can each carry a
806
+ * Retry-After; the LATEST expiry wins — a shorter one arriving second must
807
+ * never shorten a longer active window (review P2 on #128).
808
+ */
809
+ private arm;
722
810
  fetchByDate(dateISO: string): Promise<Match[]>;
723
811
  fetchWindow(startDate: string, endDate: string): Promise<Match[]>;
724
812
  /**
@@ -753,23 +841,65 @@ declare class EspnAdapter implements ProviderAdapter {
753
841
  private get;
754
842
  }
755
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
+
756
863
  /**
757
864
  * The ESPN competition slug to fetch live state from. Defaults to the 2026
758
865
  * World Cup (`fifa.world`); override with CLAUDINHO_COMPETITION (e.g.
759
- * `fifa.friendly` to follow international friendlies during pre-tournament
760
- * testing). Only affects the *live* fetch the bundled static schedule is
761
- * always the World Cup.
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.
762
871
  */
763
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
+
764
884
  /** Provider names {@link makeAdapter} can construct (the CLI validates against this). */
765
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
+ }
766
896
  /**
767
897
  * Construct a provider adapter for a `--source` name (default: espn). An
768
898
  * unknown source FAILS LOUD instead of silently running ESPN — `--source foo`
769
899
  * previously no-op'd, which lied about what the flag did (attribution stayed
770
900
  * honest, but the advertised knob did nothing).
771
901
  */
772
- declare function makeAdapter(source?: string): ProviderAdapter;
902
+ declare function makeAdapter(source?: string, opts?: AdapterOptions): ProviderAdapter;
773
903
  /**
774
904
  * Merge live matches over a base set by id. Live entries replace base entries
775
905
  * with the same id; unknown ids are appended.
@@ -841,6 +971,8 @@ interface MatchByIdResult {
841
971
  match?: Match;
842
972
  degraded: boolean;
843
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;
844
976
  }
845
977
  /**
846
978
  * The fixture a team-scoped MARKET query should be about, live-confirmed.
@@ -860,6 +992,8 @@ interface NextFixtureResult {
860
992
  degraded: boolean;
861
993
  /** The provider that served the live overlay (absent when degraded). */
862
994
  source?: string;
995
+ /** Off the bundle "next" is built on a schedule we do not have yet (audit A03). */
996
+ unsupported?: true;
863
997
  }
864
998
  /**
865
999
  * A team's next UPCOMING fixture, LIVE-RESOLVED across the knockout phase.
@@ -884,6 +1018,8 @@ interface KnockoutFixturesResult {
884
1018
  fixtures: Match[];
885
1019
  /** True when the overlay fetch failed — caller must NOT cache this as "none". */
886
1020
  degraded: boolean;
1021
+ /** Off the bundle there is no knockout window to fetch (audit A03). */
1022
+ unsupported?: true;
887
1023
  }
888
1024
  /**
889
1025
  * The RESOLVED upcoming knockout fixtures from the live overlay — the data the
@@ -1184,11 +1320,19 @@ declare function marketLine(signal: MarketSignal, match: Match): string;
1184
1320
  declare function marketBlock(signal: MarketSignal, match: Match): string[];
1185
1321
 
1186
1322
  /**
1187
- * Provider factory + graceful-degradation wrappers, mirroring `live.ts`'s
1188
- * getMatchesForDate contract: a market signal is optional enrichment, so any
1189
- * provider/network/parse error degrades to "no signal" and never throws.
1323
+ * Competitions the market sidecar covers CLAUDINHO'S implementation scope, not
1324
+ * Polymarket's. Polymarket does carry per-match moneylines for leagues (the
1325
+ * event `epl-lee-new-2026-09-14` has Leeds / draw / Newcastle legs), but this
1326
+ * sidecar derives its event slugs from the World Cup series only (`fifwc-…`,
1327
+ * series `soccer-fifwc` — see `deriveEventSlugs`) and validates fixture↔market
1328
+ * identity with nation tokens. Outside this set every fixture would derive a
1329
+ * slug that cannot exist, so the sidecar is switched off by construction
1330
+ * instead of issuing doomed requests. Supporting a league is slug-derivation,
1331
+ * mapping and validation work per competition — not widening this set.
1190
1332
  */
1191
-
1333
+ 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;
1192
1336
  /**
1193
1337
  * Resolve the market-data source: explicit arg > CLAUDINHO_MARKETS_SOURCE env >
1194
1338
  * 'polymarket' (mirrors resolveCompetition). Set CLAUDINHO_MARKETS_SOURCE=fake
@@ -1460,6 +1604,10 @@ interface ShareTableInput {
1460
1604
  tables: readonly {
1461
1605
  group: string;
1462
1606
  rows: readonly StandingRow[];
1607
+ /** Rows the provider served that could not be read (see GroupStandings). */
1608
+ partial?: {
1609
+ omitted: number;
1610
+ };
1463
1611
  }[];
1464
1612
  /** Live-data provider name for attribution; omit when degraded/static. */
1465
1613
  source?: string;
@@ -1548,4 +1696,4 @@ declare function formatShareBracket(input: ShareBracketInput, options?: ShareBra
1548
1696
  /** Compact one-line-per-match bracket for narrow share contexts. */
1549
1697
  declare function formatBracketCompactLine(mv: BracketMatchView, opts?: BracketFormatOpts): string;
1550
1698
 
1551
- export { BRACKET_STAGE_ORDER, type BatchResolution, type BoundedList, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketTopology, type BracketView, type BuildSignalInput, DEFAULT_COMPETITION, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, EspnAdapter, type EspnAdapterOptions, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type GroupStandings, KNOCKOUT_EXTRA_TIME_MS, KNOWN_SOURCES, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type LiveResult, MAX_LABEL_COLUMNS, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchByIdResult, type MatchEvent, type NextFixtureResult, type Outcome, type ParseResult, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, ProviderError, type ProviderErrorKind, type PunditPick, type ResolvedParticipant, SHARE_DISCLAIMER, SHARE_HASHTAG, type Selection, type ShareBracketInput, type ShareBracketOptions, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type Stage, type StandingRow, type StandingsResult, type Status, type Team, type TeamInfo, type TeamLookup, allFixtures, allTeams, ambiguous, asFlavorLevel, bounded, buildBracketTopology, buildBracketView, buildMarketSignal, byKickoff, cacheableKeys, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, definitiveNone, deriveFavorite, displayWidth, emptyBatch, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getStandings, groups, hasSaneDistribution, humanLabel, isCacheable, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isResolvedNation, isStaleSignal, isTournamentWindowOver, isValidDate, isValidTimeZone, knockoutWindow, liveSourceLabel, liveWindowMsFor, loadBracketTopology, localDate, lookupTeam, makeAdapter, makeMarketProvider, malformed, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSignalRendersFor, marketSourceLabel, matchFlavor, matchKey, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseCachedMarketSignal, parseCachedMatch, parseCachedMatches, parseStandings, parseTeamSlot, parsedValue, productFlag, resolveCompetition, resolveMarketSource, resolveTz, resolvedValues, sanitizeBundledFixture, scoreline, sealMarketSignal, sealMatch, selectOne, stageLabel, stageLabelI18n, t, truncateVisible, unresolved, valid };
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 };
package/dist/index.js CHANGED
@@ -271,6 +271,8 @@ var EN = {
271
271
  "bracket.slot.tbd": "TBD",
272
272
  "live.data": "Live data: {source}",
273
273
  "standings.unavailable": "Live standings unavailable.",
274
+ "standings.partial": "Partial table \u2014 {n} rows could not be read.",
275
+ "competition.unsupported": "Not available for this competition yet.",
274
276
  "share.tryIt": "Try it: {line}",
275
277
  "stage.group": "Group {group}",
276
278
  "stage.groupStage": "Group stage",
@@ -301,6 +303,8 @@ var ES = {
301
303
  "bracket.slot.tbd": "Por definir",
302
304
  "live.data": "Datos en vivo: {source}",
303
305
  "standings.unavailable": "Tabla en vivo no disponible.",
306
+ "standings.partial": "Tabla parcial \u2014 no se pudieron leer {n} filas.",
307
+ "competition.unsupported": "A\xFAn no disponible para esta competici\xF3n.",
304
308
  "share.tryIt": "Pru\xE9balo: {line}",
305
309
  "stage.group": "Grupo {group}",
306
310
  "stage.groupStage": "Fase de grupos",
@@ -331,6 +335,8 @@ var PT = {
331
335
  "bracket.slot.tbd": "A definir",
332
336
  "live.data": "Dados ao vivo: {source}",
333
337
  "standings.unavailable": "Classifica\xE7\xE3o ao vivo indispon\xEDvel.",
338
+ "standings.partial": "Tabela parcial \u2014 {n} linhas n\xE3o puderam ser lidas.",
339
+ "competition.unsupported": "Ainda n\xE3o dispon\xEDvel para esta competi\xE7\xE3o.",
334
340
  "share.tryIt": "Experimente: {line}",
335
341
  "stage.group": "Grupo {group}",
336
342
  "stage.groupStage": "Fase de grupos",
@@ -361,6 +367,8 @@ var FR = {
361
367
  "bracket.slot.tbd": "\xC0 d\xE9finir",
362
368
  "live.data": "Donn\xE9es en direct : {source}",
363
369
  "standings.unavailable": "Classement en direct indisponible.",
370
+ "standings.partial": "Classement partiel \u2014 {n} lignes n'ont pas pu \xEAtre lues.",
371
+ "competition.unsupported": "Pas encore disponible pour cette comp\xE9tition.",
364
372
  "share.tryIt": "Essayez : {line}",
365
373
  "stage.group": "Groupe {group}",
366
374
  "stage.groupStage": "Phase de groupes",
@@ -2932,11 +2940,13 @@ function fixturesByGroup(group, fixtures = SCHEDULE) {
2932
2940
  const g = group.toUpperCase();
2933
2941
  return fixtures.filter((m) => (m.group ?? "").toUpperCase() === g).sort(byKickoff);
2934
2942
  }
2943
+ function isUpcoming(m, now = /* @__PURE__ */ new Date()) {
2944
+ if (m.status === "CANCELLED" || m.status === "POSTPONED") return false;
2945
+ return Date.parse(m.kickoff) >= now.getTime();
2946
+ }
2935
2947
  function nextFixtureForTeam(code, opts = {}) {
2936
2948
  const from = opts.from ?? /* @__PURE__ */ new Date();
2937
- return fixturesByTeam(code, opts.fixtures ?? SCHEDULE).find(
2938
- (m) => new Date(m.kickoff).getTime() >= from.getTime()
2939
- );
2949
+ return fixturesByTeam(code, opts.fixtures ?? SCHEDULE).find((m) => isUpcoming(m, from));
2940
2950
  }
2941
2951
  var LIVE_WINDOW_MS = 140 * 6e4;
2942
2952
  var KNOCKOUT_EXTRA_TIME_MS = 60 * 6e4;
@@ -3381,7 +3391,7 @@ function toParticipant(raw) {
3381
3391
  const providerId = opaqueId(raw.team?.id, ESPN_ID);
3382
3392
  const known = productFlag(name) !== nationToFlag("");
3383
3393
  return valid(
3384
- providerId && known ? { kind: "team", providerId, team } : { kind: "slot", team }
3394
+ providerId && known ? { kind: "team", providerId, team } : { kind: "slot", ...providerId ? { providerId } : {}, team }
3385
3395
  );
3386
3396
  }
3387
3397
  function mapStatus(st) {
@@ -3450,7 +3460,7 @@ function parseEspnEvent(raw, ctx = {}) {
3450
3460
  if (awayP.kind !== "valid") return awayP;
3451
3461
  const h = homeP.value;
3452
3462
  const a = awayP.value;
3453
- if (h.kind === "team" && a.kind === "team" && h.providerId === a.providerId) {
3463
+ if (h.providerId !== void 0 && h.providerId === a.providerId) {
3454
3464
  return definitiveNone("both competitors are the same team");
3455
3465
  }
3456
3466
  const home = homeP.value.team;
@@ -3603,8 +3613,12 @@ function parseEspnStandings(raw) {
3603
3613
  const seenProviderIds = /* @__PURE__ */ new Set();
3604
3614
  for (const child of children) {
3605
3615
  const label = humanLabel(child?.name ?? child?.abbreviation);
3606
- const letter = label.match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
3607
- if (!letter) continue;
3616
+ const letter = label.match(/Group\s+([A-L])(?![A-Za-z0-9])/i)?.[1]?.toUpperCase();
3617
+ if (!letter) {
3618
+ const rows = child?.standings?.entries;
3619
+ if (Array.isArray(rows) && rows.length > 0) complete = false;
3620
+ continue;
3621
+ }
3608
3622
  if (seenGroups.has(letter)) {
3609
3623
  complete = false;
3610
3624
  continue;
@@ -3615,30 +3629,37 @@ function parseEspnStandings(raw) {
3615
3629
  complete = false;
3616
3630
  continue;
3617
3631
  }
3632
+ let omitted = 0;
3618
3633
  if (rawEntries.length > MAX_GROUP_ROWS) {
3619
3634
  rowsTruncated = true;
3620
3635
  complete = false;
3636
+ omitted += rawEntries.length - MAX_GROUP_ROWS;
3621
3637
  }
3622
3638
  const entries = takeBounded(rawEntries, MAX_GROUP_ROWS);
3623
- const seenTeams = /* @__PURE__ */ new Set();
3639
+ const seenCodes = /* @__PURE__ */ new Map();
3624
3640
  const seenRanks = /* @__PURE__ */ new Set();
3625
3641
  const ranked = [];
3626
3642
  for (const e of entries) {
3627
3643
  const r = entryToRow(e);
3628
3644
  if (r.kind !== "valid") {
3629
3645
  if (r.kind !== "definitive-none") complete = false;
3646
+ omitted += 1;
3630
3647
  continue;
3631
3648
  }
3632
- const key = r.value.providerId ?? r.value.team.code;
3633
- if (seenTeams.has(key) || seenRanks.has(r.value.providerRank) || r.value.providerId !== void 0 && seenProviderIds.has(r.value.providerId)) {
3649
+ const { providerId, providerRank } = r.value;
3650
+ const code = r.value.team.code;
3651
+ const priorHadId = seenCodes.get(code);
3652
+ const codeCollision = priorHadId !== void 0 && (providerId === void 0 || priorHadId === false);
3653
+ if (codeCollision || seenRanks.has(providerRank) || providerId !== void 0 && seenProviderIds.has(providerId)) {
3634
3654
  complete = false;
3655
+ omitted += 1;
3635
3656
  continue;
3636
3657
  }
3637
- seenTeams.add(key);
3638
- seenRanks.add(r.value.providerRank);
3639
- if (r.value.providerId !== void 0) seenProviderIds.add(r.value.providerId);
3658
+ seenCodes.set(code, providerId !== void 0);
3659
+ seenRanks.add(providerRank);
3660
+ if (providerId !== void 0) seenProviderIds.add(providerId);
3640
3661
  const { providerId: _dropId, providerRank: rank, ...row } = r.value;
3641
- ranked.push({ row, rank });
3662
+ ranked.push({ row: { ...row, rank }, rank });
3642
3663
  }
3643
3664
  ranked.sort((a, b) => {
3644
3665
  if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
@@ -3650,7 +3671,11 @@ function parseEspnStandings(raw) {
3650
3671
  complete = false;
3651
3672
  continue;
3652
3673
  }
3653
- out.push({ group: letter, rows: ranked.map((x) => x.row) });
3674
+ out.push({
3675
+ group: letter,
3676
+ rows: ranked.map((x) => x.row),
3677
+ ...omitted > 0 ? { partial: { omitted } } : {}
3678
+ });
3654
3679
  }
3655
3680
  return {
3656
3681
  items: out,
@@ -3662,12 +3687,75 @@ function parseEspnStandings(raw) {
3662
3687
  };
3663
3688
  }
3664
3689
 
3690
+ // src/adapters/http.ts
3691
+ var ResponseTooLargeError = class extends Error {
3692
+ constructor(bytes, limit) {
3693
+ super(`response body exceeds ${limit} bytes (${bytes} seen)`);
3694
+ this.bytes = bytes;
3695
+ this.limit = limit;
3696
+ this.name = "ResponseTooLargeError";
3697
+ }
3698
+ bytes;
3699
+ limit;
3700
+ };
3701
+ function isStream(v) {
3702
+ return typeof v?.getReader === "function";
3703
+ }
3704
+ async function readJsonBounded(res, maxBytes) {
3705
+ const r = res;
3706
+ const declared = Number(r.headers?.get?.("content-length"));
3707
+ if (Number.isFinite(declared) && declared > maxBytes) {
3708
+ if (isStream(r.body)) await r.body.cancel().catch(() => {
3709
+ });
3710
+ throw new ResponseTooLargeError(declared, maxBytes);
3711
+ }
3712
+ if (isStream(r.body)) {
3713
+ const reader = r.body.getReader();
3714
+ const chunks = [];
3715
+ let total = 0;
3716
+ for (; ; ) {
3717
+ const { done, value } = await reader.read();
3718
+ if (done) break;
3719
+ total += value.byteLength;
3720
+ if (total > maxBytes) {
3721
+ await reader.cancel().catch(() => {
3722
+ });
3723
+ throw new ResponseTooLargeError(total, maxBytes);
3724
+ }
3725
+ chunks.push(value);
3726
+ }
3727
+ return JSON.parse(new TextDecoder().decode(Buffer.concat(chunks)));
3728
+ }
3729
+ if (typeof r.text === "function") {
3730
+ const text = await r.text();
3731
+ const size = Buffer.byteLength(text);
3732
+ if (size > maxBytes) throw new ResponseTooLargeError(size, maxBytes);
3733
+ return JSON.parse(text);
3734
+ }
3735
+ if (typeof r.json === "function") return r.json();
3736
+ throw new TypeError("response has no readable body");
3737
+ }
3738
+
3665
3739
  // src/adapters/espn.ts
3666
3740
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3667
3741
  var DEFAULT_COMPETITION = "fifa.world";
3668
3742
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3669
- var USER_AGENT = `claudinho/${"0.9.4"} (+https://github.com/arturogarrido/claudinho)`;
3743
+ var USER_AGENT = `claudinho/${"0.10.1"} (+https://github.com/arturogarrido/claudinho)`;
3670
3744
  var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
3745
+ var DEFAULT_COOLDOWN_MS = 5 * 6e4;
3746
+ var MAX_COOLDOWN_MS = 15 * 6e4;
3747
+ function retryAfterMs(header, nowMs) {
3748
+ if (typeof header !== "string" || header.trim() === "") return DEFAULT_COOLDOWN_MS;
3749
+ const h = header.trim();
3750
+ let ms;
3751
+ if (/^\d+$/.test(h)) ms = Number(h) * 1e3;
3752
+ else {
3753
+ const at = Date.parse(h);
3754
+ if (Number.isFinite(at)) ms = at - nowMs;
3755
+ }
3756
+ if (ms === void 0 || !Number.isFinite(ms)) return DEFAULT_COOLDOWN_MS;
3757
+ return Math.min(Math.max(ms, 0), MAX_COOLDOWN_MS);
3758
+ }
3671
3759
  function competitionBase(slug) {
3672
3760
  return `${ESPN_SOCCER}/${slug}`;
3673
3761
  }
@@ -3676,6 +3764,8 @@ var STANDINGS_SHARE_MS = 3e4;
3676
3764
  var ProviderError = class extends Error {
3677
3765
  kind;
3678
3766
  status;
3767
+ /** For a throttle: how long the adapter will refuse to fetch (bounded). */
3768
+ retryAfterMs;
3679
3769
  constructor(message, kind, status) {
3680
3770
  super(message);
3681
3771
  this.name = "ProviderError";
@@ -3705,6 +3795,7 @@ function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length
3705
3795
  var EspnAdapter = class {
3706
3796
  constructor(opts = {}) {
3707
3797
  this.opts = opts;
3798
+ this.clock = opts.now ?? (() => Date.now());
3708
3799
  const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
3709
3800
  this.expectedStandingsGroups = expected ? [...expected] : void 0;
3710
3801
  this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
@@ -3730,6 +3821,53 @@ var EspnAdapter = class {
3730
3821
  * throttle (persist a backoff) from an ordinary blip.
3731
3822
  */
3732
3823
  lastError;
3824
+ /**
3825
+ * A retained throttle (audit A12): after a 429/403 every call inside the
3826
+ * window throws the provider's last answer WITHOUT a request. A server-
3827
+ * lifetime MCP adapter is covered by this alone; the CLI pre-arms each
3828
+ * process from its persisted cache via `armCooldown`.
3829
+ */
3830
+ cooldownUntilMs;
3831
+ cooldownError;
3832
+ cooldownListeners = /* @__PURE__ */ new Set();
3833
+ clock;
3834
+ /** Epoch ms until which requests are refused, when a cooldown is armed. */
3835
+ get cooldownUntil() {
3836
+ return this.cooldownUntilMs;
3837
+ }
3838
+ /**
3839
+ * Arm the cooldown from outside — a fresh CLI process reading the backoff
3840
+ * its refresher persisted. The retained error reads as a throttle so every
3841
+ * caller's `degraded` path and the refresher's persistence treat it as one.
3842
+ */
3843
+ armCooldown(untilMs, reason) {
3844
+ const nowMs = this.clock();
3845
+ const error = reason ?? new ProviderError("ESPN request skipped: provider cooldown in effect", "http", 429);
3846
+ error.retryAfterMs = Math.max(0, untilMs - nowMs);
3847
+ this.arm(untilMs, error);
3848
+ }
3849
+ /**
3850
+ * Be told whenever the cooldown window is armed or EXTENDED — the way a
3851
+ * caller persists a throttle that arrives from a still-running request after
3852
+ * its own call already returned (review P2 on #128). Returns unsubscribe.
3853
+ */
3854
+ onCooldown(listener) {
3855
+ this.cooldownListeners.add(listener);
3856
+ return () => {
3857
+ this.cooldownListeners.delete(listener);
3858
+ };
3859
+ }
3860
+ /**
3861
+ * The ONE place a window is set. Concurrent requests can each carry a
3862
+ * Retry-After; the LATEST expiry wins — a shorter one arriving second must
3863
+ * never shorten a longer active window (review P2 on #128).
3864
+ */
3865
+ arm(untilMs, error) {
3866
+ if (this.cooldownUntilMs !== void 0 && untilMs <= this.cooldownUntilMs) return;
3867
+ this.cooldownUntilMs = untilMs;
3868
+ this.cooldownError = error;
3869
+ for (const listener of this.cooldownListeners) listener(untilMs);
3870
+ }
3733
3871
  async fetchByDate(dateISO) {
3734
3872
  return this.fetchScoreboard(toEspnDate(dateISO));
3735
3873
  }
@@ -3817,6 +3955,11 @@ var EspnAdapter = class {
3817
3955
  return usableProviderItems("scoreboard", parsed);
3818
3956
  }
3819
3957
  async get(url) {
3958
+ const nowMs = this.clock();
3959
+ if (this.cooldownError && this.cooldownUntilMs !== void 0 && nowMs < this.cooldownUntilMs) {
3960
+ this.lastError = this.cooldownError;
3961
+ throw this.cooldownError;
3962
+ }
3820
3963
  const doFetch = this.opts.fetchImpl ?? fetch;
3821
3964
  const controller = new AbortController();
3822
3965
  const timer = setTimeout(
@@ -3838,19 +3981,24 @@ var EspnAdapter = class {
3838
3981
  throw e?.name === "AbortError" ? new ProviderError(`ESPN request timed out: ${url}`, "timeout") : new ProviderError(`ESPN request failed: ${e?.message ?? e}`, "http");
3839
3982
  }
3840
3983
  if (!res.ok) {
3841
- throw new ProviderError(
3984
+ const pe = new ProviderError(
3842
3985
  `ESPN request failed: ${res.status} ${res.statusText}`,
3843
3986
  "http",
3844
3987
  res.status
3845
3988
  );
3846
- }
3847
- const length = Number(res.headers?.get?.("content-length"));
3848
- if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3849
- throw new ProviderError(`ESPN response too large: ${length} bytes`, "parse");
3989
+ if (pe.throttled) {
3990
+ const receiptMs = this.clock();
3991
+ pe.retryAfterMs = retryAfterMs(res.headers?.get?.("retry-after"), receiptMs);
3992
+ this.arm(receiptMs + pe.retryAfterMs, pe);
3993
+ }
3994
+ throw pe;
3850
3995
  }
3851
3996
  try {
3852
- return await res.json();
3997
+ return await readJsonBounded(res, MAX_RESPONSE_BYTES);
3853
3998
  } catch (e) {
3999
+ if (e instanceof ResponseTooLargeError) {
4000
+ throw new ProviderError(`ESPN response too large: ${e.bytes} bytes`, "parse");
4001
+ }
3854
4002
  throw new ProviderError(
3855
4003
  `ESPN response unparseable: ${e?.message ?? e}`,
3856
4004
  "parse"
@@ -4057,14 +4205,30 @@ function hasGroupStarted(group, tables) {
4057
4205
  function matchesPerTeamInGroup(teamCount) {
4058
4206
  return Math.max(0, teamCount - 1);
4059
4207
  }
4060
- function isGroupStandingsComplete(table) {
4061
- const n = table?.rows.length ?? 0;
4208
+ function isGroupStandingsComplete(table, expectedTeams) {
4209
+ if (!table || table.partial) return false;
4210
+ const n = table.rows.length;
4062
4211
  if (n < 2) return false;
4063
- const required = matchesPerTeamInGroup(n);
4212
+ if (expectedTeams !== void 0 && n < expectedTeams) return false;
4213
+ const required = matchesPerTeamInGroup(Math.max(n, expectedTeams ?? n));
4064
4214
  return table.rows.every((r) => r.played >= required);
4065
4215
  }
4216
+ function bundledGroupSize(group) {
4217
+ const codes = /* @__PURE__ */ new Set();
4218
+ for (const m of fixturesByGroup(group)) {
4219
+ codes.add(m.home.code);
4220
+ codes.add(m.away.code);
4221
+ }
4222
+ return codes.size > 0 ? codes.size : void 0;
4223
+ }
4066
4224
  function isGroupComplete(group, tables) {
4067
- return isGroupStandingsComplete(tables.find((t2) => t2.group === group));
4225
+ return isGroupStandingsComplete(
4226
+ tables.find((t2) => t2.group === group),
4227
+ bundledGroupSize(group)
4228
+ );
4229
+ }
4230
+ function isGroupPartial(group, tables) {
4231
+ return tables.find((t2) => t2.group === group)?.partial !== void 0;
4068
4232
  }
4069
4233
  function resolveWinner(match) {
4070
4234
  if (!isFinished(match.status)) return void 0;
@@ -4119,7 +4283,7 @@ function resolveSlot(ref, ctx, liveTeam, fixtureInMergedSet = false) {
4119
4283
  return tbd(ref.label);
4120
4284
  case "group": {
4121
4285
  if (liveParticipant) return liveParticipant;
4122
- if (!ctx.standingsDegraded && hasGroupStarted(ref.group, ctx.tables)) {
4286
+ if (!ctx.standingsDegraded && !isGroupPartial(ref.group, ctx.tables) && hasGroupStarted(ref.group, ctx.tables)) {
4123
4287
  const team = teamFromStandings(ref.group, ref.position, ctx.tables);
4124
4288
  if (team) {
4125
4289
  const status = isGroupComplete(ref.group, ctx.tables) ? "confirmed" : "projected";
@@ -4751,7 +4915,7 @@ function loadBracketTopology() {
4751
4915
  return TOPOLOGY;
4752
4916
  }
4753
4917
 
4754
- // src/live.ts
4918
+ // src/competition.ts
4755
4919
  function resolveCompetition(explicit) {
4756
4920
  if (explicit) return explicit;
4757
4921
  if (typeof process !== "undefined" && process.env?.CLAUDINHO_COMPETITION) {
@@ -4759,13 +4923,19 @@ function resolveCompetition(explicit) {
4759
4923
  }
4760
4924
  return DEFAULT_COMPETITION;
4761
4925
  }
4926
+ var BUNDLE_COMPETITION = DEFAULT_COMPETITION;
4927
+ function bundleApplies(competition = resolveCompetition()) {
4928
+ return competition === BUNDLE_COMPETITION;
4929
+ }
4930
+
4931
+ // src/live.ts
4762
4932
  var KNOWN_SOURCES = ["espn"];
4763
- function makeAdapter(source = "espn") {
4933
+ function makeAdapter(source = "espn", opts = {}) {
4764
4934
  switch (source) {
4765
4935
  case "espn": {
4766
4936
  const competition = resolveCompetition();
4767
4937
  const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
4768
- return new EspnAdapter({ baseUrl });
4938
+ return new EspnAdapter({ baseUrl, enrichGroups: opts.enrichGroups, now: opts.now });
4769
4939
  }
4770
4940
  default:
4771
4941
  throw new Error(
@@ -4783,7 +4953,7 @@ function liveSourceLabel(source) {
4783
4953
  return known[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
4784
4954
  }
4785
4955
  async function getMatchesForDate(adapter, dateISO) {
4786
- const base = allFixtures();
4956
+ const base = bundleApplies() ? allFixtures() : [];
4787
4957
  const day = dateISO.slice(0, 10);
4788
4958
  try {
4789
4959
  const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
@@ -4828,21 +4998,27 @@ function knockoutWindow() {
4828
4998
  return knockoutWindowMemo;
4829
4999
  }
4830
5000
  async function getBracket(adapter, opts = {}) {
5001
+ if (!bundleApplies()) {
5002
+ const view2 = { stages: [], degraded: false, standingsDegraded: false, unsupported: true };
5003
+ return { view: view2, degraded: false, standingsDegraded: false, unsupported: true };
5004
+ }
4831
5005
  const topology = loadBracketTopology();
4832
5006
  const base = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY");
4833
5007
  let matches = base;
4834
5008
  let liveDegraded = true;
4835
5009
  let source;
4836
- try {
4837
- const win = knockoutWindow();
4838
- const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4839
- matches = mergeLive(base, live);
4840
- liveDegraded = false;
4841
- source = adapter.name;
4842
- } catch {
5010
+ const win = knockoutWindow();
5011
+ if (adapter.fetchWindow && win) {
5012
+ try {
5013
+ const live = await adapter.fetchWindow(win.start, win.end);
5014
+ matches = mergeLive(base, live);
5015
+ liveDegraded = false;
5016
+ source = adapter.name;
5017
+ } catch {
5018
+ }
4843
5019
  }
4844
5020
  const standings = await getStandings(adapter);
4845
- if (!source && !standings.degraded && standings.source) {
5021
+ if (!source && !standings.degraded && standings.source && standings.tables.length > 0) {
4846
5022
  source = standings.source;
4847
5023
  }
4848
5024
  const view = buildBracketView(
@@ -4864,18 +5040,18 @@ async function getBracket(adapter, opts = {}) {
4864
5040
  }
4865
5041
  var EXTRA_TIME_SLACK_MS = 60 * 6e4;
4866
5042
  async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
5043
+ if (!bundleApplies()) return { match: void 0, degraded: false, unsupported: true };
4867
5044
  const nowMs = now.getTime();
4868
5045
  let fixtures = allFixtures();
4869
5046
  let overlayFailed = false;
4870
- try {
4871
- const win = knockoutWindow();
4872
- if (adapter.fetchWindow && win) {
4873
- fixtures = mergeLive(
4874
- fixtures,
4875
- await adapter.fetchWindow(win.start, win.end)
4876
- );
5047
+ const win = knockoutWindow();
5048
+ if (adapter.fetchWindow && win) {
5049
+ try {
5050
+ fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
5051
+ } catch {
5052
+ overlayFailed = true;
4877
5053
  }
4878
- } catch {
5054
+ } else {
4879
5055
  overlayFailed = true;
4880
5056
  }
4881
5057
  const candidate = fixturesByTeam(code, fixtures).find((m) => {
@@ -4891,23 +5067,27 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
4891
5067
  return { match: next, degraded: overlayFailed };
4892
5068
  }
4893
5069
  async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
5070
+ if (!bundleApplies()) return { fixture: void 0, degraded: false, unsupported: true };
4894
5071
  const base = allFixtures();
4895
5072
  let matches = base;
4896
5073
  let degraded = true;
4897
5074
  let liveById;
4898
- try {
4899
- const win = knockoutWindow();
4900
- const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4901
- matches = mergeLive(base, live);
4902
- degraded = false;
4903
- liveById = new Set(live.map((m) => m.id));
4904
- } catch {
5075
+ const win = knockoutWindow();
5076
+ if (adapter.fetchWindow && win) {
5077
+ try {
5078
+ const live = await adapter.fetchWindow(win.start, win.end);
5079
+ matches = mergeLive(base, live);
5080
+ degraded = false;
5081
+ liveById = new Set(live.map((m) => m.id));
5082
+ } catch {
5083
+ }
4905
5084
  }
4906
5085
  const fixture = nextFixtureForTeam(code, { from: now, fixtures: matches });
4907
5086
  const source = fixture && liveById?.has(fixture.id) ? adapter.name : void 0;
4908
5087
  return { fixture, degraded, source };
4909
5088
  }
4910
5089
  async function getKnockoutFixtures(adapter, now = /* @__PURE__ */ new Date()) {
5090
+ if (!bundleApplies()) return { fixtures: [], degraded: true, unsupported: true };
4911
5091
  const win = knockoutWindow();
4912
5092
  if (!adapter.fetchWindow || !win) return { fixtures: [], degraded: true };
4913
5093
  let live;
@@ -4916,13 +5096,13 @@ async function getKnockoutFixtures(adapter, now = /* @__PURE__ */ new Date()) {
4916
5096
  } catch {
4917
5097
  return { fixtures: [], degraded: true };
4918
5098
  }
4919
- const nowMs = now.getTime();
4920
5099
  const fixtures = live.filter(
4921
- (m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY" && Date.parse(m.kickoff) >= nowMs && isResolvedNation(m.home) && isResolvedNation(m.away)
5100
+ (m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY" && isUpcoming(m, now) && isResolvedNation(m.home) && isResolvedNation(m.away)
4922
5101
  ).sort(byKickoff);
4923
5102
  return { fixtures, degraded: false };
4924
5103
  }
4925
5104
  async function getMatchById(adapter, id) {
5105
+ if (!bundleApplies()) return { match: void 0, degraded: false, unsupported: true };
4926
5106
  const base = allFixtures().find((m) => m.id === id);
4927
5107
  if (!base) return { match: void 0, degraded: false };
4928
5108
  const day = base.kickoff.slice(0, 10);
@@ -5377,11 +5557,15 @@ var PolymarketProvider = class {
5377
5557
  if (!res.ok) {
5378
5558
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
5379
5559
  }
5380
- const length = Number(res.headers?.get?.("content-length"));
5381
- if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
5382
- throw new Error(`Polymarket response too large: ${length} bytes`);
5560
+ let data;
5561
+ try {
5562
+ data = await readJsonBounded(res, MAX_RESPONSE_BYTES);
5563
+ } catch (e) {
5564
+ if (e instanceof ResponseTooLargeError) {
5565
+ throw new Error(`Polymarket response too large: ${e.bytes} bytes`);
5566
+ }
5567
+ throw e;
5383
5568
  }
5384
- const data = await res.json();
5385
5569
  if (Array.isArray(data) && data.length > 1) {
5386
5570
  return ambiguous("slug returned more than one event");
5387
5571
  }
@@ -5644,6 +5828,10 @@ function numberish(v) {
5644
5828
  }
5645
5829
 
5646
5830
  // src/markets/provider.ts
5831
+ var MARKET_COMPETITIONS = /* @__PURE__ */ new Set([DEFAULT_COMPETITION]);
5832
+ function marketsCoverCompetition(competition = resolveCompetition()) {
5833
+ return MARKET_COMPETITIONS.has(competition);
5834
+ }
5647
5835
  function resolveMarketSource(explicit) {
5648
5836
  if (explicit) return explicit;
5649
5837
  if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
@@ -5660,6 +5848,7 @@ function makeMarketProvider(source) {
5660
5848
  return new FakeMarketProvider();
5661
5849
  // no synth → yields no signals, no network
5662
5850
  default:
5851
+ if (!marketsCoverCompetition()) return new FakeMarketProvider();
5663
5852
  return new PolymarketProvider();
5664
5853
  }
5665
5854
  }
@@ -5794,10 +5983,13 @@ function formatShareTable(input, options = {}) {
5794
5983
  input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
5795
5984
  );
5796
5985
  } else {
5797
- for (const { group, rows } of input.tables) {
5798
- blocks.push(
5799
- [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, i + 1))].join("\n")
5800
- );
5986
+ for (const { group, rows, partial } of input.tables) {
5987
+ const lines = [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, r.rank ?? i + 1))];
5988
+ if (partial) {
5989
+ const n = partial.omitted;
5990
+ lines.push("", `(partial table \u2014 ${n} row${n === 1 ? "" : "s"} unreadable; positions are the provider's ranks)`);
5991
+ }
5992
+ blocks.push(lines.join("\n"));
5801
5993
  }
5802
5994
  if (input.degraded) {
5803
5995
  blocks.push("(Live standings unavailable \u2014 group roster, not live results.)");
@@ -5939,7 +6131,9 @@ function formatBracketCompactLine(mv, opts = {}) {
5939
6131
  }
5940
6132
  export {
5941
6133
  BRACKET_STAGE_ORDER,
6134
+ BUNDLE_COMPETITION,
5942
6135
  DEFAULT_COMPETITION,
6136
+ DEFAULT_COOLDOWN_MS,
5943
6137
  DEFAULT_FLAVOR,
5944
6138
  DEFAULT_MAX_AGE_MS,
5945
6139
  EspnAdapter,
@@ -5948,9 +6142,12 @@ export {
5948
6142
  KNOCKOUT_EXTRA_TIME_MS,
5949
6143
  KNOWN_SOURCES,
5950
6144
  LIVE_WINDOW_MS,
6145
+ MARKET_COMPETITIONS,
6146
+ MAX_COOLDOWN_MS,
5951
6147
  MAX_LABEL_COLUMNS,
5952
6148
  PolymarketProvider,
5953
6149
  ProviderError,
6150
+ ResponseTooLargeError,
5954
6151
  SHARE_DISCLAIMER,
5955
6152
  SHARE_HASHTAG,
5956
6153
  allFixtures,
@@ -5961,6 +6158,7 @@ export {
5961
6158
  buildBracketTopology,
5962
6159
  buildBracketView,
5963
6160
  buildMarketSignal,
6161
+ bundleApplies,
5964
6162
  byKickoff,
5965
6163
  cacheableKeys,
5966
6164
  competitionBase,
@@ -6007,6 +6205,7 @@ export {
6007
6205
  isResolvedNation,
6008
6206
  isStaleSignal,
6009
6207
  isTournamentWindowOver,
6208
+ isUpcoming,
6010
6209
  isValidDate,
6011
6210
  isValidTimeZone,
6012
6211
  knockoutWindow,
@@ -6029,6 +6228,7 @@ export {
6029
6228
  marketRelevant,
6030
6229
  marketSignalRendersFor,
6031
6230
  marketSourceLabel,
6231
+ marketsCoverCompetition,
6032
6232
  matchFlavor,
6033
6233
  matchKey,
6034
6234
  matchLocation,
@@ -6047,10 +6247,12 @@ export {
6047
6247
  parseTeamSlot,
6048
6248
  parsedValue,
6049
6249
  productFlag,
6250
+ readJsonBounded,
6050
6251
  resolveCompetition,
6051
6252
  resolveMarketSource,
6052
6253
  resolveTz,
6053
6254
  resolvedValues,
6255
+ retryAfterMs,
6054
6256
  sanitizeBundledFixture,
6055
6257
  scoreline,
6056
6258
  sealMarketSignal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/core",
3
- "version": "0.9.4",
3
+ "version": "0.10.1",
4
4
  "description": "Domain model, provider adapters (ESPN), standings, bundled schedule, and the read-only Polymarket market-signal sidecar powering Claudinho. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,12 +53,12 @@
53
53
  "agent"
54
54
  ],
55
55
  "devDependencies": {
56
- "@types/node": "^22.20.1",
57
- "@vitest/coverage-v8": "^4.1.10",
56
+ "@types/node": "^22.20.2",
57
+ "@vitest/coverage-v8": "^5.0.0",
58
58
  "tsup": "^8.0.0",
59
- "tsx": "^4.23.1",
59
+ "tsx": "^4.23.13",
60
60
  "typescript": "^5.7.0",
61
- "vitest": "^4.1.10"
61
+ "vitest": "^5.0.0"
62
62
  },
63
63
  "scripts": {
64
64
  "build": "tsup",