@claudinho/cli 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +23 -3
  2. package/dist/index.js +1503 -527
  3. package/package.json +2 -2
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);
@@ -2859,6 +2859,14 @@ var KNOCKOUT_EXTRA_TIME_MS = 60 * 6e4;
2859
2859
  function liveWindowMsFor(m) {
2860
2860
  return m.stage === "GROUP" || m.stage === "FRIENDLY" ? LIVE_WINDOW_MS : LIVE_WINDOW_MS + KNOCKOUT_EXTRA_TIME_MS;
2861
2861
  }
2862
+ function isTournamentWindowOver(now = Date.now(), fixtures = SCHEDULE) {
2863
+ if (fixtures.length === 0) return false;
2864
+ return fixtures.every((m) => {
2865
+ const k = Date.parse(m.kickoff);
2866
+ if (Number.isNaN(k)) return false;
2867
+ return now > k + liveWindowMsFor(m);
2868
+ });
2869
+ }
2862
2870
  function fixturesInLiveWindow(now = Date.now(), fixtures = SCHEDULE) {
2863
2871
  return fixtures.filter((m) => {
2864
2872
  const k = Date.parse(m.kickoff);
@@ -2945,47 +2953,285 @@ function rosterAtZero(matches) {
2945
2953
  }
2946
2954
  return [...teams.values()].sort((a, b) => a.name.localeCompare(b.name)).map(blankRow);
2947
2955
  }
2948
- var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
2949
- var DEFAULT_COMPETITION = "fifa.world";
2950
- var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
2951
- var USER_AGENT = `claudinho/${"0.9.2"} (+https://github.com/arturogarrido/claudinho)`;
2952
- var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
2953
- function competitionBase(slug) {
2954
- 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;
2955
2959
  }
2956
- var DEFAULT_TIMEOUT_MS = 6e3;
2957
- var STANDINGS_SHARE_MS = 3e4;
2958
- var ProviderError = class extends Error {
2959
- kind;
2960
- status;
2961
- constructor(message, kind, status) {
2962
- super(message);
2963
- this.name = "ProviderError";
2964
- this.kind = kind;
2965
- 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 "";
2966
3005
  }
2967
- /** 429/403 the upstream is refusing us; retrying at the live cadence makes it worse. */
2968
- get throttled() {
2969
- 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;
2970
3042
  }
2971
- };
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
+ }
2972
3215
  function mapStatus(st) {
2973
- const name = (st?.type?.name ?? "").toUpperCase();
2974
- 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 : "";
2975
3219
  if (name.includes("HALFTIME")) return "HT";
2976
3220
  if (name.includes("POSTPONED")) return "POSTPONED";
2977
3221
  if (name.includes("CANCEL")) return "CANCELLED";
2978
3222
  if (state === "pre") return "SCHEDULED";
2979
3223
  if (state === "post") return "FT";
2980
3224
  if (state === "in") return "LIVE";
2981
- return "SCHEDULED";
3225
+ return void 0;
2982
3226
  }
2983
3227
  function parseMinute(st) {
2984
- if (st?.type?.state !== "in") return void 0;
2985
- const dc = st.displayClock?.match(/(\d+)/);
2986
- if (dc) return parseInt(dc[1], 10);
2987
- if (typeof st.clock === "number" && st.clock > 0) {
2988
- 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;
2989
3235
  }
2990
3236
  return void 0;
2991
3237
  }
@@ -2999,58 +3245,74 @@ var SLUG_TO_STAGE = {
2999
3245
  final: "F"
3000
3246
  };
3001
3247
  function stageFromSlug(slug) {
3002
- if (slug && SLUG_TO_STAGE[slug]) return SLUG_TO_STAGE[slug];
3003
- 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
+ }
3004
3253
  return "FRIENDLY";
3005
3254
  }
3006
- function toInt(s) {
3007
- if (s == null || s === "") return void 0;
3008
- const n = parseInt(String(s), 10);
3009
- return Number.isFinite(n) ? n : void 0;
3010
- }
3011
- function toTeam(t2) {
3012
- const name = sanitizeFeedText(
3013
- t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD"
3014
- );
3015
- const code = sanitizeFeedText(t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
3016
- return {
3017
- code,
3018
- name,
3019
- flag: nationToFlag(sanitizeFeedText(t2?.displayName ?? t2?.abbreviation ?? name))
3020
- };
3021
- }
3022
- function mapEspnEvent(ev, ctx = {}) {
3023
- const comp = ev.competitions?.[0];
3024
- const competitors = comp?.competitors ?? [];
3025
- const homeC = competitors.find((c) => c.homeAway === "home") ?? competitors[0];
3026
- 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;
3027
3286
  const status = mapStatus(ev.status ?? comp?.status);
3028
- const stage = stageFromSlug(ev.season?.slug);
3029
- const home = toTeam(homeC?.team);
3030
- 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";
3031
3289
  let group;
3032
3290
  if (stage === "GROUP" && ctx.groupByTeam) {
3033
3291
  group = ctx.groupByTeam[home.code] ?? ctx.groupByTeam[away.code];
3034
3292
  }
3035
- const hs = toInt(homeC?.score);
3036
- const as = toInt(awayC?.score);
3037
- 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;
3038
3302
  let winnerCode;
3039
3303
  if (isFinished(status)) {
3040
- if (homeC?.winner) winnerCode = home.code;
3041
- 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;
3042
3306
  }
3043
- const hShoot = toInt(homeC?.shootoutScore);
3044
- const aShoot = toInt(awayC?.shootoutScore);
3045
- const shootout = hasScore && hShoot !== void 0 && aShoot !== void 0 ? { home: hShoot, away: aShoot } : void 0;
3046
- return {
3047
- id: ev.id,
3307
+ const venue = comp?.venue;
3308
+ return sealMatch({
3309
+ id,
3048
3310
  stage,
3049
3311
  group,
3050
- kickoff: ev.date,
3051
- venue: sanitizeFeedText(comp?.venue?.fullName ?? ""),
3052
- city: sanitizeFeedText(comp?.venue?.address?.city ?? "") || void 0,
3053
- country: sanitizeFeedText(comp?.venue?.address?.country ?? "") || void 0,
3312
+ kickoff,
3313
+ venue: venue?.fullName,
3314
+ city: venue?.address?.city,
3315
+ country: venue?.address?.country,
3054
3316
  home,
3055
3317
  away,
3056
3318
  score: hasScore ? { home: hs, away: as } : void 0,
@@ -3058,57 +3320,221 @@ function mapEspnEvent(ev, ctx = {}) {
3058
3320
  minute: parseMinute(ev.status ?? comp?.status),
3059
3321
  status,
3060
3322
  winnerCode,
3061
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3062
- };
3323
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3324
+ events: ev.events
3325
+ });
3063
3326
  }
3064
- function toEspnDate(d) {
3065
- 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
+ };
3066
3358
  }
3067
- function statVal(stats, name) {
3068
- const v = stats?.find((s) => s.name === name)?.value;
3069
- 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;
3070
3379
  }
3071
3380
  function entryToRow(e) {
3072
- return {
3073
- team: toTeam(e.team),
3074
- played: statVal(e.stats, "gamesPlayed"),
3075
- won: statVal(e.stats, "wins"),
3076
- drawn: statVal(e.stats, "ties"),
3077
- lost: statVal(e.stats, "losses"),
3078
- goalsFor: statVal(e.stats, "pointsFor"),
3079
- goalsAgainst: statVal(e.stats, "pointsAgainst"),
3080
- goalDiff: statVal(e.stats, "pointDifferential"),
3081
- points: statVal(e.stats, "points")
3082
- };
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
+ });
3083
3420
  }
3084
- 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;
3085
3429
  const out2 = [];
3086
- for (const child of data.children ?? []) {
3087
- 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();
3088
3435
  if (!letter) continue;
3089
- const ranked = (child.standings?.entries ?? []).map((e) => ({
3090
- row: entryToRow(e),
3091
- rank: statVal(e.stats, "rank")
3092
- }));
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 seenTeams = /* @__PURE__ */ new Set();
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 key = r.value.providerId ?? r.value.team.code;
3461
+ if (seenTeams.has(key) || seenRanks.has(r.value.providerRank) || r.value.providerId !== void 0 && seenProviderIds.has(r.value.providerId)) {
3462
+ complete = false;
3463
+ continue;
3464
+ }
3465
+ seenTeams.add(key);
3466
+ seenRanks.add(r.value.providerRank);
3467
+ if (r.value.providerId !== void 0) seenProviderIds.add(r.value.providerId);
3468
+ const { providerId: _dropId, providerRank: rank, ...row } = r.value;
3469
+ ranked.push({ row, rank });
3470
+ }
3093
3471
  ranked.sort((a, b) => {
3094
3472
  if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
3095
- const r = a.row;
3096
- const s = b.row;
3097
- return s.points - r.points || s.goalDiff - r.goalDiff || s.goalsFor - r.goalsFor || r.team.code.localeCompare(s.team.code);
3473
+ if (b.row.points !== a.row.points) return b.row.points - a.row.points;
3474
+ if (b.row.goalDiff !== a.row.goalDiff) return b.row.goalDiff - a.row.goalDiff;
3475
+ return b.row.goalsFor - a.row.goalsFor;
3098
3476
  });
3477
+ if (ranked.length === 0) {
3478
+ complete = false;
3479
+ continue;
3480
+ }
3099
3481
  out2.push({ group: letter, rows: ranked.map((x) => x.row) });
3100
3482
  }
3101
- out2.sort((a, b) => a.group.localeCompare(b.group));
3102
- return out2;
3483
+ return {
3484
+ items: out2,
3485
+ total: seenGroups.size,
3486
+ shown: out2.length,
3487
+ // We stopped early if the child list or any single group's rows were cut.
3488
+ truncated: !sawAllChildren || rowsTruncated,
3489
+ complete: complete && !rowsTruncated
3490
+ };
3491
+ }
3492
+ var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3493
+ var DEFAULT_COMPETITION = "fifa.world";
3494
+ var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3495
+ var USER_AGENT = `claudinho/${"0.9.4"} (+https://github.com/arturogarrido/claudinho)`;
3496
+ var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
3497
+ function competitionBase(slug) {
3498
+ return `${ESPN_SOCCER}/${slug}`;
3499
+ }
3500
+ var DEFAULT_TIMEOUT_MS = 6e3;
3501
+ var STANDINGS_SHARE_MS = 3e4;
3502
+ var ProviderError = class extends Error {
3503
+ kind;
3504
+ status;
3505
+ constructor(message, kind, status) {
3506
+ super(message);
3507
+ this.name = "ProviderError";
3508
+ this.kind = kind;
3509
+ this.status = status;
3510
+ }
3511
+ /** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
3512
+ get throttled() {
3513
+ return this.kind === "http" && (this.status === 429 || this.status === 403);
3514
+ }
3515
+ };
3516
+ function toEspnDate(d) {
3517
+ return d.replace(/\D/g, "").slice(0, 8);
3518
+ }
3519
+ function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length > 0) {
3520
+ if (!hasUsableRecord && (!parsed.complete || parsed.total > 0)) {
3521
+ throw new ProviderError(`ESPN ${kind} payload had no readable records`, "parse");
3522
+ }
3523
+ return [...parsed.items];
3103
3524
  }
3104
3525
  var EspnAdapter = class {
3105
3526
  constructor(opts = {}) {
3106
3527
  this.opts = opts;
3528
+ const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
3529
+ this.expectedStandingsGroups = expected ? [...expected] : void 0;
3530
+ this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
3107
3531
  }
3108
3532
  opts;
3109
3533
  name = "espn";
3110
3534
  capabilities = { push: false, latencyHintSec: 45 };
3111
- /** Cached team-code -> group-letter map (built lazily from standings). */
3535
+ expectedStandingsGroups;
3536
+ standingsFallbackGroups;
3537
+ /** Short-lived team-code -> group-letter map (built lazily from standings). */
3112
3538
  groupMap;
3113
3539
  /**
3114
3540
  * One in-flight/recent standings fetch shared by fetchStandings and
@@ -3151,38 +3577,48 @@ var EspnAdapter = class {
3151
3577
  if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
3152
3578
  return this.standingsShared.promise;
3153
3579
  }
3154
- const promise = this.get(this.standingsUrl()).then(
3155
- (d) => parseStandings(d)
3156
- );
3580
+ const promise = this.get(this.standingsUrl()).then((d) => {
3581
+ const parsed = parseEspnStandings(d);
3582
+ return usableProviderItems(
3583
+ "standings",
3584
+ parsed,
3585
+ parsed.items.some((table) => table.rows.length > 0)
3586
+ );
3587
+ });
3157
3588
  this.standingsShared = { at: now, promise };
3158
- promise.catch(() => {
3589
+ void promise.catch(() => {
3159
3590
  if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
3160
3591
  });
3161
3592
  return promise;
3162
3593
  }
3163
3594
  /**
3164
3595
  * Authoritative, cumulative group tables from the standings endpoint. Throws
3165
- * on fetch/parse failure (the caller decides the fallback). Group-stage only:
3166
- * non-group `children` are filtered out by {@link parseStandings}.
3596
+ * on fetch failure. Group-stage only: non-group `children` are filtered out
3597
+ * by {@link parseStandings}; malformed rows are omitted without hiding their
3598
+ * readable siblings.
3167
3599
  */
3168
3600
  async fetchStandings() {
3169
3601
  return this.sharedStandings();
3170
3602
  }
3171
3603
  /**
3172
- * Build (and cache) a team-code -> group-letter map from the standings
3604
+ * Build (and briefly cache) a team-code -> group-letter map from the standings
3173
3605
  * endpoint. Best-effort: returns {} if standings are unavailable — but a
3174
- * transient failure is NOT cached (only a successful parse pins the map), so
3175
- * one blip can't silently drop group letters for the adapter's lifetime.
3606
+ * transient failure is NOT cached, and a partial successful parse expires at
3607
+ * the standings TTL, so neither can silently drop group letters for the
3608
+ * adapter's lifetime.
3176
3609
  * Reuses the same parse/fetch as {@link fetchStandings}, so the two never
3177
3610
  * drift and one command never fetches standings twice.
3178
3611
  */
3179
3612
  async fetchGroupMap(force = false) {
3180
- if (this.groupMap && !force) return this.groupMap;
3613
+ const now = Date.now();
3614
+ if (!force && this.groupMap && now - this.groupMap.at < STANDINGS_SHARE_MS) {
3615
+ return this.groupMap.value;
3616
+ }
3181
3617
  try {
3182
3618
  const tables = await this.sharedStandings();
3183
3619
  const map = {};
3184
3620
  for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
3185
- this.groupMap = map;
3621
+ this.groupMap = { at: Date.now(), value: map };
3186
3622
  return map;
3187
3623
  } catch {
3188
3624
  return {};
@@ -3197,7 +3633,8 @@ var EspnAdapter = class {
3197
3633
  this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
3198
3634
  this.get(url.toString())
3199
3635
  ]);
3200
- return (data.events ?? []).map((ev) => mapEspnEvent(ev, { groupByTeam }));
3636
+ const parsed = parseEspnEvents(data, { groupByTeam });
3637
+ return usableProviderItems("scoreboard", parsed);
3201
3638
  }
3202
3639
  async get(url) {
3203
3640
  const doFetch = this.opts.fetchImpl ?? fetch;
@@ -4007,17 +4444,26 @@ async function getMatchesForDate(adapter, dateISO) {
4007
4444
  }
4008
4445
  async function getStandings(adapter, group) {
4009
4446
  const want = group?.toUpperCase();
4447
+ const expected = adapter.expectedStandingsGroups;
4448
+ if (want && expected && !expected.includes(want)) {
4449
+ return { tables: [], degraded: false };
4450
+ }
4010
4451
  if (adapter.fetchStandings) {
4011
4452
  try {
4012
4453
  const all = await adapter.fetchStandings();
4013
4454
  const tables2 = (want ? all.filter((t2) => t2.group === want) : all).sort(
4014
4455
  (a, b) => a.group.localeCompare(b.group)
4015
4456
  );
4016
- return { tables: tables2, degraded: false, source: adapter.name };
4457
+ const availableGroups = new Set(tables2.map((table) => table.group));
4458
+ const expectedGroupWasOmitted = want ? (expected?.includes(want) ?? false) && tables2.length === 0 : expected?.some((group2) => !availableGroups.has(group2)) ?? false;
4459
+ if (!expectedGroupWasOmitted) {
4460
+ return { tables: tables2, degraded: false, source: adapter.name };
4461
+ }
4017
4462
  } catch {
4018
4463
  }
4019
4464
  }
4020
- const letters = want ? [want] : groups();
4465
+ const fallbackGroups = adapter.standingsFallbackGroups;
4466
+ const letters = fallbackGroups ? want ? fallbackGroups.includes(want) ? [want] : [] : [...new Set(fallbackGroups)].sort((a, b) => a.localeCompare(b)) : [];
4021
4467
  const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
4022
4468
  return { tables, degraded: true };
4023
4469
  }
@@ -4074,7 +4520,10 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
4074
4520
  try {
4075
4521
  const win = knockoutWindow();
4076
4522
  if (adapter.fetchWindow && win) {
4077
- fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
4523
+ fixtures = mergeLive(
4524
+ fixtures,
4525
+ await adapter.fetchWindow(win.start, win.end)
4526
+ );
4078
4527
  }
4079
4528
  } catch {
4080
4529
  overlayFailed = true;
@@ -4138,14 +4587,144 @@ async function getMatchById(adapter, id) {
4138
4587
  async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
4139
4588
  try {
4140
4589
  const day = now.toISOString().slice(0, 10);
4141
- const matches = adapter.fetchWindow ? (await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1))).filter(
4142
- (m) => isLive(m.status)
4143
- ) : await adapter.fetchLive();
4590
+ const matches = (adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchLive()).filter((m) => isLive(m.status));
4144
4591
  return { matches, degraded: false, source: adapter.name };
4145
4592
  } catch {
4146
4593
  return { matches: [], degraded: true };
4147
4594
  }
4148
4595
  }
4596
+ function pct(p) {
4597
+ return Math.round(p * 100);
4598
+ }
4599
+ var KNOWN_MARKET_SOURCES = ["polymarket", "fake"];
4600
+ function marketSourceLabel(source) {
4601
+ if (source === "polymarket") return "Polymarket";
4602
+ if (source === "fake") return "demo data";
4603
+ return source.charAt(0).toUpperCase() + source.slice(1);
4604
+ }
4605
+ function outcomeLabel(o, match) {
4606
+ if (o.kind === "home") return match.home.name;
4607
+ if (o.kind === "away") return match.away.name;
4608
+ if (o.kind === "draw") return "Draw";
4609
+ return o.label;
4610
+ }
4611
+ function utcHhmm(iso) {
4612
+ const t2 = Date.parse(iso);
4613
+ if (!Number.isFinite(t2)) return "";
4614
+ return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
4615
+ }
4616
+ function marketFavoriteText(signal, match) {
4617
+ const fav = signal.favorite;
4618
+ if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
4619
+ if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
4620
+ const name = fav.kind === "home" ? match.home.name : match.away.name;
4621
+ return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
4622
+ }
4623
+ function marketProbabilityText(signal, match) {
4624
+ const order = ["home", "draw", "away"];
4625
+ const parts = [];
4626
+ for (const kind of order) {
4627
+ const o = signal.outcomes.find((x) => x.kind === kind);
4628
+ if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4629
+ }
4630
+ for (const o of signal.outcomes) {
4631
+ if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4632
+ }
4633
+ return parts.join(" \xB7 ");
4634
+ }
4635
+ function marketAttributionText(signal) {
4636
+ const time = utcHhmm(signal.asOf);
4637
+ const src = `Source: ${marketSourceLabel(signal.source)}`;
4638
+ return time ? `${src} \xB7 updated ${time}` : src;
4639
+ }
4640
+ function marketLine(signal, match) {
4641
+ return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
4642
+ signal.source
4643
+ )} \xB7 informational only`;
4644
+ }
4645
+ function marketBlock(signal, match) {
4646
+ const lines = [];
4647
+ if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
4648
+ lines.push(marketFavoriteText(signal, match));
4649
+ lines.push(marketProbabilityText(signal, match));
4650
+ lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
4651
+ return lines;
4652
+ }
4653
+ var MAX_OUTCOMES = 128;
4654
+ var MATCH_ID = /^[0-9]{1,20}$/;
4655
+ var MARKET_ID = /^(?:[0-9]{1,32}|fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2})$/;
4656
+ var OUTCOME_KINDS = /* @__PURE__ */ new Set(["home", "draw", "away", "other"]);
4657
+ var TEAM_CODE_COLUMNS2 = 8;
4658
+ function sealOutcome(raw) {
4659
+ if (!raw || typeof raw !== "object") return void 0;
4660
+ const o = raw;
4661
+ const kind = member(o.kind, OUTCOME_KINDS);
4662
+ const p = probability(o.probability);
4663
+ if (!kind || p === void 0) return void 0;
4664
+ const out2 = { kind };
4665
+ if (o.teamCode !== void 0) {
4666
+ if (typeof o.teamCode !== "string") return void 0;
4667
+ out2.teamCode = humanLabel(o.teamCode, TEAM_CODE_COLUMNS2);
4668
+ }
4669
+ out2.label = humanLabel(o.label);
4670
+ out2.probability = p;
4671
+ if ((out2.kind === "home" || out2.kind === "away") && !out2.teamCode) return void 0;
4672
+ return out2;
4673
+ }
4674
+ function hasDuplicateKind(outcomes) {
4675
+ const seen = /* @__PURE__ */ new Set();
4676
+ for (const o of outcomes) {
4677
+ if (o.kind === "other") continue;
4678
+ if (seen.has(o.kind)) return true;
4679
+ seen.add(o.kind);
4680
+ }
4681
+ return false;
4682
+ }
4683
+ function sealMarketSignal(raw, options = {}) {
4684
+ if (!raw || typeof raw !== "object") return malformed("signal is not an object");
4685
+ const s = raw;
4686
+ const matchId = opaqueId(s.matchId, MATCH_ID);
4687
+ if (!matchId) return malformed("signal names no fixture");
4688
+ if (!Array.isArray(s.outcomes) || s.outcomes.length > MAX_OUTCOMES) {
4689
+ return malformed("signal outcomes are absent or exceed the cap");
4690
+ }
4691
+ const outcomes = [];
4692
+ for (const rawOutcome of takeBounded(s.outcomes, MAX_OUTCOMES)) {
4693
+ const outcome = sealOutcome(rawOutcome);
4694
+ if (!outcome) return malformed("signal carries an unreadable outcome");
4695
+ outcomes.push(outcome);
4696
+ }
4697
+ if (hasDuplicateKind(outcomes)) {
4698
+ return ambiguous("two outcomes claim the same result");
4699
+ }
4700
+ const sourceMarketId = opaqueId(s.sourceMarketId, MARKET_ID);
4701
+ const liquidity = quantity(s.liquidity);
4702
+ const volume24h = quantity(s.volume24h);
4703
+ const out2 = {
4704
+ matchId,
4705
+ // Allow-listed, not merely stripped: this lands in the provider-attribution
4706
+ // slot, where `marketSourceLabel` falls through to the raw string for an
4707
+ // unrecognized provider — attacker prose where the reader expects
4708
+ // "Polymarket".
4709
+ source: member(s.source, new Set(KNOWN_MARKET_SOURCES)) ?? ""
4710
+ };
4711
+ if (sourceMarketId) out2.sourceMarketId = sourceMarketId;
4712
+ out2.asOf = canonicalTimestamp(s.asOf) ?? "";
4713
+ out2.fetchedAt = canonicalTimestamp(s.fetchedAt) ?? "";
4714
+ out2.outcomes = outcomes;
4715
+ const isAmbiguous = s.ambiguous !== false;
4716
+ const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
4717
+ if (favorite) out2.favorite = favorite;
4718
+ if (liquidity !== void 0) out2.liquidity = liquidity;
4719
+ if (volume24h !== void 0) out2.volume24h = volume24h;
4720
+ out2.stale = s.stale !== false;
4721
+ out2.ambiguous = isAmbiguous || out2.source === "";
4722
+ out2.stale = out2.stale || isStaleSignal(out2, { now: options.now, maxAgeMs: options.maxAgeMs });
4723
+ return valid(out2);
4724
+ }
4725
+ function parseCachedMarketSignal(raw, options = {}) {
4726
+ return sealMarketSignal(raw, options);
4727
+ }
4149
4728
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
4150
4729
  function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
4151
4730
  if (isLive(match.status)) return true;
@@ -4164,9 +4743,9 @@ function normalizeOutcomes(outcomes) {
4164
4743
  probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
4165
4744
  }));
4166
4745
  }
4167
- function favoriteStrength(probability) {
4168
- if (probability >= 0.65) return "clear";
4169
- if (probability >= 0.52) return "slight";
4746
+ function favoriteStrength(probability2) {
4747
+ if (probability2 >= 0.65) return "clear";
4748
+ if (probability2 >= 0.52) return "slight";
4170
4749
  return "close";
4171
4750
  }
4172
4751
  function deriveFavorite(outcomes) {
@@ -4185,14 +4764,16 @@ function deriveFavorite(outcomes) {
4185
4764
  }
4186
4765
  function mapsCleanly(match, outcomes) {
4187
4766
  if (outcomes.some((o) => o.kind === "other")) return false;
4767
+ const kinds = outcomes.map((o) => o.kind);
4768
+ if (new Set(kinds).size !== kinds.length) return false;
4188
4769
  const home = outcomes.find((o) => o.kind === "home");
4189
4770
  const away = outcomes.find((o) => o.kind === "away");
4190
4771
  const draw = outcomes.find((o) => o.kind === "draw");
4191
4772
  if (!home || !away) return false;
4192
- if (home.teamCode && home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
4773
+ if (!home.teamCode || home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
4193
4774
  return false;
4194
4775
  }
4195
- if (away.teamCode && away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
4776
+ if (!away.teamCode || away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
4196
4777
  return false;
4197
4778
  }
4198
4779
  if (match.stage === "GROUP" && !draw) return false;
@@ -4207,11 +4788,13 @@ function hasSaneDistribution(outcomes) {
4207
4788
  const sum = priced.reduce((s, o) => s + o.probability, 0);
4208
4789
  return sum > 0.97 && sum < 1.03;
4209
4790
  }
4791
+ var FUTURE_SKEW_MS = 6e4;
4210
4792
  function isStaleSignal(signal, options = {}) {
4211
4793
  const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
4212
4794
  const asOf = Date.parse(signal.asOf);
4213
4795
  if (!Number.isFinite(asOf)) return true;
4214
4796
  const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
4797
+ if (asOf - now > FUTURE_SKEW_MS) return true;
4215
4798
  return now - asOf > maxAge;
4216
4799
  }
4217
4800
  function isReliableMarketSignal(signal, options = {}) {
@@ -4227,8 +4810,8 @@ function isReliableMarketSignal(signal, options = {}) {
4227
4810
  }
4228
4811
  function buildMarketSignal(input) {
4229
4812
  const outcomes = normalizeOutcomes(input.outcomes);
4230
- const ambiguous = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
4231
- const favorite = ambiguous ? void 0 : deriveFavorite(outcomes);
4813
+ const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
4814
+ const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
4232
4815
  const signal = {
4233
4816
  matchId: input.match.id,
4234
4817
  source: input.source,
@@ -4240,88 +4823,53 @@ function buildMarketSignal(input) {
4240
4823
  liquidity: input.liquidity,
4241
4824
  volume24h: input.volume24h,
4242
4825
  stale: false,
4243
- ambiguous
4826
+ ambiguous: ambiguous2
4244
4827
  };
4245
4828
  signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
4246
- return signal;
4247
- }
4248
- function pct(p) {
4249
- return Math.round(p * 100);
4829
+ const sealed = sealMarketSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
4830
+ if (sealed.kind !== "valid") {
4831
+ return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
4832
+ }
4833
+ return { ...sealed.value, ambiguous: sealed.value.ambiguous || ambiguous2 };
4250
4834
  }
4251
- function marketSourceLabel(source) {
4252
- if (source === "polymarket") return "Polymarket";
4253
- if (source === "fake") return "demo data";
4254
- return source.charAt(0).toUpperCase() + source.slice(1);
4835
+ var NONE = { kind: "none" };
4836
+ function selectOne(candidates) {
4837
+ if (candidates.length === 1) return { kind: "one", value: candidates[0] };
4838
+ if (candidates.length === 0) return NONE;
4839
+ return { kind: "ambiguous", count: candidates.length };
4255
4840
  }
4256
- function outcomeLabel(o, match) {
4257
- if (o.kind === "home") return match.home.name;
4258
- if (o.kind === "away") return match.away.name;
4259
- if (o.kind === "draw") return "Draw";
4260
- return o.label;
4841
+ function resolvedValues(batch) {
4842
+ const out2 = /* @__PURE__ */ new Map();
4843
+ for (const [key, r] of batch.results) if (r.kind === "valid") out2.set(key, r.value);
4844
+ return out2;
4261
4845
  }
4262
- function utcHhmm(iso) {
4263
- const t2 = Date.parse(iso);
4264
- if (!Number.isFinite(t2)) return "";
4265
- return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
4846
+ function cacheableKeys(batch) {
4847
+ const out2 = /* @__PURE__ */ new Set();
4848
+ for (const [key, r] of batch.results) if (isCacheable(r)) out2.add(key);
4849
+ return out2;
4266
4850
  }
4267
- function marketFavoriteText(signal, match) {
4268
- const fav = signal.favorite;
4269
- if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
4270
- if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
4271
- const name = fav.kind === "home" ? match.home.name : match.away.name;
4272
- return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
4851
+ function emptyBatch() {
4852
+ return { results: /* @__PURE__ */ new Map(), complete: false };
4273
4853
  }
4274
- function marketProbabilityText(signal, match) {
4275
- const order = ["home", "draw", "away"];
4276
- const parts = [];
4277
- for (const kind of order) {
4278
- const o = signal.outcomes.find((x) => x.kind === kind);
4279
- if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4854
+ var FakeMarketProvider = class {
4855
+ constructor(opts = {}) {
4856
+ this.opts = opts;
4280
4857
  }
4281
- for (const o of signal.outcomes) {
4282
- if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4283
- }
4284
- return parts.join(" \xB7 ");
4285
- }
4286
- function marketAttributionText(signal) {
4287
- const time = utcHhmm(signal.asOf);
4288
- const src = `Source: ${marketSourceLabel(signal.source)}`;
4289
- return time ? `${src} \xB7 updated ${time}` : src;
4290
- }
4291
- function marketLine(signal, match) {
4292
- return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
4293
- signal.source
4294
- )} \xB7 informational only`;
4295
- }
4296
- function marketBlock(signal, match) {
4297
- const lines = [];
4298
- if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
4299
- lines.push(marketFavoriteText(signal, match));
4300
- lines.push(marketProbabilityText(signal, match));
4301
- lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
4302
- return lines;
4303
- }
4304
- var FakeMarketProvider = class {
4305
- constructor(opts = {}) {
4306
- this.opts = opts;
4307
- }
4308
- opts;
4309
- name = "fake";
4310
- async findSignal(match, options) {
4311
- const preset = this.opts.signals?.[match.id];
4312
- if (preset) return preset;
4313
- if (this.opts.synthesize) return this.synthesize(match, options);
4314
- return void 0;
4858
+ opts;
4859
+ name = "fake";
4860
+ async findSignal(match, options) {
4861
+ const preset = this.opts.signals?.[match.id];
4862
+ if (preset) return preset;
4863
+ if (this.opts.synthesize) return this.synthesize(match, options);
4864
+ return void 0;
4315
4865
  }
4316
4866
  async findSignals(matches, options) {
4317
- const signals = /* @__PURE__ */ new Map();
4318
- const checked = /* @__PURE__ */ new Set();
4867
+ const results = /* @__PURE__ */ new Map();
4319
4868
  for (const m of matches) {
4320
- checked.add(m.id);
4321
4869
  const s = await this.findSignal(m, options);
4322
- if (s) signals.set(m.id, s);
4870
+ results.set(m.id, s ? valid(s) : definitiveNone("fake provider has no signal"));
4323
4871
  }
4324
- return { signals, checked };
4872
+ return { results, complete: true };
4325
4873
  }
4326
4874
  synthesize(match, options) {
4327
4875
  const seed = hash(`${match.home.code}-${match.away.code}`);
@@ -4338,7 +4886,9 @@ var FakeMarketProvider = class {
4338
4886
  return buildMarketSignal({
4339
4887
  match,
4340
4888
  source: "fake",
4341
- sourceMarketId: `fake-${match.id}`,
4889
+ // Must satisfy the boundary's opaque-id grammar, like a real one:
4890
+ // a source id that only the live path accepts is the asymmetry itself.
4891
+ sourceMarketId: match.id,
4342
4892
  asOf,
4343
4893
  fetchedAt: now.toISOString(),
4344
4894
  outcomes,
@@ -4362,6 +4912,8 @@ var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
4362
4912
  var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
4363
4913
  var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
4364
4914
  var DEFAULT_TIMEOUT_MS2 = 8e3;
4915
+ var MAX_EVENT_MARKETS = 256;
4916
+ var DEFAULT_DEADLINE_MS = 15e3;
4365
4917
  var WC_SERIES_SLUG = "soccer-fifwc";
4366
4918
  var WC_SPORT = "fifwc";
4367
4919
  var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
@@ -4374,49 +4926,81 @@ var PolymarketProvider = class {
4374
4926
  opts;
4375
4927
  name = "polymarket";
4376
4928
  async findSignal(match, options) {
4377
- const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
4378
- return (await this.resolveOne(match, options, deadline)).signal;
4929
+ const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
4930
+ return parsedValue(await this.resolveOne(match, options, deadline));
4379
4931
  }
4380
4932
  async findSignals(matches, options) {
4381
- const signals = /* @__PURE__ */ new Map();
4382
- const checked = /* @__PURE__ */ new Set();
4383
- const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
4933
+ const results = /* @__PURE__ */ new Map();
4934
+ const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
4935
+ let complete = true;
4384
4936
  for (const m of matches) {
4385
- if (Date.now() >= deadline) break;
4937
+ if (Date.now() >= deadline) {
4938
+ results.set(m.id, unresolved("enrichment deadline expired"));
4939
+ complete = false;
4940
+ continue;
4941
+ }
4386
4942
  const r = await this.resolveOne(m, options, deadline);
4387
- if (r.checked) checked.add(m.id);
4388
- if (r.signal) signals.set(m.id, r.signal);
4943
+ if (r.kind === "unresolved" || r.kind === "malformed") complete = false;
4944
+ results.set(m.id, r);
4389
4945
  }
4390
- return { signals, checked };
4946
+ return { results, complete };
4391
4947
  }
4392
4948
  /**
4393
- * Resolve one match. `checked` distinguishes a DEFINITIVE result (reached the
4394
- * source and found no usable market, or the fixture is unmappable) from a
4395
- * provider/network error so transient failures are retried, not
4396
- * negative-cached.
4949
+ * Resolve one match into a verdict.
4950
+ *
4951
+ * Every exit says which KIND of non-answer it is, because that decides
4952
+ * whether it may be remembered — see `isCacheable`: a conclusion we drew from
4953
+ * a payload we READ is cacheable (including an ambiguity, which is stable),
4954
+ * while a shape we could not read is not. Previously a single
4955
+ * `checked: boolean` collapsed five distinct situations into two, and the
4956
+ * ones that landed on the wrong side of it — an ambiguous payload, a
4957
+ * two-legged market, an incoherent 1X2 — were negative-cached as the fact
4958
+ * that this fixture has no market.
4397
4959
  */
4398
4960
  async resolveOne(match, options, deadline = Number.POSITIVE_INFINITY) {
4399
- const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4400
- const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4401
- if (slugs.length === 0) return { checked: true };
4402
4961
  const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
4403
4962
  try {
4963
+ const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4964
+ const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4965
+ if (slugs.length === 0) return definitiveNone("fixture has no derivable event slug");
4966
+ const RANK = {
4967
+ "definitive-none": 0,
4968
+ ambiguous: 1,
4969
+ unresolved: 2,
4970
+ malformed: 3
4971
+ };
4972
+ let worst;
4973
+ const keepWorst = (r) => {
4974
+ if (r.kind === "definitive-none" || r.kind === "valid") return;
4975
+ if (!worst || (RANK[r.kind] ?? 0) > (RANK[worst.kind] ?? 0)) worst = r;
4976
+ };
4404
4977
  for (const slug of slugs) {
4405
4978
  const remaining = deadline - Date.now();
4406
- if (remaining <= 0) return { checked: false };
4407
- const event = await this.fetchEvent(slug, Math.min(configured, remaining));
4408
- const signal = event ? this.toSignal(match, slug, event, options) : void 0;
4409
- if (signal) return { signal, checked: true };
4979
+ if (remaining <= 0) return unresolved("deadline expired between candidate slugs");
4980
+ let found;
4981
+ try {
4982
+ found = await this.fetchEvent(slug, Math.min(configured, remaining));
4983
+ } catch {
4984
+ keepWorst(malformed("candidate request failed"));
4985
+ continue;
4986
+ }
4987
+ if (found.kind !== "valid") {
4988
+ keepWorst(found);
4989
+ continue;
4990
+ }
4991
+ const r = this.toSignal(match, slug, found.value, options);
4992
+ if (r.kind === "valid") return r;
4993
+ keepWorst(r);
4410
4994
  }
4411
- return { checked: true };
4995
+ return worst ?? definitiveNone("no candidate slug yielded a usable market");
4412
4996
  } catch {
4413
- return { checked: false };
4997
+ return malformed("provider request failed");
4414
4998
  }
4415
4999
  }
4416
5000
  async fetchEvent(slug, timeoutMs) {
4417
5001
  const base = this.opts.baseUrl ?? DEFAULT_BASE2;
4418
5002
  assertAllowedHost(base);
4419
- const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
5003
+ const url = `${base}/events/slug/${encodeURIComponent(slug)}`;
4420
5004
  const doFetch = this.opts.fetchImpl ?? fetch;
4421
5005
  const res = await doFetch(url, {
4422
5006
  signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
@@ -4425,7 +5009,7 @@ var PolymarketProvider = class {
4425
5009
  redirect: "error",
4426
5010
  headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
4427
5011
  });
4428
- if (res.status === 404) return void 0;
5012
+ if (res.status === 404) return definitiveNone("slug returns 404");
4429
5013
  if (!res.ok) {
4430
5014
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4431
5015
  }
@@ -4434,63 +5018,148 @@ var PolymarketProvider = class {
4434
5018
  throw new Error(`Polymarket response too large: ${length} bytes`);
4435
5019
  }
4436
5020
  const data = await res.json();
5021
+ if (Array.isArray(data) && data.length > 1) {
5022
+ return ambiguous("slug returned more than one event");
5023
+ }
5024
+ if (Array.isArray(data) && data.length === 0) return definitiveNone("slug returns no event");
4437
5025
  const event = Array.isArray(data) ? data[0] : data;
4438
- return event && typeof event === "object" ? event : void 0;
5026
+ if (!event || typeof event !== "object") return malformed("event body is not an object");
5027
+ return valid(event);
4439
5028
  }
4440
5029
  toSignal(match, eventSlug, event, options) {
4441
- if (event.active === false || event.closed === true) return void 0;
4442
- if (event.seriesSlug != null && event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
4443
- return void 0;
5030
+ if (typeof event.active !== "boolean" || typeof event.closed !== "boolean") {
5031
+ return malformed("event active/closed is not a boolean");
5032
+ }
5033
+ if (event.active === false || event.closed === true) {
5034
+ return definitiveNone("event is closed or inactive");
4444
5035
  }
4445
- if (event.slug != null && event.slug !== eventSlug) return void 0;
4446
- const start = event.startTime ? Date.parse(event.startTime) : Number.NaN;
5036
+ if (event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
5037
+ return definitiveNone("event is not in this competition");
5038
+ }
5039
+ if (typeof event.slug !== "string") {
5040
+ return malformed("event states no slug");
5041
+ }
5042
+ if (event.slug !== eventSlug) return definitiveNone("event is not the one requested");
5043
+ if (typeof event.startTime !== "string" || !canonicalTimestamp(event.startTime)) {
5044
+ return malformed("event startTime missing or unparseable");
5045
+ }
5046
+ const start = Date.parse(event.startTime);
4447
5047
  const kick = Date.parse(match.kickoff);
4448
- if (Number.isFinite(start) && Number.isFinite(kick) && Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
4449
- return void 0;
5048
+ if (!Number.isFinite(start) || !Number.isFinite(kick)) {
5049
+ return malformed("event or fixture kickoff is unreadable");
4450
5050
  }
4451
- const moneyline = (event.markets ?? []).filter(
4452
- (m) => (m.sportsMarketType ?? "moneyline") === "moneyline"
5051
+ if (Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
5052
+ return definitiveNone("event kickoff does not match the fixture");
5053
+ }
5054
+ if (!Array.isArray(event.markets)) {
5055
+ return malformed("event markets is not an array");
5056
+ }
5057
+ const marketsTruncated = Array.isArray(event.markets) && event.markets.length > MAX_EVENT_MARKETS;
5058
+ if (marketsTruncated) {
5059
+ return malformed("event market list exceeded the cap");
5060
+ }
5061
+ const marketList = takeBounded(event.markets, MAX_EVENT_MARKETS);
5062
+ if (marketList.some(
5063
+ (market) => !market || typeof market !== "object" || typeof market.sportsMarketType !== "string"
5064
+ )) {
5065
+ return malformed("event market is missing its market-type discriminator");
5066
+ }
5067
+ const moneyline = marketList.filter(
5068
+ (m) => m?.sportsMarketType === "moneyline"
4453
5069
  );
4454
- const homeMarket = pickMarket(moneyline, match.home.code, match.home.name);
4455
- const awayMarket = pickMarket(moneyline, match.away.code, match.away.name);
4456
- const drawMarket = pickDraw(moneyline);
4457
- if (!homeMarket || !awayMarket) return void 0;
5070
+ const homeSel = pickMarket(moneyline, match.home.code, match.home.name);
5071
+ const awaySel = pickMarket(moneyline, match.away.code, match.away.name);
5072
+ const drawSel = pickDraw(moneyline);
5073
+ for (const [side, sel] of [
5074
+ ["home", homeSel],
5075
+ ["away", awaySel],
5076
+ ["draw", drawSel]
5077
+ ]) {
5078
+ if (sel.kind === "ambiguous") {
5079
+ return ambiguous(`${sel.count} markets claim the ${side} outcome`);
5080
+ }
5081
+ }
5082
+ if (homeSel.kind !== "one" || awaySel.kind !== "one" || drawSel.kind !== "one") {
5083
+ return definitiveNone("event does not carry all three 1X2 legs");
5084
+ }
5085
+ const homeMarket = homeSel.value;
5086
+ const awayMarket = awaySel.value;
5087
+ const drawMarket = drawSel.value;
4458
5088
  const legIds = [homeMarket, awayMarket, drawMarket].filter((m) => m != null).map((m) => m.id ?? m.slug ?? "");
4459
- if (new Set(legIds).size !== legIds.length) return void 0;
5089
+ if (new Set(legIds).size !== legIds.length) {
5090
+ return ambiguous("two outcome legs are the same market");
5091
+ }
4460
5092
  const legs = [
4461
5093
  ["home", homeMarket, match.home.code, match.home.name],
4462
5094
  ["draw", drawMarket, void 0, "Draw"],
4463
5095
  ["away", awayMarket, match.away.code, match.away.name]
4464
5096
  ];
4465
5097
  const outcomes = [];
4466
- let asOf = event.updatedAt;
5098
+ let asOf = canonicalTimestamp(event.updatedAt);
4467
5099
  let liquidity;
4468
- for (const [kind, market, teamCode, label] of legs) {
5100
+ for (const [kind, market, teamCode2, label] of legs) {
4469
5101
  if (!market) continue;
4470
- if (market.closed === true || market.active === false) return void 0;
4471
- if (market.description && NON_REGULAR_TIME.test(market.description)) return void 0;
5102
+ if (typeof market.closed !== "boolean" || typeof market.active !== "boolean") {
5103
+ return malformed("market active/closed is not a boolean");
5104
+ }
5105
+ if (market.closed === true || market.active === false) {
5106
+ return definitiveNone("an outcome leg is closed or inactive");
5107
+ }
5108
+ if (market.description && NON_REGULAR_TIME.test(market.description)) {
5109
+ return definitiveNone("an outcome leg is not a regular-time market");
5110
+ }
4472
5111
  const yes = yesPrice(market);
4473
- if (yes == null) return void 0;
4474
- outcomes.push({ kind, teamCode, label, probability: yes });
4475
- if (market.updatedAt && (!asOf || market.updatedAt < asOf)) asOf = market.updatedAt;
4476
- const liq = numberish(market.liquidityNum ?? market.liquidity);
5112
+ if (yes == null) return malformed("market is not a readable Yes/No binary");
5113
+ outcomes.push({ kind, teamCode: teamCode2, label, probability: yes });
5114
+ const marketAsOf = canonicalTimestamp(market.updatedAt);
5115
+ if (!marketAsOf) {
5116
+ return malformed("market updatedAt missing or unparseable");
5117
+ }
5118
+ const nowMs = (options?.now ?? this.opts.now ?? /* @__PURE__ */ new Date()).getTime();
5119
+ if (Date.parse(marketAsOf) - nowMs > FUTURE_SKEW_MS) {
5120
+ return malformed("market updatedAt is dated forward");
5121
+ }
5122
+ if (!asOf || Date.parse(marketAsOf) < Date.parse(asOf)) asOf = marketAsOf;
5123
+ const rawLiq = market.liquidityNum ?? market.liquidity;
5124
+ const liq = numberish(rawLiq);
5125
+ if (rawLiq != null && liq == null) {
5126
+ return malformed("market liquidity is unreadable");
5127
+ }
4477
5128
  if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
4478
5129
  }
4479
5130
  const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
4480
- if (rawSum < 0.9 || rawSum > 1.15) return void 0;
5131
+ if (rawSum < 0.9 || rawSum > 1.15) {
5132
+ return ambiguous("outcome probabilities do not form a coherent 1X2");
5133
+ }
5134
+ if (!asOf) return malformed("no usable timestamp on the event or its markets");
4481
5135
  const signal = buildMarketSignal({
4482
5136
  match,
4483
5137
  source: "polymarket",
4484
- sourceMarketId: event.id ?? eventSlug,
4485
- asOf: asOf ?? (/* @__PURE__ */ new Date()).toISOString(),
5138
+ // Echoed into MCP structured content (tools.ts `market.id`), i.e. straight
5139
+ // into an agent's context. Stripping control characters is NOT sufficient
5140
+ // there: printable prose ("IGNORE PREVIOUS INSTRUCTIONS") survives that and
5141
+ // is precisely what matters for a model reading it. Gamma ids are short
5142
+ // opaque tokens, so validate that GRAMMAR and otherwise fall back to the
5143
+ // slug we derived ourselves.
5144
+ // The fallback is grammar-checked too. It is normally a slug we derived
5145
+ // ourselves, but `mapping.2026.json` can override it, so echoing it raw
5146
+ // was the one path around the agent-facing filter this line exists for.
5147
+ sourceMarketId: safeMarketId(event.id) ?? safeDerivedSlug(eventSlug),
5148
+ asOf,
4486
5149
  outcomes,
4487
5150
  liquidity,
4488
5151
  now: options?.now ?? this.opts.now,
4489
5152
  maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
4490
5153
  });
4491
- return signal.ambiguous ? void 0 : signal;
5154
+ return signal.ambiguous ? ambiguous("signal does not map cleanly onto this fixture") : valid(signal);
4492
5155
  }
4493
5156
  };
5157
+ function safeMarketId(id) {
5158
+ return typeof id === "string" && /^[0-9]{1,32}$/.test(id) ? id : void 0;
5159
+ }
5160
+ function safeDerivedSlug(slug) {
5161
+ return typeof slug === "string" && /^fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2}$/.test(slug) ? slug : void 0;
5162
+ }
4494
5163
  var POLYMARKET_TOKEN = {
4495
5164
  SUI: "che",
4496
5165
  // Switzerland
@@ -4504,8 +5173,16 @@ var POLYMARKET_TOKEN = {
4504
5173
  // Croatia
4505
5174
  COD: "cdr",
4506
5175
  // DR Congo
4507
- CPV: "cvi"
5176
+ CPV: "cvi",
4508
5177
  // Cabo Verde
5178
+ // TWO letters, not three — the one entry that is not ISO alpha-3. Verified
5179
+ // live: `fifwc-kor-cze-2026-06-11` is a 404, `fifwc-kr-cze-2026-06-11`
5180
+ // resolves to "Korea Republic vs. Czechia". Korea's three group fixtures
5181
+ // therefore had no market line at all. The `^[a-z]{3}$` guard in
5182
+ // `deriveEventSlugs` validates the FIFA CODE, not the token, so a two-letter
5183
+ // alias passes through it unharmed.
5184
+ KOR: "kr"
5185
+ // Korea Republic
4509
5186
  };
4510
5187
  function pmTokens(code) {
4511
5188
  const c = code.toLowerCase();
@@ -4536,18 +5213,21 @@ function slugToken(m) {
4536
5213
  return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
4537
5214
  }
4538
5215
  function isDrawMarket(m) {
4539
- return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
5216
+ const title = (m.groupItemTitle ?? "").trim().toLowerCase();
5217
+ return slugToken(m) === "draw" || title === "draw" || /^draw\s*\(/.test(title);
4540
5218
  }
4541
- function pickMarket(markets, teamCode, teamName) {
4542
- const tokens = pmTokens(teamCode);
5219
+ function pickMarket(markets, teamCode2, teamName) {
5220
+ const tokens = pmTokens(teamCode2);
4543
5221
  const name = teamName.trim().toLowerCase();
4544
5222
  const teamMarkets = markets.filter((m) => !isDrawMarket(m));
4545
- const bySlug = teamMarkets.find((m) => tokens.includes(slugToken(m)));
4546
- if (bySlug) return bySlug;
4547
- return teamMarkets.find((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name);
5223
+ const bySlug = teamMarkets.filter((m) => tokens.includes(slugToken(m)));
5224
+ if (bySlug.length > 1) return { kind: "ambiguous", count: bySlug.length };
5225
+ const byTitle = name ? teamMarkets.filter((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name) : [];
5226
+ if (byTitle.length > 1) return { kind: "ambiguous", count: byTitle.length };
5227
+ return selectOne([.../* @__PURE__ */ new Set([...bySlug, ...byTitle])]);
4548
5228
  }
4549
5229
  function pickDraw(markets) {
4550
- return markets.find(isDrawMarket);
5230
+ return selectOne(markets.filter(isDrawMarket));
4551
5231
  }
4552
5232
  function assertAllowedHost(base) {
4553
5233
  let host;
@@ -4561,20 +5241,29 @@ function assertAllowedHost(base) {
4561
5241
  }
4562
5242
  }
4563
5243
  function yesPrice(market) {
4564
- const labels = parseJsonArray(market.outcomes);
4565
- const prices = parseJsonArray(market.outcomePrices).map((p2) => Number(p2));
4566
- if (labels.length === 0 || labels.length !== prices.length) return void 0;
4567
- const i = labels.findIndex((l) => l.trim().toLowerCase() === "yes");
4568
- if (i < 0) return void 0;
4569
- const p = prices[i];
4570
- return typeof p === "number" && Number.isFinite(p) && p > 0 && p <= 1 ? p : void 0;
5244
+ const labels = parseJsonArray(market.outcomes).map((l) => l.trim().toLowerCase());
5245
+ const raw = parseJsonArray(market.outcomePrices);
5246
+ if (raw.some((v) => v.trim() === "" || !Number.isFinite(Number(v)))) return void 0;
5247
+ const prices = raw.map((v) => Number(v));
5248
+ if (labels.length !== 2 || prices.length !== 2) return void 0;
5249
+ const i = labels.indexOf("yes");
5250
+ const j = labels.indexOf("no");
5251
+ if (i < 0 || j < 0) return void 0;
5252
+ const yes = prices[i];
5253
+ const no = prices[j];
5254
+ if (![yes, no].every((v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1)) {
5255
+ return void 0;
5256
+ }
5257
+ if (Math.abs(yes + no - 1) > 0.05) return void 0;
5258
+ return yes > 0 ? yes : void 0;
4571
5259
  }
4572
5260
  function parseJsonArray(v) {
4573
- if (Array.isArray(v)) return v.map((x) => String(x));
5261
+ const asText = (x) => typeof x === "string" || typeof x === "number" ? String(x) : "";
5262
+ if (Array.isArray(v)) return v.map(asText);
4574
5263
  if (typeof v === "string") {
4575
5264
  try {
4576
5265
  const parsed = JSON.parse(v);
4577
- return Array.isArray(parsed) ? parsed.map((x) => String(x)) : [];
5266
+ return Array.isArray(parsed) ? parsed.map(asText) : [];
4578
5267
  } catch {
4579
5268
  return [];
4580
5269
  }
@@ -4582,10 +5271,10 @@ function parseJsonArray(v) {
4582
5271
  return [];
4583
5272
  }
4584
5273
  function numberish(v) {
4585
- if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
5274
+ if (typeof v === "number") return Number.isFinite(v) && v >= 0 ? v : void 0;
4586
5275
  if (typeof v === "string") {
4587
5276
  const n = Number(v);
4588
- return Number.isFinite(n) ? n : void 0;
5277
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
4589
5278
  }
4590
5279
  return void 0;
4591
5280
  }
@@ -4612,7 +5301,7 @@ async function getMarketSignals(provider, matches, options) {
4612
5301
  try {
4613
5302
  return await provider.findSignals(matches, options);
4614
5303
  } catch {
4615
- return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
5304
+ return emptyBatch();
4616
5305
  }
4617
5306
  }
4618
5307
  var SHARE_HASHTAG = "#VibingLaVidaLoca";
@@ -4693,6 +5382,9 @@ function formatShareSnippet(input, options = {}) {
4693
5382
  if (input.degraded && input.matches.length > 0) {
4694
5383
  blocks.push("(Live data unavailable \u2014 showing the bundled schedule, not live scores.)");
4695
5384
  }
5385
+ if (includeMarkets && input.marketComplete === false) {
5386
+ blocks.push("(Market data unavailable or incomplete \u2014 not all fixtures were checked.)");
5387
+ }
4696
5388
  blocks.push(
4697
5389
  shareFooter({
4698
5390
  source: input.source,
@@ -4723,7 +5415,9 @@ function formatShareTable(input, options = {}) {
4723
5415
  const includeInstall = options.includeInstallLine !== false;
4724
5416
  const blocks = [];
4725
5417
  if (input.tables.length === 0) {
4726
- blocks.push(input.emptyNote ?? "No standings available.");
5418
+ blocks.push(
5419
+ input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
5420
+ );
4727
5421
  } else {
4728
5422
  for (const { group, rows } of input.tables) {
4729
5423
  blocks.push(
@@ -4993,6 +5687,8 @@ var EN2 = {
4993
5687
  "today.none": "No matches scheduled for this date.",
4994
5688
  "live.title": "Live now",
4995
5689
  "live.none": "No matches in play right now.",
5690
+ "signoff.complete": "The World Cup is complete. Thanks for using Claudinho.",
5691
+ "signoff.star": "Star the project:",
4996
5692
  "live.degraded": "Live scores unavailable right now \u2014 couldn't reach the data provider.",
4997
5693
  "feed.degraded": "Live scores unavailable \u2014 showing the bundled schedule.",
4998
5694
  "next.none": "No upcoming fixture found for {team}.",
@@ -5005,6 +5701,7 @@ var EN2 = {
5005
5701
  "table.title": "Group {group}",
5006
5702
  "table.none": "No group found for {group}.",
5007
5703
  "table.degraded": "Live standings unavailable \u2014 showing the group roster.",
5704
+ "table.unavailable": "Live standings unavailable.",
5008
5705
  "table.empty": "No standings available.",
5009
5706
  "match.none": "No match found with id {id}.",
5010
5707
  "status.live": "LIVE",
@@ -5031,6 +5728,8 @@ var ES2 = {
5031
5728
  "today.none": "No hay partidos para esta fecha.",
5032
5729
  "live.title": "En vivo",
5033
5730
  "live.none": "No hay partidos en juego ahora mismo.",
5731
+ "signoff.complete": "El Mundial ha terminado. Gracias por usar Claudinho.",
5732
+ "signoff.star": "Dale una estrella al proyecto:",
5034
5733
  "live.degraded": "Marcadores en vivo no disponibles \u2014 no se pudo conectar con el proveedor de datos.",
5035
5734
  "feed.degraded": "Marcadores en vivo no disponibles \u2014 mostrando el calendario.",
5036
5735
  "next.none": "No se encontr\xF3 pr\xF3ximo partido para {team}.",
@@ -5043,6 +5742,7 @@ var ES2 = {
5043
5742
  "table.title": "Grupo {group}",
5044
5743
  "table.none": "No se encontr\xF3 el grupo {group}.",
5045
5744
  "table.degraded": "Tabla en vivo no disponible \u2014 mostrando la lista del grupo.",
5745
+ "table.unavailable": "Tabla en vivo no disponible.",
5046
5746
  "table.empty": "No hay clasificaci\xF3n disponible.",
5047
5747
  "match.none": "No se encontr\xF3 partido con id {id}.",
5048
5748
  "status.live": "EN VIVO",
@@ -5069,6 +5769,8 @@ var PT2 = {
5069
5769
  "today.none": "Nenhum jogo para esta data.",
5070
5770
  "live.title": "Ao vivo",
5071
5771
  "live.none": "Nenhum jogo em andamento agora.",
5772
+ "signoff.complete": "A Copa do Mundo terminou. Obrigado por usar o Claudinho.",
5773
+ "signoff.star": "D\xEA uma estrela ao projeto:",
5072
5774
  "live.degraded": "Placar ao vivo indispon\xEDvel \u2014 n\xE3o foi poss\xEDvel conectar ao provedor de dados.",
5073
5775
  "feed.degraded": "Placar ao vivo indispon\xEDvel \u2014 mostrando a tabela de jogos.",
5074
5776
  "next.none": "Nenhum pr\xF3ximo jogo encontrado para {team}.",
@@ -5081,6 +5783,7 @@ var PT2 = {
5081
5783
  "table.title": "Grupo {group}",
5082
5784
  "table.none": "Grupo {group} n\xE3o encontrado.",
5083
5785
  "table.degraded": "Classifica\xE7\xE3o ao vivo indispon\xEDvel \u2014 mostrando os times do grupo.",
5786
+ "table.unavailable": "Classifica\xE7\xE3o ao vivo indispon\xEDvel.",
5084
5787
  "table.empty": "Classifica\xE7\xE3o indispon\xEDvel.",
5085
5788
  "match.none": "Nenhum jogo encontrado com id {id}.",
5086
5789
  "status.live": "AO VIVO",
@@ -5107,6 +5810,8 @@ var FR2 = {
5107
5810
  "today.none": "Aucun match pr\xE9vu pour cette date.",
5108
5811
  "live.title": "En direct",
5109
5812
  "live.none": "Aucun match en cours pour l'instant.",
5813
+ "signoff.complete": "La Coupe du Monde est termin\xE9e. Merci d'avoir utilis\xE9 Claudinho.",
5814
+ "signoff.star": "Mettez une \xE9toile au projet :",
5110
5815
  "live.degraded": "Scores en direct indisponibles \u2014 impossible de joindre le fournisseur de donn\xE9es.",
5111
5816
  "feed.degraded": "Scores en direct indisponibles \u2014 affichage du calendrier.",
5112
5817
  "next.none": "Aucun prochain match trouv\xE9 pour {team}.",
@@ -5119,6 +5824,7 @@ var FR2 = {
5119
5824
  "table.title": "Groupe {group}",
5120
5825
  "table.none": "Groupe {group} introuvable.",
5121
5826
  "table.degraded": "Classement en direct indisponible \u2014 affichage de la composition du groupe.",
5827
+ "table.unavailable": "Classement en direct indisponible.",
5122
5828
  "table.empty": "Aucun classement disponible.",
5123
5829
  "match.none": "Aucun match trouv\xE9 avec id {id}.",
5124
5830
  "status.live": "DIRECT",
@@ -5229,7 +5935,19 @@ function dataSource(source, lang, c) {
5229
5935
  }
5230
5936
 
5231
5937
  // src/marketCache.ts
5232
- import { readFileSync as readFileSync2 } from "fs";
5938
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
5939
+ import { join as join3 } from "path";
5940
+
5941
+ // src/cache.ts
5942
+ import {
5943
+ closeSync as closeSync2,
5944
+ mkdirSync as mkdirSync2,
5945
+ openSync as openSync2,
5946
+ readFileSync as readFileSync2,
5947
+ rmSync,
5948
+ statSync,
5949
+ writeSync as writeSync2
5950
+ } from "fs";
5233
5951
  import { join as join2 } from "path";
5234
5952
 
5235
5953
  // src/paths.ts
@@ -5252,133 +5970,63 @@ function writeFileAtomic(path, data) {
5252
5970
  renameSync(tmp, path);
5253
5971
  }
5254
5972
 
5255
- // src/marketCache.ts
5256
- var POSITIVE_TTL_MS = 10 * 6e4;
5257
- var NEGATIVE_TTL_MS = 3 * 6e4;
5258
- function cachePath() {
5259
- return join2(cacheDir(), "market-signals.json");
5260
- }
5261
- function readFile() {
5262
- try {
5263
- return JSON.parse(readFileSync2(cachePath(), "utf8"));
5264
- } catch {
5265
- return void 0;
5266
- }
5267
- }
5268
- function readMarketCache(source, competition, now = Date.now()) {
5269
- const signals = /* @__PURE__ */ new Map();
5270
- const checked = /* @__PURE__ */ new Set();
5271
- const file = readFile();
5272
- if (!file || file.source !== source || file.competition !== competition) {
5273
- return { signals, checked };
5274
- }
5275
- for (const [id, entry] of Object.entries(file.entries ?? {})) {
5276
- const t2 = Date.parse(entry.fetchedAt);
5277
- if (!Number.isFinite(t2)) continue;
5278
- const ttl = entry.signal ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS;
5279
- if (now - t2 > ttl) continue;
5280
- checked.add(id);
5281
- if (entry.signal) signals.set(id, entry.signal);
5282
- }
5283
- return { signals, checked };
5284
- }
5285
- function writeMarketCache(source, competition, attempted, fetched, now = Date.now()) {
5286
- if (attempted.length === 0) return;
5287
- try {
5288
- const existing = readFile();
5289
- const base = existing && existing.source === source && existing.competition === competition ? existing : { source, competition, entries: {} };
5290
- const fetchedAt = new Date(now).toISOString();
5291
- for (const id of attempted) {
5292
- base.entries[id] = { fetchedAt, signal: fetched.get(id) ?? null };
5293
- }
5294
- writeFileAtomic(cachePath(), JSON.stringify(base));
5295
- } catch {
5296
- }
5297
- }
5298
-
5299
- // src/starNudge.ts
5300
- import { readFileSync as readFileSync3 } from "fs";
5301
- import { join as join3 } from "path";
5302
- var REPO_URL = "https://github.com/arturogarrido/claudinho";
5303
- var NUDGE_EVERY = 5;
5304
- function counterPath() {
5305
- return join3(cacheDir(), "runs.json");
5306
- }
5307
- function shouldNudge(runCount, every = NUDGE_EVERY) {
5308
- return runCount > 0 && runCount % every === 0;
5309
- }
5310
- function bumpRunCount(path = counterPath()) {
5311
- try {
5312
- let count = 0;
5313
- try {
5314
- const raw = JSON.parse(readFileSync3(path, "utf8"));
5315
- if (typeof raw.count === "number" && Number.isFinite(raw.count)) count = raw.count;
5316
- } catch {
5317
- count = 0;
5318
- }
5319
- count += 1;
5320
- writeFileAtomic(path, JSON.stringify({ count }));
5321
- return count;
5322
- } catch {
5323
- return void 0;
5324
- }
5325
- }
5326
-
5327
- // src/clipboard.ts
5328
- import { spawnSync } from "child_process";
5329
- function clipboardTools(platform) {
5330
- if (platform === "darwin") return [{ cmd: "pbcopy", args: [] }];
5331
- if (platform === "win32") return [{ cmd: "clip", args: [] }];
5332
- return [
5333
- { cmd: "wl-copy", args: [] },
5334
- { cmd: "xclip", args: ["-selection", "clipboard"] },
5335
- { cmd: "xsel", args: ["--clipboard", "--input"] }
5336
- ];
5337
- }
5338
- function copyToClipboard(text, platform = process.platform) {
5339
- for (const { cmd, args } of clipboardTools(platform)) {
5340
- try {
5341
- const res = spawnSync(cmd, args, {
5342
- input: text,
5343
- stdio: ["pipe", "ignore", "ignore"],
5344
- timeout: 1e3
5345
- });
5346
- if (!res.error && res.status === 0) return true;
5347
- } catch {
5348
- }
5349
- }
5350
- return false;
5351
- }
5352
-
5353
5973
  // src/cache.ts
5354
- import {
5355
- closeSync as closeSync2,
5356
- mkdirSync as mkdirSync2,
5357
- openSync as openSync2,
5358
- readFileSync as readFileSync4,
5359
- rmSync,
5360
- statSync,
5361
- writeSync as writeSync2
5362
- } from "fs";
5363
- import { join as join4 } from "path";
5364
5974
  var CACHE_VERSION = 2;
5975
+ var MAX_STATE_BYTES = 1024 * 1024;
5976
+ var MAX_STATE_RECORDS = 1024;
5365
5977
  var LOCK_STALE_MS = 6e4;
5366
- function cachePath2(source = "espn", competition = DEFAULT_COMPETITION) {
5978
+ function cachePath(source = "espn", competition = DEFAULT_COMPETITION) {
5367
5979
  if (source === "espn" && competition === DEFAULT_COMPETITION) {
5368
- return join4(cacheDir(), "state.json");
5980
+ return join2(cacheDir(), "state.json");
5369
5981
  }
5370
5982
  const slug = `${source}.${competition}`.replace(/[^a-zA-Z0-9._-]/g, "_");
5371
- return join4(cacheDir(), `state.${slug}.json`);
5983
+ return join2(cacheDir(), `state.${slug}.json`);
5372
5984
  }
5373
5985
  function lockPath() {
5374
- return join4(cacheDir(), "refresh.lock");
5986
+ return join2(cacheDir(), "refresh.lock");
5987
+ }
5988
+ function validStamp(value) {
5989
+ if (typeof value !== "string") return false;
5990
+ const match = value.match(
5991
+ /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,3}))?Z$/
5992
+ );
5993
+ if (!match) return false;
5994
+ const parsed = Date.parse(value);
5995
+ if (!Number.isFinite(parsed)) return false;
5996
+ const canonical = `${match[1]}.${(match[2] ?? "").padEnd(3, "0")}Z`;
5997
+ return new Date(parsed).toISOString() === canonical;
5998
+ }
5999
+ function validScope(value) {
6000
+ return typeof value === "string" && value.length > 0 && value.length <= 128 && /^[a-zA-Z0-9._-]+$/.test(value);
6001
+ }
6002
+ function isCacheState(value) {
6003
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
6004
+ const s = value;
6005
+ if (s.version !== CACHE_VERSION) return false;
6006
+ if (!validStamp(s.updatedAt) || typeof s.degraded !== "boolean") return false;
6007
+ if (!validScope(s.source) || !validScope(s.competition)) return false;
6008
+ if (!Array.isArray(s.live) || s.live.length > MAX_STATE_RECORDS) return false;
6009
+ if (s.fixtures !== void 0 && (!Array.isArray(s.fixtures) || s.fixtures.length > MAX_STATE_RECORDS)) {
6010
+ return false;
6011
+ }
6012
+ for (const key of [
6013
+ "fixturesUpdatedAt",
6014
+ "fixturesAttemptedAt",
6015
+ "backoffUntil"
6016
+ ]) {
6017
+ if (s[key] !== void 0 && !validStamp(s[key])) return false;
6018
+ }
6019
+ return true;
5375
6020
  }
5376
6021
  function readState(source = "espn", competition = DEFAULT_COMPETITION) {
5377
6022
  try {
5378
- const s = JSON.parse(
5379
- readFileSync4(cachePath2(source, competition), "utf8")
5380
- );
5381
- return s.version === CACHE_VERSION ? s : void 0;
6023
+ const path = cachePath(source, competition);
6024
+ const info = statSync(path);
6025
+ if (!info.isFile() || info.size > MAX_STATE_BYTES) return void 0;
6026
+ const bytes = readFileSync2(path);
6027
+ if (bytes.byteLength > MAX_STATE_BYTES) return void 0;
6028
+ const parsed = JSON.parse(bytes.toString("utf8"));
6029
+ return isCacheState(parsed) ? parsed : void 0;
5382
6030
  } catch {
5383
6031
  return void 0;
5384
6032
  }
@@ -5389,41 +6037,45 @@ function readCurrentState(source, competition) {
5389
6037
  }
5390
6038
  function writeState(state) {
5391
6039
  writeFileAtomic(
5392
- cachePath2(state.source, state.competition),
6040
+ cachePath(state.source, state.competition),
5393
6041
  JSON.stringify({ ...state, version: CACHE_VERSION })
5394
6042
  );
5395
6043
  }
6044
+ var MAX_BACKOFF_MS = 30 * 6e4;
5396
6045
  function backoffActive(state, now = Date.now()) {
5397
6046
  if (!state?.backoffUntil) return false;
5398
6047
  const t2 = Date.parse(state.backoffUntil);
5399
- return Number.isFinite(t2) && now < t2;
6048
+ if (!Number.isFinite(t2)) return false;
6049
+ return now < t2 && t2 - now <= MAX_BACKOFF_MS;
5400
6050
  }
5401
6051
  function fixturesAttemptAgeMs(state, now = Date.now()) {
5402
- if (!state?.fixturesAttemptedAt) return Infinity;
5403
- const t2 = Date.parse(state.fixturesAttemptedAt);
5404
- return Number.isFinite(t2) ? now - t2 : Infinity;
6052
+ return stampAgeMs(state?.fixturesAttemptedAt, now);
6053
+ }
6054
+ var FUTURE_SKEW_MS2 = 6e4;
6055
+ function stampAgeMs(value, now) {
6056
+ if (!value) return Infinity;
6057
+ const t2 = Date.parse(value);
6058
+ if (!Number.isFinite(t2)) return Infinity;
6059
+ const age = now - t2;
6060
+ return age < -FUTURE_SKEW_MS2 ? Infinity : age;
5405
6061
  }
5406
6062
  function ageMs(state, now = Date.now()) {
5407
- if (!state) return Infinity;
5408
- const t2 = Date.parse(state.updatedAt);
5409
- return Number.isFinite(t2) ? now - t2 : Infinity;
6063
+ return stampAgeMs(state?.updatedAt, now);
5410
6064
  }
5411
6065
  function fixturesAgeMs(state, now = Date.now()) {
5412
- if (!state?.fixturesUpdatedAt) return Infinity;
5413
- const t2 = Date.parse(state.fixturesUpdatedAt);
5414
- return Number.isFinite(t2) ? now - t2 : Infinity;
6066
+ return stampAgeMs(state?.fixturesUpdatedAt, now);
5415
6067
  }
5416
6068
  function lockAgeMs(now = Date.now()) {
5417
6069
  const lp = lockPath();
5418
6070
  try {
5419
- const contents = readFileSync4(lp, "utf8");
6071
+ const contents = readFileSync2(lp, "utf8");
5420
6072
  const written = Number.parseInt(contents.split(/\s+/)[1] ?? "", 10);
5421
- if (Number.isFinite(written)) return now - written;
6073
+ if (Number.isFinite(written)) return stampAgeMs(new Date(written).toISOString(), now);
5422
6074
  } catch {
5423
6075
  return Infinity;
5424
6076
  }
5425
6077
  try {
5426
- return now - statSync(lp).mtimeMs;
6078
+ return stampAgeMs(new Date(statSync(lp).mtimeMs).toISOString(), now);
5427
6079
  } catch {
5428
6080
  return Infinity;
5429
6081
  }
@@ -5471,8 +6123,172 @@ function releaseLock() {
5471
6123
  }
5472
6124
  }
5473
6125
 
6126
+ // src/marketCache.ts
6127
+ var POSITIVE_TTL_MS = 10 * 6e4;
6128
+ var NEGATIVE_TTL_MS = 3 * 6e4;
6129
+ var MAX_MARKET_CACHE_BYTES = 1024 * 1024;
6130
+ var FUTURE_SKEW_MS3 = 6e4;
6131
+ function cachePath2() {
6132
+ return join3(cacheDir(), "market-signals.json");
6133
+ }
6134
+ function readFile() {
6135
+ try {
6136
+ const path = cachePath2();
6137
+ const info = statSync2(path);
6138
+ if (!info.isFile() || info.size > MAX_MARKET_CACHE_BYTES) return void 0;
6139
+ const bytes = readFileSync3(path);
6140
+ if (bytes.byteLength > MAX_MARKET_CACHE_BYTES) return void 0;
6141
+ const parsed = JSON.parse(bytes.toString("utf8"));
6142
+ if (!parsed || typeof parsed !== "object") return void 0;
6143
+ return parsed;
6144
+ } catch {
6145
+ return void 0;
6146
+ }
6147
+ }
6148
+ function isEntryShaped(e) {
6149
+ if (!e || typeof e !== "object") return false;
6150
+ const entry = e;
6151
+ if (typeof entry.fetchedAt !== "string" || !Number.isFinite(Date.parse(entry.fetchedAt))) {
6152
+ return false;
6153
+ }
6154
+ if (entry.signal === null) return true;
6155
+ return !!entry.signal && typeof entry.signal === "object";
6156
+ }
6157
+ function isUsableSignal(s) {
6158
+ if (!s) return false;
6159
+ if (s.source === "" || s.asOf === "" || s.outcomes.length === 0) return false;
6160
+ if (s.outcomes.some((o) => o.kind === "other")) return false;
6161
+ if (s.ambiguous) return false;
6162
+ if (!s.favorite) return false;
6163
+ return hasSaneDistribution(s.outcomes);
6164
+ }
6165
+ function readMarketCache(source, competition, now = Date.now()) {
6166
+ const signals = /* @__PURE__ */ new Map();
6167
+ const checked = /* @__PURE__ */ new Set();
6168
+ const file = readFile();
6169
+ if (!file || file.source !== source || file.competition !== competition) {
6170
+ return { signals, checked };
6171
+ }
6172
+ const entries = file.entries;
6173
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
6174
+ return { signals, checked };
6175
+ }
6176
+ let examined = 0;
6177
+ for (const id in entries) {
6178
+ if (!Object.hasOwn(entries, id)) continue;
6179
+ if (examined >= 256) break;
6180
+ examined += 1;
6181
+ const raw = entries[id];
6182
+ if (!isEntryShaped(raw)) continue;
6183
+ const entry = raw;
6184
+ const t2 = Date.parse(entry.fetchedAt);
6185
+ const ttl = entry.signal ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS;
6186
+ const age = now - t2;
6187
+ if (age > ttl || age < -FUTURE_SKEW_MS3) continue;
6188
+ if (entry.signal === null) {
6189
+ checked.add(id);
6190
+ continue;
6191
+ }
6192
+ const sealed = parseCachedMarketSignal(entry.signal, { now: new Date(now) });
6193
+ if (sealed.kind !== "valid") continue;
6194
+ const clean = sealed.value;
6195
+ if (clean.matchId !== id || clean.source !== source) continue;
6196
+ if (!isUsableSignal(clean)) continue;
6197
+ checked.add(id);
6198
+ signals.set(id, clean);
6199
+ }
6200
+ return { signals, checked };
6201
+ }
6202
+ function writeMarketCache(source, competition, attempted, fetched, now = Date.now()) {
6203
+ if (attempted.length === 0) return;
6204
+ try {
6205
+ const existing = readFile();
6206
+ const reuse = existing && existing.source === source && existing.competition === competition;
6207
+ const carried = {};
6208
+ if (reuse && existing.entries && typeof existing.entries === "object" && !Array.isArray(existing.entries)) {
6209
+ let examined = 0;
6210
+ for (const id in existing.entries) {
6211
+ if (!Object.hasOwn(existing.entries, id)) continue;
6212
+ if (examined >= 256) break;
6213
+ examined += 1;
6214
+ const raw = existing.entries[id];
6215
+ if (!isEntryShaped(raw)) continue;
6216
+ const age = stampAgeMs(raw.fetchedAt, now);
6217
+ if (age > (raw.signal ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS)) continue;
6218
+ if (age < -FUTURE_SKEW_MS3) continue;
6219
+ if (raw.signal !== null && !isUsableSignal(parsedValue(parseCachedMarketSignal(raw.signal, { now: new Date(now) })))) {
6220
+ continue;
6221
+ }
6222
+ carried[id] = raw;
6223
+ }
6224
+ }
6225
+ const base = { source, competition, entries: carried };
6226
+ const fetchedAt = new Date(now).toISOString();
6227
+ for (const id of attempted.slice(0, 256)) {
6228
+ base.entries[id] = { fetchedAt, signal: fetched.get(id) ?? null };
6229
+ }
6230
+ writeFileAtomic(cachePath2(), JSON.stringify(base));
6231
+ } catch {
6232
+ }
6233
+ }
6234
+
6235
+ // src/starNudge.ts
6236
+ import { readFileSync as readFileSync4 } from "fs";
6237
+ import { join as join4 } from "path";
6238
+ var REPO_URL = "https://github.com/arturogarrido/claudinho";
6239
+ var NUDGE_EVERY = 5;
6240
+ function counterPath() {
6241
+ return join4(cacheDir(), "runs.json");
6242
+ }
6243
+ function shouldNudge(runCount, every = NUDGE_EVERY) {
6244
+ return runCount > 0 && runCount % every === 0;
6245
+ }
6246
+ function bumpRunCount(path = counterPath()) {
6247
+ try {
6248
+ let count2 = 0;
6249
+ try {
6250
+ const raw = JSON.parse(readFileSync4(path, "utf8"));
6251
+ if (typeof raw.count === "number" && Number.isFinite(raw.count)) count2 = raw.count;
6252
+ } catch {
6253
+ count2 = 0;
6254
+ }
6255
+ count2 += 1;
6256
+ writeFileAtomic(path, JSON.stringify({ count: count2 }));
6257
+ return count2;
6258
+ } catch {
6259
+ return void 0;
6260
+ }
6261
+ }
6262
+
6263
+ // src/clipboard.ts
6264
+ import { spawnSync } from "child_process";
6265
+ function clipboardTools(platform) {
6266
+ if (platform === "darwin") return [{ cmd: "pbcopy", args: [] }];
6267
+ if (platform === "win32") return [{ cmd: "clip", args: [] }];
6268
+ return [
6269
+ { cmd: "wl-copy", args: [] },
6270
+ { cmd: "xclip", args: ["-selection", "clipboard"] },
6271
+ { cmd: "xsel", args: ["--clipboard", "--input"] }
6272
+ ];
6273
+ }
6274
+ function copyToClipboard(text, platform = process.platform) {
6275
+ for (const { cmd, args } of clipboardTools(platform)) {
6276
+ try {
6277
+ const res = spawnSync(cmd, args, {
6278
+ input: text,
6279
+ stdio: ["pipe", "ignore", "ignore"],
6280
+ timeout: 1e3
6281
+ });
6282
+ if (!res.error && res.status === 0) return true;
6283
+ } catch {
6284
+ }
6285
+ }
6286
+ return false;
6287
+ }
6288
+
5474
6289
  // src/statusline.ts
5475
6290
  var DISPLAY_STALE_MS = 5 * 6e4;
6291
+ var TOURNAMENT_COMPLETE_LINE = "\u26BD World Cup 2026 is complete \xB7 Thanks for vibing with Claudinho";
5476
6292
  var FLAGLESS_TERMINALS = /* @__PURE__ */ new Set(["WarpTerminal"]);
5477
6293
  function flagsEnabled(env = process.env) {
5478
6294
  const v = (env.CLAUDINHO_FLAGS ?? "").trim().toLowerCase();
@@ -5506,35 +6322,110 @@ function matchSegment(m, compact, flags) {
5506
6322
  const away = compact ? m.away.flag : `${m.away.code} ${m.away.flag}`;
5507
6323
  return `${home} ${scoreline(m)} ${away} ${minute}`;
5508
6324
  }
6325
+ var MAX_LIVE_CONSIDERED = 64;
6326
+ var MAX_LIVE_EXAMINED = 512;
6327
+ function sealFixtures(raw) {
6328
+ if (raw === void 0) {
6329
+ return { items: [], total: 0, shown: 0, truncated: false, complete: true };
6330
+ }
6331
+ if (!Array.isArray(raw)) {
6332
+ return { items: [], total: 0, shown: 0, truncated: false, complete: false };
6333
+ }
6334
+ const out2 = [];
6335
+ let inspected = 0;
6336
+ let readable = true;
6337
+ for (const rec of raw) {
6338
+ if (out2.length >= MAX_LIVE_CONSIDERED || inspected >= MAX_LIVE_EXAMINED) break;
6339
+ inspected++;
6340
+ if (!isMatchShaped(rec)) {
6341
+ readable = false;
6342
+ continue;
6343
+ }
6344
+ const sealed = parsedValue(parseCachedMatch(rec, { events: false }));
6345
+ if (sealed) out2.push(sealed);
6346
+ else readable = false;
6347
+ }
6348
+ const exhausted = inspected === raw.length;
6349
+ const complete = exhausted && readable;
6350
+ return {
6351
+ items: out2,
6352
+ // Exact only when complete; otherwise this is the number actually sealed.
6353
+ // Callers use a nonnumeric marker for an incomplete scan.
6354
+ total: out2.length,
6355
+ shown: out2.length,
6356
+ truncated: !exhausted,
6357
+ complete
6358
+ };
6359
+ }
5509
6360
  function liveMatchesFromCache(state, nowMs = Date.now()) {
5510
6361
  const fresh = state && ageMs(state, nowMs) < DISPLAY_STALE_MS;
5511
- const liveArr = fresh && Array.isArray(state?.live) ? state.live : [];
5512
- return liveArr.filter(
5513
- (m) => !!m && typeof m === "object" && isLive(m.status) && !!m.home?.code && !!m.away?.code
5514
- ).map(sanitizeMatchStrings);
6362
+ const rawLive = fresh ? state?.live : [];
6363
+ if (!Array.isArray(rawLive)) {
6364
+ return { items: [], total: 0, shown: 0, truncated: false, complete: false };
6365
+ }
6366
+ const liveArr = rawLive;
6367
+ const out2 = [];
6368
+ let inspected = 0;
6369
+ let readable = true;
6370
+ for (let i = 0; i < liveArr.length; i++) {
6371
+ if (out2.length >= MAX_LIVE_CONSIDERED || inspected >= MAX_LIVE_EXAMINED) break;
6372
+ inspected++;
6373
+ const raw = liveArr[i];
6374
+ if (!raw || typeof raw !== "object") {
6375
+ readable = false;
6376
+ continue;
6377
+ }
6378
+ const m = raw;
6379
+ if (!isLive(m.status) || !m.home?.code || !m.away?.code) {
6380
+ readable = false;
6381
+ continue;
6382
+ }
6383
+ const sealed = parsedValue(parseCachedMatch(m, { events: false }));
6384
+ if (sealed) out2.push(sealed);
6385
+ else readable = false;
6386
+ }
6387
+ const exhausted = inspected === liveArr.length;
6388
+ const complete = exhausted && readable;
6389
+ return {
6390
+ items: out2,
6391
+ total: out2.length,
6392
+ shown: out2.length,
6393
+ truncated: !exhausted,
6394
+ // False when we stopped early or a record was unreadable. In either case an
6395
+ // empty list must not render as the authoritative "nothing is on".
6396
+ complete
6397
+ };
5515
6398
  }
6399
+ var DEFAULT_MAX_SEGMENTS = 8;
6400
+ var MAX_LINE_COLUMNS = 200;
5516
6401
  function renderPrompt(state, opts = {}) {
6402
+ return truncateVisible(renderPromptLine(state, opts), MAX_LINE_COLUMNS);
6403
+ }
6404
+ function renderPromptLine(state, opts = {}) {
5517
6405
  const now = opts.now ?? /* @__PURE__ */ new Date();
5518
6406
  const nowMs = now.getTime();
6407
+ const defaultCompetition = opts.defaultCompetition ?? true;
5519
6408
  const compact = opts.compact ?? true;
5520
6409
  const flags = opts.flags ?? true;
5521
6410
  const team = opts.team?.toUpperCase();
5522
- const live = liveMatchesFromCache(state, nowMs);
5523
- const cachedFixtures = Array.isArray(state?.fixtures) ? state.fixtures.filter(isMatchShaped).map(sanitizeMatchStrings) : [];
6411
+ const liveList = liveMatchesFromCache(state, nowMs);
6412
+ const live = liveList.items;
6413
+ const cachedFixtureList = sealFixtures(state?.fixtures);
6414
+ const cachedFixtures = [...cachedFixtureList.items];
5524
6415
  const schedule = cachedFixtures.length ? mergeLive(allFixtures(), cachedFixtures) : void 0;
5525
6416
  if (team) {
5526
6417
  const mine = live.find((m) => m.home?.code === team || m.away?.code === team);
5527
6418
  if (mine) return `\u26BD ${matchSegment(mine, compact, flags)}`;
5528
6419
  } else if (live.length > 0) {
5529
- const max = opts.max && opts.max > 0 ? opts.max : live.length;
6420
+ const max = opts.max && opts.max > 0 ? Math.min(opts.max, DEFAULT_MAX_SEGMENTS) : DEFAULT_MAX_SEGMENTS;
5530
6421
  const shown = live.slice(0, max);
5531
- let line2 = "\u26BD " + shown.map((m) => matchSegment(m, compact, flags)).join(" \xB7 ");
5532
6422
  const overflow = live.length - shown.length;
5533
- if (overflow > 0) line2 += ` +${overflow}`;
5534
- return line2;
6423
+ const marker = !liveList.complete ? " +more" : overflow > 0 ? ` +${overflow}` : "";
6424
+ const body = "\u26BD " + shown.map((m) => matchSegment(m, compact, flags)).join(" \xB7 ");
6425
+ return truncateVisible(body, MAX_LINE_COLUMNS - displayWidth(marker)) + marker;
5535
6426
  }
5536
6427
  const cacheFresh = !!state && state.degraded !== true && ageMs(state, nowMs) < DISPLAY_STALE_MS;
5537
- if (!cacheFresh) {
6428
+ if (!cacheFresh || !liveList.complete) {
5538
6429
  const win = fixturesInLiveWindow(nowMs, schedule).filter(
5539
6430
  (m) => !team || m.home.code === team || m.away.code === team
5540
6431
  );
@@ -5549,10 +6440,22 @@ function renderPrompt(state, opts = {}) {
5549
6440
  if (next && isResolvedFixture(next)) {
5550
6441
  return `${teamTok(next.home, flags)} vs ${teamTok(next.away, flags)} in ${countdown(next.kickoff, now)}`;
5551
6442
  }
6443
+ if (defaultCompetition && isTournamentWindowOver(nowMs, schedule)) {
6444
+ return TOURNAMENT_COMPLETE_LINE;
6445
+ }
5552
6446
  return "\u26BD \u2014";
5553
6447
  }
5554
6448
 
5555
6449
  // src/hook.ts
6450
+ var MAX_HOOK_MATCHES = 12;
6451
+ var MAX_HOOK_CODE_POINTS = 4096;
6452
+ function boundContext(text, marker = "") {
6453
+ const points = [...text];
6454
+ if (points.length + [...marker].length <= MAX_HOOK_CODE_POINTS) return text + marker;
6455
+ const room = Math.max(0, MAX_HOOK_CODE_POINTS - [...marker].length);
6456
+ return `${points.slice(0, room).join("")}
6457
+ (context truncated)${marker}`;
6458
+ }
5556
6459
  function rosterPinned(t2) {
5557
6460
  const { team } = lookupTeam(t2.code);
5558
6461
  return team ? { ...t2, name: team.name, flag: team.flag } : t2;
@@ -5569,7 +6472,8 @@ function renderHook(state, opts = {}) {
5569
6472
  const now = opts.now ?? /* @__PURE__ */ new Date();
5570
6473
  const team = opts.team?.toUpperCase();
5571
6474
  const flags = opts.flags ?? true;
5572
- let live = liveMatchesFromCache(state, now.getTime());
6475
+ const liveList = liveMatchesFromCache(state, now.getTime());
6476
+ let live = [...liveList.items];
5573
6477
  if (live.length === 0) return "";
5574
6478
  if (team) {
5575
6479
  live = [...live].sort((a, b) => {
@@ -5578,9 +6482,13 @@ function renderHook(state, opts = {}) {
5578
6482
  return aHas - bHas;
5579
6483
  });
5580
6484
  }
5581
- const lines = live.map((mm) => line(mm, flags)).join("\n");
5582
- return `[Claudinho \u2014 live football scores right now]
5583
- ${lines}`;
6485
+ const shown = live.slice(0, MAX_HOOK_MATCHES);
6486
+ const overflow = live.length - shown.length;
6487
+ const lines = shown.map((mm) => line(mm, flags)).join("\n");
6488
+ const more = !liveList.complete ? "\n(more live matches may not be shown)" : overflow > 0 ? `
6489
+ (+${overflow} more not shown)` : "";
6490
+ return boundContext(`[Claudinho \u2014 live football scores right now]
6491
+ ${lines}`, more);
5584
6492
  }
5585
6493
 
5586
6494
  // src/refresh.ts
@@ -5869,11 +6777,15 @@ function adapterFor({ cfg, adapter }) {
5869
6777
  }
5870
6778
  var DEFAULT_ON_MARKET_OPTS = { deadlineMs: 2e3, timeoutMs: 2500 };
5871
6779
  var MARKETS_CMD_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
5872
- async function marketSignalsFor(ctx, matches, opts = {}) {
5873
- if (ctx.marketProvider) return (await getMarketSignals(ctx.marketProvider, matches, opts)).signals;
6780
+ async function marketSignalsFor(ctx, matches, opts = {}, providerFactory = makeMarketProvider) {
6781
+ if (ctx.marketProvider) {
6782
+ const b = await getMarketSignals(ctx.marketProvider, matches, opts);
6783
+ return { signals: resolvedValues(b), complete: b.complete };
6784
+ }
5874
6785
  const source = resolveMarketSource();
5875
6786
  if (source !== "polymarket") {
5876
- return (await getMarketSignals(makeMarketProvider(source), matches, opts)).signals;
6787
+ const b = await getMarketSignals(providerFactory(source), matches, opts);
6788
+ return { signals: resolvedValues(b), complete: b.complete };
5877
6789
  }
5878
6790
  const competition = resolveCompetition();
5879
6791
  const { signals: cached, checked: cachedIds } = readMarketCache("polymarket", competition);
@@ -5881,35 +6793,39 @@ async function marketSignalsFor(ctx, matches, opts = {}) {
5881
6793
  const miss = [];
5882
6794
  for (const m of matches) {
5883
6795
  const hit = cached.get(m.id);
5884
- if (hit) result.set(m.id, hit);
5885
- else if (!cachedIds.has(m.id)) miss.push(m);
6796
+ if (hit && marketSignalRendersFor(m, hit)) {
6797
+ result.set(m.id, hit);
6798
+ continue;
6799
+ }
6800
+ if (!hit && cachedIds.has(m.id)) continue;
6801
+ miss.push(m);
5886
6802
  }
6803
+ let complete = true;
5887
6804
  if (miss.length > 0) {
5888
- const { signals: fetched, checked } = await getMarketSignals(
5889
- makeMarketProvider("polymarket"),
5890
- miss,
5891
- opts
5892
- );
5893
- writeMarketCache("polymarket", competition, [...checked], fetched);
6805
+ const batch = await getMarketSignals(providerFactory("polymarket"), miss, opts);
6806
+ const fetched = resolvedValues(batch);
6807
+ complete = batch.complete;
6808
+ writeMarketCache("polymarket", competition, [...cacheableKeys(batch)], fetched);
5894
6809
  for (const [id, s] of fetched) result.set(id, s);
5895
6810
  }
5896
- return result;
6811
+ return { signals: result, complete };
5897
6812
  }
5898
6813
  async function reliableMarketSignals(ctx, matches) {
5899
- if (ctx.cfg.markets === false) return /* @__PURE__ */ new Map();
6814
+ if (ctx.cfg.markets === false) return { signals: /* @__PURE__ */ new Map(), complete: true };
5900
6815
  const now = ctx.now ?? /* @__PURE__ */ new Date();
5901
6816
  const relevant = matches.filter((m) => marketRelevant(m, now));
5902
- if (relevant.length === 0) return /* @__PURE__ */ new Map();
6817
+ if (relevant.length === 0) return { signals: /* @__PURE__ */ new Map(), complete: true };
5903
6818
  const raw = await marketSignalsFor(ctx, relevant, DEFAULT_ON_MARKET_OPTS);
5904
6819
  const out2 = /* @__PURE__ */ new Map();
5905
- for (const [id, s] of raw) {
6820
+ for (const [id, s] of raw.signals) {
5906
6821
  const m = relevant.find((x) => x.id === id);
5907
6822
  if (m && isReliableMarketSignal(s, { now }) && marketSignalRendersFor(m, s)) out2.set(id, s);
5908
6823
  }
5909
- return out2;
6824
+ return { signals: out2, complete: raw.complete };
5910
6825
  }
5911
6826
  async function reliableMarketSignalFor(ctx, match) {
5912
- return (await reliableMarketSignals(ctx, [match])).get(match.id);
6827
+ const result = await reliableMarketSignals(ctx, [match]);
6828
+ return { signal: result.signals.get(match.id), complete: result.complete };
5913
6829
  }
5914
6830
  function out(line2 = "") {
5915
6831
  process.stdout.write(line2 + "\n");
@@ -5968,14 +6884,15 @@ async function cmdToday(date, ctx) {
5968
6884
  const targetDate = date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz);
5969
6885
  const { matches, degraded, source } = await getMatchesForDate(adapter, targetDate);
5970
6886
  const todays = fixturesByDate(targetDate, matches, cfg.tz);
5971
- const signals = await reliableMarketSignals(ctx, todays);
6887
+ const market = await reliableMarketSignals(ctx, todays);
5972
6888
  if (cfg.json) {
5973
6889
  emitJson({
5974
6890
  date: targetDate,
5975
6891
  degraded,
5976
6892
  source: source ?? null,
5977
6893
  matches: todays,
5978
- marketSignals: Object.fromEntries(signals)
6894
+ marketComplete: market.complete,
6895
+ marketSignals: Object.fromEntries(market.signals)
5979
6896
  });
5980
6897
  return;
5981
6898
  }
@@ -5990,16 +6907,19 @@ async function cmdToday(date, ctx) {
5990
6907
  } else {
5991
6908
  for (const m of todays) {
5992
6909
  out(matchLine(m, cfg, t2, c, flags));
5993
- const s = signals.get(m.id);
6910
+ const s = market.signals.get(m.id);
5994
6911
  if (s) out(" " + c.dim(marketLine(s, m)));
5995
6912
  }
5996
6913
  }
6914
+ if (!market.complete) {
6915
+ out(c.dim(" Market data unavailable or incomplete \u2014 not all fixtures were checked."));
6916
+ }
5997
6917
  out();
5998
6918
  if (degraded) out(c.dim(" " + t2("feed.degraded")));
5999
6919
  const src = dataSource(source, cfg.lang, c);
6000
6920
  if (src) out(src);
6001
6921
  out(disclaimer(t2, c));
6002
- maybeStarNudge(ctx);
6922
+ endScoreCommand(ctx);
6003
6923
  }
6004
6924
  async function cmdLive(ctx) {
6005
6925
  const { cfg, t: t2 } = ctx;
@@ -6026,7 +6946,7 @@ async function cmdLive(ctx) {
6026
6946
  const src = dataSource(source, cfg.lang, c);
6027
6947
  if (src) out(src);
6028
6948
  out(disclaimer(t2, c));
6029
- maybeStarNudge(ctx);
6949
+ endScoreCommand(ctx);
6030
6950
  }
6031
6951
  async function cmdNext(team, ctx) {
6032
6952
  const { cfg, t: t2, now } = ctx;
@@ -6048,6 +6968,7 @@ async function cmdNext(team, ctx) {
6048
6968
  out(c.dim(" " + (degraded ? t2("live.degraded") : t2("next.none", { team: code }))));
6049
6969
  out();
6050
6970
  out(disclaimer(t2, c));
6971
+ endScoreCommand(ctx);
6051
6972
  return;
6052
6973
  }
6053
6974
  out(header(t2("next.label", { team: code }), c));
@@ -6063,7 +6984,7 @@ async function cmdNext(team, ctx) {
6063
6984
  const src = dataSource(source, cfg.lang, c);
6064
6985
  if (src) out(src);
6065
6986
  out(disclaimer(t2, c));
6066
- maybeStarNudge(ctx);
6987
+ endScoreCommand(ctx);
6067
6988
  }
6068
6989
  function cmdTeam(query, ctx) {
6069
6990
  const { cfg, t: t2 } = ctx;
@@ -6077,9 +6998,9 @@ function cmdTeam(query, ctx) {
6077
6998
  const c = painterFor(cfg);
6078
6999
  const flags = flagsEnabled();
6079
7000
  const label = (tm) => {
6080
- const flag = flags ? `${tm.flag} ` : "";
7001
+ const flag2 = flags ? `${tm.flag} ` : "";
6081
7002
  const grp = tm.group ? ` \xB7 ${t2("team.group", { group: tm.group })}` : "";
6082
- return ` ${flag}${c.bold(tm.name)} ${c.dim(tm.code + grp)}`;
7003
+ return ` ${flag2}${c.bold(tm.name)} ${c.dim(tm.code + grp)}`;
6083
7004
  };
6084
7005
  out();
6085
7006
  if (!q) {
@@ -6115,11 +7036,12 @@ async function cmdTable(group, ctx) {
6115
7036
  out();
6116
7037
  out(
6117
7038
  c.dim(
6118
- " " + (group ? t2("table.none", { group: group.toUpperCase() }) : t2("table.empty"))
7039
+ " " + (degraded ? t2("table.unavailable") : group ? t2("table.none", { group: group.toUpperCase() }) : t2("table.empty"))
6119
7040
  )
6120
7041
  );
6121
7042
  out();
6122
- if (degraded) out(c.dim(" " + t2("table.degraded")));
7043
+ const src2 = dataSource(source, cfg.lang, c);
7044
+ if (src2) out(src2);
6123
7045
  out(disclaimer(t2, c));
6124
7046
  return;
6125
7047
  }
@@ -6221,7 +7143,15 @@ function cmdPrompt({ cfg }, io = {}) {
6221
7143
  const maxRaw = Number.parseInt(process.env.CLAUDINHO_MAX ?? "", 10);
6222
7144
  const max = Number.isFinite(maxRaw) && maxRaw > 0 ? maxRaw : void 0;
6223
7145
  const state = readCurrentState(cfg.source, resolveCompetition());
6224
- const scoreLine = renderPrompt(state, { team, compact, max, flags: flagsEnabled() });
7146
+ const scoreLine = renderPrompt(state, {
7147
+ team,
7148
+ compact,
7149
+ max,
7150
+ flags: flagsEnabled(),
7151
+ // The bundled schedule describes the default competition only — see the
7152
+ // sign-off gate in renderPrompt.
7153
+ defaultCompetition: resolveCompetition() === DEFAULT_COMPETITION
7154
+ });
6225
7155
  out(renderPromptOutput(scoreLine, payload));
6226
7156
  if (!state && !isLockFresh() || shouldRefresh(Date.now(), state) || shouldRefreshFixtures(Date.now(), state)) {
6227
7157
  spawnRefresh(cfg.source);
@@ -6322,13 +7252,14 @@ async function cmdMatch(id, ctx) {
6322
7252
  const { cfg, t: t2 } = ctx;
6323
7253
  precheck(cfg, t2);
6324
7254
  const { match, degraded, source: liveSource } = await getMatchById(adapterFor(ctx), id);
6325
- const marketSignal = match ? await reliableMarketSignalFor(ctx, match) : void 0;
7255
+ const market = match ? await reliableMarketSignalFor(ctx, match) : { signal: void 0, complete: true };
6326
7256
  if (cfg.json) {
6327
7257
  emitJson({
6328
7258
  degraded,
6329
7259
  match: match ?? null,
6330
7260
  source: liveSource ?? null,
6331
- marketSignal: marketSignal ?? null
7261
+ marketComplete: market.complete,
7262
+ marketSignal: market.signal ?? null
6332
7263
  });
6333
7264
  return;
6334
7265
  }
@@ -6356,9 +7287,13 @@ async function cmdMatch(id, ctx) {
6356
7287
  out(` ${e.minute}' ${e.type} ${e.teamCode}${e.player ? ` \u2014 ${e.player}` : ""}`);
6357
7288
  }
6358
7289
  }
6359
- if (marketSignal) {
7290
+ if (market.signal) {
6360
7291
  out();
6361
- for (const mline of marketBlock(marketSignal, match)) out(" " + c.dim(mline));
7292
+ for (const mline of marketBlock(market.signal, match)) out(" " + c.dim(mline));
7293
+ }
7294
+ if (!market.complete) {
7295
+ out();
7296
+ out(c.dim(" Market data unavailable or incomplete \u2014 this match could not be checked."));
6362
7297
  }
6363
7298
  out();
6364
7299
  if (degraded) out(c.dim(" " + t2("feed.degraded")));
@@ -6389,14 +7324,16 @@ async function cmdMarkets(target, team, ctx) {
6389
7324
  const code = resolveTeamArg(team, "Usage: claudinho markets next <team> (or set CLAUDINHO_TEAM)", t2);
6390
7325
  const now2 = ctx.now ?? /* @__PURE__ */ new Date();
6391
7326
  const { match: fixture, degraded } = await marketFixtureForTeam(adapterFor(ctx), code, now2);
6392
- const sig = fixture && marketRelevant(fixture, now2) ? (await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS)).get(fixture.id) : void 0;
6393
- const shown = fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
7327
+ const market = fixture && marketRelevant(fixture, now2) ? await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS) : { signals: /* @__PURE__ */ new Map(), complete: true };
7328
+ const sig = fixture ? market.signals.get(fixture.id) : void 0;
7329
+ const shown = market.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
6394
7330
  if (cfg.json) {
6395
7331
  emitJson({
6396
7332
  team: code,
6397
7333
  matchId: fixture?.id ?? null,
6398
7334
  degraded,
6399
7335
  informationalOnly: true,
7336
+ complete: market.complete,
6400
7337
  signal: shown ?? null
6401
7338
  });
6402
7339
  return;
@@ -6409,7 +7346,9 @@ async function cmdMarkets(target, team, ctx) {
6409
7346
  out(header(marketHeaderLine(fixture, cfg), c2));
6410
7347
  out();
6411
7348
  if (shown) printMarketBlock(fixture, shown, c2);
6412
- else out(c2.dim(" " + noSignalLine(fixture, now2)));
7349
+ else if (!market.complete) {
7350
+ out(c2.dim(" Market data unavailable or incomplete \u2014 this match could not be checked."));
7351
+ } else out(c2.dim(" " + noSignalLine(fixture, now2)));
6413
7352
  }
6414
7353
  out();
6415
7354
  out(disclaimer(t2, c2));
@@ -6420,10 +7359,16 @@ async function cmdMarkets(target, team, ctx) {
6420
7359
  precheck(cfg, t2);
6421
7360
  const now2 = ctx.now ?? /* @__PURE__ */ new Date();
6422
7361
  const { match } = await getMatchById(adapterFor(ctx), target);
6423
- const sig = match && marketRelevant(match, now2) ? (await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS)).get(match.id) : void 0;
6424
- const shown = match && sig && marketDisplayable(match, sig) ? sig : void 0;
7362
+ const market = match && marketRelevant(match, now2) ? await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS) : { signals: /* @__PURE__ */ new Map(), complete: true };
7363
+ const sig = match ? market.signals.get(match.id) : void 0;
7364
+ const shown = market.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
6425
7365
  if (cfg.json) {
6426
- emitJson({ matchId: target, informationalOnly: true, signal: shown ?? null });
7366
+ emitJson({
7367
+ matchId: target,
7368
+ informationalOnly: true,
7369
+ complete: market.complete,
7370
+ signal: shown ?? null
7371
+ });
6427
7372
  return;
6428
7373
  }
6429
7374
  const c2 = painterFor(cfg);
@@ -6434,7 +7379,9 @@ async function cmdMarkets(target, team, ctx) {
6434
7379
  out(header(marketHeaderLine(match, cfg), c2));
6435
7380
  out();
6436
7381
  if (shown) printMarketBlock(match, shown, c2);
6437
- else out(c2.dim(" " + noSignalLine(match, now2)));
7382
+ else if (!market.complete) {
7383
+ out(c2.dim(" Market data unavailable or incomplete \u2014 this match could not be checked."));
7384
+ } else out(c2.dim(" " + noSignalLine(match, now2)));
6438
7385
  }
6439
7386
  out();
6440
7387
  out(disclaimer(t2, c2));
@@ -6448,14 +7395,14 @@ async function cmdMarkets(target, team, ctx) {
6448
7395
  const { matches } = await getMatchesForDate(adapterFor(ctx), date);
6449
7396
  const todays = fixturesByDate(date, matches, cfg.tz);
6450
7397
  const relevant = todays.filter((m) => marketRelevant(m, now));
6451
- const signals = await marketSignalsFor(ctx, relevant, MARKETS_CMD_OPTS);
7398
+ const { signals, complete } = await marketSignalsFor(ctx, relevant, MARKETS_CMD_OPTS);
6452
7399
  const rows = relevant.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
6453
7400
  (r) => !!r.signal && marketDisplayable(r.match, r.signal)
6454
7401
  );
6455
7402
  if (cfg.json) {
6456
7403
  const marketSignals = {};
6457
7404
  for (const r of rows) marketSignals[r.match.id] = r.signal;
6458
- emitJson({ date, informationalOnly: true, marketSignals });
7405
+ emitJson({ date, informationalOnly: true, complete, marketSignals });
6459
7406
  return;
6460
7407
  }
6461
7408
  const c = painterFor(cfg);
@@ -6463,13 +7410,21 @@ async function cmdMarkets(target, team, ctx) {
6463
7410
  out(header(`Market signals \xB7 ${date}`, c));
6464
7411
  out();
6465
7412
  if (rows.length === 0) {
6466
- out(c.dim(` No market signals available for ${date}.`));
7413
+ out(
7414
+ c.dim(
7415
+ complete ? ` No market signals available for ${date}.` : ` Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`
7416
+ )
7417
+ );
6467
7418
  } else {
6468
7419
  for (const { match, signal } of rows) {
6469
7420
  out(" " + c.bold(marketHeaderLine(match, cfg)));
6470
7421
  printMarketBlock(match, signal, c);
6471
7422
  out();
6472
7423
  }
7424
+ if (!complete) {
7425
+ out(c.dim(` Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`));
7426
+ out();
7427
+ }
6473
7428
  }
6474
7429
  out(disclaimer(t2, c));
6475
7430
  out(c.dim(MARKET_INFO));
@@ -6480,11 +7435,11 @@ function pickShareStyle(v) {
6480
7435
  async function reliableShareSignals(ctx, matches) {
6481
7436
  const raw = await reliableMarketSignals(ctx, matches);
6482
7437
  const out2 = /* @__PURE__ */ new Map();
6483
- for (const [id, s] of raw) {
7438
+ for (const [id, s] of raw.signals) {
6484
7439
  const m = matches.find((x) => x.id === id);
6485
7440
  if (m && marketDisplayable(m, s)) out2.set(id, s);
6486
7441
  }
6487
- return out2;
7442
+ return { signals: out2, complete: raw.complete };
6488
7443
  }
6489
7444
  function emitShare(ctx, e, copy) {
6490
7445
  const snippet = formatShareSnippet(e.input, e.options);
@@ -6499,6 +7454,7 @@ function emitShare(ctx, e, copy) {
6499
7454
  style: e.options.style ?? "social",
6500
7455
  snippet,
6501
7456
  matches: e.input.matches,
7457
+ marketComplete: e.input.marketComplete ?? true,
6502
7458
  marketSignals: Object.fromEntries(e.input.marketSignals ?? /* @__PURE__ */ new Map())
6503
7459
  });
6504
7460
  } else {
@@ -6618,11 +7574,12 @@ async function cmdShare(target, team, opts, ctx) {
6618
7574
  {
6619
7575
  group,
6620
7576
  tables,
6621
- // Degraded ⇒ a static roster, served by no live provider: no attribution.
7577
+ // Degraded ⇒ no live provider: no attribution. Open-scope outages
7578
+ // have no compatible bundled roster, so name the outage in the empty card.
6622
7579
  source: degraded2 ? void 0 : source2,
6623
7580
  degraded: degraded2,
6624
7581
  installLine: group ? `npx @claudinho/cli table ${group}` : "npx @claudinho/cli table",
6625
- emptyNote: group ? `No group ${group}.` : "No standings available.",
7582
+ emptyNote: degraded2 ? "Live standings unavailable." : group ? `No group ${group}.` : "No standings available.",
6626
7583
  options: baseOptions
6627
7584
  },
6628
7585
  copy
@@ -6669,7 +7626,7 @@ async function cmdShare(target, team, opts, ctx) {
6669
7626
  ctx.now ?? /* @__PURE__ */ new Date()
6670
7627
  );
6671
7628
  const matches = fixture ? [fixture] : [];
6672
- const signals2 = await reliableShareSignals(ctx, matches);
7629
+ const market2 = await reliableShareSignals(ctx, matches);
6673
7630
  const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
6674
7631
  emitShare(
6675
7632
  ctx,
@@ -6680,7 +7637,8 @@ async function cmdShare(target, team, opts, ctx) {
6680
7637
  input: {
6681
7638
  title: `Next up for ${teamName}`,
6682
7639
  matches,
6683
- marketSignals: signals2,
7640
+ marketSignals: market2.signals,
7641
+ marketComplete: market2.complete,
6684
7642
  // Attribute the provider when the overlay resolved the tie (knockout);
6685
7643
  // undefined for a static group fixture — parity with CLI `next`.
6686
7644
  source: source2,
@@ -6701,7 +7659,7 @@ async function cmdShare(target, team, opts, ctx) {
6701
7659
  precheck(cfg, t2);
6702
7660
  const { match, degraded: degraded2, source: source2 } = await getMatchById(adapterFor(ctx), target);
6703
7661
  const matches = match ? [match] : [];
6704
- const signals2 = await reliableShareSignals(ctx, matches);
7662
+ const market2 = await reliableShareSignals(ctx, matches);
6705
7663
  emitShare(
6706
7664
  ctx,
6707
7665
  {
@@ -6710,7 +7668,8 @@ async function cmdShare(target, team, opts, ctx) {
6710
7668
  input: {
6711
7669
  title: "Match pulse",
6712
7670
  matches,
6713
- marketSignals: signals2,
7671
+ marketSignals: market2.signals,
7672
+ marketComplete: market2.complete,
6714
7673
  source: source2,
6715
7674
  degraded: degraded2,
6716
7675
  emptyNote: `No match found with id ${target}.`,
@@ -6729,7 +7688,7 @@ async function cmdShare(target, team, opts, ctx) {
6729
7688
  const date = explicitDate ?? localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz);
6730
7689
  const { matches: all, degraded, source } = await getMatchesForDate(adapterFor(ctx), date);
6731
7690
  const todays = fixturesByDate(date, all, cfg.tz);
6732
- const signals = await reliableShareSignals(ctx, todays);
7691
+ const market = await reliableShareSignals(ctx, todays);
6733
7692
  const human = formatDate(`${date}T12:00:00.000Z`, { tz: cfg.tz, locale: cfg.lang });
6734
7693
  const title = explicitDate ? `Matches \xB7 ${human}` : `Today's matches \xB7 ${human}`;
6735
7694
  emitShare(
@@ -6740,7 +7699,8 @@ async function cmdShare(target, team, opts, ctx) {
6740
7699
  input: {
6741
7700
  title,
6742
7701
  matches: todays,
6743
- marketSignals: signals,
7702
+ marketSignals: market.signals,
7703
+ marketComplete: market.complete,
6744
7704
  source,
6745
7705
  degraded,
6746
7706
  emptyNote: `No matches scheduled for ${human}.`,
@@ -6813,6 +7773,22 @@ function maybeStarNudge(ctx) {
6813
7773
  out();
6814
7774
  out(c.dim(` \u2B50 Enjoying Claudinho? Star it \u2192 ${REPO_URL} (claudinho star)`));
6815
7775
  }
7776
+ function endScoreCommand(ctx) {
7777
+ const over = resolveCompetition() === DEFAULT_COMPETITION && isTournamentWindowOver(ctx.now?.getTime() ?? Date.now());
7778
+ if (over) printTournamentSignOff(ctx);
7779
+ else maybeStarNudge(ctx);
7780
+ }
7781
+ function printTournamentSignOff(ctx) {
7782
+ const { cfg, t: t2 } = ctx;
7783
+ if (cfg.json) return;
7784
+ const c = painterFor(cfg);
7785
+ out();
7786
+ out(`${t2("signoff.complete")} ${SHARE_HASHTAG}`);
7787
+ if (!process.stdout.isTTY || process.env.CLAUDINHO_NO_STAR) return;
7788
+ out();
7789
+ out(c.dim(`\u2B50 ${t2("signoff.star")}`));
7790
+ out(c.cyan(REPO_URL));
7791
+ }
6816
7792
  function printInitStarCta(cfg) {
6817
7793
  if (cfg.json || !process.stdout.isTTY || process.env.CLAUDINHO_NO_STAR) return;
6818
7794
  const c = painterFor(cfg);
@@ -6827,7 +7803,7 @@ function cmdVibe(ctx) {
6827
7803
  try {
6828
7804
  const state = readCurrentState(cfg.source, resolveCompetition());
6829
7805
  liveSeg = vibeLiveSegment(
6830
- liveMatchesFromCache(state, (ctx.now ?? /* @__PURE__ */ new Date()).getTime()),
7806
+ liveMatchesFromCache(state, (ctx.now ?? /* @__PURE__ */ new Date()).getTime()).items,
6831
7807
  // Name-or-code, matching the statusline/hook (offline lookup).
6832
7808
  resolveEnvTeam(process.env.CLAUDINHO_TEAM)
6833
7809
  );
@@ -6853,7 +7829,7 @@ function handlePipeError(stream) {
6853
7829
  }
6854
7830
  handlePipeError(process.stdout);
6855
7831
  handlePipeError(process.stderr);
6856
- var VERSION = "0.9.2";
7832
+ var VERSION = "0.9.4";
6857
7833
  var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
6858
7834
  function ctxFrom(cmd) {
6859
7835
  let root = cmd;