@claudinho/core 0.9.3 → 0.9.4

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.js CHANGED
@@ -149,10 +149,11 @@ var ALIASES = [
149
149
  ["Burma", "MM"],
150
150
  ["Cape Verde", "CV"]
151
151
  ];
152
- var BY_NATION = Object.fromEntries(
153
- [...NATIONS, ...ALIASES].map(([name, code]) => [norm(name), code])
152
+ var BY_NATION = Object.assign(
153
+ /* @__PURE__ */ Object.create(null),
154
+ Object.fromEntries([...NATIONS, ...ALIASES].map(([name, code]) => [norm(name), code]))
154
155
  );
155
- var BY_CODE = {
156
+ var BY_CODE = Object.assign(/* @__PURE__ */ Object.create(null), {
156
157
  MEX: "MX",
157
158
  RSA: "ZA",
158
159
  KOR: "KR",
@@ -213,7 +214,7 @@ var BY_CODE = {
213
214
  HON: "HN",
214
215
  COD: "CD",
215
216
  MLI: "ML"
216
- };
217
+ });
217
218
  function nationToFlag(nameOrCode) {
218
219
  const region = nationToRegion(nameOrCode);
219
220
  return region ? flagEmoji(region) : NEUTRAL;
@@ -221,7 +222,7 @@ function nationToFlag(nameOrCode) {
221
222
  var INTL_BY_NAME;
222
223
  function intlNameMap() {
223
224
  if (INTL_BY_NAME) return INTL_BY_NAME;
224
- const map = {};
225
+ const map = /* @__PURE__ */ Object.create(null);
225
226
  try {
226
227
  const dn = new Intl.DisplayNames(["en"], { type: "region" });
227
228
  for (let a = 65; a <= 90; a++) {
@@ -246,11 +247,8 @@ function intlNameMap() {
246
247
  }
247
248
  function nationToRegion(nameOrCode) {
248
249
  if (!nameOrCode) return void 0;
249
- const byName = BY_NATION[norm(nameOrCode)];
250
- if (byName) return byName;
251
- const byCode = BY_CODE[nameOrCode.trim().toUpperCase()];
252
- if (byCode) return byCode;
253
- return intlNameMap()[norm(nameOrCode)];
250
+ const str = (v) => typeof v === "string" && v ? v : void 0;
251
+ return str(BY_NATION[norm(nameOrCode)]) ?? str(BY_CODE[nameOrCode.trim().toUpperCase()]) ?? str(intlNameMap()[norm(nameOrCode)]);
254
252
  }
255
253
 
256
254
  // src/i18n.ts
@@ -272,6 +270,7 @@ var EN = {
272
270
  "bracket.slot.loser": "{stage} {n} loser",
273
271
  "bracket.slot.tbd": "TBD",
274
272
  "live.data": "Live data: {source}",
273
+ "standings.unavailable": "Live standings unavailable.",
275
274
  "share.tryIt": "Try it: {line}",
276
275
  "stage.group": "Group {group}",
277
276
  "stage.groupStage": "Group stage",
@@ -301,6 +300,7 @@ var ES = {
301
300
  "bracket.slot.loser": "Perdedor {stage} {n}",
302
301
  "bracket.slot.tbd": "Por definir",
303
302
  "live.data": "Datos en vivo: {source}",
303
+ "standings.unavailable": "Tabla en vivo no disponible.",
304
304
  "share.tryIt": "Pru\xE9balo: {line}",
305
305
  "stage.group": "Grupo {group}",
306
306
  "stage.groupStage": "Fase de grupos",
@@ -330,6 +330,7 @@ var PT = {
330
330
  "bracket.slot.loser": "Perdedor {stage} {n}",
331
331
  "bracket.slot.tbd": "A definir",
332
332
  "live.data": "Dados ao vivo: {source}",
333
+ "standings.unavailable": "Classifica\xE7\xE3o ao vivo indispon\xEDvel.",
333
334
  "share.tryIt": "Experimente: {line}",
334
335
  "stage.group": "Grupo {group}",
335
336
  "stage.groupStage": "Fase de grupos",
@@ -359,6 +360,7 @@ var FR = {
359
360
  "bracket.slot.loser": "Perdant {stage} {n}",
360
361
  "bracket.slot.tbd": "\xC0 d\xE9finir",
361
362
  "live.data": "Donn\xE9es en direct : {source}",
363
+ "standings.unavailable": "Classement en direct indisponible.",
362
364
  "share.tryIt": "Essayez : {line}",
363
365
  "stage.group": "Groupe {group}",
364
366
  "stage.groupStage": "Phase de groupes",
@@ -447,7 +449,14 @@ function safeLocale(locale) {
447
449
  return "en";
448
450
  }
449
451
  }
452
+ function parsedDate(iso) {
453
+ const t2 = Date.parse(iso);
454
+ return Number.isFinite(t2) ? new Date(t2) : void 0;
455
+ }
456
+ var UNKNOWN_TIME = "\u2014";
450
457
  function formatKickoff(iso, opts = {}) {
458
+ const when = parsedDate(iso);
459
+ if (!when) return UNKNOWN_TIME;
451
460
  const tz = resolveTz(opts.tz);
452
461
  const locale = safeLocale(opts.locale);
453
462
  return new Intl.DateTimeFormat(locale, {
@@ -457,18 +466,22 @@ function formatKickoff(iso, opts = {}) {
457
466
  minute: "2-digit",
458
467
  hour12: false,
459
468
  timeZone: tz
460
- }).format(new Date(iso));
469
+ }).format(when);
461
470
  }
462
471
  function formatDate(iso, opts = {}) {
472
+ const when = parsedDate(iso);
473
+ if (!when) return UNKNOWN_TIME;
463
474
  const tz = resolveTz(opts.tz);
464
475
  const locale = safeLocale(opts.locale);
465
476
  return new Intl.DateTimeFormat(locale, {
466
477
  month: "short",
467
478
  day: "numeric",
468
479
  timeZone: tz
469
- }).format(new Date(iso));
480
+ }).format(when);
470
481
  }
471
482
  function formatTime(iso, opts = {}) {
483
+ const when = parsedDate(iso);
484
+ if (!when) return UNKNOWN_TIME;
472
485
  const tz = resolveTz(opts.tz);
473
486
  const locale = safeLocale(opts.locale);
474
487
  return new Intl.DateTimeFormat(locale, {
@@ -476,10 +489,12 @@ function formatTime(iso, opts = {}) {
476
489
  minute: "2-digit",
477
490
  hour12: false,
478
491
  timeZone: tz
479
- }).format(new Date(iso));
492
+ }).format(when);
480
493
  }
481
494
  function countdown(iso, from = /* @__PURE__ */ new Date()) {
482
- const ms = new Date(iso).getTime() - from.getTime();
495
+ const when = parsedDate(iso);
496
+ if (!when) return UNKNOWN_TIME;
497
+ const ms = when.getTime() - from.getTime();
483
498
  if (ms <= 0) return "now";
484
499
  const totalMin = Math.floor(ms / 6e4);
485
500
  const days = Math.floor(totalMin / 1440);
@@ -490,75 +505,58 @@ function countdown(iso, from = /* @__PURE__ */ new Date()) {
490
505
  return `${mins}m`;
491
506
  }
492
507
  function localDate(iso, tz) {
508
+ const when = parsedDate(iso);
509
+ if (!when) return "";
493
510
  const zone = resolveTz(tz);
494
511
  return new Intl.DateTimeFormat("en-CA", {
495
512
  year: "numeric",
496
513
  month: "2-digit",
497
514
  day: "2-digit",
498
515
  timeZone: zone
499
- }).format(new Date(iso));
516
+ }).format(when);
500
517
  }
501
518
  function shiftUtcDate(dateISO, days) {
502
519
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
503
520
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
504
521
  }
505
522
 
506
- // src/sanitize.ts
507
- var FEED_TEXT_MAX = 100;
508
- function sanitizeFeedText(value, max = FEED_TEXT_MAX) {
509
- let out = "";
510
- let count = 0;
511
- for (const ch of String(value)) {
512
- const cp = ch.codePointAt(0) ?? 0;
513
- const isWhitespaceControl = cp === 9 || cp === 10 || cp === 13;
514
- if ((cp <= 31 || cp >= 127 && cp <= 159) && !isWhitespaceControl) continue;
515
- if (count >= max) break;
516
- out += isWhitespaceControl ? " " : ch;
517
- count++;
518
- }
519
- return out;
520
- }
521
- function sanitizeTeam(t2) {
522
- return {
523
- ...t2 ?? {},
524
- code: sanitizeFeedText(t2?.code ?? ""),
525
- name: sanitizeFeedText(t2?.name ?? ""),
526
- flag: sanitizeFeedText(t2?.flag ?? "")
527
- };
528
- }
529
- function finiteOrUndefined(v) {
530
- return typeof v === "number" && Number.isFinite(v) ? v : void 0;
531
- }
532
- function sanitizeScorePair(v) {
533
- const home = finiteOrUndefined(v?.home);
534
- const away = finiteOrUndefined(v?.away);
535
- return home !== void 0 && away !== void 0 ? { home, away } : void 0;
536
- }
537
- function sanitizeMatchStrings(m) {
538
- const score = sanitizeScorePair(m.score);
539
- return {
540
- ...m,
541
- venue: sanitizeFeedText(m.venue ?? ""),
542
- city: m.city == null ? m.city : sanitizeFeedText(m.city),
543
- country: m.country == null ? m.country : sanitizeFeedText(m.country),
544
- home: sanitizeTeam(m.home),
545
- away: sanitizeTeam(m.away),
546
- score,
547
- shootout: score ? sanitizeScorePair(m.shootout) : void 0,
548
- minute: finiteOrUndefined(m.minute)
549
- };
550
- }
551
-
552
523
  // src/text.ts
553
524
  var segmenter = new Intl.Segmenter();
554
525
  var WIDE_CLUSTER = new RegExp("^(?:\\p{Regional_Indicator}|\\p{Extended_Pictographic})", "u");
526
+ var WIDE_BASE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏ꥠ-꥿가-힣豈-﫿︐-︙︰-﹯＀-⦆¢-₩\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}]/u;
527
+ var KEYCAP = /\u{20E3}/u;
528
+ var ZERO_WIDTH_BASE = /[\p{Mn}\p{Me}\p{Cf}\p{Cc}]/u;
529
+ function clusterWidth(segment) {
530
+ if (WIDE_CLUSTER.test(segment) || KEYCAP.test(segment)) return 2;
531
+ const first = segment.codePointAt(0);
532
+ if (first === void 0) return 0;
533
+ const base = String.fromCodePoint(first);
534
+ if (ZERO_WIDTH_BASE.test(base)) return 0;
535
+ return WIDE_BASE.test(base) ? 2 : 1;
536
+ }
555
537
  function displayWidth(s) {
556
538
  let w = 0;
557
539
  for (const { segment } of segmenter.segment(s)) {
558
- w += WIDE_CLUSTER.test(segment) ? 2 : 1;
540
+ w += clusterWidth(segment);
559
541
  }
560
542
  return w;
561
543
  }
544
+ function truncateVisible(s, maxColumns, marker = "\u2026") {
545
+ if (displayWidth(s) <= maxColumns) return s;
546
+ const budget = Math.max(0, maxColumns - displayWidth(marker));
547
+ let out = "";
548
+ let w = 0;
549
+ for (const { segment } of segmenter.segment(s)) {
550
+ const cw = clusterWidth(segment);
551
+ if (w + cw > budget) break;
552
+ out += segment;
553
+ w += cw;
554
+ }
555
+ return out + marker;
556
+ }
557
+ function* graphemes(s) {
558
+ for (const { segment } of segmenter.segment(s)) yield segment;
559
+ }
562
560
  function padVisible(s, width) {
563
561
  const w = displayWidth(s);
564
562
  return w >= width ? s : s + " ".repeat(width - w);
@@ -3094,48 +3092,318 @@ function rosterAtZero(matches) {
3094
3092
  return [...teams.values()].sort((a, b) => a.name.localeCompare(b.name)).map(blankRow);
3095
3093
  }
3096
3094
 
3097
- // src/adapters/espn.ts
3098
- var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3099
- var DEFAULT_COMPETITION = "fifa.world";
3100
- var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3101
- var USER_AGENT = `claudinho/${"0.9.3"} (+https://github.com/arturogarrido/claudinho)`;
3102
- var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
3103
- function competitionBase(slug) {
3104
- return `${ESPN_SOCCER}/${slug}`;
3095
+ // src/trust/bounded.ts
3096
+ function bounded(items, max, complete = true) {
3097
+ const kept = items.length > max ? items.slice(0, max) : items;
3098
+ return {
3099
+ items: kept,
3100
+ total: items.length,
3101
+ shown: kept.length,
3102
+ truncated: items.length > kept.length,
3103
+ complete
3104
+ };
3105
3105
  }
3106
- var DEFAULT_TIMEOUT_MS = 6e3;
3107
- var STANDINGS_SHARE_MS = 3e4;
3108
- var ProviderError = class extends Error {
3109
- kind;
3110
- status;
3111
- constructor(message, kind, status) {
3112
- super(message);
3113
- this.name = "ProviderError";
3114
- this.kind = kind;
3115
- this.status = status;
3106
+ function takeBounded(value, max) {
3107
+ if (!Array.isArray(value)) return [];
3108
+ return value.length > max ? value.slice(0, max) : value;
3109
+ }
3110
+
3111
+ // src/trust/result.ts
3112
+ var valid = (value) => ({ kind: "valid", value });
3113
+ var definitiveNone = (reason) => ({
3114
+ kind: "definitive-none",
3115
+ reason
3116
+ });
3117
+ var malformed = (reason) => ({ kind: "malformed", reason });
3118
+ var ambiguous = (reason) => ({ kind: "ambiguous", reason });
3119
+ var unresolved = (reason) => ({ kind: "unresolved", reason });
3120
+ function parsedValue(r) {
3121
+ return r.kind === "valid" ? r.value : void 0;
3122
+ }
3123
+ function isCacheable(r) {
3124
+ return r.kind === "valid" || r.kind === "definitive-none" || r.kind === "ambiguous";
3125
+ }
3126
+
3127
+ // src/trust/roles.ts
3128
+ var FORBIDDEN_IN_LABEL = new RegExp("[\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}\\p{Co}\\p{Cn}]|\\p{Default_Ignorable_Code_Point}", "u");
3129
+ var EMOJI_IN_LABEL = new RegExp("\\p{Extended_Pictographic}|\\p{Regional_Indicator}|\\p{Emoji_Modifier}|\\u{20E3}", "u");
3130
+ var MAX_LABEL_INPUT_UNITS = 4096;
3131
+ var MAX_LABEL_COLUMNS = 100;
3132
+ function humanLabel(value, maxColumns = MAX_LABEL_COLUMNS) {
3133
+ if (typeof value !== "string" || value === "") return "";
3134
+ const capped = value.length > MAX_LABEL_INPUT_UNITS ? value.slice(0, MAX_LABEL_INPUT_UNITS) : value;
3135
+ return visible(runToFixedPoint(capped, maxColumns));
3136
+ }
3137
+ function visible(label) {
3138
+ return label !== "" && displayWidth(label) === 0 ? "" : label;
3139
+ }
3140
+ function runToFixedPoint(capped, maxColumns) {
3141
+ const first = sealLabelOnce(capped, maxColumns);
3142
+ if (!lastPassDropped) return first;
3143
+ let out = first;
3144
+ for (let pass = 0; pass < 3; pass++) {
3145
+ const again = sealLabelOnce(out, maxColumns);
3146
+ if (again === out) return out;
3147
+ out = again;
3116
3148
  }
3117
- /** 429/403 the upstream is refusing us; retrying at the live cadence makes it worse. */
3118
- get throttled() {
3119
- return this.kind === "http" && (this.status === 429 || this.status === 403);
3149
+ return sealLabelOnce(out, maxColumns) === out ? out : "";
3150
+ }
3151
+ var lastPassDropped = false;
3152
+ function sealLabelOnce(value, maxColumns) {
3153
+ lastPassDropped = false;
3154
+ let normalized;
3155
+ try {
3156
+ normalized = value.normalize("NFC");
3157
+ } catch {
3158
+ return "";
3120
3159
  }
3121
- };
3160
+ const maxCodePoints = Math.max(16, maxColumns * 4);
3161
+ let out = "";
3162
+ let width = 0;
3163
+ let points = 0;
3164
+ for (const cluster of graphemes(normalized)) {
3165
+ if ([...cluster].length > 8) {
3166
+ lastPassDropped = true;
3167
+ continue;
3168
+ }
3169
+ if (EMOJI_IN_LABEL.test(cluster)) {
3170
+ lastPassDropped = true;
3171
+ continue;
3172
+ }
3173
+ let piece = "";
3174
+ for (const ch of cluster) {
3175
+ const cp = ch.codePointAt(0) ?? 0;
3176
+ if (cp === 9 || cp === 10 || cp === 13) {
3177
+ piece += " ";
3178
+ continue;
3179
+ }
3180
+ if (FORBIDDEN_IN_LABEL.test(ch)) {
3181
+ lastPassDropped = true;
3182
+ continue;
3183
+ }
3184
+ piece += ch;
3185
+ }
3186
+ if (!piece) {
3187
+ lastPassDropped = true;
3188
+ continue;
3189
+ }
3190
+ const w = displayWidth(piece);
3191
+ const cps = [...piece].length;
3192
+ if (width + w > maxColumns || points + cps > maxCodePoints) break;
3193
+ out += piece;
3194
+ width += w;
3195
+ points += cps;
3196
+ }
3197
+ try {
3198
+ return out.normalize("NFC").trim();
3199
+ } catch {
3200
+ return out.trim();
3201
+ }
3202
+ }
3203
+ function opaqueId(value, grammar) {
3204
+ return typeof value === "string" && grammar.test(value) ? value : void 0;
3205
+ }
3206
+ var ESPN_ID = /^[0-9]{1,20}$/;
3207
+ var ISO_INSTANT = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:[Zz]|[+-]\d{2}(?::?\d{2})?)$/;
3208
+ var ISO_CANONICAL = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
3209
+ function calendarValid(iso) {
3210
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
3211
+ if (!m) return false;
3212
+ const year = Number(m[1]);
3213
+ const month = Number(m[2]);
3214
+ const day = Number(m[3]);
3215
+ if (month < 1 || month > 12 || day < 1) return false;
3216
+ return day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
3217
+ }
3218
+ function canonicalTimestamp(value) {
3219
+ if (typeof value !== "string" || !ISO_INSTANT.test(value)) return void 0;
3220
+ if (!calendarValid(value)) return void 0;
3221
+ const t2 = Date.parse(value);
3222
+ if (!Number.isFinite(t2)) return void 0;
3223
+ const out = new Date(t2).toISOString();
3224
+ return ISO_CANONICAL.test(out) ? out : void 0;
3225
+ }
3226
+ function productFlag(nameOrCode) {
3227
+ return nationToFlag(nameOrCode);
3228
+ }
3229
+ function count(value, max) {
3230
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= max ? value : void 0;
3231
+ }
3232
+ function quantity(value) {
3233
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
3234
+ }
3235
+ function probability(value) {
3236
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
3237
+ }
3238
+ function member(value, allowed) {
3239
+ return typeof value === "string" && allowed.has(value) ? value : void 0;
3240
+ }
3241
+ function flag(value) {
3242
+ return typeof value === "boolean" ? value : void 0;
3243
+ }
3244
+
3245
+ // src/trust/match.ts
3246
+ var MAX_GOALS = 99;
3247
+ var MAX_MINUTE = 200;
3248
+ var MAX_MATCH_EVENTS = 128;
3249
+ var TEAM_CODE_COLUMNS = 8;
3250
+ var STAGES = /* @__PURE__ */ new Set(["GROUP", "R32", "R16", "QF", "SF", "3P", "F", "FRIENDLY"]);
3251
+ var STATUSES = /* @__PURE__ */ new Set(["SCHEDULED", "LIVE", "HT", "FT", "POSTPONED", "CANCELLED"]);
3252
+ var EVENT_TYPES = /* @__PURE__ */ new Set(["GOAL", "OWN_GOAL", "PEN", "YELLOW", "RED", "SUB"]);
3253
+ function teamCode(raw, fallbackName) {
3254
+ const upper = typeof raw === "string" ? raw.toUpperCase() : raw;
3255
+ const stated = humanLabel(upper, TEAM_CODE_COLUMNS);
3256
+ if (stated) return stated;
3257
+ return humanLabel([...fallbackName].slice(0, 3).join("").toUpperCase(), TEAM_CODE_COLUMNS);
3258
+ }
3259
+ function sealTeam(raw) {
3260
+ if (!raw || typeof raw !== "object") return void 0;
3261
+ const t2 = raw;
3262
+ const name = humanLabel(t2.name);
3263
+ if (!name) return void 0;
3264
+ const code = teamCode(t2.code, name);
3265
+ return { code, name, flag: productFlag(name) };
3266
+ }
3267
+ function sealScorePair(raw) {
3268
+ if (!raw || typeof raw !== "object") return void 0;
3269
+ const v = raw;
3270
+ const home = count(v.home, MAX_GOALS);
3271
+ const away = count(v.away, MAX_GOALS);
3272
+ return home !== void 0 && away !== void 0 ? { home, away } : void 0;
3273
+ }
3274
+ function sealEvent(raw) {
3275
+ if (!raw || typeof raw !== "object") return void 0;
3276
+ const e = raw;
3277
+ const type = member(e.type, EVENT_TYPES);
3278
+ if (!type) return void 0;
3279
+ const minute = count(e.minute, MAX_MINUTE);
3280
+ if (minute === void 0) return void 0;
3281
+ const out = { type, minute, teamCode: humanLabel(e.teamCode, TEAM_CODE_COLUMNS) };
3282
+ const player = humanLabel(e.player);
3283
+ if (player) out.player = player;
3284
+ return out;
3285
+ }
3286
+ function sealMatch(parts, opts = {}) {
3287
+ if (!parts || typeof parts !== "object") return malformed("match is not an object");
3288
+ const id = opaqueId(parts.id, ESPN_ID);
3289
+ if (!id) return malformed("match id is absent or not an identifier");
3290
+ const kickoff = canonicalTimestamp(parts.kickoff);
3291
+ if (!kickoff) return malformed("match kickoff is absent or not one instant");
3292
+ const stage = member(parts.stage, STAGES);
3293
+ if (!stage) return malformed("match stage is not a known stage");
3294
+ const status = member(parts.status, STATUSES);
3295
+ if (!status) return malformed("match status is not a known status");
3296
+ const home = sealTeam(parts.home);
3297
+ const away = sealTeam(parts.away);
3298
+ if (!home || !away) return malformed("match does not name both teams");
3299
+ if (home.code === away.code && home.name === away.name) {
3300
+ return definitiveNone("both competitors are the same team");
3301
+ }
3302
+ const group = humanLabel(parts.group) || void 0;
3303
+ const city = humanLabel(parts.city) || void 0;
3304
+ const country = humanLabel(parts.country) || void 0;
3305
+ const score = status === "SCHEDULED" ? void 0 : sealScorePair(parts.score);
3306
+ if ((status === "LIVE" || status === "HT" || status === "FT") && !score) {
3307
+ return malformed("match claims an unreadable score");
3308
+ }
3309
+ const finished = status === "FT";
3310
+ const canGoToPenalties = stage !== "GROUP";
3311
+ const shootoutPresent = parts.shootout !== void 0 && parts.shootout !== null;
3312
+ const parsedShootout = shootoutPresent ? sealScorePair(parts.shootout) : void 0;
3313
+ const shootoutStatus = status === "LIVE" || status === "FT";
3314
+ const shootout = parsedShootout && score && canGoToPenalties && shootoutStatus && !(finished && parsedShootout.home === parsedShootout.away) ? parsedShootout : void 0;
3315
+ const claimedWinner = teamCode(parts.winnerCode, "");
3316
+ const claimedSide = claimedWinner === home.code ? "home" : claimedWinner === away.code ? "away" : void 0;
3317
+ let winnerCode;
3318
+ const unusableShootoutCouldDecide = canGoToPenalties && shootoutPresent && !shootout;
3319
+ if (claimedSide && score && finished && !unusableShootoutCouldDecide) {
3320
+ const level = score.home === score.away;
3321
+ const decider = shootout ?? score;
3322
+ if (decider && decider.home !== decider.away) {
3323
+ if ((decider.home > decider.away ? "home" : "away") === claimedSide) winnerCode = claimedWinner;
3324
+ } else if (level && !shootoutPresent && canGoToPenalties) {
3325
+ winnerCode = claimedWinner;
3326
+ }
3327
+ }
3328
+ const events = opts.events === false ? [] : takeBounded(parts.events, MAX_MATCH_EVENTS).map(sealEvent).filter((e) => !!e);
3329
+ const out = { id, stage };
3330
+ if (group) out.group = group;
3331
+ out.kickoff = kickoff;
3332
+ out.venue = humanLabel(parts.venue);
3333
+ if (city) out.city = city;
3334
+ if (country) out.country = country;
3335
+ out.home = home;
3336
+ out.away = away;
3337
+ if (score) out.score = score;
3338
+ if (shootout) out.shootout = shootout;
3339
+ const minute = count(parts.minute, MAX_MINUTE);
3340
+ if (minute !== void 0) out.minute = minute;
3341
+ out.status = status;
3342
+ if (events.length) out.events = events;
3343
+ if (winnerCode) out.winnerCode = winnerCode;
3344
+ out.updatedAt = canonicalTimestamp(parts.updatedAt) ?? "";
3345
+ return valid(out);
3346
+ }
3347
+ function parseCachedMatch(raw, opts = {}) {
3348
+ if (!raw || typeof raw !== "object") return definitiveNone("cache entry is not an object");
3349
+ return sealMatch(raw, opts);
3350
+ }
3351
+ function parseCachedMatches(raw, max, opts = {}) {
3352
+ const total = Array.isArray(raw) ? raw.length : 0;
3353
+ const considered = takeBounded(raw, max);
3354
+ const items = considered.map((m) => parseCachedMatch(m, opts)).flatMap((r) => r.kind === "valid" ? [r.value] : []);
3355
+ return {
3356
+ items,
3357
+ total,
3358
+ shown: items.length,
3359
+ truncated: total > considered.length,
3360
+ // Some record in the window was unreadable, or the window did not cover the
3361
+ // file — either way we are not reporting on all of it.
3362
+ complete: items.length === total
3363
+ };
3364
+ }
3365
+
3366
+ // src/trust/espn.ts
3367
+ var MAX_EVENTS = 300;
3368
+ var MAX_GROUPS = 16;
3369
+ var MAX_GROUP_ROWS = 32;
3370
+ var STAGES2 = /* @__PURE__ */ new Set(["GROUP", "R32", "R16", "QF", "SF", "3P", "F", "FRIENDLY"]);
3371
+ function teamNames(t2) {
3372
+ return [t2?.displayName, t2?.name, t2?.location, t2?.shortDisplayName, t2?.abbreviation].map((v) => humanLabel(v)).filter((v) => v !== "");
3373
+ }
3374
+ function toParticipant(raw) {
3375
+ if (!raw || typeof raw !== "object") return malformed("competitor is not an object");
3376
+ const names = teamNames(raw.team);
3377
+ if (names.length === 0) return malformed("competitor names no team");
3378
+ const name = names[0];
3379
+ const code = teamCode(raw.team?.abbreviation, name);
3380
+ const team = { code, name, flag: productFlag(name) };
3381
+ const providerId = opaqueId(raw.team?.id, ESPN_ID);
3382
+ const known = productFlag(name) !== nationToFlag("");
3383
+ return valid(
3384
+ providerId && known ? { kind: "team", providerId, team } : { kind: "slot", team }
3385
+ );
3386
+ }
3122
3387
  function mapStatus(st) {
3123
- const name = (st?.type?.name ?? "").toUpperCase();
3124
- const state = st?.type?.state ?? "";
3388
+ const type = st?.type;
3389
+ const name = typeof type?.name === "string" ? type.name.toUpperCase() : "";
3390
+ const state = typeof type?.state === "string" ? type.state : "";
3125
3391
  if (name.includes("HALFTIME")) return "HT";
3126
3392
  if (name.includes("POSTPONED")) return "POSTPONED";
3127
3393
  if (name.includes("CANCEL")) return "CANCELLED";
3128
3394
  if (state === "pre") return "SCHEDULED";
3129
3395
  if (state === "post") return "FT";
3130
3396
  if (state === "in") return "LIVE";
3131
- return "SCHEDULED";
3397
+ return void 0;
3132
3398
  }
3133
3399
  function parseMinute(st) {
3134
- if (st?.type?.state !== "in") return void 0;
3135
- const dc = st.displayClock?.match(/(\d+)/);
3136
- if (dc) return parseInt(dc[1], 10);
3137
- if (typeof st.clock === "number" && st.clock > 0) {
3138
- return Math.floor(st.clock / 60) || void 0;
3400
+ const s = st;
3401
+ if (s?.type?.state !== "in") return void 0;
3402
+ const dc = typeof s.displayClock === "string" ? s.displayClock.match(/(\d+)/) : null;
3403
+ if (dc) return count(Number.parseInt(dc[1], 10), MAX_MINUTE);
3404
+ if (typeof s.clock === "number" && s.clock > 0) {
3405
+ const n = Math.floor(s.clock / 60);
3406
+ return n > 0 ? count(n, MAX_MINUTE) : void 0;
3139
3407
  }
3140
3408
  return void 0;
3141
3409
  }
@@ -3149,58 +3417,74 @@ var SLUG_TO_STAGE = {
3149
3417
  final: "F"
3150
3418
  };
3151
3419
  function stageFromSlug(slug) {
3152
- if (slug && SLUG_TO_STAGE[slug]) return SLUG_TO_STAGE[slug];
3153
- if (!slug) return "GROUP";
3420
+ if (slug == null || slug === "") return "GROUP";
3421
+ if (typeof slug === "string" && Object.hasOwn(SLUG_TO_STAGE, slug)) {
3422
+ const mapped = SLUG_TO_STAGE[slug];
3423
+ if (mapped) return mapped;
3424
+ }
3154
3425
  return "FRIENDLY";
3155
3426
  }
3156
- function toInt(s) {
3157
- if (s == null || s === "") return void 0;
3158
- const n = parseInt(String(s), 10);
3159
- return Number.isFinite(n) ? n : void 0;
3160
- }
3161
- function toTeam(t2) {
3162
- const name = sanitizeFeedText(
3163
- t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD"
3164
- );
3165
- const code = sanitizeFeedText(t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
3166
- return {
3167
- code,
3168
- name,
3169
- flag: nationToFlag(sanitizeFeedText(t2?.displayName ?? t2?.abbreviation ?? name))
3170
- };
3171
- }
3172
- function mapEspnEvent(ev, ctx = {}) {
3173
- const comp = ev.competitions?.[0];
3174
- const competitors = comp?.competitors ?? [];
3175
- const homeC = competitors.find((c) => c.homeAway === "home") ?? competitors[0];
3176
- const awayC = competitors.find((c) => c.homeAway === "away") ?? competitors[1];
3427
+ function toGoals(v) {
3428
+ if (typeof v === "number") return count(v, MAX_GOALS);
3429
+ if (typeof v !== "string" || v.trim() === "") return void 0;
3430
+ return /^-?\d{1,9}$/.test(v.trim()) ? count(Number(v.trim()), MAX_GOALS) : void 0;
3431
+ }
3432
+ function parseEspnEvent(raw, ctx = {}) {
3433
+ if (!raw || typeof raw !== "object") return malformed("event is not an object");
3434
+ const ev = raw;
3435
+ const id = opaqueId(ev.id, ESPN_ID);
3436
+ if (!id) return malformed("event id is absent or not an identifier");
3437
+ const kickoff = canonicalTimestamp(ev.date);
3438
+ if (!kickoff) return malformed("event date is absent or not one instant");
3439
+ const comp = Array.isArray(ev.competitions) ? ev.competitions[0] : void 0;
3440
+ const rawCompetitors = takeBounded(comp?.competitors, 4);
3441
+ if (rawCompetitors.length !== 2) {
3442
+ return definitiveNone(`expected 2 competitors, found ${rawCompetitors.length}`);
3443
+ }
3444
+ const homeRaw = rawCompetitors.find((c) => c?.homeAway === "home");
3445
+ const awayRaw = rawCompetitors.find((c) => c?.homeAway === "away");
3446
+ if (!homeRaw || !awayRaw) return definitiveNone("competitors do not state home and away");
3447
+ const homeP = toParticipant(homeRaw);
3448
+ const awayP = toParticipant(awayRaw);
3449
+ if (homeP.kind !== "valid") return homeP;
3450
+ if (awayP.kind !== "valid") return awayP;
3451
+ const h = homeP.value;
3452
+ const a = awayP.value;
3453
+ if (h.kind === "team" && a.kind === "team" && h.providerId === a.providerId) {
3454
+ return definitiveNone("both competitors are the same team");
3455
+ }
3456
+ const home = homeP.value.team;
3457
+ const away = awayP.value.team;
3177
3458
  const status = mapStatus(ev.status ?? comp?.status);
3178
- const stage = stageFromSlug(ev.season?.slug);
3179
- const home = toTeam(homeC?.team);
3180
- const away = toTeam(awayC?.team);
3459
+ if (!status) return malformed("event status is not a status we recognize");
3460
+ const stage = member(stageFromSlug(ev.season?.slug), STAGES2) ?? "FRIENDLY";
3181
3461
  let group;
3182
3462
  if (stage === "GROUP" && ctx.groupByTeam) {
3183
3463
  group = ctx.groupByTeam[home.code] ?? ctx.groupByTeam[away.code];
3184
3464
  }
3185
- const hs = toInt(homeC?.score);
3186
- const as = toInt(awayC?.score);
3187
- const hasScore = status !== "SCHEDULED" && hs !== void 0 && as !== void 0;
3465
+ const hs = toGoals(homeRaw.score);
3466
+ const as = toGoals(awayRaw.score);
3467
+ const scoreExpected = status === "LIVE" || status === "HT" || status === "FT";
3468
+ const hasScore = scoreExpected && hs !== void 0 && as !== void 0;
3469
+ if (scoreExpected && !hasScore) return malformed("event score is absent or unreadable");
3470
+ const hShoot = toGoals(homeRaw.shootoutScore);
3471
+ const aShoot = toGoals(awayRaw.shootoutScore);
3472
+ const shootoutPresent = homeRaw.shootoutScore !== void 0 || awayRaw.shootoutScore !== void 0;
3473
+ const shootout = shootoutPresent ? { home: hShoot, away: aShoot } : void 0;
3188
3474
  let winnerCode;
3189
3475
  if (isFinished(status)) {
3190
- if (homeC?.winner) winnerCode = home.code;
3191
- else if (awayC?.winner) winnerCode = away.code;
3476
+ const winners = [homeRaw, awayRaw].filter((c) => flag(c.winner) === true);
3477
+ if (winners.length === 1) winnerCode = winners[0] === homeRaw ? home.code : away.code;
3192
3478
  }
3193
- const hShoot = toInt(homeC?.shootoutScore);
3194
- const aShoot = toInt(awayC?.shootoutScore);
3195
- const shootout = hasScore && hShoot !== void 0 && aShoot !== void 0 ? { home: hShoot, away: aShoot } : void 0;
3196
- return {
3197
- id: ev.id,
3479
+ const venue = comp?.venue;
3480
+ return sealMatch({
3481
+ id,
3198
3482
  stage,
3199
3483
  group,
3200
- kickoff: ev.date,
3201
- venue: sanitizeFeedText(comp?.venue?.fullName ?? ""),
3202
- city: sanitizeFeedText(comp?.venue?.address?.city ?? "") || void 0,
3203
- country: sanitizeFeedText(comp?.venue?.address?.country ?? "") || void 0,
3484
+ kickoff,
3485
+ venue: venue?.fullName,
3486
+ city: venue?.address?.city,
3487
+ country: venue?.address?.country,
3204
3488
  home,
3205
3489
  away,
3206
3490
  score: hasScore ? { home: hs, away: as } : void 0,
@@ -3208,57 +3492,229 @@ function mapEspnEvent(ev, ctx = {}) {
3208
3492
  minute: parseMinute(ev.status ?? comp?.status),
3209
3493
  status,
3210
3494
  winnerCode,
3211
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3212
- };
3495
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3496
+ events: ev.events
3497
+ });
3213
3498
  }
3214
- function toEspnDate(d) {
3215
- return d.replace(/\D/g, "").slice(0, 8);
3499
+ function parseEspnEvents(raw, ctx = {}) {
3500
+ const all = raw?.events;
3501
+ const readable = Array.isArray(all);
3502
+ const total = readable ? all.length : 0;
3503
+ const considered = takeBounded(all, MAX_EVENTS);
3504
+ const items = [];
3505
+ const seenIds = /* @__PURE__ */ new Set();
3506
+ let complete = readable && considered.length === total;
3507
+ for (const event of considered) {
3508
+ const parsed = parseEspnEvent(event, ctx);
3509
+ if (parsed.kind !== "valid") {
3510
+ if (parsed.kind !== "definitive-none") complete = false;
3511
+ continue;
3512
+ }
3513
+ if (seenIds.has(parsed.value.id)) {
3514
+ complete = false;
3515
+ continue;
3516
+ }
3517
+ seenIds.add(parsed.value.id);
3518
+ items.push(parsed.value);
3519
+ }
3520
+ return {
3521
+ items,
3522
+ total,
3523
+ shown: items.length,
3524
+ truncated: total > considered.length,
3525
+ // Some record was unreadable, or the window did not cover the payload, or
3526
+ // the envelope itself was not a list — none of those is a complete account
3527
+ // of what the provider sent.
3528
+ complete
3529
+ };
3216
3530
  }
3217
- function statVal(stats, name) {
3218
- const v = stats?.find((s) => s.name === name)?.value;
3219
- return typeof v === "number" && Number.isFinite(v) ? Math.round(v) : 0;
3531
+ function statVal(stats, name, signed = false) {
3532
+ if (!Array.isArray(stats) || stats.length > 64) return void 0;
3533
+ const matches = takeBounded(stats, 64).filter(
3534
+ (s) => s?.name === name
3535
+ );
3536
+ if (matches.length !== 1) return void 0;
3537
+ const v = matches[0]?.value;
3538
+ if (typeof v !== "number" || !Number.isFinite(v) || !Number.isInteger(v)) return void 0;
3539
+ const limit = 1e3;
3540
+ return v > limit || v < (signed ? -limit : 0) ? void 0 : v;
3541
+ }
3542
+ function optionalStatVal(stats, name) {
3543
+ if (!Array.isArray(stats) || stats.length > 64) return void 0;
3544
+ const matches = takeBounded(stats, 64).filter(
3545
+ (s) => s?.name === name
3546
+ );
3547
+ if (matches.length === 0) return 0;
3548
+ if (matches.length !== 1) return void 0;
3549
+ const v = matches[0]?.value;
3550
+ return typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e3 ? v : void 0;
3220
3551
  }
3221
3552
  function entryToRow(e) {
3222
- return {
3223
- team: toTeam(e.team),
3224
- played: statVal(e.stats, "gamesPlayed"),
3225
- won: statVal(e.stats, "wins"),
3226
- drawn: statVal(e.stats, "ties"),
3227
- lost: statVal(e.stats, "losses"),
3228
- goalsFor: statVal(e.stats, "pointsFor"),
3229
- goalsAgainst: statVal(e.stats, "pointsAgainst"),
3230
- goalDiff: statVal(e.stats, "pointDifferential"),
3231
- points: statVal(e.stats, "points")
3232
- };
3553
+ const names = teamNames(e?.team);
3554
+ if (names.length === 0) return definitiveNone("standings entry names no team");
3555
+ const name = names[0];
3556
+ const code = teamCode(e?.team?.abbreviation, name);
3557
+ const played = statVal(e.stats, "gamesPlayed");
3558
+ const won = statVal(e.stats, "wins");
3559
+ const drawn = statVal(e.stats, "ties");
3560
+ const lost = statVal(e.stats, "losses");
3561
+ const goalsFor = statVal(e.stats, "pointsFor");
3562
+ const goalsAgainst = statVal(e.stats, "pointsAgainst");
3563
+ const goalDiff = statVal(e.stats, "pointDifferential", true);
3564
+ const points = statVal(e.stats, "points", true);
3565
+ const deductions = optionalStatVal(e.stats, "deductions");
3566
+ const providerRank = statVal(e.stats, "rank");
3567
+ if (played === void 0 || won === void 0 || drawn === void 0 || lost === void 0 || goalsFor === void 0 || goalsAgainst === void 0 || goalDiff === void 0 || points === void 0 || deductions === void 0 || providerRank === void 0 || providerRank < 1) {
3568
+ return malformed("standings entry has missing or invalid statistics");
3569
+ }
3570
+ if (played !== won + drawn + lost) {
3571
+ return malformed("standings entry games do not add up");
3572
+ }
3573
+ if (goalDiff !== goalsFor - goalsAgainst) {
3574
+ return malformed("standings entry goal difference does not add up");
3575
+ }
3576
+ if (points !== won * 3 + drawn - deductions) {
3577
+ return malformed("standings entry points do not add up");
3578
+ }
3579
+ return valid({
3580
+ team: { code, name, flag: productFlag(name) },
3581
+ played,
3582
+ won,
3583
+ drawn,
3584
+ lost,
3585
+ goalsFor,
3586
+ goalsAgainst,
3587
+ goalDiff,
3588
+ points,
3589
+ providerId: opaqueId(e?.team?.id, ESPN_ID),
3590
+ providerRank
3591
+ });
3233
3592
  }
3234
- function parseStandings(data) {
3593
+ function parseEspnStandings(raw) {
3594
+ const rawChildren = raw?.children;
3595
+ const readable = Array.isArray(rawChildren);
3596
+ const rawCount = readable ? rawChildren.length : 0;
3597
+ const children = takeBounded(rawChildren, MAX_GROUPS * 4);
3598
+ const sawAllChildren = rawCount === children.length;
3599
+ let rowsTruncated = false;
3600
+ let complete = readable && sawAllChildren;
3235
3601
  const out = [];
3236
- for (const child of data.children ?? []) {
3237
- const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
3602
+ const seenGroups = /* @__PURE__ */ new Set();
3603
+ const seenProviderIds = /* @__PURE__ */ new Set();
3604
+ for (const child of children) {
3605
+ const label = humanLabel(child?.name ?? child?.abbreviation);
3606
+ const letter = label.match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
3238
3607
  if (!letter) continue;
3239
- const ranked = (child.standings?.entries ?? []).map((e) => ({
3240
- row: entryToRow(e),
3241
- rank: statVal(e.stats, "rank")
3242
- }));
3608
+ if (seenGroups.has(letter)) {
3609
+ complete = false;
3610
+ continue;
3611
+ }
3612
+ seenGroups.add(letter);
3613
+ const rawEntries = child?.standings?.entries;
3614
+ if (!Array.isArray(rawEntries) || rawEntries.length === 0) {
3615
+ complete = false;
3616
+ continue;
3617
+ }
3618
+ if (rawEntries.length > MAX_GROUP_ROWS) {
3619
+ rowsTruncated = true;
3620
+ complete = false;
3621
+ }
3622
+ const entries = takeBounded(rawEntries, MAX_GROUP_ROWS);
3623
+ const seenTeams = /* @__PURE__ */ new Set();
3624
+ const seenRanks = /* @__PURE__ */ new Set();
3625
+ const ranked = [];
3626
+ for (const e of entries) {
3627
+ const r = entryToRow(e);
3628
+ if (r.kind !== "valid") {
3629
+ if (r.kind !== "definitive-none") complete = false;
3630
+ continue;
3631
+ }
3632
+ const key = r.value.providerId ?? r.value.team.code;
3633
+ if (seenTeams.has(key) || seenRanks.has(r.value.providerRank) || r.value.providerId !== void 0 && seenProviderIds.has(r.value.providerId)) {
3634
+ complete = false;
3635
+ continue;
3636
+ }
3637
+ seenTeams.add(key);
3638
+ seenRanks.add(r.value.providerRank);
3639
+ if (r.value.providerId !== void 0) seenProviderIds.add(r.value.providerId);
3640
+ const { providerId: _dropId, providerRank: rank, ...row } = r.value;
3641
+ ranked.push({ row, rank });
3642
+ }
3243
3643
  ranked.sort((a, b) => {
3244
3644
  if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
3245
- const r = a.row;
3246
- const s = b.row;
3247
- return s.points - r.points || s.goalDiff - r.goalDiff || s.goalsFor - r.goalsFor || r.team.code.localeCompare(s.team.code);
3645
+ if (b.row.points !== a.row.points) return b.row.points - a.row.points;
3646
+ if (b.row.goalDiff !== a.row.goalDiff) return b.row.goalDiff - a.row.goalDiff;
3647
+ return b.row.goalsFor - a.row.goalsFor;
3248
3648
  });
3649
+ if (ranked.length === 0) {
3650
+ complete = false;
3651
+ continue;
3652
+ }
3249
3653
  out.push({ group: letter, rows: ranked.map((x) => x.row) });
3250
3654
  }
3251
- out.sort((a, b) => a.group.localeCompare(b.group));
3252
- return out;
3655
+ return {
3656
+ items: out,
3657
+ total: seenGroups.size,
3658
+ shown: out.length,
3659
+ // We stopped early if the child list or any single group's rows were cut.
3660
+ truncated: !sawAllChildren || rowsTruncated,
3661
+ complete: complete && !rowsTruncated
3662
+ };
3663
+ }
3664
+
3665
+ // src/adapters/espn.ts
3666
+ var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3667
+ var DEFAULT_COMPETITION = "fifa.world";
3668
+ var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3669
+ var USER_AGENT = `claudinho/${"0.9.4"} (+https://github.com/arturogarrido/claudinho)`;
3670
+ var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
3671
+ function competitionBase(slug) {
3672
+ return `${ESPN_SOCCER}/${slug}`;
3673
+ }
3674
+ var DEFAULT_TIMEOUT_MS = 6e3;
3675
+ var STANDINGS_SHARE_MS = 3e4;
3676
+ var ProviderError = class extends Error {
3677
+ kind;
3678
+ status;
3679
+ constructor(message, kind, status) {
3680
+ super(message);
3681
+ this.name = "ProviderError";
3682
+ this.kind = kind;
3683
+ this.status = status;
3684
+ }
3685
+ /** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
3686
+ get throttled() {
3687
+ return this.kind === "http" && (this.status === 429 || this.status === 403);
3688
+ }
3689
+ };
3690
+ function toEspnDate(d) {
3691
+ return d.replace(/\D/g, "").slice(0, 8);
3692
+ }
3693
+ function mapEspnEvent(ev, ctx = {}) {
3694
+ return parsedValue(parseEspnEvent(ev, ctx));
3695
+ }
3696
+ function parseStandings(data) {
3697
+ return [...parseEspnStandings(data).items];
3698
+ }
3699
+ function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length > 0) {
3700
+ if (!hasUsableRecord && (!parsed.complete || parsed.total > 0)) {
3701
+ throw new ProviderError(`ESPN ${kind} payload had no readable records`, "parse");
3702
+ }
3703
+ return [...parsed.items];
3253
3704
  }
3254
3705
  var EspnAdapter = class {
3255
3706
  constructor(opts = {}) {
3256
3707
  this.opts = opts;
3708
+ const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
3709
+ this.expectedStandingsGroups = expected ? [...expected] : void 0;
3710
+ this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
3257
3711
  }
3258
3712
  opts;
3259
3713
  name = "espn";
3260
3714
  capabilities = { push: false, latencyHintSec: 45 };
3261
- /** Cached team-code -> group-letter map (built lazily from standings). */
3715
+ expectedStandingsGroups;
3716
+ standingsFallbackGroups;
3717
+ /** Short-lived team-code -> group-letter map (built lazily from standings). */
3262
3718
  groupMap;
3263
3719
  /**
3264
3720
  * One in-flight/recent standings fetch shared by fetchStandings and
@@ -3301,38 +3757,48 @@ var EspnAdapter = class {
3301
3757
  if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
3302
3758
  return this.standingsShared.promise;
3303
3759
  }
3304
- const promise = this.get(this.standingsUrl()).then(
3305
- (d) => parseStandings(d)
3306
- );
3760
+ const promise = this.get(this.standingsUrl()).then((d) => {
3761
+ const parsed = parseEspnStandings(d);
3762
+ return usableProviderItems(
3763
+ "standings",
3764
+ parsed,
3765
+ parsed.items.some((table) => table.rows.length > 0)
3766
+ );
3767
+ });
3307
3768
  this.standingsShared = { at: now, promise };
3308
- promise.catch(() => {
3769
+ void promise.catch(() => {
3309
3770
  if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
3310
3771
  });
3311
3772
  return promise;
3312
3773
  }
3313
3774
  /**
3314
3775
  * Authoritative, cumulative group tables from the standings endpoint. Throws
3315
- * on fetch/parse failure (the caller decides the fallback). Group-stage only:
3316
- * non-group `children` are filtered out by {@link parseStandings}.
3776
+ * on fetch failure. Group-stage only: non-group `children` are filtered out
3777
+ * by {@link parseStandings}; malformed rows are omitted without hiding their
3778
+ * readable siblings.
3317
3779
  */
3318
3780
  async fetchStandings() {
3319
3781
  return this.sharedStandings();
3320
3782
  }
3321
3783
  /**
3322
- * Build (and cache) a team-code -> group-letter map from the standings
3784
+ * Build (and briefly cache) a team-code -> group-letter map from the standings
3323
3785
  * endpoint. Best-effort: returns {} if standings are unavailable — but a
3324
- * transient failure is NOT cached (only a successful parse pins the map), so
3325
- * one blip can't silently drop group letters for the adapter's lifetime.
3786
+ * transient failure is NOT cached, and a partial successful parse expires at
3787
+ * the standings TTL, so neither can silently drop group letters for the
3788
+ * adapter's lifetime.
3326
3789
  * Reuses the same parse/fetch as {@link fetchStandings}, so the two never
3327
3790
  * drift and one command never fetches standings twice.
3328
3791
  */
3329
3792
  async fetchGroupMap(force = false) {
3330
- if (this.groupMap && !force) return this.groupMap;
3793
+ const now = Date.now();
3794
+ if (!force && this.groupMap && now - this.groupMap.at < STANDINGS_SHARE_MS) {
3795
+ return this.groupMap.value;
3796
+ }
3331
3797
  try {
3332
3798
  const tables = await this.sharedStandings();
3333
3799
  const map = {};
3334
3800
  for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
3335
- this.groupMap = map;
3801
+ this.groupMap = { at: Date.now(), value: map };
3336
3802
  return map;
3337
3803
  } catch {
3338
3804
  return {};
@@ -3347,7 +3813,8 @@ var EspnAdapter = class {
3347
3813
  this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
3348
3814
  this.get(url.toString())
3349
3815
  ]);
3350
- return (data.events ?? []).map((ev) => mapEspnEvent(ev, { groupByTeam }));
3816
+ const parsed = parseEspnEvents(data, { groupByTeam });
3817
+ return usableProviderItems("scoreboard", parsed);
3351
3818
  }
3352
3819
  async get(url) {
3353
3820
  const doFetch = this.opts.fetchImpl ?? fetch;
@@ -3539,17 +4006,17 @@ function validateWinnerChain(nodes, problems) {
3539
4006
  { from: "QF", to: "SF", count: 4 },
3540
4007
  { from: "SF", to: "F", count: 2 }
3541
4008
  ];
3542
- for (const { from, to, count } of chains) {
4009
+ for (const { from, to, count: count2 } of chains) {
3543
4010
  const refs = nodes.filter((n) => n.stage === to).flatMap((n) => [n.home, n.away]).filter((r) => r.kind === "winner" && r.stage === from);
3544
4011
  const indices = refs.map((r) => r.index);
3545
- if (indices.length !== count) {
3546
- problems.push(`${to}: expected ${count} ${from} winner refs, got ${indices.length}`);
4012
+ if (indices.length !== count2) {
4013
+ problems.push(`${to}: expected ${count2} ${from} winner refs, got ${indices.length}`);
3547
4014
  }
3548
4015
  const unique = new Set(indices);
3549
- if (unique.size !== count) {
3550
- problems.push(`${to}: ${from} winner indices must be unique 1..${count}`);
4016
+ if (unique.size !== count2) {
4017
+ problems.push(`${to}: ${from} winner indices must be unique 1..${count2}`);
3551
4018
  }
3552
- for (let i = 1; i <= count; i++) {
4019
+ for (let i = 1; i <= count2; i++) {
3553
4020
  if (!unique.has(i)) problems.push(`${to}: missing ${from} winner index ${i}`);
3554
4021
  if (!indexMap.has(matchKey(from, i))) problems.push(`missing ${from} match index ${i}`);
3555
4022
  }
@@ -4327,17 +4794,26 @@ async function getMatchesForDate(adapter, dateISO) {
4327
4794
  }
4328
4795
  async function getStandings(adapter, group) {
4329
4796
  const want = group?.toUpperCase();
4797
+ const expected = adapter.expectedStandingsGroups;
4798
+ if (want && expected && !expected.includes(want)) {
4799
+ return { tables: [], degraded: false };
4800
+ }
4330
4801
  if (adapter.fetchStandings) {
4331
4802
  try {
4332
4803
  const all = await adapter.fetchStandings();
4333
4804
  const tables2 = (want ? all.filter((t2) => t2.group === want) : all).sort(
4334
4805
  (a, b) => a.group.localeCompare(b.group)
4335
4806
  );
4336
- return { tables: tables2, degraded: false, source: adapter.name };
4807
+ const availableGroups = new Set(tables2.map((table) => table.group));
4808
+ const expectedGroupWasOmitted = want ? (expected?.includes(want) ?? false) && tables2.length === 0 : expected?.some((group2) => !availableGroups.has(group2)) ?? false;
4809
+ if (!expectedGroupWasOmitted) {
4810
+ return { tables: tables2, degraded: false, source: adapter.name };
4811
+ }
4337
4812
  } catch {
4338
4813
  }
4339
4814
  }
4340
- const letters = want ? [want] : groups();
4815
+ const fallbackGroups = adapter.standingsFallbackGroups;
4816
+ const letters = fallbackGroups ? want ? fallbackGroups.includes(want) ? [want] : [] : [...new Set(fallbackGroups)].sort((a, b) => a.localeCompare(b)) : [];
4341
4817
  const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
4342
4818
  return { tables, degraded: true };
4343
4819
  }
@@ -4394,7 +4870,10 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
4394
4870
  try {
4395
4871
  const win = knockoutWindow();
4396
4872
  if (adapter.fetchWindow && win) {
4397
- fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
4873
+ fixtures = mergeLive(
4874
+ fixtures,
4875
+ await adapter.fetchWindow(win.start, win.end)
4876
+ );
4398
4877
  }
4399
4878
  } catch {
4400
4879
  overlayFailed = true;
@@ -4458,15 +4937,149 @@ async function getMatchById(adapter, id) {
4458
4937
  async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
4459
4938
  try {
4460
4939
  const day = now.toISOString().slice(0, 10);
4461
- const matches = adapter.fetchWindow ? (await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1))).filter(
4462
- (m) => isLive(m.status)
4463
- ) : await adapter.fetchLive();
4940
+ const matches = (adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchLive()).filter((m) => isLive(m.status));
4464
4941
  return { matches, degraded: false, source: adapter.name };
4465
4942
  } catch {
4466
4943
  return { matches: [], degraded: true };
4467
4944
  }
4468
4945
  }
4469
4946
 
4947
+ // src/markets/format.ts
4948
+ function pct(p) {
4949
+ return Math.round(p * 100);
4950
+ }
4951
+ var KNOWN_MARKET_SOURCES = ["polymarket", "fake"];
4952
+ function marketSourceLabel(source) {
4953
+ if (source === "polymarket") return "Polymarket";
4954
+ if (source === "fake") return "demo data";
4955
+ return source.charAt(0).toUpperCase() + source.slice(1);
4956
+ }
4957
+ function outcomeLabel(o, match) {
4958
+ if (o.kind === "home") return match.home.name;
4959
+ if (o.kind === "away") return match.away.name;
4960
+ if (o.kind === "draw") return "Draw";
4961
+ return o.label;
4962
+ }
4963
+ function utcHhmm(iso) {
4964
+ const t2 = Date.parse(iso);
4965
+ if (!Number.isFinite(t2)) return "";
4966
+ return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
4967
+ }
4968
+ function marketFavoriteText(signal, match) {
4969
+ const fav = signal.favorite;
4970
+ if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
4971
+ if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
4972
+ const name = fav.kind === "home" ? match.home.name : match.away.name;
4973
+ return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
4974
+ }
4975
+ function marketProbabilityText(signal, match) {
4976
+ const order = ["home", "draw", "away"];
4977
+ const parts = [];
4978
+ for (const kind of order) {
4979
+ const o = signal.outcomes.find((x) => x.kind === kind);
4980
+ if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4981
+ }
4982
+ for (const o of signal.outcomes) {
4983
+ if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4984
+ }
4985
+ return parts.join(" \xB7 ");
4986
+ }
4987
+ function marketAttributionText(signal) {
4988
+ const time = utcHhmm(signal.asOf);
4989
+ const src = `Source: ${marketSourceLabel(signal.source)}`;
4990
+ return time ? `${src} \xB7 updated ${time}` : src;
4991
+ }
4992
+ function marketLine(signal, match) {
4993
+ return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
4994
+ signal.source
4995
+ )} \xB7 informational only`;
4996
+ }
4997
+ function marketBlock(signal, match) {
4998
+ const lines = [];
4999
+ if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
5000
+ lines.push(marketFavoriteText(signal, match));
5001
+ lines.push(marketProbabilityText(signal, match));
5002
+ lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
5003
+ return lines;
5004
+ }
5005
+
5006
+ // src/trust/market.ts
5007
+ var MAX_OUTCOMES = 128;
5008
+ var MATCH_ID = /^[0-9]{1,20}$/;
5009
+ var MARKET_ID = /^(?:[0-9]{1,32}|fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2})$/;
5010
+ var OUTCOME_KINDS = /* @__PURE__ */ new Set(["home", "draw", "away", "other"]);
5011
+ var TEAM_CODE_COLUMNS2 = 8;
5012
+ function sealOutcome(raw) {
5013
+ if (!raw || typeof raw !== "object") return void 0;
5014
+ const o = raw;
5015
+ const kind = member(o.kind, OUTCOME_KINDS);
5016
+ const p = probability(o.probability);
5017
+ if (!kind || p === void 0) return void 0;
5018
+ const out = { kind };
5019
+ if (o.teamCode !== void 0) {
5020
+ if (typeof o.teamCode !== "string") return void 0;
5021
+ out.teamCode = humanLabel(o.teamCode, TEAM_CODE_COLUMNS2);
5022
+ }
5023
+ out.label = humanLabel(o.label);
5024
+ out.probability = p;
5025
+ if ((out.kind === "home" || out.kind === "away") && !out.teamCode) return void 0;
5026
+ return out;
5027
+ }
5028
+ function hasDuplicateKind(outcomes) {
5029
+ const seen = /* @__PURE__ */ new Set();
5030
+ for (const o of outcomes) {
5031
+ if (o.kind === "other") continue;
5032
+ if (seen.has(o.kind)) return true;
5033
+ seen.add(o.kind);
5034
+ }
5035
+ return false;
5036
+ }
5037
+ function sealMarketSignal(raw, options = {}) {
5038
+ if (!raw || typeof raw !== "object") return malformed("signal is not an object");
5039
+ const s = raw;
5040
+ const matchId = opaqueId(s.matchId, MATCH_ID);
5041
+ if (!matchId) return malformed("signal names no fixture");
5042
+ if (!Array.isArray(s.outcomes) || s.outcomes.length > MAX_OUTCOMES) {
5043
+ return malformed("signal outcomes are absent or exceed the cap");
5044
+ }
5045
+ const outcomes = [];
5046
+ for (const rawOutcome of takeBounded(s.outcomes, MAX_OUTCOMES)) {
5047
+ const outcome = sealOutcome(rawOutcome);
5048
+ if (!outcome) return malformed("signal carries an unreadable outcome");
5049
+ outcomes.push(outcome);
5050
+ }
5051
+ if (hasDuplicateKind(outcomes)) {
5052
+ return ambiguous("two outcomes claim the same result");
5053
+ }
5054
+ const sourceMarketId = opaqueId(s.sourceMarketId, MARKET_ID);
5055
+ const liquidity = quantity(s.liquidity);
5056
+ const volume24h = quantity(s.volume24h);
5057
+ const out = {
5058
+ matchId,
5059
+ // Allow-listed, not merely stripped: this lands in the provider-attribution
5060
+ // slot, where `marketSourceLabel` falls through to the raw string for an
5061
+ // unrecognized provider — attacker prose where the reader expects
5062
+ // "Polymarket".
5063
+ source: member(s.source, new Set(KNOWN_MARKET_SOURCES)) ?? ""
5064
+ };
5065
+ if (sourceMarketId) out.sourceMarketId = sourceMarketId;
5066
+ out.asOf = canonicalTimestamp(s.asOf) ?? "";
5067
+ out.fetchedAt = canonicalTimestamp(s.fetchedAt) ?? "";
5068
+ out.outcomes = outcomes;
5069
+ const isAmbiguous = s.ambiguous !== false;
5070
+ const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
5071
+ if (favorite) out.favorite = favorite;
5072
+ if (liquidity !== void 0) out.liquidity = liquidity;
5073
+ if (volume24h !== void 0) out.volume24h = volume24h;
5074
+ out.stale = s.stale !== false;
5075
+ out.ambiguous = isAmbiguous || out.source === "";
5076
+ out.stale = out.stale || isStaleSignal(out, { now: options.now, maxAgeMs: options.maxAgeMs });
5077
+ return valid(out);
5078
+ }
5079
+ function parseCachedMarketSignal(raw, options = {}) {
5080
+ return sealMarketSignal(raw, options);
5081
+ }
5082
+
4470
5083
  // src/markets/normalize.ts
4471
5084
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
4472
5085
  function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
@@ -4486,9 +5099,9 @@ function normalizeOutcomes(outcomes) {
4486
5099
  probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
4487
5100
  }));
4488
5101
  }
4489
- function favoriteStrength(probability) {
4490
- if (probability >= 0.65) return "clear";
4491
- if (probability >= 0.52) return "slight";
5102
+ function favoriteStrength(probability2) {
5103
+ if (probability2 >= 0.65) return "clear";
5104
+ if (probability2 >= 0.52) return "slight";
4492
5105
  return "close";
4493
5106
  }
4494
5107
  function deriveFavorite(outcomes) {
@@ -4507,14 +5120,16 @@ function deriveFavorite(outcomes) {
4507
5120
  }
4508
5121
  function mapsCleanly(match, outcomes) {
4509
5122
  if (outcomes.some((o) => o.kind === "other")) return false;
5123
+ const kinds = outcomes.map((o) => o.kind);
5124
+ if (new Set(kinds).size !== kinds.length) return false;
4510
5125
  const home = outcomes.find((o) => o.kind === "home");
4511
5126
  const away = outcomes.find((o) => o.kind === "away");
4512
5127
  const draw = outcomes.find((o) => o.kind === "draw");
4513
5128
  if (!home || !away) return false;
4514
- if (home.teamCode && home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
5129
+ if (!home.teamCode || home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
4515
5130
  return false;
4516
5131
  }
4517
- if (away.teamCode && away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
5132
+ if (!away.teamCode || away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
4518
5133
  return false;
4519
5134
  }
4520
5135
  if (match.stage === "GROUP" && !draw) return false;
@@ -4529,11 +5144,13 @@ function hasSaneDistribution(outcomes) {
4529
5144
  const sum = priced.reduce((s, o) => s + o.probability, 0);
4530
5145
  return sum > 0.97 && sum < 1.03;
4531
5146
  }
5147
+ var FUTURE_SKEW_MS = 6e4;
4532
5148
  function isStaleSignal(signal, options = {}) {
4533
5149
  const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
4534
5150
  const asOf = Date.parse(signal.asOf);
4535
5151
  if (!Number.isFinite(asOf)) return true;
4536
5152
  const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
5153
+ if (asOf - now > FUTURE_SKEW_MS) return true;
4537
5154
  return now - asOf > maxAge;
4538
5155
  }
4539
5156
  function isReliableMarketSignal(signal, options = {}) {
@@ -4549,8 +5166,8 @@ function isReliableMarketSignal(signal, options = {}) {
4549
5166
  }
4550
5167
  function buildMarketSignal(input) {
4551
5168
  const outcomes = normalizeOutcomes(input.outcomes);
4552
- const ambiguous = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
4553
- const favorite = ambiguous ? void 0 : deriveFavorite(outcomes);
5169
+ const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
5170
+ const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
4554
5171
  const signal = {
4555
5172
  matchId: input.match.id,
4556
5173
  source: input.source,
@@ -4562,68 +5179,35 @@ function buildMarketSignal(input) {
4562
5179
  liquidity: input.liquidity,
4563
5180
  volume24h: input.volume24h,
4564
5181
  stale: false,
4565
- ambiguous
5182
+ ambiguous: ambiguous2
4566
5183
  };
4567
5184
  signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
4568
- return signal;
4569
- }
4570
-
4571
- // src/markets/format.ts
4572
- function pct(p) {
4573
- return Math.round(p * 100);
4574
- }
4575
- function marketSourceLabel(source) {
4576
- if (source === "polymarket") return "Polymarket";
4577
- if (source === "fake") return "demo data";
4578
- return source.charAt(0).toUpperCase() + source.slice(1);
4579
- }
4580
- function outcomeLabel(o, match) {
4581
- if (o.kind === "home") return match.home.name;
4582
- if (o.kind === "away") return match.away.name;
4583
- if (o.kind === "draw") return "Draw";
4584
- return o.label;
4585
- }
4586
- function utcHhmm(iso) {
4587
- const t2 = Date.parse(iso);
4588
- if (!Number.isFinite(t2)) return "";
4589
- return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
4590
- }
4591
- function marketFavoriteText(signal, match) {
4592
- const fav = signal.favorite;
4593
- if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
4594
- if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
4595
- const name = fav.kind === "home" ? match.home.name : match.away.name;
4596
- return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
4597
- }
4598
- function marketProbabilityText(signal, match) {
4599
- const order = ["home", "draw", "away"];
4600
- const parts = [];
4601
- for (const kind of order) {
4602
- const o = signal.outcomes.find((x) => x.kind === kind);
4603
- if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
5185
+ const sealed = sealMarketSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
5186
+ if (sealed.kind !== "valid") {
5187
+ return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
4604
5188
  }
4605
- for (const o of signal.outcomes) {
4606
- if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4607
- }
4608
- return parts.join(" \xB7 ");
5189
+ return { ...sealed.value, ambiguous: sealed.value.ambiguous || ambiguous2 };
4609
5190
  }
4610
- function marketAttributionText(signal) {
4611
- const time = utcHhmm(signal.asOf);
4612
- const src = `Source: ${marketSourceLabel(signal.source)}`;
4613
- return time ? `${src} \xB7 updated ${time}` : src;
5191
+
5192
+ // src/trust/batch.ts
5193
+ var NONE = { kind: "none" };
5194
+ function selectOne(candidates) {
5195
+ if (candidates.length === 1) return { kind: "one", value: candidates[0] };
5196
+ if (candidates.length === 0) return NONE;
5197
+ return { kind: "ambiguous", count: candidates.length };
5198
+ }
5199
+ function resolvedValues(batch) {
5200
+ const out = /* @__PURE__ */ new Map();
5201
+ for (const [key, r] of batch.results) if (r.kind === "valid") out.set(key, r.value);
5202
+ return out;
4614
5203
  }
4615
- function marketLine(signal, match) {
4616
- return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
4617
- signal.source
4618
- )} \xB7 informational only`;
5204
+ function cacheableKeys(batch) {
5205
+ const out = /* @__PURE__ */ new Set();
5206
+ for (const [key, r] of batch.results) if (isCacheable(r)) out.add(key);
5207
+ return out;
4619
5208
  }
4620
- function marketBlock(signal, match) {
4621
- const lines = [];
4622
- if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
4623
- lines.push(marketFavoriteText(signal, match));
4624
- lines.push(marketProbabilityText(signal, match));
4625
- lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
4626
- return lines;
5209
+ function emptyBatch() {
5210
+ return { results: /* @__PURE__ */ new Map(), complete: false };
4627
5211
  }
4628
5212
 
4629
5213
  // src/markets/fake.ts
@@ -4640,14 +5224,12 @@ var FakeMarketProvider = class {
4640
5224
  return void 0;
4641
5225
  }
4642
5226
  async findSignals(matches, options) {
4643
- const signals = /* @__PURE__ */ new Map();
4644
- const checked = /* @__PURE__ */ new Set();
5227
+ const results = /* @__PURE__ */ new Map();
4645
5228
  for (const m of matches) {
4646
- checked.add(m.id);
4647
5229
  const s = await this.findSignal(m, options);
4648
- if (s) signals.set(m.id, s);
5230
+ results.set(m.id, s ? valid(s) : definitiveNone("fake provider has no signal"));
4649
5231
  }
4650
- return { signals, checked };
5232
+ return { results, complete: true };
4651
5233
  }
4652
5234
  synthesize(match, options) {
4653
5235
  const seed = hash(`${match.home.code}-${match.away.code}`);
@@ -4664,7 +5246,9 @@ var FakeMarketProvider = class {
4664
5246
  return buildMarketSignal({
4665
5247
  match,
4666
5248
  source: "fake",
4667
- sourceMarketId: `fake-${match.id}`,
5249
+ // Must satisfy the boundary's opaque-id grammar, like a real one:
5250
+ // a source id that only the live path accepts is the asymmetry itself.
5251
+ sourceMarketId: match.id,
4668
5252
  asOf,
4669
5253
  fetchedAt: now.toISOString(),
4670
5254
  outcomes,
@@ -4692,6 +5276,8 @@ var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
4692
5276
  var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
4693
5277
  var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
4694
5278
  var DEFAULT_TIMEOUT_MS2 = 8e3;
5279
+ var MAX_EVENT_MARKETS = 256;
5280
+ var DEFAULT_DEADLINE_MS = 15e3;
4695
5281
  var WC_SERIES_SLUG = "soccer-fifwc";
4696
5282
  var WC_SPORT = "fifwc";
4697
5283
  var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
@@ -4704,49 +5290,81 @@ var PolymarketProvider = class {
4704
5290
  opts;
4705
5291
  name = "polymarket";
4706
5292
  async findSignal(match, options) {
4707
- const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
4708
- return (await this.resolveOne(match, options, deadline)).signal;
5293
+ const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
5294
+ return parsedValue(await this.resolveOne(match, options, deadline));
4709
5295
  }
4710
5296
  async findSignals(matches, options) {
4711
- const signals = /* @__PURE__ */ new Map();
4712
- const checked = /* @__PURE__ */ new Set();
4713
- const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
5297
+ const results = /* @__PURE__ */ new Map();
5298
+ const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
5299
+ let complete = true;
4714
5300
  for (const m of matches) {
4715
- if (Date.now() >= deadline) break;
5301
+ if (Date.now() >= deadline) {
5302
+ results.set(m.id, unresolved("enrichment deadline expired"));
5303
+ complete = false;
5304
+ continue;
5305
+ }
4716
5306
  const r = await this.resolveOne(m, options, deadline);
4717
- if (r.checked) checked.add(m.id);
4718
- if (r.signal) signals.set(m.id, r.signal);
5307
+ if (r.kind === "unresolved" || r.kind === "malformed") complete = false;
5308
+ results.set(m.id, r);
4719
5309
  }
4720
- return { signals, checked };
5310
+ return { results, complete };
4721
5311
  }
4722
5312
  /**
4723
- * Resolve one match. `checked` distinguishes a DEFINITIVE result (reached the
4724
- * source and found no usable market, or the fixture is unmappable) from a
4725
- * provider/network error so transient failures are retried, not
4726
- * negative-cached.
5313
+ * Resolve one match into a verdict.
5314
+ *
5315
+ * Every exit says which KIND of non-answer it is, because that decides
5316
+ * whether it may be remembered — see `isCacheable`: a conclusion we drew from
5317
+ * a payload we READ is cacheable (including an ambiguity, which is stable),
5318
+ * while a shape we could not read is not. Previously a single
5319
+ * `checked: boolean` collapsed five distinct situations into two, and the
5320
+ * ones that landed on the wrong side of it — an ambiguous payload, a
5321
+ * two-legged market, an incoherent 1X2 — were negative-cached as the fact
5322
+ * that this fixture has no market.
4727
5323
  */
4728
5324
  async resolveOne(match, options, deadline = Number.POSITIVE_INFINITY) {
4729
- const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4730
- const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4731
- if (slugs.length === 0) return { checked: true };
4732
5325
  const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
4733
5326
  try {
5327
+ const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
5328
+ const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
5329
+ if (slugs.length === 0) return definitiveNone("fixture has no derivable event slug");
5330
+ const RANK = {
5331
+ "definitive-none": 0,
5332
+ ambiguous: 1,
5333
+ unresolved: 2,
5334
+ malformed: 3
5335
+ };
5336
+ let worst;
5337
+ const keepWorst = (r) => {
5338
+ if (r.kind === "definitive-none" || r.kind === "valid") return;
5339
+ if (!worst || (RANK[r.kind] ?? 0) > (RANK[worst.kind] ?? 0)) worst = r;
5340
+ };
4734
5341
  for (const slug of slugs) {
4735
5342
  const remaining = deadline - Date.now();
4736
- if (remaining <= 0) return { checked: false };
4737
- const event = await this.fetchEvent(slug, Math.min(configured, remaining));
4738
- const signal = event ? this.toSignal(match, slug, event, options) : void 0;
4739
- if (signal) return { signal, checked: true };
5343
+ if (remaining <= 0) return unresolved("deadline expired between candidate slugs");
5344
+ let found;
5345
+ try {
5346
+ found = await this.fetchEvent(slug, Math.min(configured, remaining));
5347
+ } catch {
5348
+ keepWorst(malformed("candidate request failed"));
5349
+ continue;
5350
+ }
5351
+ if (found.kind !== "valid") {
5352
+ keepWorst(found);
5353
+ continue;
5354
+ }
5355
+ const r = this.toSignal(match, slug, found.value, options);
5356
+ if (r.kind === "valid") return r;
5357
+ keepWorst(r);
4740
5358
  }
4741
- return { checked: true };
5359
+ return worst ?? definitiveNone("no candidate slug yielded a usable market");
4742
5360
  } catch {
4743
- return { checked: false };
5361
+ return malformed("provider request failed");
4744
5362
  }
4745
5363
  }
4746
5364
  async fetchEvent(slug, timeoutMs) {
4747
5365
  const base = this.opts.baseUrl ?? DEFAULT_BASE2;
4748
5366
  assertAllowedHost(base);
4749
- const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
5367
+ const url = `${base}/events/slug/${encodeURIComponent(slug)}`;
4750
5368
  const doFetch = this.opts.fetchImpl ?? fetch;
4751
5369
  const res = await doFetch(url, {
4752
5370
  signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
@@ -4755,7 +5373,7 @@ var PolymarketProvider = class {
4755
5373
  redirect: "error",
4756
5374
  headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
4757
5375
  });
4758
- if (res.status === 404) return void 0;
5376
+ if (res.status === 404) return definitiveNone("slug returns 404");
4759
5377
  if (!res.ok) {
4760
5378
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4761
5379
  }
@@ -4764,63 +5382,148 @@ var PolymarketProvider = class {
4764
5382
  throw new Error(`Polymarket response too large: ${length} bytes`);
4765
5383
  }
4766
5384
  const data = await res.json();
5385
+ if (Array.isArray(data) && data.length > 1) {
5386
+ return ambiguous("slug returned more than one event");
5387
+ }
5388
+ if (Array.isArray(data) && data.length === 0) return definitiveNone("slug returns no event");
4767
5389
  const event = Array.isArray(data) ? data[0] : data;
4768
- return event && typeof event === "object" ? event : void 0;
5390
+ if (!event || typeof event !== "object") return malformed("event body is not an object");
5391
+ return valid(event);
4769
5392
  }
4770
5393
  toSignal(match, eventSlug, event, options) {
4771
- if (event.active === false || event.closed === true) return void 0;
4772
- if (event.seriesSlug != null && event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
4773
- return void 0;
5394
+ if (typeof event.active !== "boolean" || typeof event.closed !== "boolean") {
5395
+ return malformed("event active/closed is not a boolean");
5396
+ }
5397
+ if (event.active === false || event.closed === true) {
5398
+ return definitiveNone("event is closed or inactive");
4774
5399
  }
4775
- if (event.slug != null && event.slug !== eventSlug) return void 0;
4776
- const start = event.startTime ? Date.parse(event.startTime) : Number.NaN;
5400
+ if (event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
5401
+ return definitiveNone("event is not in this competition");
5402
+ }
5403
+ if (typeof event.slug !== "string") {
5404
+ return malformed("event states no slug");
5405
+ }
5406
+ if (event.slug !== eventSlug) return definitiveNone("event is not the one requested");
5407
+ if (typeof event.startTime !== "string" || !canonicalTimestamp(event.startTime)) {
5408
+ return malformed("event startTime missing or unparseable");
5409
+ }
5410
+ const start = Date.parse(event.startTime);
4777
5411
  const kick = Date.parse(match.kickoff);
4778
- if (Number.isFinite(start) && Number.isFinite(kick) && Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
4779
- return void 0;
5412
+ if (!Number.isFinite(start) || !Number.isFinite(kick)) {
5413
+ return malformed("event or fixture kickoff is unreadable");
5414
+ }
5415
+ if (Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
5416
+ return definitiveNone("event kickoff does not match the fixture");
4780
5417
  }
4781
- const moneyline = (event.markets ?? []).filter(
4782
- (m) => (m.sportsMarketType ?? "moneyline") === "moneyline"
5418
+ if (!Array.isArray(event.markets)) {
5419
+ return malformed("event markets is not an array");
5420
+ }
5421
+ const marketsTruncated = Array.isArray(event.markets) && event.markets.length > MAX_EVENT_MARKETS;
5422
+ if (marketsTruncated) {
5423
+ return malformed("event market list exceeded the cap");
5424
+ }
5425
+ const marketList = takeBounded(event.markets, MAX_EVENT_MARKETS);
5426
+ if (marketList.some(
5427
+ (market) => !market || typeof market !== "object" || typeof market.sportsMarketType !== "string"
5428
+ )) {
5429
+ return malformed("event market is missing its market-type discriminator");
5430
+ }
5431
+ const moneyline = marketList.filter(
5432
+ (m) => m?.sportsMarketType === "moneyline"
4783
5433
  );
4784
- const homeMarket = pickMarket(moneyline, match.home.code, match.home.name);
4785
- const awayMarket = pickMarket(moneyline, match.away.code, match.away.name);
4786
- const drawMarket = pickDraw(moneyline);
4787
- if (!homeMarket || !awayMarket) return void 0;
5434
+ const homeSel = pickMarket(moneyline, match.home.code, match.home.name);
5435
+ const awaySel = pickMarket(moneyline, match.away.code, match.away.name);
5436
+ const drawSel = pickDraw(moneyline);
5437
+ for (const [side, sel] of [
5438
+ ["home", homeSel],
5439
+ ["away", awaySel],
5440
+ ["draw", drawSel]
5441
+ ]) {
5442
+ if (sel.kind === "ambiguous") {
5443
+ return ambiguous(`${sel.count} markets claim the ${side} outcome`);
5444
+ }
5445
+ }
5446
+ if (homeSel.kind !== "one" || awaySel.kind !== "one" || drawSel.kind !== "one") {
5447
+ return definitiveNone("event does not carry all three 1X2 legs");
5448
+ }
5449
+ const homeMarket = homeSel.value;
5450
+ const awayMarket = awaySel.value;
5451
+ const drawMarket = drawSel.value;
4788
5452
  const legIds = [homeMarket, awayMarket, drawMarket].filter((m) => m != null).map((m) => m.id ?? m.slug ?? "");
4789
- if (new Set(legIds).size !== legIds.length) return void 0;
5453
+ if (new Set(legIds).size !== legIds.length) {
5454
+ return ambiguous("two outcome legs are the same market");
5455
+ }
4790
5456
  const legs = [
4791
5457
  ["home", homeMarket, match.home.code, match.home.name],
4792
5458
  ["draw", drawMarket, void 0, "Draw"],
4793
5459
  ["away", awayMarket, match.away.code, match.away.name]
4794
5460
  ];
4795
5461
  const outcomes = [];
4796
- let asOf = event.updatedAt;
5462
+ let asOf = canonicalTimestamp(event.updatedAt);
4797
5463
  let liquidity;
4798
- for (const [kind, market, teamCode, label] of legs) {
5464
+ for (const [kind, market, teamCode2, label] of legs) {
4799
5465
  if (!market) continue;
4800
- if (market.closed === true || market.active === false) return void 0;
4801
- if (market.description && NON_REGULAR_TIME.test(market.description)) return void 0;
5466
+ if (typeof market.closed !== "boolean" || typeof market.active !== "boolean") {
5467
+ return malformed("market active/closed is not a boolean");
5468
+ }
5469
+ if (market.closed === true || market.active === false) {
5470
+ return definitiveNone("an outcome leg is closed or inactive");
5471
+ }
5472
+ if (market.description && NON_REGULAR_TIME.test(market.description)) {
5473
+ return definitiveNone("an outcome leg is not a regular-time market");
5474
+ }
4802
5475
  const yes = yesPrice(market);
4803
- if (yes == null) return void 0;
4804
- outcomes.push({ kind, teamCode, label, probability: yes });
4805
- if (market.updatedAt && (!asOf || market.updatedAt < asOf)) asOf = market.updatedAt;
4806
- const liq = numberish(market.liquidityNum ?? market.liquidity);
5476
+ if (yes == null) return malformed("market is not a readable Yes/No binary");
5477
+ outcomes.push({ kind, teamCode: teamCode2, label, probability: yes });
5478
+ const marketAsOf = canonicalTimestamp(market.updatedAt);
5479
+ if (!marketAsOf) {
5480
+ return malformed("market updatedAt missing or unparseable");
5481
+ }
5482
+ const nowMs = (options?.now ?? this.opts.now ?? /* @__PURE__ */ new Date()).getTime();
5483
+ if (Date.parse(marketAsOf) - nowMs > FUTURE_SKEW_MS) {
5484
+ return malformed("market updatedAt is dated forward");
5485
+ }
5486
+ if (!asOf || Date.parse(marketAsOf) < Date.parse(asOf)) asOf = marketAsOf;
5487
+ const rawLiq = market.liquidityNum ?? market.liquidity;
5488
+ const liq = numberish(rawLiq);
5489
+ if (rawLiq != null && liq == null) {
5490
+ return malformed("market liquidity is unreadable");
5491
+ }
4807
5492
  if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
4808
5493
  }
4809
5494
  const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
4810
- if (rawSum < 0.9 || rawSum > 1.15) return void 0;
5495
+ if (rawSum < 0.9 || rawSum > 1.15) {
5496
+ return ambiguous("outcome probabilities do not form a coherent 1X2");
5497
+ }
5498
+ if (!asOf) return malformed("no usable timestamp on the event or its markets");
4811
5499
  const signal = buildMarketSignal({
4812
5500
  match,
4813
5501
  source: "polymarket",
4814
- sourceMarketId: event.id ?? eventSlug,
4815
- asOf: asOf ?? (/* @__PURE__ */ new Date()).toISOString(),
5502
+ // Echoed into MCP structured content (tools.ts `market.id`), i.e. straight
5503
+ // into an agent's context. Stripping control characters is NOT sufficient
5504
+ // there: printable prose ("IGNORE PREVIOUS INSTRUCTIONS") survives that and
5505
+ // is precisely what matters for a model reading it. Gamma ids are short
5506
+ // opaque tokens, so validate that GRAMMAR and otherwise fall back to the
5507
+ // slug we derived ourselves.
5508
+ // The fallback is grammar-checked too. It is normally a slug we derived
5509
+ // ourselves, but `mapping.2026.json` can override it, so echoing it raw
5510
+ // was the one path around the agent-facing filter this line exists for.
5511
+ sourceMarketId: safeMarketId(event.id) ?? safeDerivedSlug(eventSlug),
5512
+ asOf,
4816
5513
  outcomes,
4817
5514
  liquidity,
4818
5515
  now: options?.now ?? this.opts.now,
4819
5516
  maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
4820
5517
  });
4821
- return signal.ambiguous ? void 0 : signal;
5518
+ return signal.ambiguous ? ambiguous("signal does not map cleanly onto this fixture") : valid(signal);
4822
5519
  }
4823
5520
  };
5521
+ function safeMarketId(id) {
5522
+ return typeof id === "string" && /^[0-9]{1,32}$/.test(id) ? id : void 0;
5523
+ }
5524
+ function safeDerivedSlug(slug) {
5525
+ return typeof slug === "string" && /^fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2}$/.test(slug) ? slug : void 0;
5526
+ }
4824
5527
  var POLYMARKET_TOKEN = {
4825
5528
  SUI: "che",
4826
5529
  // Switzerland
@@ -4834,8 +5537,16 @@ var POLYMARKET_TOKEN = {
4834
5537
  // Croatia
4835
5538
  COD: "cdr",
4836
5539
  // DR Congo
4837
- CPV: "cvi"
5540
+ CPV: "cvi",
4838
5541
  // Cabo Verde
5542
+ // TWO letters, not three — the one entry that is not ISO alpha-3. Verified
5543
+ // live: `fifwc-kor-cze-2026-06-11` is a 404, `fifwc-kr-cze-2026-06-11`
5544
+ // resolves to "Korea Republic vs. Czechia". Korea's three group fixtures
5545
+ // therefore had no market line at all. The `^[a-z]{3}$` guard in
5546
+ // `deriveEventSlugs` validates the FIFA CODE, not the token, so a two-letter
5547
+ // alias passes through it unharmed.
5548
+ KOR: "kr"
5549
+ // Korea Republic
4839
5550
  };
4840
5551
  function pmTokens(code) {
4841
5552
  const c = code.toLowerCase();
@@ -4866,18 +5577,21 @@ function slugToken(m) {
4866
5577
  return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
4867
5578
  }
4868
5579
  function isDrawMarket(m) {
4869
- return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
5580
+ const title = (m.groupItemTitle ?? "").trim().toLowerCase();
5581
+ return slugToken(m) === "draw" || title === "draw" || /^draw\s*\(/.test(title);
4870
5582
  }
4871
- function pickMarket(markets, teamCode, teamName) {
4872
- const tokens = pmTokens(teamCode);
5583
+ function pickMarket(markets, teamCode2, teamName) {
5584
+ const tokens = pmTokens(teamCode2);
4873
5585
  const name = teamName.trim().toLowerCase();
4874
5586
  const teamMarkets = markets.filter((m) => !isDrawMarket(m));
4875
- const bySlug = teamMarkets.find((m) => tokens.includes(slugToken(m)));
4876
- if (bySlug) return bySlug;
4877
- return teamMarkets.find((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name);
5587
+ const bySlug = teamMarkets.filter((m) => tokens.includes(slugToken(m)));
5588
+ if (bySlug.length > 1) return { kind: "ambiguous", count: bySlug.length };
5589
+ const byTitle = name ? teamMarkets.filter((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name) : [];
5590
+ if (byTitle.length > 1) return { kind: "ambiguous", count: byTitle.length };
5591
+ return selectOne([.../* @__PURE__ */ new Set([...bySlug, ...byTitle])]);
4878
5592
  }
4879
5593
  function pickDraw(markets) {
4880
- return markets.find(isDrawMarket);
5594
+ return selectOne(markets.filter(isDrawMarket));
4881
5595
  }
4882
5596
  function assertAllowedHost(base) {
4883
5597
  let host;
@@ -4891,20 +5605,29 @@ function assertAllowedHost(base) {
4891
5605
  }
4892
5606
  }
4893
5607
  function yesPrice(market) {
4894
- const labels = parseJsonArray(market.outcomes);
4895
- const prices = parseJsonArray(market.outcomePrices).map((p2) => Number(p2));
4896
- if (labels.length === 0 || labels.length !== prices.length) return void 0;
4897
- const i = labels.findIndex((l) => l.trim().toLowerCase() === "yes");
4898
- if (i < 0) return void 0;
4899
- const p = prices[i];
4900
- return typeof p === "number" && Number.isFinite(p) && p > 0 && p <= 1 ? p : void 0;
5608
+ const labels = parseJsonArray(market.outcomes).map((l) => l.trim().toLowerCase());
5609
+ const raw = parseJsonArray(market.outcomePrices);
5610
+ if (raw.some((v) => v.trim() === "" || !Number.isFinite(Number(v)))) return void 0;
5611
+ const prices = raw.map((v) => Number(v));
5612
+ if (labels.length !== 2 || prices.length !== 2) return void 0;
5613
+ const i = labels.indexOf("yes");
5614
+ const j = labels.indexOf("no");
5615
+ if (i < 0 || j < 0) return void 0;
5616
+ const yes = prices[i];
5617
+ const no = prices[j];
5618
+ if (![yes, no].every((v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1)) {
5619
+ return void 0;
5620
+ }
5621
+ if (Math.abs(yes + no - 1) > 0.05) return void 0;
5622
+ return yes > 0 ? yes : void 0;
4901
5623
  }
4902
5624
  function parseJsonArray(v) {
4903
- if (Array.isArray(v)) return v.map((x) => String(x));
5625
+ const asText = (x) => typeof x === "string" || typeof x === "number" ? String(x) : "";
5626
+ if (Array.isArray(v)) return v.map(asText);
4904
5627
  if (typeof v === "string") {
4905
5628
  try {
4906
5629
  const parsed = JSON.parse(v);
4907
- return Array.isArray(parsed) ? parsed.map((x) => String(x)) : [];
5630
+ return Array.isArray(parsed) ? parsed.map(asText) : [];
4908
5631
  } catch {
4909
5632
  return [];
4910
5633
  }
@@ -4912,10 +5635,10 @@ function parseJsonArray(v) {
4912
5635
  return [];
4913
5636
  }
4914
5637
  function numberish(v) {
4915
- if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
5638
+ if (typeof v === "number") return Number.isFinite(v) && v >= 0 ? v : void 0;
4916
5639
  if (typeof v === "string") {
4917
5640
  const n = Number(v);
4918
- return Number.isFinite(n) ? n : void 0;
5641
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
4919
5642
  }
4920
5643
  return void 0;
4921
5644
  }
@@ -4951,7 +5674,7 @@ async function getMarketSignals(provider, matches, options) {
4951
5674
  try {
4952
5675
  return await provider.findSignals(matches, options);
4953
5676
  } catch {
4954
- return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
5677
+ return emptyBatch();
4955
5678
  }
4956
5679
  }
4957
5680
 
@@ -5034,6 +5757,9 @@ function formatShareSnippet(input, options = {}) {
5034
5757
  if (input.degraded && input.matches.length > 0) {
5035
5758
  blocks.push("(Live data unavailable \u2014 showing the bundled schedule, not live scores.)");
5036
5759
  }
5760
+ if (includeMarkets && input.marketComplete === false) {
5761
+ blocks.push("(Market data unavailable or incomplete \u2014 not all fixtures were checked.)");
5762
+ }
5037
5763
  blocks.push(
5038
5764
  shareFooter({
5039
5765
  source: input.source,
@@ -5064,7 +5790,9 @@ function formatShareTable(input, options = {}) {
5064
5790
  const includeInstall = options.includeInstallLine !== false;
5065
5791
  const blocks = [];
5066
5792
  if (input.tables.length === 0) {
5067
- blocks.push(input.emptyNote ?? "No standings available.");
5793
+ blocks.push(
5794
+ input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
5795
+ );
5068
5796
  } else {
5069
5797
  for (const { group, rows } of input.tables) {
5070
5798
  blocks.push(
@@ -5215,29 +5943,34 @@ export {
5215
5943
  DEFAULT_FLAVOR,
5216
5944
  DEFAULT_MAX_AGE_MS,
5217
5945
  EspnAdapter,
5218
- FEED_TEXT_MAX,
5219
5946
  FLAVOR_LEVELS,
5220
5947
  FakeMarketProvider,
5221
5948
  KNOCKOUT_EXTRA_TIME_MS,
5222
5949
  KNOWN_SOURCES,
5223
5950
  LIVE_WINDOW_MS,
5951
+ MAX_LABEL_COLUMNS,
5224
5952
  PolymarketProvider,
5225
5953
  ProviderError,
5226
5954
  SHARE_DISCLAIMER,
5227
5955
  SHARE_HASHTAG,
5228
5956
  allFixtures,
5229
5957
  allTeams,
5958
+ ambiguous,
5230
5959
  asFlavorLevel,
5960
+ bounded,
5231
5961
  buildBracketTopology,
5232
5962
  buildBracketView,
5233
5963
  buildMarketSignal,
5234
5964
  byKickoff,
5965
+ cacheableKeys,
5235
5966
  competitionBase,
5236
5967
  computeStandings,
5237
5968
  countdown,
5238
5969
  currentOrNextFixtureForTeam,
5970
+ definitiveNone,
5239
5971
  deriveFavorite,
5240
5972
  displayWidth,
5973
+ emptyBatch,
5241
5974
  favoriteStrength,
5242
5975
  fixturesByDate,
5243
5976
  fixturesByGroup,
@@ -5265,6 +5998,8 @@ export {
5265
5998
  getStandings,
5266
5999
  groups,
5267
6000
  hasSaneDistribution,
6001
+ humanLabel,
6002
+ isCacheable,
5268
6003
  isFinished,
5269
6004
  isFlavorLevel,
5270
6005
  isLive,
@@ -5282,6 +6017,7 @@ export {
5282
6017
  lookupTeam,
5283
6018
  makeAdapter,
5284
6019
  makeMarketProvider,
6020
+ malformed,
5285
6021
  mapEspnEvent,
5286
6022
  mapsCleanly,
5287
6023
  marketAttributionText,
@@ -5304,16 +6040,26 @@ export {
5304
6040
  normalizeOutcomes,
5305
6041
  outcomeFromScore,
5306
6042
  padVisible,
6043
+ parseCachedMarketSignal,
6044
+ parseCachedMatch,
6045
+ parseCachedMatches,
5307
6046
  parseStandings,
5308
6047
  parseTeamSlot,
6048
+ parsedValue,
6049
+ productFlag,
5309
6050
  resolveCompetition,
5310
6051
  resolveMarketSource,
5311
6052
  resolveTz,
6053
+ resolvedValues,
5312
6054
  sanitizeBundledFixture,
5313
- sanitizeFeedText,
5314
- sanitizeMatchStrings,
5315
6055
  scoreline,
6056
+ sealMarketSignal,
6057
+ sealMatch,
6058
+ selectOne,
5316
6059
  stageLabel,
5317
6060
  stageLabelI18n,
5318
- t
6061
+ t,
6062
+ truncateVisible,
6063
+ unresolved,
6064
+ valid
5319
6065
  };