@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 +130 -1
- package/dist/index.js +246 -56
- package/package.json +3 -3
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.
|
|
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)
|
|
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({
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
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
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
fixtures = mergeLive(
|
|
4879
|
-
|
|
4880
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
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" &&
|
|
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
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
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
|
-
|
|
5809
|
-
|
|
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.
|
|
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": "^
|
|
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": "^
|
|
61
|
+
"vitest": "^5.0.0"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"build": "tsup",
|