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