@claudinho/core 0.10.0 → 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,6 +841,25 @@ 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.
@@ -763,6 +870,16 @@ declare class EspnAdapter implements ProviderAdapter {
763
870
  * read the ONE resolver without importing each other.
764
871
  */
765
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;
766
883
 
767
884
  /** Provider names {@link makeAdapter} can construct (the CLI validates against this). */
768
885
  declare const KNOWN_SOURCES: readonly ["espn"];
@@ -773,6 +890,8 @@ interface AdapterOptions {
773
890
  * it never renders group letters.
774
891
  */
775
892
  enrichGroups?: boolean;
893
+ /** Clock, injectable for tests; drives the provider cooldown window. */
894
+ now?: () => number;
776
895
  }
777
896
  /**
778
897
  * Construct a provider adapter for a `--source` name (default: espn). An
@@ -852,6 +971,8 @@ interface MatchByIdResult {
852
971
  match?: Match;
853
972
  degraded: boolean;
854
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;
855
976
  }
856
977
  /**
857
978
  * The fixture a team-scoped MARKET query should be about, live-confirmed.
@@ -871,6 +992,8 @@ interface NextFixtureResult {
871
992
  degraded: boolean;
872
993
  /** The provider that served the live overlay (absent when degraded). */
873
994
  source?: string;
995
+ /** Off the bundle "next" is built on a schedule we do not have yet (audit A03). */
996
+ unsupported?: true;
874
997
  }
875
998
  /**
876
999
  * A team's next UPCOMING fixture, LIVE-RESOLVED across the knockout phase.
@@ -895,6 +1018,8 @@ interface KnockoutFixturesResult {
895
1018
  fixtures: Match[];
896
1019
  /** True when the overlay fetch failed — caller must NOT cache this as "none". */
897
1020
  degraded: boolean;
1021
+ /** Off the bundle there is no knockout window to fetch (audit A03). */
1022
+ unsupported?: true;
898
1023
  }
899
1024
  /**
900
1025
  * The RESOLVED upcoming knockout fixtures from the live overlay — the data the
@@ -1479,6 +1604,10 @@ interface ShareTableInput {
1479
1604
  tables: readonly {
1480
1605
  group: string;
1481
1606
  rows: readonly StandingRow[];
1607
+ /** Rows the provider served that could not be read (see GroupStandings). */
1608
+ partial?: {
1609
+ omitted: number;
1610
+ };
1482
1611
  }[];
1483
1612
  /** Live-data provider name for attribution; omit when degraded/static. */
1484
1613
  source?: string;
@@ -1567,4 +1696,4 @@ declare function formatShareBracket(input: ShareBracketInput, options?: ShareBra
1567
1696
  /** Compact one-line-per-match bracket for narrow share contexts. */
1568
1697
  declare function formatBracketCompactLine(mv: BracketMatchView, opts?: BracketFormatOpts): string;
1569
1698
 
1570
- export { BRACKET_STAGE_ORDER, type BatchResolution, type BoundedList, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketTopology, type BracketView, type BuildSignalInput, DEFAULT_COMPETITION, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, EspnAdapter, type EspnAdapterOptions, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type GroupStandings, KNOCKOUT_EXTRA_TIME_MS, KNOWN_SOURCES, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type LiveResult, MARKET_COMPETITIONS, MAX_LABEL_COLUMNS, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchByIdResult, type MatchEvent, type NextFixtureResult, type Outcome, type ParseResult, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, ProviderError, type ProviderErrorKind, type PunditPick, type ResolvedParticipant, SHARE_DISCLAIMER, SHARE_HASHTAG, type Selection, type ShareBracketInput, type ShareBracketOptions, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type Stage, type StandingRow, type StandingsResult, type Status, type Team, type TeamInfo, type TeamLookup, allFixtures, allTeams, ambiguous, asFlavorLevel, bounded, buildBracketTopology, buildBracketView, buildMarketSignal, byKickoff, cacheableKeys, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, definitiveNone, deriveFavorite, displayWidth, emptyBatch, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getStandings, groups, hasSaneDistribution, humanLabel, isCacheable, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isResolvedNation, isStaleSignal, isTournamentWindowOver, isValidDate, isValidTimeZone, knockoutWindow, liveSourceLabel, liveWindowMsFor, loadBracketTopology, localDate, lookupTeam, makeAdapter, makeMarketProvider, malformed, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSignalRendersFor, marketSourceLabel, marketsCoverCompetition, matchFlavor, matchKey, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseCachedMarketSignal, parseCachedMatch, parseCachedMatches, parseStandings, parseTeamSlot, parsedValue, productFlag, resolveCompetition, resolveMarketSource, resolveTz, resolvedValues, sanitizeBundledFixture, scoreline, sealMarketSignal, sealMatch, selectOne, stageLabel, stageLabelI18n, t, truncateVisible, unresolved, valid };
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,9 +3629,11 @@ 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
3639
  const seenCodes = /* @__PURE__ */ new Map();
@@ -3627,6 +3643,7 @@ function parseEspnStandings(raw) {
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
3649
  const { providerId, providerRank } = r.value;
@@ -3635,13 +3652,14 @@ function parseEspnStandings(raw) {
3635
3652
  const codeCollision = priorHadId !== void 0 && (providerId === void 0 || priorHadId === false);
3636
3653
  if (codeCollision || seenRanks.has(providerRank) || providerId !== void 0 && seenProviderIds.has(providerId)) {
3637
3654
  complete = false;
3655
+ omitted += 1;
3638
3656
  continue;
3639
3657
  }
3640
3658
  seenCodes.set(code, providerId !== void 0);
3641
3659
  seenRanks.add(providerRank);
3642
3660
  if (providerId !== void 0) seenProviderIds.add(providerId);
3643
3661
  const { providerId: _dropId, providerRank: rank, ...row } = r.value;
3644
- ranked.push({ row, rank });
3662
+ ranked.push({ row: { ...row, rank }, rank });
3645
3663
  }
3646
3664
  ranked.sort((a, b) => {
3647
3665
  if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
@@ -3653,7 +3671,11 @@ function parseEspnStandings(raw) {
3653
3671
  complete = false;
3654
3672
  continue;
3655
3673
  }
3656
- 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
+ });
3657
3679
  }
3658
3680
  return {
3659
3681
  items: out,
@@ -3665,12 +3687,75 @@ function parseEspnStandings(raw) {
3665
3687
  };
3666
3688
  }
3667
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
+
3668
3739
  // src/adapters/espn.ts
3669
3740
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3670
3741
  var DEFAULT_COMPETITION = "fifa.world";
3671
3742
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3672
- var USER_AGENT = `claudinho/${"0.10.0"} (+https://github.com/arturogarrido/claudinho)`;
3743
+ var USER_AGENT = `claudinho/${"0.10.1"} (+https://github.com/arturogarrido/claudinho)`;
3673
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
+ }
3674
3759
  function competitionBase(slug) {
3675
3760
  return `${ESPN_SOCCER}/${slug}`;
3676
3761
  }
@@ -3679,6 +3764,8 @@ var STANDINGS_SHARE_MS = 3e4;
3679
3764
  var ProviderError = class extends Error {
3680
3765
  kind;
3681
3766
  status;
3767
+ /** For a throttle: how long the adapter will refuse to fetch (bounded). */
3768
+ retryAfterMs;
3682
3769
  constructor(message, kind, status) {
3683
3770
  super(message);
3684
3771
  this.name = "ProviderError";
@@ -3708,6 +3795,7 @@ function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length
3708
3795
  var EspnAdapter = class {
3709
3796
  constructor(opts = {}) {
3710
3797
  this.opts = opts;
3798
+ this.clock = opts.now ?? (() => Date.now());
3711
3799
  const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
3712
3800
  this.expectedStandingsGroups = expected ? [...expected] : void 0;
3713
3801
  this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
@@ -3733,6 +3821,53 @@ var EspnAdapter = class {
3733
3821
  * throttle (persist a backoff) from an ordinary blip.
3734
3822
  */
3735
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
+ }
3736
3871
  async fetchByDate(dateISO) {
3737
3872
  return this.fetchScoreboard(toEspnDate(dateISO));
3738
3873
  }
@@ -3820,6 +3955,11 @@ var EspnAdapter = class {
3820
3955
  return usableProviderItems("scoreboard", parsed);
3821
3956
  }
3822
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
+ }
3823
3963
  const doFetch = this.opts.fetchImpl ?? fetch;
3824
3964
  const controller = new AbortController();
3825
3965
  const timer = setTimeout(
@@ -3841,19 +3981,24 @@ var EspnAdapter = class {
3841
3981
  throw e?.name === "AbortError" ? new ProviderError(`ESPN request timed out: ${url}`, "timeout") : new ProviderError(`ESPN request failed: ${e?.message ?? e}`, "http");
3842
3982
  }
3843
3983
  if (!res.ok) {
3844
- throw new ProviderError(
3984
+ const pe = new ProviderError(
3845
3985
  `ESPN request failed: ${res.status} ${res.statusText}`,
3846
3986
  "http",
3847
3987
  res.status
3848
3988
  );
3849
- }
3850
- const length = Number(res.headers?.get?.("content-length"));
3851
- if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3852
- 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;
3853
3995
  }
3854
3996
  try {
3855
- return await res.json();
3997
+ return await readJsonBounded(res, MAX_RESPONSE_BYTES);
3856
3998
  } catch (e) {
3999
+ if (e instanceof ResponseTooLargeError) {
4000
+ throw new ProviderError(`ESPN response too large: ${e.bytes} bytes`, "parse");
4001
+ }
3857
4002
  throw new ProviderError(
3858
4003
  `ESPN response unparseable: ${e?.message ?? e}`,
3859
4004
  "parse"
@@ -4060,14 +4205,30 @@ function hasGroupStarted(group, tables) {
4060
4205
  function matchesPerTeamInGroup(teamCount) {
4061
4206
  return Math.max(0, teamCount - 1);
4062
4207
  }
4063
- function isGroupStandingsComplete(table) {
4064
- const n = table?.rows.length ?? 0;
4208
+ function isGroupStandingsComplete(table, expectedTeams) {
4209
+ if (!table || table.partial) return false;
4210
+ const n = table.rows.length;
4065
4211
  if (n < 2) return false;
4066
- const required = matchesPerTeamInGroup(n);
4212
+ if (expectedTeams !== void 0 && n < expectedTeams) return false;
4213
+ const required = matchesPerTeamInGroup(Math.max(n, expectedTeams ?? n));
4067
4214
  return table.rows.every((r) => r.played >= required);
4068
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
+ }
4069
4224
  function isGroupComplete(group, tables) {
4070
- 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;
4071
4232
  }
4072
4233
  function resolveWinner(match) {
4073
4234
  if (!isFinished(match.status)) return void 0;
@@ -4122,7 +4283,7 @@ function resolveSlot(ref, ctx, liveTeam, fixtureInMergedSet = false) {
4122
4283
  return tbd(ref.label);
4123
4284
  case "group": {
4124
4285
  if (liveParticipant) return liveParticipant;
4125
- if (!ctx.standingsDegraded && hasGroupStarted(ref.group, ctx.tables)) {
4286
+ if (!ctx.standingsDegraded && !isGroupPartial(ref.group, ctx.tables) && hasGroupStarted(ref.group, ctx.tables)) {
4126
4287
  const team = teamFromStandings(ref.group, ref.position, ctx.tables);
4127
4288
  if (team) {
4128
4289
  const status = isGroupComplete(ref.group, ctx.tables) ? "confirmed" : "projected";
@@ -4762,6 +4923,10 @@ function resolveCompetition(explicit) {
4762
4923
  }
4763
4924
  return DEFAULT_COMPETITION;
4764
4925
  }
4926
+ var BUNDLE_COMPETITION = DEFAULT_COMPETITION;
4927
+ function bundleApplies(competition = resolveCompetition()) {
4928
+ return competition === BUNDLE_COMPETITION;
4929
+ }
4765
4930
 
4766
4931
  // src/live.ts
4767
4932
  var KNOWN_SOURCES = ["espn"];
@@ -4770,7 +4935,7 @@ function makeAdapter(source = "espn", opts = {}) {
4770
4935
  case "espn": {
4771
4936
  const competition = resolveCompetition();
4772
4937
  const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
4773
- return new EspnAdapter({ baseUrl, enrichGroups: opts.enrichGroups });
4938
+ return new EspnAdapter({ baseUrl, enrichGroups: opts.enrichGroups, now: opts.now });
4774
4939
  }
4775
4940
  default:
4776
4941
  throw new Error(
@@ -4788,7 +4953,7 @@ function liveSourceLabel(source) {
4788
4953
  return known[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
4789
4954
  }
4790
4955
  async function getMatchesForDate(adapter, dateISO) {
4791
- const base = allFixtures();
4956
+ const base = bundleApplies() ? allFixtures() : [];
4792
4957
  const day = dateISO.slice(0, 10);
4793
4958
  try {
4794
4959
  const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
@@ -4833,21 +4998,27 @@ function knockoutWindow() {
4833
4998
  return knockoutWindowMemo;
4834
4999
  }
4835
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
+ }
4836
5005
  const topology = loadBracketTopology();
4837
5006
  const base = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY");
4838
5007
  let matches = base;
4839
5008
  let liveDegraded = true;
4840
5009
  let source;
4841
- try {
4842
- const win = knockoutWindow();
4843
- const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4844
- matches = mergeLive(base, live);
4845
- liveDegraded = false;
4846
- source = adapter.name;
4847
- } 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
+ }
4848
5019
  }
4849
5020
  const standings = await getStandings(adapter);
4850
- if (!source && !standings.degraded && standings.source) {
5021
+ if (!source && !standings.degraded && standings.source && standings.tables.length > 0) {
4851
5022
  source = standings.source;
4852
5023
  }
4853
5024
  const view = buildBracketView(
@@ -4869,18 +5040,18 @@ async function getBracket(adapter, opts = {}) {
4869
5040
  }
4870
5041
  var EXTRA_TIME_SLACK_MS = 60 * 6e4;
4871
5042
  async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
5043
+ if (!bundleApplies()) return { match: void 0, degraded: false, unsupported: true };
4872
5044
  const nowMs = now.getTime();
4873
5045
  let fixtures = allFixtures();
4874
5046
  let overlayFailed = false;
4875
- try {
4876
- const win = knockoutWindow();
4877
- if (adapter.fetchWindow && win) {
4878
- fixtures = mergeLive(
4879
- fixtures,
4880
- await adapter.fetchWindow(win.start, win.end)
4881
- );
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;
4882
5053
  }
4883
- } catch {
5054
+ } else {
4884
5055
  overlayFailed = true;
4885
5056
  }
4886
5057
  const candidate = fixturesByTeam(code, fixtures).find((m) => {
@@ -4896,23 +5067,27 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
4896
5067
  return { match: next, degraded: overlayFailed };
4897
5068
  }
4898
5069
  async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
5070
+ if (!bundleApplies()) return { fixture: void 0, degraded: false, unsupported: true };
4899
5071
  const base = allFixtures();
4900
5072
  let matches = base;
4901
5073
  let degraded = true;
4902
5074
  let liveById;
4903
- try {
4904
- const win = knockoutWindow();
4905
- const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4906
- matches = mergeLive(base, live);
4907
- degraded = false;
4908
- liveById = new Set(live.map((m) => m.id));
4909
- } 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
+ }
4910
5084
  }
4911
5085
  const fixture = nextFixtureForTeam(code, { from: now, fixtures: matches });
4912
5086
  const source = fixture && liveById?.has(fixture.id) ? adapter.name : void 0;
4913
5087
  return { fixture, degraded, source };
4914
5088
  }
4915
5089
  async function getKnockoutFixtures(adapter, now = /* @__PURE__ */ new Date()) {
5090
+ if (!bundleApplies()) return { fixtures: [], degraded: true, unsupported: true };
4916
5091
  const win = knockoutWindow();
4917
5092
  if (!adapter.fetchWindow || !win) return { fixtures: [], degraded: true };
4918
5093
  let live;
@@ -4921,13 +5096,13 @@ async function getKnockoutFixtures(adapter, now = /* @__PURE__ */ new Date()) {
4921
5096
  } catch {
4922
5097
  return { fixtures: [], degraded: true };
4923
5098
  }
4924
- const nowMs = now.getTime();
4925
5099
  const fixtures = live.filter(
4926
- (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)
4927
5101
  ).sort(byKickoff);
4928
5102
  return { fixtures, degraded: false };
4929
5103
  }
4930
5104
  async function getMatchById(adapter, id) {
5105
+ if (!bundleApplies()) return { match: void 0, degraded: false, unsupported: true };
4931
5106
  const base = allFixtures().find((m) => m.id === id);
4932
5107
  if (!base) return { match: void 0, degraded: false };
4933
5108
  const day = base.kickoff.slice(0, 10);
@@ -5382,11 +5557,15 @@ var PolymarketProvider = class {
5382
5557
  if (!res.ok) {
5383
5558
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
5384
5559
  }
5385
- const length = Number(res.headers?.get?.("content-length"));
5386
- if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
5387
- 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;
5388
5568
  }
5389
- const data = await res.json();
5390
5569
  if (Array.isArray(data) && data.length > 1) {
5391
5570
  return ambiguous("slug returned more than one event");
5392
5571
  }
@@ -5804,10 +5983,13 @@ function formatShareTable(input, options = {}) {
5804
5983
  input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
5805
5984
  );
5806
5985
  } else {
5807
- for (const { group, rows } of input.tables) {
5808
- blocks.push(
5809
- [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, i + 1))].join("\n")
5810
- );
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"));
5811
5993
  }
5812
5994
  if (input.degraded) {
5813
5995
  blocks.push("(Live standings unavailable \u2014 group roster, not live results.)");
@@ -5949,7 +6131,9 @@ function formatBracketCompactLine(mv, opts = {}) {
5949
6131
  }
5950
6132
  export {
5951
6133
  BRACKET_STAGE_ORDER,
6134
+ BUNDLE_COMPETITION,
5952
6135
  DEFAULT_COMPETITION,
6136
+ DEFAULT_COOLDOWN_MS,
5953
6137
  DEFAULT_FLAVOR,
5954
6138
  DEFAULT_MAX_AGE_MS,
5955
6139
  EspnAdapter,
@@ -5959,9 +6143,11 @@ export {
5959
6143
  KNOWN_SOURCES,
5960
6144
  LIVE_WINDOW_MS,
5961
6145
  MARKET_COMPETITIONS,
6146
+ MAX_COOLDOWN_MS,
5962
6147
  MAX_LABEL_COLUMNS,
5963
6148
  PolymarketProvider,
5964
6149
  ProviderError,
6150
+ ResponseTooLargeError,
5965
6151
  SHARE_DISCLAIMER,
5966
6152
  SHARE_HASHTAG,
5967
6153
  allFixtures,
@@ -5972,6 +6158,7 @@ export {
5972
6158
  buildBracketTopology,
5973
6159
  buildBracketView,
5974
6160
  buildMarketSignal,
6161
+ bundleApplies,
5975
6162
  byKickoff,
5976
6163
  cacheableKeys,
5977
6164
  competitionBase,
@@ -6018,6 +6205,7 @@ export {
6018
6205
  isResolvedNation,
6019
6206
  isStaleSignal,
6020
6207
  isTournamentWindowOver,
6208
+ isUpcoming,
6021
6209
  isValidDate,
6022
6210
  isValidTimeZone,
6023
6211
  knockoutWindow,
@@ -6059,10 +6247,12 @@ export {
6059
6247
  parseTeamSlot,
6060
6248
  parsedValue,
6061
6249
  productFlag,
6250
+ readJsonBounded,
6062
6251
  resolveCompetition,
6063
6252
  resolveMarketSource,
6064
6253
  resolveTz,
6065
6254
  resolvedValues,
6255
+ retryAfterMs,
6066
6256
  sanitizeBundledFixture,
6067
6257
  scoreline,
6068
6258
  sealMarketSignal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/core",
3
- "version": "0.10.0",
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",
@@ -54,11 +54,11 @@
54
54
  ],
55
55
  "devDependencies": {
56
56
  "@types/node": "^22.20.2",
57
- "@vitest/coverage-v8": "^4.1.11",
57
+ "@vitest/coverage-v8": "^5.0.0",
58
58
  "tsup": "^8.0.0",
59
59
  "tsx": "^4.23.13",
60
60
  "typescript": "^5.7.0",
61
- "vitest": "^4.1.11"
61
+ "vitest": "^5.0.0"
62
62
  },
63
63
  "scripts": {
64
64
  "build": "tsup",