@claudinho/core 0.8.19 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -485,6 +485,18 @@ interface ProviderAdapter {
485
485
  fetchStandings?(): Promise<GroupStandings[]>;
486
486
  /** Optional push subscription (websocket/SSE providers). Returns an unsubscribe fn. */
487
487
  subscribe?(onBatch: (matches: Match[]) => void): () => void;
488
+ /**
489
+ * Optional: the most recent request failure, cleared when a new request
490
+ * starts (best-effort under concurrency). Lets a caller whose result path
491
+ * fails closed to `degraded` booleans still distinguish a throttle/block
492
+ * (`throttled` — persist a backoff, hammering makes it worse) from an
493
+ * ordinary blip (retry on the normal cadence).
494
+ */
495
+ readonly lastError?: {
496
+ readonly kind: string;
497
+ readonly status?: number;
498
+ readonly throttled?: boolean;
499
+ };
488
500
  }
489
501
 
490
502
  /**
@@ -503,6 +515,19 @@ interface ProviderAdapter {
503
515
  declare const DEFAULT_COMPETITION = "fifa.world";
504
516
  /** Build an ESPN soccer base URL for a competition slug (e.g. "fifa.friendly"). */
505
517
  declare function competitionBase(slug: string): string;
518
+ type ProviderErrorKind = 'http' | 'timeout' | 'parse';
519
+ /**
520
+ * Classified provider failure, so a caller that got a degraded result can tell
521
+ * a throttle/block (back off — hammering makes it worse) from a timeout or a
522
+ * shape change (plain degraded, retry on the normal cadence).
523
+ */
524
+ declare class ProviderError extends Error {
525
+ readonly kind: ProviderErrorKind;
526
+ readonly status?: number;
527
+ constructor(message: string, kind: ProviderErrorKind, status?: number);
528
+ /** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
529
+ get throttled(): boolean;
530
+ }
506
531
  interface EspnStatusType {
507
532
  name?: string;
508
533
  state?: string;
@@ -608,6 +633,20 @@ declare class EspnAdapter implements ProviderAdapter {
608
633
  readonly capabilities: ProviderCapabilities;
609
634
  /** Cached team-code -> group-letter map (built lazily from standings). */
610
635
  private groupMap?;
636
+ /**
637
+ * One in-flight/recent standings fetch shared by fetchStandings and
638
+ * fetchGroupMap, so a command like `bracket` hits the endpoint once instead
639
+ * of twice, and a long-lived MCP server stays fresh (short TTL). A rejected
640
+ * fetch clears the slot — a transient failure is never cached.
641
+ */
642
+ private standingsShared?;
643
+ /**
644
+ * The most recent request failure (best-effort under concurrency; cleared
645
+ * when a new request starts). A post-hoc hint for callers whose result path
646
+ * fails closed to `degraded` booleans but who still need to distinguish a
647
+ * throttle (persist a backoff) from an ordinary blip.
648
+ */
649
+ lastError?: ProviderError;
611
650
  constructor(opts?: EspnAdapterOptions);
612
651
  fetchByDate(dateISO: string): Promise<Match[]>;
613
652
  fetchWindow(startDate: string, endDate: string): Promise<Match[]>;
@@ -620,6 +659,8 @@ declare class EspnAdapter implements ProviderAdapter {
620
659
  fetchLive(): Promise<Match[]>;
621
660
  /** Standings endpoint URL (lives under apis/v2, not site/v2; derived from base). */
622
661
  private standingsUrl;
662
+ /** The shared standings fetch (see {@link standingsShared}). */
663
+ private sharedStandings;
623
664
  /**
624
665
  * Authoritative, cumulative group tables from the standings endpoint. Throws
625
666
  * on fetch/parse failure (the caller decides the fallback). Group-stage only:
@@ -628,8 +669,11 @@ declare class EspnAdapter implements ProviderAdapter {
628
669
  fetchStandings(): Promise<GroupStandings[]>;
629
670
  /**
630
671
  * Build (and cache) a team-code -> group-letter map from the standings
631
- * endpoint. Best-effort: returns {} if standings are unavailable. Reuses the
632
- * same parse as {@link fetchStandings}, so the two never drift.
672
+ * endpoint. Best-effort: returns {} if standings are unavailable but a
673
+ * transient failure is NOT cached (only a successful parse pins the map), so
674
+ * one blip can't silently drop group letters for the adapter's lifetime.
675
+ * Reuses the same parse/fetch as {@link fetchStandings}, so the two never
676
+ * drift and one command never fetches standings twice.
633
677
  */
634
678
  fetchGroupMap(force?: boolean): Promise<Record<string, string>>;
635
679
  private fetchScoreboard;
@@ -644,7 +688,14 @@ declare class EspnAdapter implements ProviderAdapter {
644
688
  * always the World Cup.
645
689
  */
646
690
  declare function resolveCompetition(explicit?: string): string;
647
- /** Construct a provider adapter for a `--source` name (default: espn). */
691
+ /** Provider names {@link makeAdapter} can construct (the CLI validates against this). */
692
+ declare const KNOWN_SOURCES: readonly ["espn"];
693
+ /**
694
+ * Construct a provider adapter for a `--source` name (default: espn). An
695
+ * unknown source FAILS LOUD instead of silently running ESPN — `--source foo`
696
+ * previously no-op'd, which lied about what the flag did (attribution stayed
697
+ * honest, but the advertised knob did nothing).
698
+ */
648
699
  declare function makeAdapter(source?: string): ProviderAdapter;
649
700
  /**
650
701
  * Merge live matches over a base set by id. Live entries replace base entries
@@ -690,6 +741,10 @@ interface StandingsResult {
690
741
  * asked-for group isn't in it (caller renders "no such group").
691
742
  */
692
743
  declare function getStandings(adapter: ProviderAdapter, group?: string): Promise<StandingsResult>;
744
+ declare function knockoutWindow(): {
745
+ start: string;
746
+ end: string;
747
+ } | null;
693
748
  /**
694
749
  * Knockout bracket with hybrid slot resolution: confirmed FT winners/losers from
695
750
  * the live overlay; group slots project from live standings once a group has started.
@@ -1224,4 +1279,4 @@ declare function formatShareBracket(input: ShareBracketInput, options?: ShareBra
1224
1279
  /** Compact one-line-per-match bracket for narrow share contexts. */
1225
1280
  declare function formatBracketCompactLine(mv: BracketMatchView, opts?: BracketFormatOpts): string;
1226
1281
 
1227
- export { BRACKET_STAGE_ORDER, 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, FEED_TEXT_MAX, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type GroupStandings, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type LiveResult, 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, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, type PunditPick, type ResolvedParticipant, SHARE_DISCLAIMER, SHARE_HASHTAG, 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, asFlavorLevel, buildBracketTopology, buildBracketView, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, deriveFavorite, displayWidth, 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, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isResolvedNation, isStaleSignal, isValidDate, isValidTimeZone, liveSourceLabel, loadBracketTopology, localDate, lookupTeam, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSignalRendersFor, marketSourceLabel, matchFlavor, matchKey, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseStandings, parseTeamSlot, resolveCompetition, resolveMarketSource, resolveTz, sanitizeBundledFixture, sanitizeFeedText, sanitizeMatchStrings, scoreline, stageLabel, stageLabelI18n, t };
1282
+ export { BRACKET_STAGE_ORDER, 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, FEED_TEXT_MAX, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type GroupStandings, KNOWN_SOURCES, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type LiveResult, 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, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, ProviderError, type ProviderErrorKind, type PunditPick, type ResolvedParticipant, SHARE_DISCLAIMER, SHARE_HASHTAG, 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, asFlavorLevel, buildBracketTopology, buildBracketView, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, deriveFavorite, displayWidth, 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, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isResolvedNation, isStaleSignal, isValidDate, isValidTimeZone, knockoutWindow, liveSourceLabel, loadBracketTopology, localDate, lookupTeam, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSignalRendersFor, marketSourceLabel, matchFlavor, matchKey, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseStandings, parseTeamSlot, resolveCompetition, resolveMarketSource, resolveTz, sanitizeBundledFixture, sanitizeFeedText, sanitizeMatchStrings, scoreline, stageLabel, stageLabelI18n, t };
package/dist/index.js CHANGED
@@ -378,7 +378,8 @@ function normalizeLang(lang) {
378
378
  }
379
379
  function t(lang, key, vars) {
380
380
  const dict = CATALOGS[normalizeLang(lang)];
381
- let s = dict[key] ?? EN[key] ?? key;
381
+ const en = EN;
382
+ let s = dict[key] ?? en[key] ?? key;
382
383
  if (vars) {
383
384
  for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, v);
384
385
  }
@@ -3085,11 +3086,27 @@ function rosterAtZero(matches) {
3085
3086
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3086
3087
  var DEFAULT_COMPETITION = "fifa.world";
3087
3088
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3088
- var USER_AGENT = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
3089
+ var USER_AGENT = `claudinho/${"0.9.1"} (+https://github.com/arturogarrido/claudinho)`;
3089
3090
  var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
3090
3091
  function competitionBase(slug) {
3091
3092
  return `${ESPN_SOCCER}/${slug}`;
3092
3093
  }
3094
+ var DEFAULT_TIMEOUT_MS = 6e3;
3095
+ var STANDINGS_SHARE_MS = 3e4;
3096
+ var ProviderError = class extends Error {
3097
+ kind;
3098
+ status;
3099
+ constructor(message, kind, status) {
3100
+ super(message);
3101
+ this.name = "ProviderError";
3102
+ this.kind = kind;
3103
+ this.status = status;
3104
+ }
3105
+ /** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
3106
+ get throttled() {
3107
+ return this.kind === "http" && (this.status === 429 || this.status === 403);
3108
+ }
3109
+ };
3093
3110
  function mapStatus(st) {
3094
3111
  const name = (st?.type?.name ?? "").toUpperCase();
3095
3112
  const state = st?.type?.state ?? "";
@@ -3231,6 +3248,20 @@ var EspnAdapter = class {
3231
3248
  capabilities = { push: false, latencyHintSec: 45 };
3232
3249
  /** Cached team-code -> group-letter map (built lazily from standings). */
3233
3250
  groupMap;
3251
+ /**
3252
+ * One in-flight/recent standings fetch shared by fetchStandings and
3253
+ * fetchGroupMap, so a command like `bracket` hits the endpoint once instead
3254
+ * of twice, and a long-lived MCP server stays fresh (short TTL). A rejected
3255
+ * fetch clears the slot — a transient failure is never cached.
3256
+ */
3257
+ standingsShared;
3258
+ /**
3259
+ * The most recent request failure (best-effort under concurrency; cleared
3260
+ * when a new request starts). A post-hoc hint for callers whose result path
3261
+ * fails closed to `degraded` booleans but who still need to distinguish a
3262
+ * throttle (persist a backoff) from an ordinary blip.
3263
+ */
3264
+ lastError;
3234
3265
  async fetchByDate(dateISO) {
3235
3266
  return this.fetchScoreboard(toEspnDate(dateISO));
3236
3267
  }
@@ -3252,37 +3283,58 @@ var EspnAdapter = class {
3252
3283
  const base = this.opts.baseUrl ?? DEFAULT_BASE;
3253
3284
  return `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
3254
3285
  }
3286
+ /** The shared standings fetch (see {@link standingsShared}). */
3287
+ sharedStandings() {
3288
+ const now = Date.now();
3289
+ if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
3290
+ return this.standingsShared.promise;
3291
+ }
3292
+ const promise = this.get(this.standingsUrl()).then(
3293
+ (d) => parseStandings(d)
3294
+ );
3295
+ this.standingsShared = { at: now, promise };
3296
+ promise.catch(() => {
3297
+ if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
3298
+ });
3299
+ return promise;
3300
+ }
3255
3301
  /**
3256
3302
  * Authoritative, cumulative group tables from the standings endpoint. Throws
3257
3303
  * on fetch/parse failure (the caller decides the fallback). Group-stage only:
3258
3304
  * non-group `children` are filtered out by {@link parseStandings}.
3259
3305
  */
3260
3306
  async fetchStandings() {
3261
- return parseStandings(await this.get(this.standingsUrl()));
3307
+ return this.sharedStandings();
3262
3308
  }
3263
3309
  /**
3264
3310
  * Build (and cache) a team-code -> group-letter map from the standings
3265
- * endpoint. Best-effort: returns {} if standings are unavailable. Reuses the
3266
- * same parse as {@link fetchStandings}, so the two never drift.
3311
+ * endpoint. Best-effort: returns {} if standings are unavailable but a
3312
+ * transient failure is NOT cached (only a successful parse pins the map), so
3313
+ * one blip can't silently drop group letters for the adapter's lifetime.
3314
+ * Reuses the same parse/fetch as {@link fetchStandings}, so the two never
3315
+ * drift and one command never fetches standings twice.
3267
3316
  */
3268
3317
  async fetchGroupMap(force = false) {
3269
3318
  if (this.groupMap && !force) return this.groupMap;
3270
- const map = {};
3271
3319
  try {
3272
- const tables = parseStandings(await this.get(this.standingsUrl()));
3320
+ const tables = await this.sharedStandings();
3321
+ const map = {};
3273
3322
  for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
3323
+ this.groupMap = map;
3324
+ return map;
3274
3325
  } catch {
3326
+ return {};
3275
3327
  }
3276
- this.groupMap = map;
3277
- return map;
3278
3328
  }
3279
3329
  async fetchScoreboard(dates) {
3280
3330
  const base = this.opts.baseUrl ?? DEFAULT_BASE;
3281
3331
  const url = new URL(`${base}/scoreboard`);
3282
3332
  url.searchParams.set("limit", "300");
3283
3333
  if (dates) url.searchParams.set("dates", dates);
3284
- const groupByTeam = this.opts.enrichGroups === false ? {} : await this.fetchGroupMap();
3285
- const data = await this.get(url.toString());
3334
+ const [groupByTeam, data] = await Promise.all([
3335
+ this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
3336
+ this.get(url.toString())
3337
+ ]);
3286
3338
  return (data.events ?? []).map((ev) => mapEspnEvent(ev, { groupByTeam }));
3287
3339
  }
3288
3340
  async get(url) {
@@ -3290,24 +3342,51 @@ var EspnAdapter = class {
3290
3342
  const controller = new AbortController();
3291
3343
  const timer = setTimeout(
3292
3344
  () => controller.abort(),
3293
- this.opts.timeoutMs ?? 15e3
3345
+ this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
3294
3346
  );
3347
+ this.lastError = void 0;
3295
3348
  try {
3296
- const res = await doFetch(url, {
3297
- signal: controller.signal,
3298
- // ESPN never legitimately redirects; following one would sidestep the
3299
- // fixed-host guarantee, so treat any redirect as a failure (degraded).
3300
- redirect: "error",
3301
- headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3302
- });
3349
+ let res;
3350
+ try {
3351
+ res = await doFetch(url, {
3352
+ signal: controller.signal,
3353
+ // ESPN never legitimately redirects; following one would sidestep the
3354
+ // fixed-host guarantee, so treat any redirect as a failure (degraded).
3355
+ redirect: "error",
3356
+ headers: { Accept: "application/json", "User-Agent": USER_AGENT }
3357
+ });
3358
+ } catch (e) {
3359
+ throw e?.name === "AbortError" ? new ProviderError(`ESPN request timed out: ${url}`, "timeout") : new ProviderError(`ESPN request failed: ${e?.message ?? e}`, "http");
3360
+ }
3303
3361
  if (!res.ok) {
3304
- throw new Error(`ESPN request failed: ${res.status} ${res.statusText}`);
3362
+ throw new ProviderError(
3363
+ `ESPN request failed: ${res.status} ${res.statusText}`,
3364
+ "http",
3365
+ res.status
3366
+ );
3305
3367
  }
3306
3368
  const length = Number(res.headers?.get?.("content-length"));
3307
3369
  if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3308
- throw new Error(`ESPN response too large: ${length} bytes`);
3370
+ throw new ProviderError(`ESPN response too large: ${length} bytes`, "parse");
3371
+ }
3372
+ try {
3373
+ return await res.json();
3374
+ } catch (e) {
3375
+ throw new ProviderError(
3376
+ `ESPN response unparseable: ${e?.message ?? e}`,
3377
+ "parse"
3378
+ );
3309
3379
  }
3310
- return await res.json();
3380
+ } catch (e) {
3381
+ const pe = e instanceof ProviderError ? e : new ProviderError(String(e?.message ?? e), "http");
3382
+ this.lastError = pe;
3383
+ if (typeof process !== "undefined" && process.env?.CLAUDINHO_DEBUG) {
3384
+ process.stderr.write(
3385
+ `claudinho: espn ${pe.kind}${pe.status ? ` ${pe.status}` : ""}: ${url}
3386
+ `
3387
+ );
3388
+ }
3389
+ throw pe;
3311
3390
  } finally {
3312
3391
  clearTimeout(timer);
3313
3392
  }
@@ -4201,13 +4280,18 @@ function resolveCompetition(explicit) {
4201
4280
  }
4202
4281
  return DEFAULT_COMPETITION;
4203
4282
  }
4283
+ var KNOWN_SOURCES = ["espn"];
4204
4284
  function makeAdapter(source = "espn") {
4205
4285
  switch (source) {
4206
- default: {
4286
+ case "espn": {
4207
4287
  const competition = resolveCompetition();
4208
4288
  const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
4209
4289
  return new EspnAdapter({ baseUrl });
4210
4290
  }
4291
+ default:
4292
+ throw new Error(
4293
+ `Unknown data source "${source}" (available: ${KNOWN_SOURCES.join(", ")})`
4294
+ );
4211
4295
  }
4212
4296
  }
4213
4297
  function mergeLive(base, live) {
@@ -4245,8 +4329,16 @@ async function getStandings(adapter, group) {
4245
4329
  const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
4246
4330
  return { tables, degraded: true };
4247
4331
  }
4248
- var KNOCKOUT_WINDOW_START = "20260628";
4249
- var KNOCKOUT_WINDOW_END = "20260719";
4332
+ var knockoutWindowMemo;
4333
+ function knockoutWindow() {
4334
+ if (knockoutWindowMemo !== void 0) return knockoutWindowMemo;
4335
+ const days = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY").map((m) => m.kickoff.slice(0, 10).replace(/-/g, "")).filter((d) => d.length === 8);
4336
+ knockoutWindowMemo = days.length ? {
4337
+ start: days.reduce((a, b) => a < b ? a : b),
4338
+ end: days.reduce((a, b) => a > b ? a : b)
4339
+ } : null;
4340
+ return knockoutWindowMemo;
4341
+ }
4250
4342
  async function getBracket(adapter, opts = {}) {
4251
4343
  const topology = loadBracketTopology();
4252
4344
  const base = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY");
@@ -4254,7 +4346,8 @@ async function getBracket(adapter, opts = {}) {
4254
4346
  let liveDegraded = true;
4255
4347
  let source;
4256
4348
  try {
4257
- const live = adapter.fetchWindow ? await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END) : [];
4349
+ const win = knockoutWindow();
4350
+ const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4258
4351
  matches = mergeLive(base, live);
4259
4352
  liveDegraded = false;
4260
4353
  source = adapter.name;
@@ -4287,8 +4380,9 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
4287
4380
  let fixtures = allFixtures();
4288
4381
  let overlayFailed = false;
4289
4382
  try {
4290
- if (adapter.fetchWindow) {
4291
- fixtures = mergeLive(fixtures, await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END));
4383
+ const win = knockoutWindow();
4384
+ if (adapter.fetchWindow && win) {
4385
+ fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
4292
4386
  }
4293
4387
  } catch {
4294
4388
  overlayFailed = true;
@@ -4311,7 +4405,8 @@ async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Da
4311
4405
  let degraded = true;
4312
4406
  let liveById;
4313
4407
  try {
4314
- const live = adapter.fetchWindow ? await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END) : [];
4408
+ const win = knockoutWindow();
4409
+ const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4315
4410
  matches = mergeLive(base, live);
4316
4411
  degraded = false;
4317
4412
  liveById = new Set(live.map((m) => m.id));
@@ -4322,10 +4417,11 @@ async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Da
4322
4417
  return { fixture, degraded, source };
4323
4418
  }
4324
4419
  async function getKnockoutFixtures(adapter, now = /* @__PURE__ */ new Date()) {
4325
- if (!adapter.fetchWindow) return { fixtures: [], degraded: true };
4420
+ const win = knockoutWindow();
4421
+ if (!adapter.fetchWindow || !win) return { fixtures: [], degraded: true };
4326
4422
  let live;
4327
4423
  try {
4328
- live = await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END);
4424
+ live = await adapter.fetchWindow(win.start, win.end);
4329
4425
  } catch {
4330
4426
  return { fixtures: [], degraded: true };
4331
4427
  }
@@ -4583,7 +4679,7 @@ var mapping_2026_default = {
4583
4679
  var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
4584
4680
  var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
4585
4681
  var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
4586
- var DEFAULT_TIMEOUT_MS = 8e3;
4682
+ var DEFAULT_TIMEOUT_MS2 = 8e3;
4587
4683
  var WC_SERIES_SLUG = "soccer-fifwc";
4588
4684
  var WC_SPORT = "fifwc";
4589
4685
  var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
@@ -4621,7 +4717,7 @@ var PolymarketProvider = class {
4621
4717
  const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4622
4718
  const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4623
4719
  if (slugs.length === 0) return { checked: true };
4624
- const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4720
+ const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
4625
4721
  try {
4626
4722
  for (const slug of slugs) {
4627
4723
  const remaining = deadline - Date.now();
@@ -4641,7 +4737,7 @@ var PolymarketProvider = class {
4641
4737
  const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
4642
4738
  const doFetch = this.opts.fetchImpl ?? fetch;
4643
4739
  const res = await doFetch(url, {
4644
- signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
4740
+ signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
4645
4741
  // Gamma never legitimately redirects; following one would sidestep the
4646
4742
  // host allow-list (it only validates the base URL), so reject redirects.
4647
4743
  redirect: "error",
@@ -5110,8 +5206,10 @@ export {
5110
5206
  FEED_TEXT_MAX,
5111
5207
  FLAVOR_LEVELS,
5112
5208
  FakeMarketProvider,
5209
+ KNOWN_SOURCES,
5113
5210
  LIVE_WINDOW_MS,
5114
5211
  PolymarketProvider,
5212
+ ProviderError,
5115
5213
  SHARE_DISCLAIMER,
5116
5214
  SHARE_HASHTAG,
5117
5215
  allFixtures,
@@ -5162,6 +5260,7 @@ export {
5162
5260
  isStaleSignal,
5163
5261
  isValidDate,
5164
5262
  isValidTimeZone,
5263
+ knockoutWindow,
5165
5264
  liveSourceLabel,
5166
5265
  loadBracketTopology,
5167
5266
  localDate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/core",
3
- "version": "0.8.19",
3
+ "version": "0.9.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,8 +54,9 @@
54
54
  ],
55
55
  "devDependencies": {
56
56
  "@types/node": "^22.20.0",
57
+ "@vitest/coverage-v8": "^4.1.9",
57
58
  "tsup": "^8.0.0",
58
- "tsx": "^4.19.0",
59
+ "tsx": "^4.23.0",
59
60
  "typescript": "^5.7.0",
60
61
  "vitest": "^4.1.9"
61
62
  },