@claudinho/cli 0.9.3 → 0.10.0

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.
Files changed (3) hide show
  1. package/README.md +11 -3
  2. package/dist/index.js +1469 -525
  3. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -154,10 +154,11 @@ var ALIASES = [
154
154
  ["Burma", "MM"],
155
155
  ["Cape Verde", "CV"]
156
156
  ];
157
- var BY_NATION = Object.fromEntries(
158
- [...NATIONS, ...ALIASES].map(([name, code]) => [norm(name), code])
157
+ var BY_NATION = Object.assign(
158
+ /* @__PURE__ */ Object.create(null),
159
+ Object.fromEntries([...NATIONS, ...ALIASES].map(([name, code]) => [norm(name), code]))
159
160
  );
160
- var BY_CODE = {
161
+ var BY_CODE = Object.assign(/* @__PURE__ */ Object.create(null), {
161
162
  MEX: "MX",
162
163
  RSA: "ZA",
163
164
  KOR: "KR",
@@ -218,7 +219,7 @@ var BY_CODE = {
218
219
  HON: "HN",
219
220
  COD: "CD",
220
221
  MLI: "ML"
221
- };
222
+ });
222
223
  function nationToFlag(nameOrCode) {
223
224
  const region = nationToRegion(nameOrCode);
224
225
  return region ? flagEmoji(region) : NEUTRAL;
@@ -226,7 +227,7 @@ function nationToFlag(nameOrCode) {
226
227
  var INTL_BY_NAME;
227
228
  function intlNameMap() {
228
229
  if (INTL_BY_NAME) return INTL_BY_NAME;
229
- const map = {};
230
+ const map = /* @__PURE__ */ Object.create(null);
230
231
  try {
231
232
  const dn = new Intl.DisplayNames(["en"], { type: "region" });
232
233
  for (let a = 65; a <= 90; a++) {
@@ -251,11 +252,8 @@ function intlNameMap() {
251
252
  }
252
253
  function nationToRegion(nameOrCode) {
253
254
  if (!nameOrCode) return void 0;
254
- const byName = BY_NATION[norm(nameOrCode)];
255
- if (byName) return byName;
256
- const byCode = BY_CODE[nameOrCode.trim().toUpperCase()];
257
- if (byCode) return byCode;
258
- return intlNameMap()[norm(nameOrCode)];
255
+ const str = (v) => typeof v === "string" && v ? v : void 0;
256
+ return str(BY_NATION[norm(nameOrCode)]) ?? str(BY_CODE[nameOrCode.trim().toUpperCase()]) ?? str(intlNameMap()[norm(nameOrCode)]);
259
257
  }
260
258
  var EN = {
261
259
  "bracket.title": "Knockout bracket",
@@ -275,6 +273,7 @@ var EN = {
275
273
  "bracket.slot.loser": "{stage} {n} loser",
276
274
  "bracket.slot.tbd": "TBD",
277
275
  "live.data": "Live data: {source}",
276
+ "standings.unavailable": "Live standings unavailable.",
278
277
  "share.tryIt": "Try it: {line}",
279
278
  "stage.group": "Group {group}",
280
279
  "stage.groupStage": "Group stage",
@@ -304,6 +303,7 @@ var ES = {
304
303
  "bracket.slot.loser": "Perdedor {stage} {n}",
305
304
  "bracket.slot.tbd": "Por definir",
306
305
  "live.data": "Datos en vivo: {source}",
306
+ "standings.unavailable": "Tabla en vivo no disponible.",
307
307
  "share.tryIt": "Pru\xE9balo: {line}",
308
308
  "stage.group": "Grupo {group}",
309
309
  "stage.groupStage": "Fase de grupos",
@@ -333,6 +333,7 @@ var PT = {
333
333
  "bracket.slot.loser": "Perdedor {stage} {n}",
334
334
  "bracket.slot.tbd": "A definir",
335
335
  "live.data": "Dados ao vivo: {source}",
336
+ "standings.unavailable": "Classifica\xE7\xE3o ao vivo indispon\xEDvel.",
336
337
  "share.tryIt": "Experimente: {line}",
337
338
  "stage.group": "Grupo {group}",
338
339
  "stage.groupStage": "Fase de grupos",
@@ -362,6 +363,7 @@ var FR = {
362
363
  "bracket.slot.loser": "Perdant {stage} {n}",
363
364
  "bracket.slot.tbd": "\xC0 d\xE9finir",
364
365
  "live.data": "Donn\xE9es en direct : {source}",
366
+ "standings.unavailable": "Classement en direct indisponible.",
365
367
  "share.tryIt": "Essayez : {line}",
366
368
  "stage.group": "Groupe {group}",
367
369
  "stage.groupStage": "Phase de groupes",
@@ -446,7 +448,14 @@ function safeLocale(locale) {
446
448
  return "en";
447
449
  }
448
450
  }
451
+ function parsedDate(iso) {
452
+ const t2 = Date.parse(iso);
453
+ return Number.isFinite(t2) ? new Date(t2) : void 0;
454
+ }
455
+ var UNKNOWN_TIME = "\u2014";
449
456
  function formatKickoff(iso, opts = {}) {
457
+ const when = parsedDate(iso);
458
+ if (!when) return UNKNOWN_TIME;
450
459
  const tz = resolveTz(opts.tz);
451
460
  const locale = safeLocale(opts.locale);
452
461
  return new Intl.DateTimeFormat(locale, {
@@ -456,18 +465,22 @@ function formatKickoff(iso, opts = {}) {
456
465
  minute: "2-digit",
457
466
  hour12: false,
458
467
  timeZone: tz
459
- }).format(new Date(iso));
468
+ }).format(when);
460
469
  }
461
470
  function formatDate(iso, opts = {}) {
471
+ const when = parsedDate(iso);
472
+ if (!when) return UNKNOWN_TIME;
462
473
  const tz = resolveTz(opts.tz);
463
474
  const locale = safeLocale(opts.locale);
464
475
  return new Intl.DateTimeFormat(locale, {
465
476
  month: "short",
466
477
  day: "numeric",
467
478
  timeZone: tz
468
- }).format(new Date(iso));
479
+ }).format(when);
469
480
  }
470
481
  function formatTime(iso, opts = {}) {
482
+ const when = parsedDate(iso);
483
+ if (!when) return UNKNOWN_TIME;
471
484
  const tz = resolveTz(opts.tz);
472
485
  const locale = safeLocale(opts.locale);
473
486
  return new Intl.DateTimeFormat(locale, {
@@ -475,10 +488,12 @@ function formatTime(iso, opts = {}) {
475
488
  minute: "2-digit",
476
489
  hour12: false,
477
490
  timeZone: tz
478
- }).format(new Date(iso));
491
+ }).format(when);
479
492
  }
480
493
  function countdown(iso, from = /* @__PURE__ */ new Date()) {
481
- const ms = new Date(iso).getTime() - from.getTime();
494
+ const when = parsedDate(iso);
495
+ if (!when) return UNKNOWN_TIME;
496
+ const ms = when.getTime() - from.getTime();
482
497
  if (ms <= 0) return "now";
483
498
  const totalMin = Math.floor(ms / 6e4);
484
499
  const days = Math.floor(totalMin / 1440);
@@ -489,71 +504,56 @@ function countdown(iso, from = /* @__PURE__ */ new Date()) {
489
504
  return `${mins}m`;
490
505
  }
491
506
  function localDate(iso, tz) {
507
+ const when = parsedDate(iso);
508
+ if (!when) return "";
492
509
  const zone = resolveTz(tz);
493
510
  return new Intl.DateTimeFormat("en-CA", {
494
511
  year: "numeric",
495
512
  month: "2-digit",
496
513
  day: "2-digit",
497
514
  timeZone: zone
498
- }).format(new Date(iso));
515
+ }).format(when);
499
516
  }
500
517
  function shiftUtcDate(dateISO, days) {
501
518
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
502
519
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
503
520
  }
504
- var FEED_TEXT_MAX = 100;
505
- function sanitizeFeedText(value, max = FEED_TEXT_MAX) {
506
- let out2 = "";
507
- let count = 0;
508
- for (const ch of String(value)) {
509
- const cp = ch.codePointAt(0) ?? 0;
510
- const isWhitespaceControl = cp === 9 || cp === 10 || cp === 13;
511
- if ((cp <= 31 || cp >= 127 && cp <= 159) && !isWhitespaceControl) continue;
512
- if (count >= max) break;
513
- out2 += isWhitespaceControl ? " " : ch;
514
- count++;
515
- }
516
- return out2;
517
- }
518
- function sanitizeTeam(t2) {
519
- return {
520
- ...t2 ?? {},
521
- code: sanitizeFeedText(t2?.code ?? ""),
522
- name: sanitizeFeedText(t2?.name ?? ""),
523
- flag: sanitizeFeedText(t2?.flag ?? "")
524
- };
525
- }
526
- function finiteOrUndefined(v) {
527
- return typeof v === "number" && Number.isFinite(v) ? v : void 0;
528
- }
529
- function sanitizeScorePair(v) {
530
- const home = finiteOrUndefined(v?.home);
531
- const away = finiteOrUndefined(v?.away);
532
- return home !== void 0 && away !== void 0 ? { home, away } : void 0;
533
- }
534
- function sanitizeMatchStrings(m) {
535
- const score = sanitizeScorePair(m.score);
536
- return {
537
- ...m,
538
- venue: sanitizeFeedText(m.venue ?? ""),
539
- city: m.city == null ? m.city : sanitizeFeedText(m.city),
540
- country: m.country == null ? m.country : sanitizeFeedText(m.country),
541
- home: sanitizeTeam(m.home),
542
- away: sanitizeTeam(m.away),
543
- score,
544
- shootout: score ? sanitizeScorePair(m.shootout) : void 0,
545
- minute: finiteOrUndefined(m.minute)
546
- };
547
- }
548
521
  var segmenter = new Intl.Segmenter();
549
522
  var WIDE_CLUSTER = new RegExp("^(?:\\p{Regional_Indicator}|\\p{Extended_Pictographic})", "u");
523
+ var WIDE_BASE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏ꥠ-꥿가-힣豈-﫿︐-︙︰-﹯＀-⦆¢-₩\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}]/u;
524
+ var KEYCAP = /\u{20E3}/u;
525
+ var ZERO_WIDTH_BASE = /[\p{Mn}\p{Me}\p{Cf}\p{Cc}]/u;
526
+ function clusterWidth(segment) {
527
+ if (WIDE_CLUSTER.test(segment) || KEYCAP.test(segment)) return 2;
528
+ const first = segment.codePointAt(0);
529
+ if (first === void 0) return 0;
530
+ const base = String.fromCodePoint(first);
531
+ if (ZERO_WIDTH_BASE.test(base)) return 0;
532
+ return WIDE_BASE.test(base) ? 2 : 1;
533
+ }
550
534
  function displayWidth(s) {
551
535
  let w = 0;
552
536
  for (const { segment } of segmenter.segment(s)) {
553
- w += WIDE_CLUSTER.test(segment) ? 2 : 1;
537
+ w += clusterWidth(segment);
554
538
  }
555
539
  return w;
556
540
  }
541
+ function truncateVisible(s, maxColumns, marker = "\u2026") {
542
+ if (displayWidth(s) <= maxColumns) return s;
543
+ const budget = Math.max(0, maxColumns - displayWidth(marker));
544
+ let out2 = "";
545
+ let w = 0;
546
+ for (const { segment } of segmenter.segment(s)) {
547
+ const cw = clusterWidth(segment);
548
+ if (w + cw > budget) break;
549
+ out2 += segment;
550
+ w += cw;
551
+ }
552
+ return out2 + marker;
553
+ }
554
+ function* graphemes(s) {
555
+ for (const { segment } of segmenter.segment(s)) yield segment;
556
+ }
557
557
  function padVisible(s, width) {
558
558
  const w = displayWidth(s);
559
559
  return w >= width ? s : s + " ".repeat(width - w);
@@ -2953,47 +2953,285 @@ function rosterAtZero(matches) {
2953
2953
  }
2954
2954
  return [...teams.values()].sort((a, b) => a.name.localeCompare(b.name)).map(blankRow);
2955
2955
  }
2956
- var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
2957
- var DEFAULT_COMPETITION = "fifa.world";
2958
- var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
2959
- var USER_AGENT = `claudinho/${"0.9.3"} (+https://github.com/arturogarrido/claudinho)`;
2960
- var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
2961
- function competitionBase(slug) {
2962
- return `${ESPN_SOCCER}/${slug}`;
2956
+ function takeBounded(value, max) {
2957
+ if (!Array.isArray(value)) return [];
2958
+ return value.length > max ? value.slice(0, max) : value;
2963
2959
  }
2964
- var DEFAULT_TIMEOUT_MS = 6e3;
2965
- var STANDINGS_SHARE_MS = 3e4;
2966
- var ProviderError = class extends Error {
2967
- kind;
2968
- status;
2969
- constructor(message, kind, status) {
2970
- super(message);
2971
- this.name = "ProviderError";
2972
- this.kind = kind;
2973
- this.status = status;
2960
+ var valid = (value) => ({ kind: "valid", value });
2961
+ var definitiveNone = (reason) => ({
2962
+ kind: "definitive-none",
2963
+ reason
2964
+ });
2965
+ var malformed = (reason) => ({ kind: "malformed", reason });
2966
+ var ambiguous = (reason) => ({ kind: "ambiguous", reason });
2967
+ var unresolved = (reason) => ({ kind: "unresolved", reason });
2968
+ function parsedValue(r) {
2969
+ return r.kind === "valid" ? r.value : void 0;
2970
+ }
2971
+ function isCacheable(r) {
2972
+ return r.kind === "valid" || r.kind === "definitive-none" || r.kind === "ambiguous";
2973
+ }
2974
+ 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");
2975
+ var EMOJI_IN_LABEL = new RegExp("\\p{Extended_Pictographic}|\\p{Regional_Indicator}|\\p{Emoji_Modifier}|\\u{20E3}", "u");
2976
+ var MAX_LABEL_INPUT_UNITS = 4096;
2977
+ var MAX_LABEL_COLUMNS = 100;
2978
+ function humanLabel(value, maxColumns = MAX_LABEL_COLUMNS) {
2979
+ if (typeof value !== "string" || value === "") return "";
2980
+ const capped = value.length > MAX_LABEL_INPUT_UNITS ? value.slice(0, MAX_LABEL_INPUT_UNITS) : value;
2981
+ return visible(runToFixedPoint(capped, maxColumns));
2982
+ }
2983
+ function visible(label) {
2984
+ return label !== "" && displayWidth(label) === 0 ? "" : label;
2985
+ }
2986
+ function runToFixedPoint(capped, maxColumns) {
2987
+ const first = sealLabelOnce(capped, maxColumns);
2988
+ if (!lastPassDropped) return first;
2989
+ let out2 = first;
2990
+ for (let pass = 0; pass < 3; pass++) {
2991
+ const again = sealLabelOnce(out2, maxColumns);
2992
+ if (again === out2) return out2;
2993
+ out2 = again;
2994
+ }
2995
+ return sealLabelOnce(out2, maxColumns) === out2 ? out2 : "";
2996
+ }
2997
+ var lastPassDropped = false;
2998
+ function sealLabelOnce(value, maxColumns) {
2999
+ lastPassDropped = false;
3000
+ let normalized;
3001
+ try {
3002
+ normalized = value.normalize("NFC");
3003
+ } catch {
3004
+ return "";
2974
3005
  }
2975
- /** 429/403 the upstream is refusing us; retrying at the live cadence makes it worse. */
2976
- get throttled() {
2977
- return this.kind === "http" && (this.status === 429 || this.status === 403);
3006
+ const maxCodePoints = Math.max(16, maxColumns * 4);
3007
+ let out2 = "";
3008
+ let width = 0;
3009
+ let points = 0;
3010
+ for (const cluster of graphemes(normalized)) {
3011
+ if ([...cluster].length > 8) {
3012
+ lastPassDropped = true;
3013
+ continue;
3014
+ }
3015
+ if (EMOJI_IN_LABEL.test(cluster)) {
3016
+ lastPassDropped = true;
3017
+ continue;
3018
+ }
3019
+ let piece = "";
3020
+ for (const ch of cluster) {
3021
+ const cp = ch.codePointAt(0) ?? 0;
3022
+ if (cp === 9 || cp === 10 || cp === 13) {
3023
+ piece += " ";
3024
+ continue;
3025
+ }
3026
+ if (FORBIDDEN_IN_LABEL.test(ch)) {
3027
+ lastPassDropped = true;
3028
+ continue;
3029
+ }
3030
+ piece += ch;
3031
+ }
3032
+ if (!piece) {
3033
+ lastPassDropped = true;
3034
+ continue;
3035
+ }
3036
+ const w = displayWidth(piece);
3037
+ const cps = [...piece].length;
3038
+ if (width + w > maxColumns || points + cps > maxCodePoints) break;
3039
+ out2 += piece;
3040
+ width += w;
3041
+ points += cps;
2978
3042
  }
2979
- };
3043
+ try {
3044
+ return out2.normalize("NFC").trim();
3045
+ } catch {
3046
+ return out2.trim();
3047
+ }
3048
+ }
3049
+ function opaqueId(value, grammar) {
3050
+ return typeof value === "string" && grammar.test(value) ? value : void 0;
3051
+ }
3052
+ var ESPN_ID = /^[0-9]{1,20}$/;
3053
+ var ISO_INSTANT = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:[Zz]|[+-]\d{2}(?::?\d{2})?)$/;
3054
+ var ISO_CANONICAL = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
3055
+ function calendarValid(iso) {
3056
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
3057
+ if (!m) return false;
3058
+ const year = Number(m[1]);
3059
+ const month = Number(m[2]);
3060
+ const day = Number(m[3]);
3061
+ if (month < 1 || month > 12 || day < 1) return false;
3062
+ return day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
3063
+ }
3064
+ function canonicalTimestamp(value) {
3065
+ if (typeof value !== "string" || !ISO_INSTANT.test(value)) return void 0;
3066
+ if (!calendarValid(value)) return void 0;
3067
+ const t2 = Date.parse(value);
3068
+ if (!Number.isFinite(t2)) return void 0;
3069
+ const out2 = new Date(t2).toISOString();
3070
+ return ISO_CANONICAL.test(out2) ? out2 : void 0;
3071
+ }
3072
+ function productFlag(nameOrCode) {
3073
+ return nationToFlag(nameOrCode);
3074
+ }
3075
+ function count(value, max) {
3076
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= max ? value : void 0;
3077
+ }
3078
+ function quantity(value) {
3079
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
3080
+ }
3081
+ function probability(value) {
3082
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
3083
+ }
3084
+ function member(value, allowed) {
3085
+ return typeof value === "string" && allowed.has(value) ? value : void 0;
3086
+ }
3087
+ function flag(value) {
3088
+ return typeof value === "boolean" ? value : void 0;
3089
+ }
3090
+ var MAX_GOALS = 99;
3091
+ var MAX_MINUTE = 200;
3092
+ var MAX_MATCH_EVENTS = 128;
3093
+ var TEAM_CODE_COLUMNS = 8;
3094
+ var STAGES = /* @__PURE__ */ new Set(["GROUP", "R32", "R16", "QF", "SF", "3P", "F", "FRIENDLY"]);
3095
+ var STATUSES = /* @__PURE__ */ new Set(["SCHEDULED", "LIVE", "HT", "FT", "POSTPONED", "CANCELLED"]);
3096
+ var EVENT_TYPES = /* @__PURE__ */ new Set(["GOAL", "OWN_GOAL", "PEN", "YELLOW", "RED", "SUB"]);
3097
+ function teamCode(raw, fallbackName) {
3098
+ const upper = typeof raw === "string" ? raw.toUpperCase() : raw;
3099
+ const stated = humanLabel(upper, TEAM_CODE_COLUMNS);
3100
+ if (stated) return stated;
3101
+ return humanLabel([...fallbackName].slice(0, 3).join("").toUpperCase(), TEAM_CODE_COLUMNS);
3102
+ }
3103
+ function sealTeam(raw) {
3104
+ if (!raw || typeof raw !== "object") return void 0;
3105
+ const t2 = raw;
3106
+ const name = humanLabel(t2.name);
3107
+ if (!name) return void 0;
3108
+ const code = teamCode(t2.code, name);
3109
+ return { code, name, flag: productFlag(name) };
3110
+ }
3111
+ function sealScorePair(raw) {
3112
+ if (!raw || typeof raw !== "object") return void 0;
3113
+ const v = raw;
3114
+ const home = count(v.home, MAX_GOALS);
3115
+ const away = count(v.away, MAX_GOALS);
3116
+ return home !== void 0 && away !== void 0 ? { home, away } : void 0;
3117
+ }
3118
+ function sealEvent(raw) {
3119
+ if (!raw || typeof raw !== "object") return void 0;
3120
+ const e = raw;
3121
+ const type = member(e.type, EVENT_TYPES);
3122
+ if (!type) return void 0;
3123
+ const minute = count(e.minute, MAX_MINUTE);
3124
+ if (minute === void 0) return void 0;
3125
+ const out2 = { type, minute, teamCode: humanLabel(e.teamCode, TEAM_CODE_COLUMNS) };
3126
+ const player = humanLabel(e.player);
3127
+ if (player) out2.player = player;
3128
+ return out2;
3129
+ }
3130
+ function sealMatch(parts, opts = {}) {
3131
+ if (!parts || typeof parts !== "object") return malformed("match is not an object");
3132
+ const id = opaqueId(parts.id, ESPN_ID);
3133
+ if (!id) return malformed("match id is absent or not an identifier");
3134
+ const kickoff = canonicalTimestamp(parts.kickoff);
3135
+ if (!kickoff) return malformed("match kickoff is absent or not one instant");
3136
+ const stage = member(parts.stage, STAGES);
3137
+ if (!stage) return malformed("match stage is not a known stage");
3138
+ const status = member(parts.status, STATUSES);
3139
+ if (!status) return malformed("match status is not a known status");
3140
+ const home = sealTeam(parts.home);
3141
+ const away = sealTeam(parts.away);
3142
+ if (!home || !away) return malformed("match does not name both teams");
3143
+ if (home.code === away.code && home.name === away.name) {
3144
+ return definitiveNone("both competitors are the same team");
3145
+ }
3146
+ const group = humanLabel(parts.group) || void 0;
3147
+ const city = humanLabel(parts.city) || void 0;
3148
+ const country = humanLabel(parts.country) || void 0;
3149
+ const score = status === "SCHEDULED" ? void 0 : sealScorePair(parts.score);
3150
+ if ((status === "LIVE" || status === "HT" || status === "FT") && !score) {
3151
+ return malformed("match claims an unreadable score");
3152
+ }
3153
+ const finished = status === "FT";
3154
+ const canGoToPenalties = stage !== "GROUP";
3155
+ const shootoutPresent = parts.shootout !== void 0 && parts.shootout !== null;
3156
+ const parsedShootout = shootoutPresent ? sealScorePair(parts.shootout) : void 0;
3157
+ const shootoutStatus = status === "LIVE" || status === "FT";
3158
+ const shootout = parsedShootout && score && canGoToPenalties && shootoutStatus && !(finished && parsedShootout.home === parsedShootout.away) ? parsedShootout : void 0;
3159
+ const claimedWinner = teamCode(parts.winnerCode, "");
3160
+ const claimedSide = claimedWinner === home.code ? "home" : claimedWinner === away.code ? "away" : void 0;
3161
+ let winnerCode;
3162
+ const unusableShootoutCouldDecide = canGoToPenalties && shootoutPresent && !shootout;
3163
+ if (claimedSide && score && finished && !unusableShootoutCouldDecide) {
3164
+ const level = score.home === score.away;
3165
+ const decider = shootout ?? score;
3166
+ if (decider && decider.home !== decider.away) {
3167
+ if ((decider.home > decider.away ? "home" : "away") === claimedSide) winnerCode = claimedWinner;
3168
+ } else if (level && !shootoutPresent && canGoToPenalties) {
3169
+ winnerCode = claimedWinner;
3170
+ }
3171
+ }
3172
+ const events = opts.events === false ? [] : takeBounded(parts.events, MAX_MATCH_EVENTS).map(sealEvent).filter((e) => !!e);
3173
+ const out2 = { id, stage };
3174
+ if (group) out2.group = group;
3175
+ out2.kickoff = kickoff;
3176
+ out2.venue = humanLabel(parts.venue);
3177
+ if (city) out2.city = city;
3178
+ if (country) out2.country = country;
3179
+ out2.home = home;
3180
+ out2.away = away;
3181
+ if (score) out2.score = score;
3182
+ if (shootout) out2.shootout = shootout;
3183
+ const minute = count(parts.minute, MAX_MINUTE);
3184
+ if (minute !== void 0) out2.minute = minute;
3185
+ out2.status = status;
3186
+ if (events.length) out2.events = events;
3187
+ if (winnerCode) out2.winnerCode = winnerCode;
3188
+ out2.updatedAt = canonicalTimestamp(parts.updatedAt) ?? "";
3189
+ return valid(out2);
3190
+ }
3191
+ function parseCachedMatch(raw, opts = {}) {
3192
+ if (!raw || typeof raw !== "object") return definitiveNone("cache entry is not an object");
3193
+ return sealMatch(raw, opts);
3194
+ }
3195
+ var MAX_EVENTS = 300;
3196
+ var MAX_GROUPS = 16;
3197
+ var MAX_GROUP_ROWS = 32;
3198
+ var STAGES2 = /* @__PURE__ */ new Set(["GROUP", "R32", "R16", "QF", "SF", "3P", "F", "FRIENDLY"]);
3199
+ function teamNames(t2) {
3200
+ return [t2?.displayName, t2?.name, t2?.location, t2?.shortDisplayName, t2?.abbreviation].map((v) => humanLabel(v)).filter((v) => v !== "");
3201
+ }
3202
+ function toParticipant(raw) {
3203
+ if (!raw || typeof raw !== "object") return malformed("competitor is not an object");
3204
+ const names = teamNames(raw.team);
3205
+ if (names.length === 0) return malformed("competitor names no team");
3206
+ const name = names[0];
3207
+ const code = teamCode(raw.team?.abbreviation, name);
3208
+ const team = { code, name, flag: productFlag(name) };
3209
+ const providerId = opaqueId(raw.team?.id, ESPN_ID);
3210
+ const known = productFlag(name) !== nationToFlag("");
3211
+ return valid(
3212
+ providerId && known ? { kind: "team", providerId, team } : { kind: "slot", team }
3213
+ );
3214
+ }
2980
3215
  function mapStatus(st) {
2981
- const name = (st?.type?.name ?? "").toUpperCase();
2982
- const state = st?.type?.state ?? "";
3216
+ const type = st?.type;
3217
+ const name = typeof type?.name === "string" ? type.name.toUpperCase() : "";
3218
+ const state = typeof type?.state === "string" ? type.state : "";
2983
3219
  if (name.includes("HALFTIME")) return "HT";
2984
3220
  if (name.includes("POSTPONED")) return "POSTPONED";
2985
3221
  if (name.includes("CANCEL")) return "CANCELLED";
2986
3222
  if (state === "pre") return "SCHEDULED";
2987
3223
  if (state === "post") return "FT";
2988
3224
  if (state === "in") return "LIVE";
2989
- return "SCHEDULED";
3225
+ return void 0;
2990
3226
  }
2991
3227
  function parseMinute(st) {
2992
- if (st?.type?.state !== "in") return void 0;
2993
- const dc = st.displayClock?.match(/(\d+)/);
2994
- if (dc) return parseInt(dc[1], 10);
2995
- if (typeof st.clock === "number" && st.clock > 0) {
2996
- return Math.floor(st.clock / 60) || void 0;
3228
+ const s = st;
3229
+ if (s?.type?.state !== "in") return void 0;
3230
+ const dc = typeof s.displayClock === "string" ? s.displayClock.match(/(\d+)/) : null;
3231
+ if (dc) return count(Number.parseInt(dc[1], 10), MAX_MINUTE);
3232
+ if (typeof s.clock === "number" && s.clock > 0) {
3233
+ const n = Math.floor(s.clock / 60);
3234
+ return n > 0 ? count(n, MAX_MINUTE) : void 0;
2997
3235
  }
2998
3236
  return void 0;
2999
3237
  }
@@ -3007,58 +3245,74 @@ var SLUG_TO_STAGE = {
3007
3245
  final: "F"
3008
3246
  };
3009
3247
  function stageFromSlug(slug) {
3010
- if (slug && SLUG_TO_STAGE[slug]) return SLUG_TO_STAGE[slug];
3011
- if (!slug) return "GROUP";
3248
+ if (slug == null || slug === "") return "GROUP";
3249
+ if (typeof slug === "string" && Object.hasOwn(SLUG_TO_STAGE, slug)) {
3250
+ const mapped = SLUG_TO_STAGE[slug];
3251
+ if (mapped) return mapped;
3252
+ }
3012
3253
  return "FRIENDLY";
3013
3254
  }
3014
- function toInt(s) {
3015
- if (s == null || s === "") return void 0;
3016
- const n = parseInt(String(s), 10);
3017
- return Number.isFinite(n) ? n : void 0;
3018
- }
3019
- function toTeam(t2) {
3020
- const name = sanitizeFeedText(
3021
- t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD"
3022
- );
3023
- const code = sanitizeFeedText(t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
3024
- return {
3025
- code,
3026
- name,
3027
- flag: nationToFlag(sanitizeFeedText(t2?.displayName ?? t2?.abbreviation ?? name))
3028
- };
3029
- }
3030
- function mapEspnEvent(ev, ctx = {}) {
3031
- const comp = ev.competitions?.[0];
3032
- const competitors = comp?.competitors ?? [];
3033
- const homeC = competitors.find((c) => c.homeAway === "home") ?? competitors[0];
3034
- const awayC = competitors.find((c) => c.homeAway === "away") ?? competitors[1];
3255
+ function toGoals(v) {
3256
+ if (typeof v === "number") return count(v, MAX_GOALS);
3257
+ if (typeof v !== "string" || v.trim() === "") return void 0;
3258
+ return /^-?\d{1,9}$/.test(v.trim()) ? count(Number(v.trim()), MAX_GOALS) : void 0;
3259
+ }
3260
+ function parseEspnEvent(raw, ctx = {}) {
3261
+ if (!raw || typeof raw !== "object") return malformed("event is not an object");
3262
+ const ev = raw;
3263
+ const id = opaqueId(ev.id, ESPN_ID);
3264
+ if (!id) return malformed("event id is absent or not an identifier");
3265
+ const kickoff = canonicalTimestamp(ev.date);
3266
+ if (!kickoff) return malformed("event date is absent or not one instant");
3267
+ const comp = Array.isArray(ev.competitions) ? ev.competitions[0] : void 0;
3268
+ const rawCompetitors = takeBounded(comp?.competitors, 4);
3269
+ if (rawCompetitors.length !== 2) {
3270
+ return definitiveNone(`expected 2 competitors, found ${rawCompetitors.length}`);
3271
+ }
3272
+ const homeRaw = rawCompetitors.find((c) => c?.homeAway === "home");
3273
+ const awayRaw = rawCompetitors.find((c) => c?.homeAway === "away");
3274
+ if (!homeRaw || !awayRaw) return definitiveNone("competitors do not state home and away");
3275
+ const homeP = toParticipant(homeRaw);
3276
+ const awayP = toParticipant(awayRaw);
3277
+ if (homeP.kind !== "valid") return homeP;
3278
+ if (awayP.kind !== "valid") return awayP;
3279
+ const h = homeP.value;
3280
+ const a = awayP.value;
3281
+ if (h.kind === "team" && a.kind === "team" && h.providerId === a.providerId) {
3282
+ return definitiveNone("both competitors are the same team");
3283
+ }
3284
+ const home = homeP.value.team;
3285
+ const away = awayP.value.team;
3035
3286
  const status = mapStatus(ev.status ?? comp?.status);
3036
- const stage = stageFromSlug(ev.season?.slug);
3037
- const home = toTeam(homeC?.team);
3038
- const away = toTeam(awayC?.team);
3287
+ if (!status) return malformed("event status is not a status we recognize");
3288
+ const stage = member(stageFromSlug(ev.season?.slug), STAGES2) ?? "FRIENDLY";
3039
3289
  let group;
3040
3290
  if (stage === "GROUP" && ctx.groupByTeam) {
3041
3291
  group = ctx.groupByTeam[home.code] ?? ctx.groupByTeam[away.code];
3042
3292
  }
3043
- const hs = toInt(homeC?.score);
3044
- const as = toInt(awayC?.score);
3045
- const hasScore = status !== "SCHEDULED" && hs !== void 0 && as !== void 0;
3293
+ const hs = toGoals(homeRaw.score);
3294
+ const as = toGoals(awayRaw.score);
3295
+ const scoreExpected = status === "LIVE" || status === "HT" || status === "FT";
3296
+ const hasScore = scoreExpected && hs !== void 0 && as !== void 0;
3297
+ if (scoreExpected && !hasScore) return malformed("event score is absent or unreadable");
3298
+ const hShoot = toGoals(homeRaw.shootoutScore);
3299
+ const aShoot = toGoals(awayRaw.shootoutScore);
3300
+ const shootoutPresent = homeRaw.shootoutScore !== void 0 || awayRaw.shootoutScore !== void 0;
3301
+ const shootout = shootoutPresent ? { home: hShoot, away: aShoot } : void 0;
3046
3302
  let winnerCode;
3047
3303
  if (isFinished(status)) {
3048
- if (homeC?.winner) winnerCode = home.code;
3049
- else if (awayC?.winner) winnerCode = away.code;
3304
+ const winners = [homeRaw, awayRaw].filter((c) => flag(c.winner) === true);
3305
+ if (winners.length === 1) winnerCode = winners[0] === homeRaw ? home.code : away.code;
3050
3306
  }
3051
- const hShoot = toInt(homeC?.shootoutScore);
3052
- const aShoot = toInt(awayC?.shootoutScore);
3053
- const shootout = hasScore && hShoot !== void 0 && aShoot !== void 0 ? { home: hShoot, away: aShoot } : void 0;
3054
- return {
3055
- id: ev.id,
3307
+ const venue = comp?.venue;
3308
+ return sealMatch({
3309
+ id,
3056
3310
  stage,
3057
3311
  group,
3058
- kickoff: ev.date,
3059
- venue: sanitizeFeedText(comp?.venue?.fullName ?? ""),
3060
- city: sanitizeFeedText(comp?.venue?.address?.city ?? "") || void 0,
3061
- country: sanitizeFeedText(comp?.venue?.address?.country ?? "") || void 0,
3312
+ kickoff,
3313
+ venue: venue?.fullName,
3314
+ city: venue?.address?.city,
3315
+ country: venue?.address?.country,
3062
3316
  home,
3063
3317
  away,
3064
3318
  score: hasScore ? { home: hs, away: as } : void 0,
@@ -3066,57 +3320,224 @@ function mapEspnEvent(ev, ctx = {}) {
3066
3320
  minute: parseMinute(ev.status ?? comp?.status),
3067
3321
  status,
3068
3322
  winnerCode,
3069
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3070
- };
3323
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3324
+ events: ev.events
3325
+ });
3071
3326
  }
3072
- function toEspnDate(d) {
3073
- return d.replace(/\D/g, "").slice(0, 8);
3327
+ function parseEspnEvents(raw, ctx = {}) {
3328
+ const all = raw?.events;
3329
+ const readable = Array.isArray(all);
3330
+ const total = readable ? all.length : 0;
3331
+ const considered = takeBounded(all, MAX_EVENTS);
3332
+ const items = [];
3333
+ const seenIds = /* @__PURE__ */ new Set();
3334
+ let complete = readable && considered.length === total;
3335
+ for (const event of considered) {
3336
+ const parsed = parseEspnEvent(event, ctx);
3337
+ if (parsed.kind !== "valid") {
3338
+ if (parsed.kind !== "definitive-none") complete = false;
3339
+ continue;
3340
+ }
3341
+ if (seenIds.has(parsed.value.id)) {
3342
+ complete = false;
3343
+ continue;
3344
+ }
3345
+ seenIds.add(parsed.value.id);
3346
+ items.push(parsed.value);
3347
+ }
3348
+ return {
3349
+ items,
3350
+ total,
3351
+ shown: items.length,
3352
+ truncated: total > considered.length,
3353
+ // Some record was unreadable, or the window did not cover the payload, or
3354
+ // the envelope itself was not a list — none of those is a complete account
3355
+ // of what the provider sent.
3356
+ complete
3357
+ };
3074
3358
  }
3075
- function statVal(stats, name) {
3076
- const v = stats?.find((s) => s.name === name)?.value;
3077
- return typeof v === "number" && Number.isFinite(v) ? Math.round(v) : 0;
3359
+ function statVal(stats, name, signed = false) {
3360
+ if (!Array.isArray(stats) || stats.length > 64) return void 0;
3361
+ const matches = takeBounded(stats, 64).filter(
3362
+ (s) => s?.name === name
3363
+ );
3364
+ if (matches.length !== 1) return void 0;
3365
+ const v = matches[0]?.value;
3366
+ if (typeof v !== "number" || !Number.isFinite(v) || !Number.isInteger(v)) return void 0;
3367
+ const limit = 1e3;
3368
+ return v > limit || v < (signed ? -limit : 0) ? void 0 : v;
3369
+ }
3370
+ function optionalStatVal(stats, name) {
3371
+ if (!Array.isArray(stats) || stats.length > 64) return void 0;
3372
+ const matches = takeBounded(stats, 64).filter(
3373
+ (s) => s?.name === name
3374
+ );
3375
+ if (matches.length === 0) return 0;
3376
+ if (matches.length !== 1) return void 0;
3377
+ const v = matches[0]?.value;
3378
+ return typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e3 ? v : void 0;
3078
3379
  }
3079
3380
  function entryToRow(e) {
3080
- return {
3081
- team: toTeam(e.team),
3082
- played: statVal(e.stats, "gamesPlayed"),
3083
- won: statVal(e.stats, "wins"),
3084
- drawn: statVal(e.stats, "ties"),
3085
- lost: statVal(e.stats, "losses"),
3086
- goalsFor: statVal(e.stats, "pointsFor"),
3087
- goalsAgainst: statVal(e.stats, "pointsAgainst"),
3088
- goalDiff: statVal(e.stats, "pointDifferential"),
3089
- points: statVal(e.stats, "points")
3090
- };
3381
+ const names = teamNames(e?.team);
3382
+ if (names.length === 0) return definitiveNone("standings entry names no team");
3383
+ const name = names[0];
3384
+ const code = teamCode(e?.team?.abbreviation, name);
3385
+ const played = statVal(e.stats, "gamesPlayed");
3386
+ const won = statVal(e.stats, "wins");
3387
+ const drawn = statVal(e.stats, "ties");
3388
+ const lost = statVal(e.stats, "losses");
3389
+ const goalsFor = statVal(e.stats, "pointsFor");
3390
+ const goalsAgainst = statVal(e.stats, "pointsAgainst");
3391
+ const goalDiff = statVal(e.stats, "pointDifferential", true);
3392
+ const points = statVal(e.stats, "points", true);
3393
+ const deductions = optionalStatVal(e.stats, "deductions");
3394
+ const providerRank = statVal(e.stats, "rank");
3395
+ 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) {
3396
+ return malformed("standings entry has missing or invalid statistics");
3397
+ }
3398
+ if (played !== won + drawn + lost) {
3399
+ return malformed("standings entry games do not add up");
3400
+ }
3401
+ if (goalDiff !== goalsFor - goalsAgainst) {
3402
+ return malformed("standings entry goal difference does not add up");
3403
+ }
3404
+ if (points !== won * 3 + drawn - deductions) {
3405
+ return malformed("standings entry points do not add up");
3406
+ }
3407
+ return valid({
3408
+ team: { code, name, flag: productFlag(name) },
3409
+ played,
3410
+ won,
3411
+ drawn,
3412
+ lost,
3413
+ goalsFor,
3414
+ goalsAgainst,
3415
+ goalDiff,
3416
+ points,
3417
+ providerId: opaqueId(e?.team?.id, ESPN_ID),
3418
+ providerRank
3419
+ });
3091
3420
  }
3092
- function parseStandings(data) {
3421
+ function parseEspnStandings(raw) {
3422
+ const rawChildren = raw?.children;
3423
+ const readable = Array.isArray(rawChildren);
3424
+ const rawCount = readable ? rawChildren.length : 0;
3425
+ const children = takeBounded(rawChildren, MAX_GROUPS * 4);
3426
+ const sawAllChildren = rawCount === children.length;
3427
+ let rowsTruncated = false;
3428
+ let complete = readable && sawAllChildren;
3093
3429
  const out2 = [];
3094
- for (const child of data.children ?? []) {
3095
- const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
3430
+ const seenGroups = /* @__PURE__ */ new Set();
3431
+ const seenProviderIds = /* @__PURE__ */ new Set();
3432
+ for (const child of children) {
3433
+ const label = humanLabel(child?.name ?? child?.abbreviation);
3434
+ const letter = label.match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
3096
3435
  if (!letter) continue;
3097
- const ranked = (child.standings?.entries ?? []).map((e) => ({
3098
- row: entryToRow(e),
3099
- rank: statVal(e.stats, "rank")
3100
- }));
3436
+ if (seenGroups.has(letter)) {
3437
+ complete = false;
3438
+ continue;
3439
+ }
3440
+ seenGroups.add(letter);
3441
+ const rawEntries = child?.standings?.entries;
3442
+ if (!Array.isArray(rawEntries) || rawEntries.length === 0) {
3443
+ complete = false;
3444
+ continue;
3445
+ }
3446
+ if (rawEntries.length > MAX_GROUP_ROWS) {
3447
+ rowsTruncated = true;
3448
+ complete = false;
3449
+ }
3450
+ const entries = takeBounded(rawEntries, MAX_GROUP_ROWS);
3451
+ const seenCodes = /* @__PURE__ */ new Map();
3452
+ const seenRanks = /* @__PURE__ */ new Set();
3453
+ const ranked = [];
3454
+ for (const e of entries) {
3455
+ const r = entryToRow(e);
3456
+ if (r.kind !== "valid") {
3457
+ if (r.kind !== "definitive-none") complete = false;
3458
+ continue;
3459
+ }
3460
+ const { providerId, providerRank } = r.value;
3461
+ const code = r.value.team.code;
3462
+ const priorHadId = seenCodes.get(code);
3463
+ const codeCollision = priorHadId !== void 0 && (providerId === void 0 || priorHadId === false);
3464
+ if (codeCollision || seenRanks.has(providerRank) || providerId !== void 0 && seenProviderIds.has(providerId)) {
3465
+ complete = false;
3466
+ continue;
3467
+ }
3468
+ seenCodes.set(code, providerId !== void 0);
3469
+ seenRanks.add(providerRank);
3470
+ if (providerId !== void 0) seenProviderIds.add(providerId);
3471
+ const { providerId: _dropId, providerRank: rank, ...row } = r.value;
3472
+ ranked.push({ row, rank });
3473
+ }
3101
3474
  ranked.sort((a, b) => {
3102
3475
  if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
3103
- const r = a.row;
3104
- const s = b.row;
3105
- return s.points - r.points || s.goalDiff - r.goalDiff || s.goalsFor - r.goalsFor || r.team.code.localeCompare(s.team.code);
3476
+ if (b.row.points !== a.row.points) return b.row.points - a.row.points;
3477
+ if (b.row.goalDiff !== a.row.goalDiff) return b.row.goalDiff - a.row.goalDiff;
3478
+ return b.row.goalsFor - a.row.goalsFor;
3106
3479
  });
3480
+ if (ranked.length === 0) {
3481
+ complete = false;
3482
+ continue;
3483
+ }
3107
3484
  out2.push({ group: letter, rows: ranked.map((x) => x.row) });
3108
3485
  }
3109
- out2.sort((a, b) => a.group.localeCompare(b.group));
3110
- return out2;
3486
+ return {
3487
+ items: out2,
3488
+ total: seenGroups.size,
3489
+ shown: out2.length,
3490
+ // We stopped early if the child list or any single group's rows were cut.
3491
+ truncated: !sawAllChildren || rowsTruncated,
3492
+ complete: complete && !rowsTruncated
3493
+ };
3494
+ }
3495
+ var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3496
+ var DEFAULT_COMPETITION = "fifa.world";
3497
+ var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3498
+ var USER_AGENT = `claudinho/${"0.10.0"} (+https://github.com/arturogarrido/claudinho)`;
3499
+ var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
3500
+ function competitionBase(slug) {
3501
+ return `${ESPN_SOCCER}/${slug}`;
3502
+ }
3503
+ var DEFAULT_TIMEOUT_MS = 6e3;
3504
+ var STANDINGS_SHARE_MS = 3e4;
3505
+ var ProviderError = class extends Error {
3506
+ kind;
3507
+ status;
3508
+ constructor(message, kind, status) {
3509
+ super(message);
3510
+ this.name = "ProviderError";
3511
+ this.kind = kind;
3512
+ this.status = status;
3513
+ }
3514
+ /** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
3515
+ get throttled() {
3516
+ return this.kind === "http" && (this.status === 429 || this.status === 403);
3517
+ }
3518
+ };
3519
+ function toEspnDate(d) {
3520
+ return d.replace(/\D/g, "").slice(0, 8);
3521
+ }
3522
+ function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length > 0) {
3523
+ if (!hasUsableRecord && (!parsed.complete || parsed.total > 0)) {
3524
+ throw new ProviderError(`ESPN ${kind} payload had no readable records`, "parse");
3525
+ }
3526
+ return [...parsed.items];
3111
3527
  }
3112
3528
  var EspnAdapter = class {
3113
3529
  constructor(opts = {}) {
3114
3530
  this.opts = opts;
3531
+ const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
3532
+ this.expectedStandingsGroups = expected ? [...expected] : void 0;
3533
+ this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
3115
3534
  }
3116
3535
  opts;
3117
3536
  name = "espn";
3118
3537
  capabilities = { push: false, latencyHintSec: 45 };
3119
- /** Cached team-code -> group-letter map (built lazily from standings). */
3538
+ expectedStandingsGroups;
3539
+ standingsFallbackGroups;
3540
+ /** Short-lived team-code -> group-letter map (built lazily from standings). */
3120
3541
  groupMap;
3121
3542
  /**
3122
3543
  * One in-flight/recent standings fetch shared by fetchStandings and
@@ -3159,38 +3580,48 @@ var EspnAdapter = class {
3159
3580
  if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
3160
3581
  return this.standingsShared.promise;
3161
3582
  }
3162
- const promise = this.get(this.standingsUrl()).then(
3163
- (d) => parseStandings(d)
3164
- );
3583
+ const promise = this.get(this.standingsUrl()).then((d) => {
3584
+ const parsed = parseEspnStandings(d);
3585
+ return usableProviderItems(
3586
+ "standings",
3587
+ parsed,
3588
+ parsed.items.some((table) => table.rows.length > 0)
3589
+ );
3590
+ });
3165
3591
  this.standingsShared = { at: now, promise };
3166
- promise.catch(() => {
3592
+ void promise.catch(() => {
3167
3593
  if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
3168
3594
  });
3169
3595
  return promise;
3170
3596
  }
3171
3597
  /**
3172
3598
  * Authoritative, cumulative group tables from the standings endpoint. Throws
3173
- * on fetch/parse failure (the caller decides the fallback). Group-stage only:
3174
- * non-group `children` are filtered out by {@link parseStandings}.
3599
+ * on fetch failure. Group-stage only: non-group `children` are filtered out
3600
+ * by {@link parseStandings}; malformed rows are omitted without hiding their
3601
+ * readable siblings.
3175
3602
  */
3176
3603
  async fetchStandings() {
3177
3604
  return this.sharedStandings();
3178
3605
  }
3179
3606
  /**
3180
- * Build (and cache) a team-code -> group-letter map from the standings
3607
+ * Build (and briefly cache) a team-code -> group-letter map from the standings
3181
3608
  * endpoint. Best-effort: returns {} if standings are unavailable — but a
3182
- * transient failure is NOT cached (only a successful parse pins the map), so
3183
- * one blip can't silently drop group letters for the adapter's lifetime.
3609
+ * transient failure is NOT cached, and a partial successful parse expires at
3610
+ * the standings TTL, so neither can silently drop group letters for the
3611
+ * adapter's lifetime.
3184
3612
  * Reuses the same parse/fetch as {@link fetchStandings}, so the two never
3185
3613
  * drift and one command never fetches standings twice.
3186
3614
  */
3187
3615
  async fetchGroupMap(force = false) {
3188
- if (this.groupMap && !force) return this.groupMap;
3616
+ const now = Date.now();
3617
+ if (!force && this.groupMap && now - this.groupMap.at < STANDINGS_SHARE_MS) {
3618
+ return this.groupMap.value;
3619
+ }
3189
3620
  try {
3190
3621
  const tables = await this.sharedStandings();
3191
3622
  const map = {};
3192
3623
  for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
3193
- this.groupMap = map;
3624
+ this.groupMap = { at: Date.now(), value: map };
3194
3625
  return map;
3195
3626
  } catch {
3196
3627
  return {};
@@ -3205,7 +3636,8 @@ var EspnAdapter = class {
3205
3636
  this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
3206
3637
  this.get(url.toString())
3207
3638
  ]);
3208
- return (data.events ?? []).map((ev) => mapEspnEvent(ev, { groupByTeam }));
3639
+ const parsed = parseEspnEvents(data, { groupByTeam });
3640
+ return usableProviderItems("scoreboard", parsed);
3209
3641
  }
3210
3642
  async get(url) {
3211
3643
  const doFetch = this.opts.fetchImpl ?? fetch;
@@ -3981,12 +4413,12 @@ function resolveCompetition(explicit) {
3981
4413
  return DEFAULT_COMPETITION;
3982
4414
  }
3983
4415
  var KNOWN_SOURCES = ["espn"];
3984
- function makeAdapter(source = "espn") {
4416
+ function makeAdapter(source = "espn", opts = {}) {
3985
4417
  switch (source) {
3986
4418
  case "espn": {
3987
4419
  const competition = resolveCompetition();
3988
4420
  const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
3989
- return new EspnAdapter({ baseUrl });
4421
+ return new EspnAdapter({ baseUrl, enrichGroups: opts.enrichGroups });
3990
4422
  }
3991
4423
  default:
3992
4424
  throw new Error(
@@ -4015,17 +4447,26 @@ async function getMatchesForDate(adapter, dateISO) {
4015
4447
  }
4016
4448
  async function getStandings(adapter, group) {
4017
4449
  const want = group?.toUpperCase();
4450
+ const expected = adapter.expectedStandingsGroups;
4451
+ if (want && expected && !expected.includes(want)) {
4452
+ return { tables: [], degraded: false };
4453
+ }
4018
4454
  if (adapter.fetchStandings) {
4019
4455
  try {
4020
4456
  const all = await adapter.fetchStandings();
4021
4457
  const tables2 = (want ? all.filter((t2) => t2.group === want) : all).sort(
4022
4458
  (a, b) => a.group.localeCompare(b.group)
4023
4459
  );
4024
- return { tables: tables2, degraded: false, source: adapter.name };
4460
+ const availableGroups = new Set(tables2.map((table) => table.group));
4461
+ const expectedGroupWasOmitted = want ? (expected?.includes(want) ?? false) && tables2.length === 0 : expected?.some((group2) => !availableGroups.has(group2)) ?? false;
4462
+ if (!expectedGroupWasOmitted) {
4463
+ return { tables: tables2, degraded: false, source: adapter.name };
4464
+ }
4025
4465
  } catch {
4026
4466
  }
4027
4467
  }
4028
- const letters = want ? [want] : groups();
4468
+ const fallbackGroups = adapter.standingsFallbackGroups;
4469
+ const letters = fallbackGroups ? want ? fallbackGroups.includes(want) ? [want] : [] : [...new Set(fallbackGroups)].sort((a, b) => a.localeCompare(b)) : [];
4029
4470
  const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
4030
4471
  return { tables, degraded: true };
4031
4472
  }
@@ -4082,7 +4523,10 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
4082
4523
  try {
4083
4524
  const win = knockoutWindow();
4084
4525
  if (adapter.fetchWindow && win) {
4085
- fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
4526
+ fixtures = mergeLive(
4527
+ fixtures,
4528
+ await adapter.fetchWindow(win.start, win.end)
4529
+ );
4086
4530
  }
4087
4531
  } catch {
4088
4532
  overlayFailed = true;
@@ -4146,14 +4590,144 @@ async function getMatchById(adapter, id) {
4146
4590
  async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
4147
4591
  try {
4148
4592
  const day = now.toISOString().slice(0, 10);
4149
- const matches = adapter.fetchWindow ? (await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1))).filter(
4150
- (m) => isLive(m.status)
4151
- ) : await adapter.fetchLive();
4593
+ const matches = (adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchLive()).filter((m) => isLive(m.status));
4152
4594
  return { matches, degraded: false, source: adapter.name };
4153
4595
  } catch {
4154
4596
  return { matches: [], degraded: true };
4155
4597
  }
4156
4598
  }
4599
+ function pct(p) {
4600
+ return Math.round(p * 100);
4601
+ }
4602
+ var KNOWN_MARKET_SOURCES = ["polymarket", "fake"];
4603
+ function marketSourceLabel(source) {
4604
+ if (source === "polymarket") return "Polymarket";
4605
+ if (source === "fake") return "demo data";
4606
+ return source.charAt(0).toUpperCase() + source.slice(1);
4607
+ }
4608
+ function outcomeLabel(o, match) {
4609
+ if (o.kind === "home") return match.home.name;
4610
+ if (o.kind === "away") return match.away.name;
4611
+ if (o.kind === "draw") return "Draw";
4612
+ return o.label;
4613
+ }
4614
+ function utcHhmm(iso) {
4615
+ const t2 = Date.parse(iso);
4616
+ if (!Number.isFinite(t2)) return "";
4617
+ return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
4618
+ }
4619
+ function marketFavoriteText(signal, match) {
4620
+ const fav = signal.favorite;
4621
+ if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
4622
+ if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
4623
+ const name = fav.kind === "home" ? match.home.name : match.away.name;
4624
+ return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
4625
+ }
4626
+ function marketProbabilityText(signal, match) {
4627
+ const order = ["home", "draw", "away"];
4628
+ const parts = [];
4629
+ for (const kind of order) {
4630
+ const o = signal.outcomes.find((x) => x.kind === kind);
4631
+ if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4632
+ }
4633
+ for (const o of signal.outcomes) {
4634
+ if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4635
+ }
4636
+ return parts.join(" \xB7 ");
4637
+ }
4638
+ function marketAttributionText(signal) {
4639
+ const time = utcHhmm(signal.asOf);
4640
+ const src = `Source: ${marketSourceLabel(signal.source)}`;
4641
+ return time ? `${src} \xB7 updated ${time}` : src;
4642
+ }
4643
+ function marketLine(signal, match) {
4644
+ return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
4645
+ signal.source
4646
+ )} \xB7 informational only`;
4647
+ }
4648
+ function marketBlock(signal, match) {
4649
+ const lines = [];
4650
+ if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
4651
+ lines.push(marketFavoriteText(signal, match));
4652
+ lines.push(marketProbabilityText(signal, match));
4653
+ lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
4654
+ return lines;
4655
+ }
4656
+ var MAX_OUTCOMES = 128;
4657
+ var MATCH_ID = /^[0-9]{1,20}$/;
4658
+ var MARKET_ID = /^(?:[0-9]{1,32}|fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2})$/;
4659
+ var OUTCOME_KINDS = /* @__PURE__ */ new Set(["home", "draw", "away", "other"]);
4660
+ var TEAM_CODE_COLUMNS2 = 8;
4661
+ function sealOutcome(raw) {
4662
+ if (!raw || typeof raw !== "object") return void 0;
4663
+ const o = raw;
4664
+ const kind = member(o.kind, OUTCOME_KINDS);
4665
+ const p = probability(o.probability);
4666
+ if (!kind || p === void 0) return void 0;
4667
+ const out2 = { kind };
4668
+ if (o.teamCode !== void 0) {
4669
+ if (typeof o.teamCode !== "string") return void 0;
4670
+ out2.teamCode = humanLabel(o.teamCode, TEAM_CODE_COLUMNS2);
4671
+ }
4672
+ out2.label = humanLabel(o.label);
4673
+ out2.probability = p;
4674
+ if ((out2.kind === "home" || out2.kind === "away") && !out2.teamCode) return void 0;
4675
+ return out2;
4676
+ }
4677
+ function hasDuplicateKind(outcomes) {
4678
+ const seen = /* @__PURE__ */ new Set();
4679
+ for (const o of outcomes) {
4680
+ if (o.kind === "other") continue;
4681
+ if (seen.has(o.kind)) return true;
4682
+ seen.add(o.kind);
4683
+ }
4684
+ return false;
4685
+ }
4686
+ function sealMarketSignal(raw, options = {}) {
4687
+ if (!raw || typeof raw !== "object") return malformed("signal is not an object");
4688
+ const s = raw;
4689
+ const matchId = opaqueId(s.matchId, MATCH_ID);
4690
+ if (!matchId) return malformed("signal names no fixture");
4691
+ if (!Array.isArray(s.outcomes) || s.outcomes.length > MAX_OUTCOMES) {
4692
+ return malformed("signal outcomes are absent or exceed the cap");
4693
+ }
4694
+ const outcomes = [];
4695
+ for (const rawOutcome of takeBounded(s.outcomes, MAX_OUTCOMES)) {
4696
+ const outcome = sealOutcome(rawOutcome);
4697
+ if (!outcome) return malformed("signal carries an unreadable outcome");
4698
+ outcomes.push(outcome);
4699
+ }
4700
+ if (hasDuplicateKind(outcomes)) {
4701
+ return ambiguous("two outcomes claim the same result");
4702
+ }
4703
+ const sourceMarketId = opaqueId(s.sourceMarketId, MARKET_ID);
4704
+ const liquidity = quantity(s.liquidity);
4705
+ const volume24h = quantity(s.volume24h);
4706
+ const out2 = {
4707
+ matchId,
4708
+ // Allow-listed, not merely stripped: this lands in the provider-attribution
4709
+ // slot, where `marketSourceLabel` falls through to the raw string for an
4710
+ // unrecognized provider — attacker prose where the reader expects
4711
+ // "Polymarket".
4712
+ source: member(s.source, new Set(KNOWN_MARKET_SOURCES)) ?? ""
4713
+ };
4714
+ if (sourceMarketId) out2.sourceMarketId = sourceMarketId;
4715
+ out2.asOf = canonicalTimestamp(s.asOf) ?? "";
4716
+ out2.fetchedAt = canonicalTimestamp(s.fetchedAt) ?? "";
4717
+ out2.outcomes = outcomes;
4718
+ const isAmbiguous = s.ambiguous !== false;
4719
+ const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
4720
+ if (favorite) out2.favorite = favorite;
4721
+ if (liquidity !== void 0) out2.liquidity = liquidity;
4722
+ if (volume24h !== void 0) out2.volume24h = volume24h;
4723
+ out2.stale = s.stale !== false;
4724
+ out2.ambiguous = isAmbiguous || out2.source === "";
4725
+ out2.stale = out2.stale || isStaleSignal(out2, { now: options.now, maxAgeMs: options.maxAgeMs });
4726
+ return valid(out2);
4727
+ }
4728
+ function parseCachedMarketSignal(raw, options = {}) {
4729
+ return sealMarketSignal(raw, options);
4730
+ }
4157
4731
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
4158
4732
  function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
4159
4733
  if (isLive(match.status)) return true;
@@ -4172,9 +4746,9 @@ function normalizeOutcomes(outcomes) {
4172
4746
  probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
4173
4747
  }));
4174
4748
  }
4175
- function favoriteStrength(probability) {
4176
- if (probability >= 0.65) return "clear";
4177
- if (probability >= 0.52) return "slight";
4749
+ function favoriteStrength(probability2) {
4750
+ if (probability2 >= 0.65) return "clear";
4751
+ if (probability2 >= 0.52) return "slight";
4178
4752
  return "close";
4179
4753
  }
4180
4754
  function deriveFavorite(outcomes) {
@@ -4193,14 +4767,16 @@ function deriveFavorite(outcomes) {
4193
4767
  }
4194
4768
  function mapsCleanly(match, outcomes) {
4195
4769
  if (outcomes.some((o) => o.kind === "other")) return false;
4770
+ const kinds = outcomes.map((o) => o.kind);
4771
+ if (new Set(kinds).size !== kinds.length) return false;
4196
4772
  const home = outcomes.find((o) => o.kind === "home");
4197
4773
  const away = outcomes.find((o) => o.kind === "away");
4198
4774
  const draw = outcomes.find((o) => o.kind === "draw");
4199
4775
  if (!home || !away) return false;
4200
- if (home.teamCode && home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
4776
+ if (!home.teamCode || home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
4201
4777
  return false;
4202
4778
  }
4203
- if (away.teamCode && away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
4779
+ if (!away.teamCode || away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
4204
4780
  return false;
4205
4781
  }
4206
4782
  if (match.stage === "GROUP" && !draw) return false;
@@ -4215,11 +4791,13 @@ function hasSaneDistribution(outcomes) {
4215
4791
  const sum = priced.reduce((s, o) => s + o.probability, 0);
4216
4792
  return sum > 0.97 && sum < 1.03;
4217
4793
  }
4794
+ var FUTURE_SKEW_MS = 6e4;
4218
4795
  function isStaleSignal(signal, options = {}) {
4219
4796
  const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
4220
4797
  const asOf = Date.parse(signal.asOf);
4221
4798
  if (!Number.isFinite(asOf)) return true;
4222
4799
  const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
4800
+ if (asOf - now > FUTURE_SKEW_MS) return true;
4223
4801
  return now - asOf > maxAge;
4224
4802
  }
4225
4803
  function isReliableMarketSignal(signal, options = {}) {
@@ -4235,8 +4813,8 @@ function isReliableMarketSignal(signal, options = {}) {
4235
4813
  }
4236
4814
  function buildMarketSignal(input) {
4237
4815
  const outcomes = normalizeOutcomes(input.outcomes);
4238
- const ambiguous = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
4239
- const favorite = ambiguous ? void 0 : deriveFavorite(outcomes);
4816
+ const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
4817
+ const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
4240
4818
  const signal = {
4241
4819
  matchId: input.match.id,
4242
4820
  source: input.source,
@@ -4248,66 +4826,33 @@ function buildMarketSignal(input) {
4248
4826
  liquidity: input.liquidity,
4249
4827
  volume24h: input.volume24h,
4250
4828
  stale: false,
4251
- ambiguous
4829
+ ambiguous: ambiguous2
4252
4830
  };
4253
4831
  signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
4254
- return signal;
4255
- }
4256
- function pct(p) {
4257
- return Math.round(p * 100);
4258
- }
4259
- function marketSourceLabel(source) {
4260
- if (source === "polymarket") return "Polymarket";
4261
- if (source === "fake") return "demo data";
4262
- return source.charAt(0).toUpperCase() + source.slice(1);
4263
- }
4264
- function outcomeLabel(o, match) {
4265
- if (o.kind === "home") return match.home.name;
4266
- if (o.kind === "away") return match.away.name;
4267
- if (o.kind === "draw") return "Draw";
4268
- return o.label;
4269
- }
4270
- function utcHhmm(iso) {
4271
- const t2 = Date.parse(iso);
4272
- if (!Number.isFinite(t2)) return "";
4273
- return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
4274
- }
4275
- function marketFavoriteText(signal, match) {
4276
- const fav = signal.favorite;
4277
- if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
4278
- if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
4279
- const name = fav.kind === "home" ? match.home.name : match.away.name;
4280
- return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
4281
- }
4282
- function marketProbabilityText(signal, match) {
4283
- const order = ["home", "draw", "away"];
4284
- const parts = [];
4285
- for (const kind of order) {
4286
- const o = signal.outcomes.find((x) => x.kind === kind);
4287
- if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4832
+ const sealed = sealMarketSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
4833
+ if (sealed.kind !== "valid") {
4834
+ return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
4288
4835
  }
4289
- for (const o of signal.outcomes) {
4290
- if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4291
- }
4292
- return parts.join(" \xB7 ");
4836
+ return { ...sealed.value, ambiguous: sealed.value.ambiguous || ambiguous2 };
4293
4837
  }
4294
- function marketAttributionText(signal) {
4295
- const time = utcHhmm(signal.asOf);
4296
- const src = `Source: ${marketSourceLabel(signal.source)}`;
4297
- return time ? `${src} \xB7 updated ${time}` : src;
4838
+ var NONE = { kind: "none" };
4839
+ function selectOne(candidates) {
4840
+ if (candidates.length === 1) return { kind: "one", value: candidates[0] };
4841
+ if (candidates.length === 0) return NONE;
4842
+ return { kind: "ambiguous", count: candidates.length };
4298
4843
  }
4299
- function marketLine(signal, match) {
4300
- return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
4301
- signal.source
4302
- )} \xB7 informational only`;
4844
+ function resolvedValues(batch) {
4845
+ const out2 = /* @__PURE__ */ new Map();
4846
+ for (const [key, r] of batch.results) if (r.kind === "valid") out2.set(key, r.value);
4847
+ return out2;
4303
4848
  }
4304
- function marketBlock(signal, match) {
4305
- const lines = [];
4306
- if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
4307
- lines.push(marketFavoriteText(signal, match));
4308
- lines.push(marketProbabilityText(signal, match));
4309
- lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
4310
- return lines;
4849
+ function cacheableKeys(batch) {
4850
+ const out2 = /* @__PURE__ */ new Set();
4851
+ for (const [key, r] of batch.results) if (isCacheable(r)) out2.add(key);
4852
+ return out2;
4853
+ }
4854
+ function emptyBatch() {
4855
+ return { results: /* @__PURE__ */ new Map(), complete: false };
4311
4856
  }
4312
4857
  var FakeMarketProvider = class {
4313
4858
  constructor(opts = {}) {
@@ -4322,14 +4867,12 @@ var FakeMarketProvider = class {
4322
4867
  return void 0;
4323
4868
  }
4324
4869
  async findSignals(matches, options) {
4325
- const signals = /* @__PURE__ */ new Map();
4326
- const checked = /* @__PURE__ */ new Set();
4870
+ const results = /* @__PURE__ */ new Map();
4327
4871
  for (const m of matches) {
4328
- checked.add(m.id);
4329
4872
  const s = await this.findSignal(m, options);
4330
- if (s) signals.set(m.id, s);
4873
+ results.set(m.id, s ? valid(s) : definitiveNone("fake provider has no signal"));
4331
4874
  }
4332
- return { signals, checked };
4875
+ return { results, complete: true };
4333
4876
  }
4334
4877
  synthesize(match, options) {
4335
4878
  const seed = hash(`${match.home.code}-${match.away.code}`);
@@ -4346,7 +4889,9 @@ var FakeMarketProvider = class {
4346
4889
  return buildMarketSignal({
4347
4890
  match,
4348
4891
  source: "fake",
4349
- sourceMarketId: `fake-${match.id}`,
4892
+ // Must satisfy the boundary's opaque-id grammar, like a real one:
4893
+ // a source id that only the live path accepts is the asymmetry itself.
4894
+ sourceMarketId: match.id,
4350
4895
  asOf,
4351
4896
  fetchedAt: now.toISOString(),
4352
4897
  outcomes,
@@ -4370,6 +4915,8 @@ var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
4370
4915
  var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
4371
4916
  var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
4372
4917
  var DEFAULT_TIMEOUT_MS2 = 8e3;
4918
+ var MAX_EVENT_MARKETS = 256;
4919
+ var DEFAULT_DEADLINE_MS = 15e3;
4373
4920
  var WC_SERIES_SLUG = "soccer-fifwc";
4374
4921
  var WC_SPORT = "fifwc";
4375
4922
  var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
@@ -4382,49 +4929,81 @@ var PolymarketProvider = class {
4382
4929
  opts;
4383
4930
  name = "polymarket";
4384
4931
  async findSignal(match, options) {
4385
- const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
4386
- return (await this.resolveOne(match, options, deadline)).signal;
4932
+ const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
4933
+ return parsedValue(await this.resolveOne(match, options, deadline));
4387
4934
  }
4388
4935
  async findSignals(matches, options) {
4389
- const signals = /* @__PURE__ */ new Map();
4390
- const checked = /* @__PURE__ */ new Set();
4391
- const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
4936
+ const results = /* @__PURE__ */ new Map();
4937
+ const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
4938
+ let complete = true;
4392
4939
  for (const m of matches) {
4393
- if (Date.now() >= deadline) break;
4940
+ if (Date.now() >= deadline) {
4941
+ results.set(m.id, unresolved("enrichment deadline expired"));
4942
+ complete = false;
4943
+ continue;
4944
+ }
4394
4945
  const r = await this.resolveOne(m, options, deadline);
4395
- if (r.checked) checked.add(m.id);
4396
- if (r.signal) signals.set(m.id, r.signal);
4946
+ if (r.kind === "unresolved" || r.kind === "malformed") complete = false;
4947
+ results.set(m.id, r);
4397
4948
  }
4398
- return { signals, checked };
4949
+ return { results, complete };
4399
4950
  }
4400
4951
  /**
4401
- * Resolve one match. `checked` distinguishes a DEFINITIVE result (reached the
4402
- * source and found no usable market, or the fixture is unmappable) from a
4403
- * provider/network error so transient failures are retried, not
4404
- * negative-cached.
4952
+ * Resolve one match into a verdict.
4953
+ *
4954
+ * Every exit says which KIND of non-answer it is, because that decides
4955
+ * whether it may be remembered — see `isCacheable`: a conclusion we drew from
4956
+ * a payload we READ is cacheable (including an ambiguity, which is stable),
4957
+ * while a shape we could not read is not. Previously a single
4958
+ * `checked: boolean` collapsed five distinct situations into two, and the
4959
+ * ones that landed on the wrong side of it — an ambiguous payload, a
4960
+ * two-legged market, an incoherent 1X2 — were negative-cached as the fact
4961
+ * that this fixture has no market.
4405
4962
  */
4406
4963
  async resolveOne(match, options, deadline = Number.POSITIVE_INFINITY) {
4407
- const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4408
- const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4409
- if (slugs.length === 0) return { checked: true };
4410
4964
  const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
4411
4965
  try {
4966
+ const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4967
+ const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4968
+ if (slugs.length === 0) return definitiveNone("fixture has no derivable event slug");
4969
+ const RANK = {
4970
+ "definitive-none": 0,
4971
+ ambiguous: 1,
4972
+ unresolved: 2,
4973
+ malformed: 3
4974
+ };
4975
+ let worst;
4976
+ const keepWorst = (r) => {
4977
+ if (r.kind === "definitive-none" || r.kind === "valid") return;
4978
+ if (!worst || (RANK[r.kind] ?? 0) > (RANK[worst.kind] ?? 0)) worst = r;
4979
+ };
4412
4980
  for (const slug of slugs) {
4413
4981
  const remaining = deadline - Date.now();
4414
- if (remaining <= 0) return { checked: false };
4415
- const event = await this.fetchEvent(slug, Math.min(configured, remaining));
4416
- const signal = event ? this.toSignal(match, slug, event, options) : void 0;
4417
- if (signal) return { signal, checked: true };
4982
+ if (remaining <= 0) return unresolved("deadline expired between candidate slugs");
4983
+ let found;
4984
+ try {
4985
+ found = await this.fetchEvent(slug, Math.min(configured, remaining));
4986
+ } catch {
4987
+ keepWorst(malformed("candidate request failed"));
4988
+ continue;
4989
+ }
4990
+ if (found.kind !== "valid") {
4991
+ keepWorst(found);
4992
+ continue;
4993
+ }
4994
+ const r = this.toSignal(match, slug, found.value, options);
4995
+ if (r.kind === "valid") return r;
4996
+ keepWorst(r);
4418
4997
  }
4419
- return { checked: true };
4998
+ return worst ?? definitiveNone("no candidate slug yielded a usable market");
4420
4999
  } catch {
4421
- return { checked: false };
5000
+ return malformed("provider request failed");
4422
5001
  }
4423
5002
  }
4424
5003
  async fetchEvent(slug, timeoutMs) {
4425
5004
  const base = this.opts.baseUrl ?? DEFAULT_BASE2;
4426
5005
  assertAllowedHost(base);
4427
- const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
5006
+ const url = `${base}/events/slug/${encodeURIComponent(slug)}`;
4428
5007
  const doFetch = this.opts.fetchImpl ?? fetch;
4429
5008
  const res = await doFetch(url, {
4430
5009
  signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
@@ -4433,7 +5012,7 @@ var PolymarketProvider = class {
4433
5012
  redirect: "error",
4434
5013
  headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
4435
5014
  });
4436
- if (res.status === 404) return void 0;
5015
+ if (res.status === 404) return definitiveNone("slug returns 404");
4437
5016
  if (!res.ok) {
4438
5017
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4439
5018
  }
@@ -4442,63 +5021,148 @@ var PolymarketProvider = class {
4442
5021
  throw new Error(`Polymarket response too large: ${length} bytes`);
4443
5022
  }
4444
5023
  const data = await res.json();
5024
+ if (Array.isArray(data) && data.length > 1) {
5025
+ return ambiguous("slug returned more than one event");
5026
+ }
5027
+ if (Array.isArray(data) && data.length === 0) return definitiveNone("slug returns no event");
4445
5028
  const event = Array.isArray(data) ? data[0] : data;
4446
- return event && typeof event === "object" ? event : void 0;
5029
+ if (!event || typeof event !== "object") return malformed("event body is not an object");
5030
+ return valid(event);
4447
5031
  }
4448
5032
  toSignal(match, eventSlug, event, options) {
4449
- if (event.active === false || event.closed === true) return void 0;
4450
- if (event.seriesSlug != null && event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
4451
- return void 0;
5033
+ if (typeof event.active !== "boolean" || typeof event.closed !== "boolean") {
5034
+ return malformed("event active/closed is not a boolean");
5035
+ }
5036
+ if (event.active === false || event.closed === true) {
5037
+ return definitiveNone("event is closed or inactive");
5038
+ }
5039
+ if (event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
5040
+ return definitiveNone("event is not in this competition");
5041
+ }
5042
+ if (typeof event.slug !== "string") {
5043
+ return malformed("event states no slug");
5044
+ }
5045
+ if (event.slug !== eventSlug) return definitiveNone("event is not the one requested");
5046
+ if (typeof event.startTime !== "string" || !canonicalTimestamp(event.startTime)) {
5047
+ return malformed("event startTime missing or unparseable");
4452
5048
  }
4453
- if (event.slug != null && event.slug !== eventSlug) return void 0;
4454
- const start = event.startTime ? Date.parse(event.startTime) : Number.NaN;
5049
+ const start = Date.parse(event.startTime);
4455
5050
  const kick = Date.parse(match.kickoff);
4456
- if (Number.isFinite(start) && Number.isFinite(kick) && Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
4457
- return void 0;
5051
+ if (!Number.isFinite(start) || !Number.isFinite(kick)) {
5052
+ return malformed("event or fixture kickoff is unreadable");
5053
+ }
5054
+ if (Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
5055
+ return definitiveNone("event kickoff does not match the fixture");
5056
+ }
5057
+ if (!Array.isArray(event.markets)) {
5058
+ return malformed("event markets is not an array");
5059
+ }
5060
+ const marketsTruncated = Array.isArray(event.markets) && event.markets.length > MAX_EVENT_MARKETS;
5061
+ if (marketsTruncated) {
5062
+ return malformed("event market list exceeded the cap");
4458
5063
  }
4459
- const moneyline = (event.markets ?? []).filter(
4460
- (m) => (m.sportsMarketType ?? "moneyline") === "moneyline"
5064
+ const marketList = takeBounded(event.markets, MAX_EVENT_MARKETS);
5065
+ if (marketList.some(
5066
+ (market) => !market || typeof market !== "object" || typeof market.sportsMarketType !== "string"
5067
+ )) {
5068
+ return malformed("event market is missing its market-type discriminator");
5069
+ }
5070
+ const moneyline = marketList.filter(
5071
+ (m) => m?.sportsMarketType === "moneyline"
4461
5072
  );
4462
- const homeMarket = pickMarket(moneyline, match.home.code, match.home.name);
4463
- const awayMarket = pickMarket(moneyline, match.away.code, match.away.name);
4464
- const drawMarket = pickDraw(moneyline);
4465
- if (!homeMarket || !awayMarket) return void 0;
5073
+ const homeSel = pickMarket(moneyline, match.home.code, match.home.name);
5074
+ const awaySel = pickMarket(moneyline, match.away.code, match.away.name);
5075
+ const drawSel = pickDraw(moneyline);
5076
+ for (const [side, sel] of [
5077
+ ["home", homeSel],
5078
+ ["away", awaySel],
5079
+ ["draw", drawSel]
5080
+ ]) {
5081
+ if (sel.kind === "ambiguous") {
5082
+ return ambiguous(`${sel.count} markets claim the ${side} outcome`);
5083
+ }
5084
+ }
5085
+ if (homeSel.kind !== "one" || awaySel.kind !== "one" || drawSel.kind !== "one") {
5086
+ return definitiveNone("event does not carry all three 1X2 legs");
5087
+ }
5088
+ const homeMarket = homeSel.value;
5089
+ const awayMarket = awaySel.value;
5090
+ const drawMarket = drawSel.value;
4466
5091
  const legIds = [homeMarket, awayMarket, drawMarket].filter((m) => m != null).map((m) => m.id ?? m.slug ?? "");
4467
- if (new Set(legIds).size !== legIds.length) return void 0;
5092
+ if (new Set(legIds).size !== legIds.length) {
5093
+ return ambiguous("two outcome legs are the same market");
5094
+ }
4468
5095
  const legs = [
4469
5096
  ["home", homeMarket, match.home.code, match.home.name],
4470
5097
  ["draw", drawMarket, void 0, "Draw"],
4471
5098
  ["away", awayMarket, match.away.code, match.away.name]
4472
5099
  ];
4473
5100
  const outcomes = [];
4474
- let asOf = event.updatedAt;
5101
+ let asOf = canonicalTimestamp(event.updatedAt);
4475
5102
  let liquidity;
4476
- for (const [kind, market, teamCode, label] of legs) {
5103
+ for (const [kind, market, teamCode2, label] of legs) {
4477
5104
  if (!market) continue;
4478
- if (market.closed === true || market.active === false) return void 0;
4479
- if (market.description && NON_REGULAR_TIME.test(market.description)) return void 0;
5105
+ if (typeof market.closed !== "boolean" || typeof market.active !== "boolean") {
5106
+ return malformed("market active/closed is not a boolean");
5107
+ }
5108
+ if (market.closed === true || market.active === false) {
5109
+ return definitiveNone("an outcome leg is closed or inactive");
5110
+ }
5111
+ if (market.description && NON_REGULAR_TIME.test(market.description)) {
5112
+ return definitiveNone("an outcome leg is not a regular-time market");
5113
+ }
4480
5114
  const yes = yesPrice(market);
4481
- if (yes == null) return void 0;
4482
- outcomes.push({ kind, teamCode, label, probability: yes });
4483
- if (market.updatedAt && (!asOf || market.updatedAt < asOf)) asOf = market.updatedAt;
4484
- const liq = numberish(market.liquidityNum ?? market.liquidity);
5115
+ if (yes == null) return malformed("market is not a readable Yes/No binary");
5116
+ outcomes.push({ kind, teamCode: teamCode2, label, probability: yes });
5117
+ const marketAsOf = canonicalTimestamp(market.updatedAt);
5118
+ if (!marketAsOf) {
5119
+ return malformed("market updatedAt missing or unparseable");
5120
+ }
5121
+ const nowMs = (options?.now ?? this.opts.now ?? /* @__PURE__ */ new Date()).getTime();
5122
+ if (Date.parse(marketAsOf) - nowMs > FUTURE_SKEW_MS) {
5123
+ return malformed("market updatedAt is dated forward");
5124
+ }
5125
+ if (!asOf || Date.parse(marketAsOf) < Date.parse(asOf)) asOf = marketAsOf;
5126
+ const rawLiq = market.liquidityNum ?? market.liquidity;
5127
+ const liq = numberish(rawLiq);
5128
+ if (rawLiq != null && liq == null) {
5129
+ return malformed("market liquidity is unreadable");
5130
+ }
4485
5131
  if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
4486
5132
  }
4487
5133
  const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
4488
- if (rawSum < 0.9 || rawSum > 1.15) return void 0;
5134
+ if (rawSum < 0.9 || rawSum > 1.15) {
5135
+ return ambiguous("outcome probabilities do not form a coherent 1X2");
5136
+ }
5137
+ if (!asOf) return malformed("no usable timestamp on the event or its markets");
4489
5138
  const signal = buildMarketSignal({
4490
5139
  match,
4491
5140
  source: "polymarket",
4492
- sourceMarketId: event.id ?? eventSlug,
4493
- asOf: asOf ?? (/* @__PURE__ */ new Date()).toISOString(),
5141
+ // Echoed into MCP structured content (tools.ts `market.id`), i.e. straight
5142
+ // into an agent's context. Stripping control characters is NOT sufficient
5143
+ // there: printable prose ("IGNORE PREVIOUS INSTRUCTIONS") survives that and
5144
+ // is precisely what matters for a model reading it. Gamma ids are short
5145
+ // opaque tokens, so validate that GRAMMAR and otherwise fall back to the
5146
+ // slug we derived ourselves.
5147
+ // The fallback is grammar-checked too. It is normally a slug we derived
5148
+ // ourselves, but `mapping.2026.json` can override it, so echoing it raw
5149
+ // was the one path around the agent-facing filter this line exists for.
5150
+ sourceMarketId: safeMarketId(event.id) ?? safeDerivedSlug(eventSlug),
5151
+ asOf,
4494
5152
  outcomes,
4495
5153
  liquidity,
4496
5154
  now: options?.now ?? this.opts.now,
4497
5155
  maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
4498
5156
  });
4499
- return signal.ambiguous ? void 0 : signal;
5157
+ return signal.ambiguous ? ambiguous("signal does not map cleanly onto this fixture") : valid(signal);
4500
5158
  }
4501
5159
  };
5160
+ function safeMarketId(id) {
5161
+ return typeof id === "string" && /^[0-9]{1,32}$/.test(id) ? id : void 0;
5162
+ }
5163
+ function safeDerivedSlug(slug) {
5164
+ return typeof slug === "string" && /^fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2}$/.test(slug) ? slug : void 0;
5165
+ }
4502
5166
  var POLYMARKET_TOKEN = {
4503
5167
  SUI: "che",
4504
5168
  // Switzerland
@@ -4512,8 +5176,16 @@ var POLYMARKET_TOKEN = {
4512
5176
  // Croatia
4513
5177
  COD: "cdr",
4514
5178
  // DR Congo
4515
- CPV: "cvi"
5179
+ CPV: "cvi",
4516
5180
  // Cabo Verde
5181
+ // TWO letters, not three — the one entry that is not ISO alpha-3. Verified
5182
+ // live: `fifwc-kor-cze-2026-06-11` is a 404, `fifwc-kr-cze-2026-06-11`
5183
+ // resolves to "Korea Republic vs. Czechia". Korea's three group fixtures
5184
+ // therefore had no market line at all. The `^[a-z]{3}$` guard in
5185
+ // `deriveEventSlugs` validates the FIFA CODE, not the token, so a two-letter
5186
+ // alias passes through it unharmed.
5187
+ KOR: "kr"
5188
+ // Korea Republic
4517
5189
  };
4518
5190
  function pmTokens(code) {
4519
5191
  const c = code.toLowerCase();
@@ -4544,18 +5216,21 @@ function slugToken(m) {
4544
5216
  return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
4545
5217
  }
4546
5218
  function isDrawMarket(m) {
4547
- return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
5219
+ const title = (m.groupItemTitle ?? "").trim().toLowerCase();
5220
+ return slugToken(m) === "draw" || title === "draw" || /^draw\s*\(/.test(title);
4548
5221
  }
4549
- function pickMarket(markets, teamCode, teamName) {
4550
- const tokens = pmTokens(teamCode);
5222
+ function pickMarket(markets, teamCode2, teamName) {
5223
+ const tokens = pmTokens(teamCode2);
4551
5224
  const name = teamName.trim().toLowerCase();
4552
5225
  const teamMarkets = markets.filter((m) => !isDrawMarket(m));
4553
- const bySlug = teamMarkets.find((m) => tokens.includes(slugToken(m)));
4554
- if (bySlug) return bySlug;
4555
- return teamMarkets.find((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name);
5226
+ const bySlug = teamMarkets.filter((m) => tokens.includes(slugToken(m)));
5227
+ if (bySlug.length > 1) return { kind: "ambiguous", count: bySlug.length };
5228
+ const byTitle = name ? teamMarkets.filter((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name) : [];
5229
+ if (byTitle.length > 1) return { kind: "ambiguous", count: byTitle.length };
5230
+ return selectOne([.../* @__PURE__ */ new Set([...bySlug, ...byTitle])]);
4556
5231
  }
4557
5232
  function pickDraw(markets) {
4558
- return markets.find(isDrawMarket);
5233
+ return selectOne(markets.filter(isDrawMarket));
4559
5234
  }
4560
5235
  function assertAllowedHost(base) {
4561
5236
  let host;
@@ -4569,20 +5244,29 @@ function assertAllowedHost(base) {
4569
5244
  }
4570
5245
  }
4571
5246
  function yesPrice(market) {
4572
- const labels = parseJsonArray(market.outcomes);
4573
- const prices = parseJsonArray(market.outcomePrices).map((p2) => Number(p2));
4574
- if (labels.length === 0 || labels.length !== prices.length) return void 0;
4575
- const i = labels.findIndex((l) => l.trim().toLowerCase() === "yes");
4576
- if (i < 0) return void 0;
4577
- const p = prices[i];
4578
- return typeof p === "number" && Number.isFinite(p) && p > 0 && p <= 1 ? p : void 0;
5247
+ const labels = parseJsonArray(market.outcomes).map((l) => l.trim().toLowerCase());
5248
+ const raw = parseJsonArray(market.outcomePrices);
5249
+ if (raw.some((v) => v.trim() === "" || !Number.isFinite(Number(v)))) return void 0;
5250
+ const prices = raw.map((v) => Number(v));
5251
+ if (labels.length !== 2 || prices.length !== 2) return void 0;
5252
+ const i = labels.indexOf("yes");
5253
+ const j = labels.indexOf("no");
5254
+ if (i < 0 || j < 0) return void 0;
5255
+ const yes = prices[i];
5256
+ const no = prices[j];
5257
+ if (![yes, no].every((v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1)) {
5258
+ return void 0;
5259
+ }
5260
+ if (Math.abs(yes + no - 1) > 0.05) return void 0;
5261
+ return yes > 0 ? yes : void 0;
4579
5262
  }
4580
5263
  function parseJsonArray(v) {
4581
- if (Array.isArray(v)) return v.map((x) => String(x));
5264
+ const asText = (x) => typeof x === "string" || typeof x === "number" ? String(x) : "";
5265
+ if (Array.isArray(v)) return v.map(asText);
4582
5266
  if (typeof v === "string") {
4583
5267
  try {
4584
5268
  const parsed = JSON.parse(v);
4585
- return Array.isArray(parsed) ? parsed.map((x) => String(x)) : [];
5269
+ return Array.isArray(parsed) ? parsed.map(asText) : [];
4586
5270
  } catch {
4587
5271
  return [];
4588
5272
  }
@@ -4590,13 +5274,17 @@ function parseJsonArray(v) {
4590
5274
  return [];
4591
5275
  }
4592
5276
  function numberish(v) {
4593
- if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
5277
+ if (typeof v === "number") return Number.isFinite(v) && v >= 0 ? v : void 0;
4594
5278
  if (typeof v === "string") {
4595
5279
  const n = Number(v);
4596
- return Number.isFinite(n) ? n : void 0;
5280
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
4597
5281
  }
4598
5282
  return void 0;
4599
5283
  }
5284
+ var MARKET_COMPETITIONS = /* @__PURE__ */ new Set([DEFAULT_COMPETITION]);
5285
+ function marketsCoverCompetition(competition = resolveCompetition()) {
5286
+ return MARKET_COMPETITIONS.has(competition);
5287
+ }
4600
5288
  function resolveMarketSource(explicit) {
4601
5289
  if (explicit) return explicit;
4602
5290
  if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
@@ -4613,6 +5301,7 @@ function makeMarketProvider(source) {
4613
5301
  return new FakeMarketProvider();
4614
5302
  // no synth → yields no signals, no network
4615
5303
  default:
5304
+ if (!marketsCoverCompetition()) return new FakeMarketProvider();
4616
5305
  return new PolymarketProvider();
4617
5306
  }
4618
5307
  }
@@ -4620,7 +5309,7 @@ async function getMarketSignals(provider, matches, options) {
4620
5309
  try {
4621
5310
  return await provider.findSignals(matches, options);
4622
5311
  } catch {
4623
- return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
5312
+ return emptyBatch();
4624
5313
  }
4625
5314
  }
4626
5315
  var SHARE_HASHTAG = "#VibingLaVidaLoca";
@@ -4701,6 +5390,9 @@ function formatShareSnippet(input, options = {}) {
4701
5390
  if (input.degraded && input.matches.length > 0) {
4702
5391
  blocks.push("(Live data unavailable \u2014 showing the bundled schedule, not live scores.)");
4703
5392
  }
5393
+ if (includeMarkets && input.marketComplete === false) {
5394
+ blocks.push("(Market data unavailable or incomplete \u2014 not all fixtures were checked.)");
5395
+ }
4704
5396
  blocks.push(
4705
5397
  shareFooter({
4706
5398
  source: input.source,
@@ -4731,7 +5423,9 @@ function formatShareTable(input, options = {}) {
4731
5423
  const includeInstall = options.includeInstallLine !== false;
4732
5424
  const blocks = [];
4733
5425
  if (input.tables.length === 0) {
4734
- blocks.push(input.emptyNote ?? "No standings available.");
5426
+ blocks.push(
5427
+ input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
5428
+ );
4735
5429
  } else {
4736
5430
  for (const { group, rows } of input.tables) {
4737
5431
  blocks.push(
@@ -5015,6 +5709,7 @@ var EN2 = {
5015
5709
  "table.title": "Group {group}",
5016
5710
  "table.none": "No group found for {group}.",
5017
5711
  "table.degraded": "Live standings unavailable \u2014 showing the group roster.",
5712
+ "table.unavailable": "Live standings unavailable.",
5018
5713
  "table.empty": "No standings available.",
5019
5714
  "match.none": "No match found with id {id}.",
5020
5715
  "status.live": "LIVE",
@@ -5055,6 +5750,7 @@ var ES2 = {
5055
5750
  "table.title": "Grupo {group}",
5056
5751
  "table.none": "No se encontr\xF3 el grupo {group}.",
5057
5752
  "table.degraded": "Tabla en vivo no disponible \u2014 mostrando la lista del grupo.",
5753
+ "table.unavailable": "Tabla en vivo no disponible.",
5058
5754
  "table.empty": "No hay clasificaci\xF3n disponible.",
5059
5755
  "match.none": "No se encontr\xF3 partido con id {id}.",
5060
5756
  "status.live": "EN VIVO",
@@ -5095,6 +5791,7 @@ var PT2 = {
5095
5791
  "table.title": "Grupo {group}",
5096
5792
  "table.none": "Grupo {group} n\xE3o encontrado.",
5097
5793
  "table.degraded": "Classifica\xE7\xE3o ao vivo indispon\xEDvel \u2014 mostrando os times do grupo.",
5794
+ "table.unavailable": "Classifica\xE7\xE3o ao vivo indispon\xEDvel.",
5098
5795
  "table.empty": "Classifica\xE7\xE3o indispon\xEDvel.",
5099
5796
  "match.none": "Nenhum jogo encontrado com id {id}.",
5100
5797
  "status.live": "AO VIVO",
@@ -5135,6 +5832,7 @@ var FR2 = {
5135
5832
  "table.title": "Groupe {group}",
5136
5833
  "table.none": "Groupe {group} introuvable.",
5137
5834
  "table.degraded": "Classement en direct indisponible \u2014 affichage de la composition du groupe.",
5835
+ "table.unavailable": "Classement en direct indisponible.",
5138
5836
  "table.empty": "Aucun classement disponible.",
5139
5837
  "match.none": "Aucun match trouv\xE9 avec id {id}.",
5140
5838
  "status.live": "DIRECT",
@@ -5245,7 +5943,19 @@ function dataSource(source, lang, c) {
5245
5943
  }
5246
5944
 
5247
5945
  // src/marketCache.ts
5248
- import { readFileSync as readFileSync2 } from "fs";
5946
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
5947
+ import { join as join3 } from "path";
5948
+
5949
+ // src/cache.ts
5950
+ import {
5951
+ closeSync as closeSync2,
5952
+ mkdirSync as mkdirSync2,
5953
+ openSync as openSync2,
5954
+ readFileSync as readFileSync2,
5955
+ rmSync,
5956
+ statSync,
5957
+ writeSync as writeSync2
5958
+ } from "fs";
5249
5959
  import { join as join2 } from "path";
5250
5960
 
5251
5961
  // src/paths.ts
@@ -5268,133 +5978,63 @@ function writeFileAtomic(path, data) {
5268
5978
  renameSync(tmp, path);
5269
5979
  }
5270
5980
 
5271
- // src/marketCache.ts
5272
- var POSITIVE_TTL_MS = 10 * 6e4;
5273
- var NEGATIVE_TTL_MS = 3 * 6e4;
5274
- function cachePath() {
5275
- return join2(cacheDir(), "market-signals.json");
5276
- }
5277
- function readFile() {
5278
- try {
5279
- return JSON.parse(readFileSync2(cachePath(), "utf8"));
5280
- } catch {
5281
- return void 0;
5282
- }
5283
- }
5284
- function readMarketCache(source, competition, now = Date.now()) {
5285
- const signals = /* @__PURE__ */ new Map();
5286
- const checked = /* @__PURE__ */ new Set();
5287
- const file = readFile();
5288
- if (!file || file.source !== source || file.competition !== competition) {
5289
- return { signals, checked };
5290
- }
5291
- for (const [id, entry] of Object.entries(file.entries ?? {})) {
5292
- const t2 = Date.parse(entry.fetchedAt);
5293
- if (!Number.isFinite(t2)) continue;
5294
- const ttl = entry.signal ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS;
5295
- if (now - t2 > ttl) continue;
5296
- checked.add(id);
5297
- if (entry.signal) signals.set(id, entry.signal);
5298
- }
5299
- return { signals, checked };
5300
- }
5301
- function writeMarketCache(source, competition, attempted, fetched, now = Date.now()) {
5302
- if (attempted.length === 0) return;
5303
- try {
5304
- const existing = readFile();
5305
- const base = existing && existing.source === source && existing.competition === competition ? existing : { source, competition, entries: {} };
5306
- const fetchedAt = new Date(now).toISOString();
5307
- for (const id of attempted) {
5308
- base.entries[id] = { fetchedAt, signal: fetched.get(id) ?? null };
5309
- }
5310
- writeFileAtomic(cachePath(), JSON.stringify(base));
5311
- } catch {
5312
- }
5313
- }
5314
-
5315
- // src/starNudge.ts
5316
- import { readFileSync as readFileSync3 } from "fs";
5317
- import { join as join3 } from "path";
5318
- var REPO_URL = "https://github.com/arturogarrido/claudinho";
5319
- var NUDGE_EVERY = 5;
5320
- function counterPath() {
5321
- return join3(cacheDir(), "runs.json");
5322
- }
5323
- function shouldNudge(runCount, every = NUDGE_EVERY) {
5324
- return runCount > 0 && runCount % every === 0;
5325
- }
5326
- function bumpRunCount(path = counterPath()) {
5327
- try {
5328
- let count = 0;
5329
- try {
5330
- const raw = JSON.parse(readFileSync3(path, "utf8"));
5331
- if (typeof raw.count === "number" && Number.isFinite(raw.count)) count = raw.count;
5332
- } catch {
5333
- count = 0;
5334
- }
5335
- count += 1;
5336
- writeFileAtomic(path, JSON.stringify({ count }));
5337
- return count;
5338
- } catch {
5339
- return void 0;
5340
- }
5341
- }
5342
-
5343
- // src/clipboard.ts
5344
- import { spawnSync } from "child_process";
5345
- function clipboardTools(platform) {
5346
- if (platform === "darwin") return [{ cmd: "pbcopy", args: [] }];
5347
- if (platform === "win32") return [{ cmd: "clip", args: [] }];
5348
- return [
5349
- { cmd: "wl-copy", args: [] },
5350
- { cmd: "xclip", args: ["-selection", "clipboard"] },
5351
- { cmd: "xsel", args: ["--clipboard", "--input"] }
5352
- ];
5353
- }
5354
- function copyToClipboard(text, platform = process.platform) {
5355
- for (const { cmd, args } of clipboardTools(platform)) {
5356
- try {
5357
- const res = spawnSync(cmd, args, {
5358
- input: text,
5359
- stdio: ["pipe", "ignore", "ignore"],
5360
- timeout: 1e3
5361
- });
5362
- if (!res.error && res.status === 0) return true;
5363
- } catch {
5364
- }
5365
- }
5366
- return false;
5367
- }
5368
-
5369
5981
  // src/cache.ts
5370
- import {
5371
- closeSync as closeSync2,
5372
- mkdirSync as mkdirSync2,
5373
- openSync as openSync2,
5374
- readFileSync as readFileSync4,
5375
- rmSync,
5376
- statSync,
5377
- writeSync as writeSync2
5378
- } from "fs";
5379
- import { join as join4 } from "path";
5380
5982
  var CACHE_VERSION = 2;
5983
+ var MAX_STATE_BYTES = 1024 * 1024;
5984
+ var MAX_STATE_RECORDS = 1024;
5381
5985
  var LOCK_STALE_MS = 6e4;
5382
- function cachePath2(source = "espn", competition = DEFAULT_COMPETITION) {
5986
+ function cachePath(source = "espn", competition = DEFAULT_COMPETITION) {
5383
5987
  if (source === "espn" && competition === DEFAULT_COMPETITION) {
5384
- return join4(cacheDir(), "state.json");
5988
+ return join2(cacheDir(), "state.json");
5385
5989
  }
5386
5990
  const slug = `${source}.${competition}`.replace(/[^a-zA-Z0-9._-]/g, "_");
5387
- return join4(cacheDir(), `state.${slug}.json`);
5991
+ return join2(cacheDir(), `state.${slug}.json`);
5388
5992
  }
5389
5993
  function lockPath() {
5390
- return join4(cacheDir(), "refresh.lock");
5994
+ return join2(cacheDir(), "refresh.lock");
5995
+ }
5996
+ function validStamp(value) {
5997
+ if (typeof value !== "string") return false;
5998
+ const match = value.match(
5999
+ /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,3}))?Z$/
6000
+ );
6001
+ if (!match) return false;
6002
+ const parsed = Date.parse(value);
6003
+ if (!Number.isFinite(parsed)) return false;
6004
+ const canonical = `${match[1]}.${(match[2] ?? "").padEnd(3, "0")}Z`;
6005
+ return new Date(parsed).toISOString() === canonical;
6006
+ }
6007
+ function validScope(value) {
6008
+ return typeof value === "string" && value.length > 0 && value.length <= 128 && /^[a-zA-Z0-9._-]+$/.test(value);
6009
+ }
6010
+ function isCacheState(value) {
6011
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
6012
+ const s = value;
6013
+ if (s.version !== CACHE_VERSION) return false;
6014
+ if (!validStamp(s.updatedAt) || typeof s.degraded !== "boolean") return false;
6015
+ if (!validScope(s.source) || !validScope(s.competition)) return false;
6016
+ if (!Array.isArray(s.live) || s.live.length > MAX_STATE_RECORDS) return false;
6017
+ if (s.fixtures !== void 0 && (!Array.isArray(s.fixtures) || s.fixtures.length > MAX_STATE_RECORDS)) {
6018
+ return false;
6019
+ }
6020
+ for (const key of [
6021
+ "fixturesUpdatedAt",
6022
+ "fixturesAttemptedAt",
6023
+ "backoffUntil"
6024
+ ]) {
6025
+ if (s[key] !== void 0 && !validStamp(s[key])) return false;
6026
+ }
6027
+ return true;
5391
6028
  }
5392
6029
  function readState(source = "espn", competition = DEFAULT_COMPETITION) {
5393
6030
  try {
5394
- const s = JSON.parse(
5395
- readFileSync4(cachePath2(source, competition), "utf8")
5396
- );
5397
- return s.version === CACHE_VERSION ? s : void 0;
6031
+ const path = cachePath(source, competition);
6032
+ const info = statSync(path);
6033
+ if (!info.isFile() || info.size > MAX_STATE_BYTES) return void 0;
6034
+ const bytes = readFileSync2(path);
6035
+ if (bytes.byteLength > MAX_STATE_BYTES) return void 0;
6036
+ const parsed = JSON.parse(bytes.toString("utf8"));
6037
+ return isCacheState(parsed) ? parsed : void 0;
5398
6038
  } catch {
5399
6039
  return void 0;
5400
6040
  }
@@ -5405,41 +6045,45 @@ function readCurrentState(source, competition) {
5405
6045
  }
5406
6046
  function writeState(state) {
5407
6047
  writeFileAtomic(
5408
- cachePath2(state.source, state.competition),
6048
+ cachePath(state.source, state.competition),
5409
6049
  JSON.stringify({ ...state, version: CACHE_VERSION })
5410
6050
  );
5411
6051
  }
6052
+ var MAX_BACKOFF_MS = 30 * 6e4;
5412
6053
  function backoffActive(state, now = Date.now()) {
5413
6054
  if (!state?.backoffUntil) return false;
5414
6055
  const t2 = Date.parse(state.backoffUntil);
5415
- return Number.isFinite(t2) && now < t2;
6056
+ if (!Number.isFinite(t2)) return false;
6057
+ return now < t2 && t2 - now <= MAX_BACKOFF_MS;
5416
6058
  }
5417
6059
  function fixturesAttemptAgeMs(state, now = Date.now()) {
5418
- if (!state?.fixturesAttemptedAt) return Infinity;
5419
- const t2 = Date.parse(state.fixturesAttemptedAt);
5420
- return Number.isFinite(t2) ? now - t2 : Infinity;
6060
+ return stampAgeMs(state?.fixturesAttemptedAt, now);
6061
+ }
6062
+ var FUTURE_SKEW_MS2 = 6e4;
6063
+ function stampAgeMs(value, now) {
6064
+ if (!value) return Infinity;
6065
+ const t2 = Date.parse(value);
6066
+ if (!Number.isFinite(t2)) return Infinity;
6067
+ const age = now - t2;
6068
+ return age < -FUTURE_SKEW_MS2 ? Infinity : age;
5421
6069
  }
5422
6070
  function ageMs(state, now = Date.now()) {
5423
- if (!state) return Infinity;
5424
- const t2 = Date.parse(state.updatedAt);
5425
- return Number.isFinite(t2) ? now - t2 : Infinity;
6071
+ return stampAgeMs(state?.updatedAt, now);
5426
6072
  }
5427
6073
  function fixturesAgeMs(state, now = Date.now()) {
5428
- if (!state?.fixturesUpdatedAt) return Infinity;
5429
- const t2 = Date.parse(state.fixturesUpdatedAt);
5430
- return Number.isFinite(t2) ? now - t2 : Infinity;
6074
+ return stampAgeMs(state?.fixturesUpdatedAt, now);
5431
6075
  }
5432
6076
  function lockAgeMs(now = Date.now()) {
5433
6077
  const lp = lockPath();
5434
6078
  try {
5435
- const contents = readFileSync4(lp, "utf8");
6079
+ const contents = readFileSync2(lp, "utf8");
5436
6080
  const written = Number.parseInt(contents.split(/\s+/)[1] ?? "", 10);
5437
- if (Number.isFinite(written)) return now - written;
6081
+ if (Number.isFinite(written)) return stampAgeMs(new Date(written).toISOString(), now);
5438
6082
  } catch {
5439
6083
  return Infinity;
5440
6084
  }
5441
6085
  try {
5442
- return now - statSync(lp).mtimeMs;
6086
+ return stampAgeMs(new Date(statSync(lp).mtimeMs).toISOString(), now);
5443
6087
  } catch {
5444
6088
  return Infinity;
5445
6089
  }
@@ -5487,6 +6131,169 @@ function releaseLock() {
5487
6131
  }
5488
6132
  }
5489
6133
 
6134
+ // src/marketCache.ts
6135
+ var POSITIVE_TTL_MS = 10 * 6e4;
6136
+ var NEGATIVE_TTL_MS = 3 * 6e4;
6137
+ var MAX_MARKET_CACHE_BYTES = 1024 * 1024;
6138
+ var FUTURE_SKEW_MS3 = 6e4;
6139
+ function cachePath2() {
6140
+ return join3(cacheDir(), "market-signals.json");
6141
+ }
6142
+ function readFile() {
6143
+ try {
6144
+ const path = cachePath2();
6145
+ const info = statSync2(path);
6146
+ if (!info.isFile() || info.size > MAX_MARKET_CACHE_BYTES) return void 0;
6147
+ const bytes = readFileSync3(path);
6148
+ if (bytes.byteLength > MAX_MARKET_CACHE_BYTES) return void 0;
6149
+ const parsed = JSON.parse(bytes.toString("utf8"));
6150
+ if (!parsed || typeof parsed !== "object") return void 0;
6151
+ return parsed;
6152
+ } catch {
6153
+ return void 0;
6154
+ }
6155
+ }
6156
+ function isEntryShaped(e) {
6157
+ if (!e || typeof e !== "object") return false;
6158
+ const entry = e;
6159
+ if (typeof entry.fetchedAt !== "string" || !Number.isFinite(Date.parse(entry.fetchedAt))) {
6160
+ return false;
6161
+ }
6162
+ if (entry.signal === null) return true;
6163
+ return !!entry.signal && typeof entry.signal === "object";
6164
+ }
6165
+ function isUsableSignal(s) {
6166
+ if (!s) return false;
6167
+ if (s.source === "" || s.asOf === "" || s.outcomes.length === 0) return false;
6168
+ if (s.outcomes.some((o) => o.kind === "other")) return false;
6169
+ if (s.ambiguous) return false;
6170
+ if (!s.favorite) return false;
6171
+ return hasSaneDistribution(s.outcomes);
6172
+ }
6173
+ function readMarketCache(source, competition, now = Date.now()) {
6174
+ const signals = /* @__PURE__ */ new Map();
6175
+ const checked = /* @__PURE__ */ new Set();
6176
+ const file = readFile();
6177
+ if (!file || file.source !== source || file.competition !== competition) {
6178
+ return { signals, checked };
6179
+ }
6180
+ const entries = file.entries;
6181
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
6182
+ return { signals, checked };
6183
+ }
6184
+ let examined = 0;
6185
+ for (const id in entries) {
6186
+ if (!Object.hasOwn(entries, id)) continue;
6187
+ if (examined >= 256) break;
6188
+ examined += 1;
6189
+ const raw = entries[id];
6190
+ if (!isEntryShaped(raw)) continue;
6191
+ const entry = raw;
6192
+ const t2 = Date.parse(entry.fetchedAt);
6193
+ const ttl = entry.signal ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS;
6194
+ const age = now - t2;
6195
+ if (age > ttl || age < -FUTURE_SKEW_MS3) continue;
6196
+ if (entry.signal === null) {
6197
+ checked.add(id);
6198
+ continue;
6199
+ }
6200
+ const sealed = parseCachedMarketSignal(entry.signal, { now: new Date(now) });
6201
+ if (sealed.kind !== "valid") continue;
6202
+ const clean = sealed.value;
6203
+ if (clean.matchId !== id || clean.source !== source) continue;
6204
+ if (!isUsableSignal(clean)) continue;
6205
+ checked.add(id);
6206
+ signals.set(id, clean);
6207
+ }
6208
+ return { signals, checked };
6209
+ }
6210
+ function writeMarketCache(source, competition, attempted, fetched, now = Date.now()) {
6211
+ if (attempted.length === 0) return;
6212
+ try {
6213
+ const existing = readFile();
6214
+ const reuse = existing && existing.source === source && existing.competition === competition;
6215
+ const carried = {};
6216
+ if (reuse && existing.entries && typeof existing.entries === "object" && !Array.isArray(existing.entries)) {
6217
+ let examined = 0;
6218
+ for (const id in existing.entries) {
6219
+ if (!Object.hasOwn(existing.entries, id)) continue;
6220
+ if (examined >= 256) break;
6221
+ examined += 1;
6222
+ const raw = existing.entries[id];
6223
+ if (!isEntryShaped(raw)) continue;
6224
+ const age = stampAgeMs(raw.fetchedAt, now);
6225
+ if (age > (raw.signal ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS)) continue;
6226
+ if (age < -FUTURE_SKEW_MS3) continue;
6227
+ if (raw.signal !== null && !isUsableSignal(parsedValue(parseCachedMarketSignal(raw.signal, { now: new Date(now) })))) {
6228
+ continue;
6229
+ }
6230
+ carried[id] = raw;
6231
+ }
6232
+ }
6233
+ const base = { source, competition, entries: carried };
6234
+ const fetchedAt = new Date(now).toISOString();
6235
+ for (const id of attempted.slice(0, 256)) {
6236
+ base.entries[id] = { fetchedAt, signal: fetched.get(id) ?? null };
6237
+ }
6238
+ writeFileAtomic(cachePath2(), JSON.stringify(base));
6239
+ } catch {
6240
+ }
6241
+ }
6242
+
6243
+ // src/starNudge.ts
6244
+ import { readFileSync as readFileSync4 } from "fs";
6245
+ import { join as join4 } from "path";
6246
+ var REPO_URL = "https://github.com/arturogarrido/claudinho";
6247
+ var NUDGE_EVERY = 5;
6248
+ function counterPath() {
6249
+ return join4(cacheDir(), "runs.json");
6250
+ }
6251
+ function shouldNudge(runCount, every = NUDGE_EVERY) {
6252
+ return runCount > 0 && runCount % every === 0;
6253
+ }
6254
+ function bumpRunCount(path = counterPath()) {
6255
+ try {
6256
+ let count2 = 0;
6257
+ try {
6258
+ const raw = JSON.parse(readFileSync4(path, "utf8"));
6259
+ if (typeof raw.count === "number" && Number.isFinite(raw.count)) count2 = raw.count;
6260
+ } catch {
6261
+ count2 = 0;
6262
+ }
6263
+ count2 += 1;
6264
+ writeFileAtomic(path, JSON.stringify({ count: count2 }));
6265
+ return count2;
6266
+ } catch {
6267
+ return void 0;
6268
+ }
6269
+ }
6270
+
6271
+ // src/clipboard.ts
6272
+ import { spawnSync } from "child_process";
6273
+ function clipboardTools(platform) {
6274
+ if (platform === "darwin") return [{ cmd: "pbcopy", args: [] }];
6275
+ if (platform === "win32") return [{ cmd: "clip", args: [] }];
6276
+ return [
6277
+ { cmd: "wl-copy", args: [] },
6278
+ { cmd: "xclip", args: ["-selection", "clipboard"] },
6279
+ { cmd: "xsel", args: ["--clipboard", "--input"] }
6280
+ ];
6281
+ }
6282
+ function copyToClipboard(text, platform = process.platform) {
6283
+ for (const { cmd, args } of clipboardTools(platform)) {
6284
+ try {
6285
+ const res = spawnSync(cmd, args, {
6286
+ input: text,
6287
+ stdio: ["pipe", "ignore", "ignore"],
6288
+ timeout: 1e3
6289
+ });
6290
+ if (!res.error && res.status === 0) return true;
6291
+ } catch {
6292
+ }
6293
+ }
6294
+ return false;
6295
+ }
6296
+
5490
6297
  // src/statusline.ts
5491
6298
  var DISPLAY_STALE_MS = 5 * 6e4;
5492
6299
  var TOURNAMENT_COMPLETE_LINE = "\u26BD World Cup 2026 is complete \xB7 Thanks for vibing with Claudinho";
@@ -5523,36 +6330,110 @@ function matchSegment(m, compact, flags) {
5523
6330
  const away = compact ? m.away.flag : `${m.away.code} ${m.away.flag}`;
5524
6331
  return `${home} ${scoreline(m)} ${away} ${minute}`;
5525
6332
  }
6333
+ var MAX_LIVE_CONSIDERED = 64;
6334
+ var MAX_LIVE_EXAMINED = 512;
6335
+ function sealFixtures(raw) {
6336
+ if (raw === void 0) {
6337
+ return { items: [], total: 0, shown: 0, truncated: false, complete: true };
6338
+ }
6339
+ if (!Array.isArray(raw)) {
6340
+ return { items: [], total: 0, shown: 0, truncated: false, complete: false };
6341
+ }
6342
+ const out2 = [];
6343
+ let inspected = 0;
6344
+ let readable = true;
6345
+ for (const rec of raw) {
6346
+ if (out2.length >= MAX_LIVE_CONSIDERED || inspected >= MAX_LIVE_EXAMINED) break;
6347
+ inspected++;
6348
+ if (!isMatchShaped(rec)) {
6349
+ readable = false;
6350
+ continue;
6351
+ }
6352
+ const sealed = parsedValue(parseCachedMatch(rec, { events: false }));
6353
+ if (sealed) out2.push(sealed);
6354
+ else readable = false;
6355
+ }
6356
+ const exhausted = inspected === raw.length;
6357
+ const complete = exhausted && readable;
6358
+ return {
6359
+ items: out2,
6360
+ // Exact only when complete; otherwise this is the number actually sealed.
6361
+ // Callers use a nonnumeric marker for an incomplete scan.
6362
+ total: out2.length,
6363
+ shown: out2.length,
6364
+ truncated: !exhausted,
6365
+ complete
6366
+ };
6367
+ }
5526
6368
  function liveMatchesFromCache(state, nowMs = Date.now()) {
5527
6369
  const fresh = state && ageMs(state, nowMs) < DISPLAY_STALE_MS;
5528
- const liveArr = fresh && Array.isArray(state?.live) ? state.live : [];
5529
- return liveArr.filter(
5530
- (m) => !!m && typeof m === "object" && isLive(m.status) && !!m.home?.code && !!m.away?.code
5531
- ).map(sanitizeMatchStrings);
6370
+ const rawLive = fresh ? state?.live : [];
6371
+ if (!Array.isArray(rawLive)) {
6372
+ return { items: [], total: 0, shown: 0, truncated: false, complete: false };
6373
+ }
6374
+ const liveArr = rawLive;
6375
+ const out2 = [];
6376
+ let inspected = 0;
6377
+ let readable = true;
6378
+ for (let i = 0; i < liveArr.length; i++) {
6379
+ if (out2.length >= MAX_LIVE_CONSIDERED || inspected >= MAX_LIVE_EXAMINED) break;
6380
+ inspected++;
6381
+ const raw = liveArr[i];
6382
+ if (!raw || typeof raw !== "object") {
6383
+ readable = false;
6384
+ continue;
6385
+ }
6386
+ const m = raw;
6387
+ if (!isLive(m.status) || !m.home?.code || !m.away?.code) {
6388
+ readable = false;
6389
+ continue;
6390
+ }
6391
+ const sealed = parsedValue(parseCachedMatch(m, { events: false }));
6392
+ if (sealed) out2.push(sealed);
6393
+ else readable = false;
6394
+ }
6395
+ const exhausted = inspected === liveArr.length;
6396
+ const complete = exhausted && readable;
6397
+ return {
6398
+ items: out2,
6399
+ total: out2.length,
6400
+ shown: out2.length,
6401
+ truncated: !exhausted,
6402
+ // False when we stopped early or a record was unreadable. In either case an
6403
+ // empty list must not render as the authoritative "nothing is on".
6404
+ complete
6405
+ };
5532
6406
  }
6407
+ var DEFAULT_MAX_SEGMENTS = 8;
6408
+ var MAX_LINE_COLUMNS = 200;
5533
6409
  function renderPrompt(state, opts = {}) {
6410
+ return truncateVisible(renderPromptLine(state, opts), MAX_LINE_COLUMNS);
6411
+ }
6412
+ function renderPromptLine(state, opts = {}) {
5534
6413
  const now = opts.now ?? /* @__PURE__ */ new Date();
5535
6414
  const nowMs = now.getTime();
5536
6415
  const defaultCompetition = opts.defaultCompetition ?? true;
5537
6416
  const compact = opts.compact ?? true;
5538
6417
  const flags = opts.flags ?? true;
5539
6418
  const team = opts.team?.toUpperCase();
5540
- const live = liveMatchesFromCache(state, nowMs);
5541
- const cachedFixtures = Array.isArray(state?.fixtures) ? state.fixtures.filter(isMatchShaped).map(sanitizeMatchStrings) : [];
6419
+ const liveList = liveMatchesFromCache(state, nowMs);
6420
+ const live = liveList.items;
6421
+ const cachedFixtureList = sealFixtures(state?.fixtures);
6422
+ const cachedFixtures = [...cachedFixtureList.items];
5542
6423
  const schedule = cachedFixtures.length ? mergeLive(allFixtures(), cachedFixtures) : void 0;
5543
6424
  if (team) {
5544
6425
  const mine = live.find((m) => m.home?.code === team || m.away?.code === team);
5545
6426
  if (mine) return `\u26BD ${matchSegment(mine, compact, flags)}`;
5546
6427
  } else if (live.length > 0) {
5547
- const max = opts.max && opts.max > 0 ? opts.max : live.length;
6428
+ const max = opts.max && opts.max > 0 ? Math.min(opts.max, DEFAULT_MAX_SEGMENTS) : DEFAULT_MAX_SEGMENTS;
5548
6429
  const shown = live.slice(0, max);
5549
- let line2 = "\u26BD " + shown.map((m) => matchSegment(m, compact, flags)).join(" \xB7 ");
5550
6430
  const overflow = live.length - shown.length;
5551
- if (overflow > 0) line2 += ` +${overflow}`;
5552
- return line2;
6431
+ const marker = !liveList.complete ? " +more" : overflow > 0 ? ` +${overflow}` : "";
6432
+ const body = "\u26BD " + shown.map((m) => matchSegment(m, compact, flags)).join(" \xB7 ");
6433
+ return truncateVisible(body, MAX_LINE_COLUMNS - displayWidth(marker)) + marker;
5553
6434
  }
5554
6435
  const cacheFresh = !!state && state.degraded !== true && ageMs(state, nowMs) < DISPLAY_STALE_MS;
5555
- if (!cacheFresh) {
6436
+ if (!cacheFresh || !liveList.complete) {
5556
6437
  const win = fixturesInLiveWindow(nowMs, schedule).filter(
5557
6438
  (m) => !team || m.home.code === team || m.away.code === team
5558
6439
  );
@@ -5574,14 +6455,24 @@ function renderPrompt(state, opts = {}) {
5574
6455
  }
5575
6456
 
5576
6457
  // src/hook.ts
5577
- function rosterPinned(t2) {
6458
+ var MAX_HOOK_MATCHES = 12;
6459
+ var MAX_HOOK_CODE_POINTS = 4096;
6460
+ function boundContext(text, marker = "") {
6461
+ const points = [...text];
6462
+ if (points.length + [...marker].length <= MAX_HOOK_CODE_POINTS) return text + marker;
6463
+ const room = Math.max(0, MAX_HOOK_CODE_POINTS - [...marker].length);
6464
+ return `${points.slice(0, room).join("")}
6465
+ (context truncated)${marker}`;
6466
+ }
6467
+ function rosterPinned(t2, pin) {
6468
+ if (!pin) return t2;
5578
6469
  const { team } = lookupTeam(t2.code);
5579
6470
  return team ? { ...t2, name: team.name, flag: team.flag } : t2;
5580
6471
  }
5581
- function line(m, flags) {
6472
+ function line(m, flags, pin) {
5582
6473
  const minute = m.status === "HT" ? "half-time" : m.minute ? `${m.minute}'` : "live";
5583
- const h = rosterPinned(m.home);
5584
- const a = rosterPinned(m.away);
6474
+ const h = rosterPinned(m.home, pin);
6475
+ const a = rosterPinned(m.away, pin);
5585
6476
  const home = flags ? `${h.flag} ${h.name}` : h.name;
5586
6477
  const away = flags ? `${a.name} ${a.flag}` : a.name;
5587
6478
  return `${home} ${scoreline(m)} ${away} (${minute})`;
@@ -5590,7 +6481,9 @@ function renderHook(state, opts = {}) {
5590
6481
  const now = opts.now ?? /* @__PURE__ */ new Date();
5591
6482
  const team = opts.team?.toUpperCase();
5592
6483
  const flags = opts.flags ?? true;
5593
- let live = liveMatchesFromCache(state, now.getTime());
6484
+ const pin = opts.defaultCompetition ?? true;
6485
+ const liveList = liveMatchesFromCache(state, now.getTime());
6486
+ let live = [...liveList.items];
5594
6487
  if (live.length === 0) return "";
5595
6488
  if (team) {
5596
6489
  live = [...live].sort((a, b) => {
@@ -5599,9 +6492,13 @@ function renderHook(state, opts = {}) {
5599
6492
  return aHas - bHas;
5600
6493
  });
5601
6494
  }
5602
- const lines = live.map((mm) => line(mm, flags)).join("\n");
5603
- return `[Claudinho \u2014 live football scores right now]
5604
- ${lines}`;
6495
+ const shown = live.slice(0, MAX_HOOK_MATCHES);
6496
+ const overflow = live.length - shown.length;
6497
+ const lines = shown.map((mm) => line(mm, flags, pin)).join("\n");
6498
+ const more = !liveList.complete ? "\n(more live matches may not be shown)" : overflow > 0 ? `
6499
+ (+${overflow} more not shown)` : "";
6500
+ return boundContext(`[Claudinho \u2014 live football scores right now]
6501
+ ${lines}`, more);
5605
6502
  }
5606
6503
 
5607
6504
  // src/refresh.ts
@@ -5629,11 +6526,7 @@ function liveWindowActive(nowMs) {
5629
6526
  return inLiveWindow(nowMs);
5630
6527
  }
5631
6528
  function liveAdapter(source) {
5632
- const competition = resolveCompetition();
5633
- if (source === "espn" && competition === DEFAULT_COMPETITION) {
5634
- return new EspnAdapter({ enrichGroups: false });
5635
- }
5636
- return makeAdapter(source);
6529
+ return makeAdapter(source, { enrichGroups: false });
5637
6530
  }
5638
6531
  async function runRefresh(opts = {}) {
5639
6532
  const now = opts.now ?? /* @__PURE__ */ new Date();
@@ -5890,11 +6783,15 @@ function adapterFor({ cfg, adapter }) {
5890
6783
  }
5891
6784
  var DEFAULT_ON_MARKET_OPTS = { deadlineMs: 2e3, timeoutMs: 2500 };
5892
6785
  var MARKETS_CMD_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
5893
- async function marketSignalsFor(ctx, matches, opts = {}) {
5894
- if (ctx.marketProvider) return (await getMarketSignals(ctx.marketProvider, matches, opts)).signals;
6786
+ async function marketSignalsFor(ctx, matches, opts = {}, providerFactory = makeMarketProvider) {
6787
+ if (ctx.marketProvider) {
6788
+ const b = await getMarketSignals(ctx.marketProvider, matches, opts);
6789
+ return { signals: resolvedValues(b), complete: b.complete };
6790
+ }
5895
6791
  const source = resolveMarketSource();
5896
6792
  if (source !== "polymarket") {
5897
- return (await getMarketSignals(makeMarketProvider(source), matches, opts)).signals;
6793
+ const b = await getMarketSignals(providerFactory(source), matches, opts);
6794
+ return { signals: resolvedValues(b), complete: b.complete };
5898
6795
  }
5899
6796
  const competition = resolveCompetition();
5900
6797
  const { signals: cached, checked: cachedIds } = readMarketCache("polymarket", competition);
@@ -5902,35 +6799,39 @@ async function marketSignalsFor(ctx, matches, opts = {}) {
5902
6799
  const miss = [];
5903
6800
  for (const m of matches) {
5904
6801
  const hit = cached.get(m.id);
5905
- if (hit) result.set(m.id, hit);
5906
- else if (!cachedIds.has(m.id)) miss.push(m);
6802
+ if (hit && marketSignalRendersFor(m, hit)) {
6803
+ result.set(m.id, hit);
6804
+ continue;
6805
+ }
6806
+ if (!hit && cachedIds.has(m.id)) continue;
6807
+ miss.push(m);
5907
6808
  }
6809
+ let complete = true;
5908
6810
  if (miss.length > 0) {
5909
- const { signals: fetched, checked } = await getMarketSignals(
5910
- makeMarketProvider("polymarket"),
5911
- miss,
5912
- opts
5913
- );
5914
- writeMarketCache("polymarket", competition, [...checked], fetched);
6811
+ const batch = await getMarketSignals(providerFactory("polymarket"), miss, opts);
6812
+ const fetched = resolvedValues(batch);
6813
+ complete = batch.complete;
6814
+ writeMarketCache("polymarket", competition, [...cacheableKeys(batch)], fetched);
5915
6815
  for (const [id, s] of fetched) result.set(id, s);
5916
6816
  }
5917
- return result;
6817
+ return { signals: result, complete };
5918
6818
  }
5919
6819
  async function reliableMarketSignals(ctx, matches) {
5920
- if (ctx.cfg.markets === false) return /* @__PURE__ */ new Map();
6820
+ if (ctx.cfg.markets === false) return { signals: /* @__PURE__ */ new Map(), complete: true };
5921
6821
  const now = ctx.now ?? /* @__PURE__ */ new Date();
5922
6822
  const relevant = matches.filter((m) => marketRelevant(m, now));
5923
- if (relevant.length === 0) return /* @__PURE__ */ new Map();
6823
+ if (relevant.length === 0) return { signals: /* @__PURE__ */ new Map(), complete: true };
5924
6824
  const raw = await marketSignalsFor(ctx, relevant, DEFAULT_ON_MARKET_OPTS);
5925
6825
  const out2 = /* @__PURE__ */ new Map();
5926
- for (const [id, s] of raw) {
6826
+ for (const [id, s] of raw.signals) {
5927
6827
  const m = relevant.find((x) => x.id === id);
5928
6828
  if (m && isReliableMarketSignal(s, { now }) && marketSignalRendersFor(m, s)) out2.set(id, s);
5929
6829
  }
5930
- return out2;
6830
+ return { signals: out2, complete: raw.complete };
5931
6831
  }
5932
6832
  async function reliableMarketSignalFor(ctx, match) {
5933
- return (await reliableMarketSignals(ctx, [match])).get(match.id);
6833
+ const result = await reliableMarketSignals(ctx, [match]);
6834
+ return { signal: result.signals.get(match.id), complete: result.complete };
5934
6835
  }
5935
6836
  function out(line2 = "") {
5936
6837
  process.stdout.write(line2 + "\n");
@@ -5989,14 +6890,15 @@ async function cmdToday(date, ctx) {
5989
6890
  const targetDate = date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz);
5990
6891
  const { matches, degraded, source } = await getMatchesForDate(adapter, targetDate);
5991
6892
  const todays = fixturesByDate(targetDate, matches, cfg.tz);
5992
- const signals = await reliableMarketSignals(ctx, todays);
6893
+ const market = await reliableMarketSignals(ctx, todays);
5993
6894
  if (cfg.json) {
5994
6895
  emitJson({
5995
6896
  date: targetDate,
5996
6897
  degraded,
5997
6898
  source: source ?? null,
5998
6899
  matches: todays,
5999
- marketSignals: Object.fromEntries(signals)
6900
+ marketComplete: market.complete,
6901
+ marketSignals: Object.fromEntries(market.signals)
6000
6902
  });
6001
6903
  return;
6002
6904
  }
@@ -6011,10 +6913,13 @@ async function cmdToday(date, ctx) {
6011
6913
  } else {
6012
6914
  for (const m of todays) {
6013
6915
  out(matchLine(m, cfg, t2, c, flags));
6014
- const s = signals.get(m.id);
6916
+ const s = market.signals.get(m.id);
6015
6917
  if (s) out(" " + c.dim(marketLine(s, m)));
6016
6918
  }
6017
6919
  }
6920
+ if (!market.complete) {
6921
+ out(c.dim(" Market data unavailable or incomplete \u2014 not all fixtures were checked."));
6922
+ }
6018
6923
  out();
6019
6924
  if (degraded) out(c.dim(" " + t2("feed.degraded")));
6020
6925
  const src = dataSource(source, cfg.lang, c);
@@ -6099,9 +7004,9 @@ function cmdTeam(query, ctx) {
6099
7004
  const c = painterFor(cfg);
6100
7005
  const flags = flagsEnabled();
6101
7006
  const label = (tm) => {
6102
- const flag = flags ? `${tm.flag} ` : "";
7007
+ const flag2 = flags ? `${tm.flag} ` : "";
6103
7008
  const grp = tm.group ? ` \xB7 ${t2("team.group", { group: tm.group })}` : "";
6104
- return ` ${flag}${c.bold(tm.name)} ${c.dim(tm.code + grp)}`;
7009
+ return ` ${flag2}${c.bold(tm.name)} ${c.dim(tm.code + grp)}`;
6105
7010
  };
6106
7011
  out();
6107
7012
  if (!q) {
@@ -6137,11 +7042,12 @@ async function cmdTable(group, ctx) {
6137
7042
  out();
6138
7043
  out(
6139
7044
  c.dim(
6140
- " " + (group ? t2("table.none", { group: group.toUpperCase() }) : t2("table.empty"))
7045
+ " " + (degraded ? t2("table.unavailable") : group ? t2("table.none", { group: group.toUpperCase() }) : t2("table.empty"))
6141
7046
  )
6142
7047
  );
6143
7048
  out();
6144
- if (degraded) out(c.dim(" " + t2("table.degraded")));
7049
+ const src2 = dataSource(source, cfg.lang, c);
7050
+ if (src2) out(src2);
6145
7051
  out(disclaimer(t2, c));
6146
7052
  return;
6147
7053
  }
@@ -6264,7 +7170,13 @@ function cmdHook({ cfg }) {
6264
7170
  try {
6265
7171
  const team = resolveEnvTeam(process.env.CLAUDINHO_TEAM);
6266
7172
  const state = readCurrentState(cfg.source, resolveCompetition());
6267
- const ctx = renderHook(state, { team, flags: flagsEnabled() });
7173
+ const ctx = renderHook(state, {
7174
+ team,
7175
+ flags: flagsEnabled(),
7176
+ // The bundled roster names World Cup nations only; on another competition
7177
+ // a club sharing a nation's code must not be renamed to that nation.
7178
+ defaultCompetition: resolveCompetition() === DEFAULT_COMPETITION
7179
+ });
6268
7180
  if (ctx) out(ctx);
6269
7181
  if (!state && !isLockFresh() || shouldRefresh(Date.now(), state) || shouldRefreshFixtures(Date.now(), state)) {
6270
7182
  spawnRefresh(cfg.source);
@@ -6352,13 +7264,14 @@ async function cmdMatch(id, ctx) {
6352
7264
  const { cfg, t: t2 } = ctx;
6353
7265
  precheck(cfg, t2);
6354
7266
  const { match, degraded, source: liveSource } = await getMatchById(adapterFor(ctx), id);
6355
- const marketSignal = match ? await reliableMarketSignalFor(ctx, match) : void 0;
7267
+ const market = match ? await reliableMarketSignalFor(ctx, match) : { signal: void 0, complete: true };
6356
7268
  if (cfg.json) {
6357
7269
  emitJson({
6358
7270
  degraded,
6359
7271
  match: match ?? null,
6360
7272
  source: liveSource ?? null,
6361
- marketSignal: marketSignal ?? null
7273
+ marketComplete: market.complete,
7274
+ marketSignal: market.signal ?? null
6362
7275
  });
6363
7276
  return;
6364
7277
  }
@@ -6386,9 +7299,13 @@ async function cmdMatch(id, ctx) {
6386
7299
  out(` ${e.minute}' ${e.type} ${e.teamCode}${e.player ? ` \u2014 ${e.player}` : ""}`);
6387
7300
  }
6388
7301
  }
6389
- if (marketSignal) {
7302
+ if (market.signal) {
6390
7303
  out();
6391
- for (const mline of marketBlock(marketSignal, match)) out(" " + c.dim(mline));
7304
+ for (const mline of marketBlock(market.signal, match)) out(" " + c.dim(mline));
7305
+ }
7306
+ if (!market.complete) {
7307
+ out();
7308
+ out(c.dim(" Market data unavailable or incomplete \u2014 this match could not be checked."));
6392
7309
  }
6393
7310
  out();
6394
7311
  if (degraded) out(c.dim(" " + t2("feed.degraded")));
@@ -6398,6 +7315,7 @@ async function cmdMatch(id, ctx) {
6398
7315
  maybeStarNudge(ctx);
6399
7316
  }
6400
7317
  var MARKET_INFO = "Prediction-market data is informational only.";
7318
+ var MARKETS_SCOPE_NOTE = "Market signals cover the World Cup only; none are read for this competition.";
6401
7319
  function marketDisplayable(match, sig) {
6402
7320
  return marketSignalRendersFor(match, sig) && !sig.ambiguous && sig.favorite != null && hasSaneDistribution(sig.outcomes);
6403
7321
  }
@@ -6406,6 +7324,7 @@ function marketHeaderLine(m, cfg) {
6406
7324
  return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag} \xB7 ${when}`;
6407
7325
  }
6408
7326
  function noSignalLine(m, now) {
7327
+ if (!marketsCoverCompetition()) return MARKETS_SCOPE_NOTE;
6409
7328
  if (marketRelevant(m, now)) return "No market signal for this match.";
6410
7329
  return isFinished(m.status) ? "Match has finished \u2014 market signals are pre-match and in-play reads." : "Match appears to have finished \u2014 market signals are pre-match and in-play reads.";
6411
7330
  }
@@ -6419,14 +7338,16 @@ async function cmdMarkets(target, team, ctx) {
6419
7338
  const code = resolveTeamArg(team, "Usage: claudinho markets next <team> (or set CLAUDINHO_TEAM)", t2);
6420
7339
  const now2 = ctx.now ?? /* @__PURE__ */ new Date();
6421
7340
  const { match: fixture, degraded } = await marketFixtureForTeam(adapterFor(ctx), code, now2);
6422
- const sig = fixture && marketRelevant(fixture, now2) ? (await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS)).get(fixture.id) : void 0;
6423
- const shown = fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
7341
+ const market = fixture && marketRelevant(fixture, now2) ? await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS) : { signals: /* @__PURE__ */ new Map(), complete: true };
7342
+ const sig = fixture ? market.signals.get(fixture.id) : void 0;
7343
+ const shown = market.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
6424
7344
  if (cfg.json) {
6425
7345
  emitJson({
6426
7346
  team: code,
6427
7347
  matchId: fixture?.id ?? null,
6428
7348
  degraded,
6429
7349
  informationalOnly: true,
7350
+ complete: market.complete,
6430
7351
  signal: shown ?? null
6431
7352
  });
6432
7353
  return;
@@ -6439,7 +7360,9 @@ async function cmdMarkets(target, team, ctx) {
6439
7360
  out(header(marketHeaderLine(fixture, cfg), c2));
6440
7361
  out();
6441
7362
  if (shown) printMarketBlock(fixture, shown, c2);
6442
- else out(c2.dim(" " + noSignalLine(fixture, now2)));
7363
+ else if (!market.complete) {
7364
+ out(c2.dim(" Market data unavailable or incomplete \u2014 this match could not be checked."));
7365
+ } else out(c2.dim(" " + noSignalLine(fixture, now2)));
6443
7366
  }
6444
7367
  out();
6445
7368
  out(disclaimer(t2, c2));
@@ -6450,10 +7373,16 @@ async function cmdMarkets(target, team, ctx) {
6450
7373
  precheck(cfg, t2);
6451
7374
  const now2 = ctx.now ?? /* @__PURE__ */ new Date();
6452
7375
  const { match } = await getMatchById(adapterFor(ctx), target);
6453
- const sig = match && marketRelevant(match, now2) ? (await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS)).get(match.id) : void 0;
6454
- const shown = match && sig && marketDisplayable(match, sig) ? sig : void 0;
7376
+ const market = match && marketRelevant(match, now2) ? await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS) : { signals: /* @__PURE__ */ new Map(), complete: true };
7377
+ const sig = match ? market.signals.get(match.id) : void 0;
7378
+ const shown = market.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
6455
7379
  if (cfg.json) {
6456
- emitJson({ matchId: target, informationalOnly: true, signal: shown ?? null });
7380
+ emitJson({
7381
+ matchId: target,
7382
+ informationalOnly: true,
7383
+ complete: market.complete,
7384
+ signal: shown ?? null
7385
+ });
6457
7386
  return;
6458
7387
  }
6459
7388
  const c2 = painterFor(cfg);
@@ -6464,7 +7393,9 @@ async function cmdMarkets(target, team, ctx) {
6464
7393
  out(header(marketHeaderLine(match, cfg), c2));
6465
7394
  out();
6466
7395
  if (shown) printMarketBlock(match, shown, c2);
6467
- else out(c2.dim(" " + noSignalLine(match, now2)));
7396
+ else if (!market.complete) {
7397
+ out(c2.dim(" Market data unavailable or incomplete \u2014 this match could not be checked."));
7398
+ } else out(c2.dim(" " + noSignalLine(match, now2)));
6468
7399
  }
6469
7400
  out();
6470
7401
  out(disclaimer(t2, c2));
@@ -6478,14 +7409,14 @@ async function cmdMarkets(target, team, ctx) {
6478
7409
  const { matches } = await getMatchesForDate(adapterFor(ctx), date);
6479
7410
  const todays = fixturesByDate(date, matches, cfg.tz);
6480
7411
  const relevant = todays.filter((m) => marketRelevant(m, now));
6481
- const signals = await marketSignalsFor(ctx, relevant, MARKETS_CMD_OPTS);
7412
+ const { signals, complete } = await marketSignalsFor(ctx, relevant, MARKETS_CMD_OPTS);
6482
7413
  const rows = relevant.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
6483
7414
  (r) => !!r.signal && marketDisplayable(r.match, r.signal)
6484
7415
  );
6485
7416
  if (cfg.json) {
6486
7417
  const marketSignals = {};
6487
7418
  for (const r of rows) marketSignals[r.match.id] = r.signal;
6488
- emitJson({ date, informationalOnly: true, marketSignals });
7419
+ emitJson({ date, informationalOnly: true, complete, marketSignals });
6489
7420
  return;
6490
7421
  }
6491
7422
  const c = painterFor(cfg);
@@ -6493,13 +7424,21 @@ async function cmdMarkets(target, team, ctx) {
6493
7424
  out(header(`Market signals \xB7 ${date}`, c));
6494
7425
  out();
6495
7426
  if (rows.length === 0) {
6496
- out(c.dim(` No market signals available for ${date}.`));
7427
+ out(
7428
+ c.dim(
7429
+ !marketsCoverCompetition() ? ` ${MARKETS_SCOPE_NOTE}` : complete ? ` No market signals available for ${date}.` : ` Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`
7430
+ )
7431
+ );
6497
7432
  } else {
6498
7433
  for (const { match, signal } of rows) {
6499
7434
  out(" " + c.bold(marketHeaderLine(match, cfg)));
6500
7435
  printMarketBlock(match, signal, c);
6501
7436
  out();
6502
7437
  }
7438
+ if (!complete) {
7439
+ out(c.dim(` Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`));
7440
+ out();
7441
+ }
6503
7442
  }
6504
7443
  out(disclaimer(t2, c));
6505
7444
  out(c.dim(MARKET_INFO));
@@ -6510,11 +7449,11 @@ function pickShareStyle(v) {
6510
7449
  async function reliableShareSignals(ctx, matches) {
6511
7450
  const raw = await reliableMarketSignals(ctx, matches);
6512
7451
  const out2 = /* @__PURE__ */ new Map();
6513
- for (const [id, s] of raw) {
7452
+ for (const [id, s] of raw.signals) {
6514
7453
  const m = matches.find((x) => x.id === id);
6515
7454
  if (m && marketDisplayable(m, s)) out2.set(id, s);
6516
7455
  }
6517
- return out2;
7456
+ return { signals: out2, complete: raw.complete };
6518
7457
  }
6519
7458
  function emitShare(ctx, e, copy) {
6520
7459
  const snippet = formatShareSnippet(e.input, e.options);
@@ -6529,6 +7468,7 @@ function emitShare(ctx, e, copy) {
6529
7468
  style: e.options.style ?? "social",
6530
7469
  snippet,
6531
7470
  matches: e.input.matches,
7471
+ marketComplete: e.input.marketComplete ?? true,
6532
7472
  marketSignals: Object.fromEntries(e.input.marketSignals ?? /* @__PURE__ */ new Map())
6533
7473
  });
6534
7474
  } else {
@@ -6648,11 +7588,12 @@ async function cmdShare(target, team, opts, ctx) {
6648
7588
  {
6649
7589
  group,
6650
7590
  tables,
6651
- // Degraded ⇒ a static roster, served by no live provider: no attribution.
7591
+ // Degraded ⇒ no live provider: no attribution. Open-scope outages
7592
+ // have no compatible bundled roster, so name the outage in the empty card.
6652
7593
  source: degraded2 ? void 0 : source2,
6653
7594
  degraded: degraded2,
6654
7595
  installLine: group ? `npx @claudinho/cli table ${group}` : "npx @claudinho/cli table",
6655
- emptyNote: group ? `No group ${group}.` : "No standings available.",
7596
+ emptyNote: degraded2 ? "Live standings unavailable." : group ? `No group ${group}.` : "No standings available.",
6656
7597
  options: baseOptions
6657
7598
  },
6658
7599
  copy
@@ -6699,7 +7640,7 @@ async function cmdShare(target, team, opts, ctx) {
6699
7640
  ctx.now ?? /* @__PURE__ */ new Date()
6700
7641
  );
6701
7642
  const matches = fixture ? [fixture] : [];
6702
- const signals2 = await reliableShareSignals(ctx, matches);
7643
+ const market2 = await reliableShareSignals(ctx, matches);
6703
7644
  const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
6704
7645
  emitShare(
6705
7646
  ctx,
@@ -6710,7 +7651,8 @@ async function cmdShare(target, team, opts, ctx) {
6710
7651
  input: {
6711
7652
  title: `Next up for ${teamName}`,
6712
7653
  matches,
6713
- marketSignals: signals2,
7654
+ marketSignals: market2.signals,
7655
+ marketComplete: market2.complete,
6714
7656
  // Attribute the provider when the overlay resolved the tie (knockout);
6715
7657
  // undefined for a static group fixture — parity with CLI `next`.
6716
7658
  source: source2,
@@ -6731,7 +7673,7 @@ async function cmdShare(target, team, opts, ctx) {
6731
7673
  precheck(cfg, t2);
6732
7674
  const { match, degraded: degraded2, source: source2 } = await getMatchById(adapterFor(ctx), target);
6733
7675
  const matches = match ? [match] : [];
6734
- const signals2 = await reliableShareSignals(ctx, matches);
7676
+ const market2 = await reliableShareSignals(ctx, matches);
6735
7677
  emitShare(
6736
7678
  ctx,
6737
7679
  {
@@ -6740,7 +7682,8 @@ async function cmdShare(target, team, opts, ctx) {
6740
7682
  input: {
6741
7683
  title: "Match pulse",
6742
7684
  matches,
6743
- marketSignals: signals2,
7685
+ marketSignals: market2.signals,
7686
+ marketComplete: market2.complete,
6744
7687
  source: source2,
6745
7688
  degraded: degraded2,
6746
7689
  emptyNote: `No match found with id ${target}.`,
@@ -6759,7 +7702,7 @@ async function cmdShare(target, team, opts, ctx) {
6759
7702
  const date = explicitDate ?? localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz);
6760
7703
  const { matches: all, degraded, source } = await getMatchesForDate(adapterFor(ctx), date);
6761
7704
  const todays = fixturesByDate(date, all, cfg.tz);
6762
- const signals = await reliableShareSignals(ctx, todays);
7705
+ const market = await reliableShareSignals(ctx, todays);
6763
7706
  const human = formatDate(`${date}T12:00:00.000Z`, { tz: cfg.tz, locale: cfg.lang });
6764
7707
  const title = explicitDate ? `Matches \xB7 ${human}` : `Today's matches \xB7 ${human}`;
6765
7708
  emitShare(
@@ -6770,7 +7713,8 @@ async function cmdShare(target, team, opts, ctx) {
6770
7713
  input: {
6771
7714
  title,
6772
7715
  matches: todays,
6773
- marketSignals: signals,
7716
+ marketSignals: market.signals,
7717
+ marketComplete: market.complete,
6774
7718
  source,
6775
7719
  degraded,
6776
7720
  emptyNote: `No matches scheduled for ${human}.`,
@@ -6873,7 +7817,7 @@ function cmdVibe(ctx) {
6873
7817
  try {
6874
7818
  const state = readCurrentState(cfg.source, resolveCompetition());
6875
7819
  liveSeg = vibeLiveSegment(
6876
- liveMatchesFromCache(state, (ctx.now ?? /* @__PURE__ */ new Date()).getTime()),
7820
+ liveMatchesFromCache(state, (ctx.now ?? /* @__PURE__ */ new Date()).getTime()).items,
6877
7821
  // Name-or-code, matching the statusline/hook (offline lookup).
6878
7822
  resolveEnvTeam(process.env.CLAUDINHO_TEAM)
6879
7823
  );
@@ -6899,7 +7843,7 @@ function handlePipeError(stream) {
6899
7843
  }
6900
7844
  handlePipeError(process.stdout);
6901
7845
  handlePipeError(process.stderr);
6902
- var VERSION = "0.9.3";
7846
+ var VERSION = "0.10.0";
6903
7847
  var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
6904
7848
  function ctxFrom(cmd) {
6905
7849
  let root = cmd;