@claudinho/mcp 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -1
  2. package/dist/index.js +1385 -376
  3. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -5,7 +5,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
 
6
6
  // src/server.ts
7
7
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { z } from "zod";
8
+ import { types as utilTypes } from "util";
9
+ import { z } from "zod/v3";
9
10
 
10
11
  // ../core/dist/index.js
11
12
  var REGIONAL_INDICATOR_A = 127462;
@@ -158,10 +159,11 @@ var ALIASES = [
158
159
  ["Burma", "MM"],
159
160
  ["Cape Verde", "CV"]
160
161
  ];
161
- var BY_NATION = Object.fromEntries(
162
- [...NATIONS, ...ALIASES].map(([name, code]) => [norm(name), code])
162
+ var BY_NATION = Object.assign(
163
+ /* @__PURE__ */ Object.create(null),
164
+ Object.fromEntries([...NATIONS, ...ALIASES].map(([name, code]) => [norm(name), code]))
163
165
  );
164
- var BY_CODE = {
166
+ var BY_CODE = Object.assign(/* @__PURE__ */ Object.create(null), {
165
167
  MEX: "MX",
166
168
  RSA: "ZA",
167
169
  KOR: "KR",
@@ -222,7 +224,7 @@ var BY_CODE = {
222
224
  HON: "HN",
223
225
  COD: "CD",
224
226
  MLI: "ML"
225
- };
227
+ });
226
228
  function nationToFlag(nameOrCode) {
227
229
  const region = nationToRegion(nameOrCode);
228
230
  return region ? flagEmoji(region) : NEUTRAL;
@@ -230,7 +232,7 @@ function nationToFlag(nameOrCode) {
230
232
  var INTL_BY_NAME;
231
233
  function intlNameMap() {
232
234
  if (INTL_BY_NAME) return INTL_BY_NAME;
233
- const map = {};
235
+ const map = /* @__PURE__ */ Object.create(null);
234
236
  try {
235
237
  const dn = new Intl.DisplayNames(["en"], { type: "region" });
236
238
  for (let a = 65; a <= 90; a++) {
@@ -255,11 +257,8 @@ function intlNameMap() {
255
257
  }
256
258
  function nationToRegion(nameOrCode) {
257
259
  if (!nameOrCode) return void 0;
258
- const byName = BY_NATION[norm(nameOrCode)];
259
- if (byName) return byName;
260
- const byCode = BY_CODE[nameOrCode.trim().toUpperCase()];
261
- if (byCode) return byCode;
262
- return intlNameMap()[norm(nameOrCode)];
260
+ const str = (v) => typeof v === "string" && v ? v : void 0;
261
+ return str(BY_NATION[norm(nameOrCode)]) ?? str(BY_CODE[nameOrCode.trim().toUpperCase()]) ?? str(intlNameMap()[norm(nameOrCode)]);
263
262
  }
264
263
  var EN = {
265
264
  "bracket.title": "Knockout bracket",
@@ -279,6 +278,7 @@ var EN = {
279
278
  "bracket.slot.loser": "{stage} {n} loser",
280
279
  "bracket.slot.tbd": "TBD",
281
280
  "live.data": "Live data: {source}",
281
+ "standings.unavailable": "Live standings unavailable.",
282
282
  "share.tryIt": "Try it: {line}",
283
283
  "stage.group": "Group {group}",
284
284
  "stage.groupStage": "Group stage",
@@ -308,6 +308,7 @@ var ES = {
308
308
  "bracket.slot.loser": "Perdedor {stage} {n}",
309
309
  "bracket.slot.tbd": "Por definir",
310
310
  "live.data": "Datos en vivo: {source}",
311
+ "standings.unavailable": "Tabla en vivo no disponible.",
311
312
  "share.tryIt": "Pru\xE9balo: {line}",
312
313
  "stage.group": "Grupo {group}",
313
314
  "stage.groupStage": "Fase de grupos",
@@ -337,6 +338,7 @@ var PT = {
337
338
  "bracket.slot.loser": "Perdedor {stage} {n}",
338
339
  "bracket.slot.tbd": "A definir",
339
340
  "live.data": "Dados ao vivo: {source}",
341
+ "standings.unavailable": "Classifica\xE7\xE3o ao vivo indispon\xEDvel.",
340
342
  "share.tryIt": "Experimente: {line}",
341
343
  "stage.group": "Grupo {group}",
342
344
  "stage.groupStage": "Fase de grupos",
@@ -366,6 +368,7 @@ var FR = {
366
368
  "bracket.slot.loser": "Perdant {stage} {n}",
367
369
  "bracket.slot.tbd": "\xC0 d\xE9finir",
368
370
  "live.data": "Donn\xE9es en direct : {source}",
371
+ "standings.unavailable": "Classement en direct indisponible.",
369
372
  "share.tryIt": "Essayez : {line}",
370
373
  "stage.group": "Groupe {group}",
371
374
  "stage.groupStage": "Phase de groupes",
@@ -450,7 +453,14 @@ function safeLocale(locale) {
450
453
  return "en";
451
454
  }
452
455
  }
456
+ function parsedDate(iso) {
457
+ const t2 = Date.parse(iso);
458
+ return Number.isFinite(t2) ? new Date(t2) : void 0;
459
+ }
460
+ var UNKNOWN_TIME = "\u2014";
453
461
  function formatKickoff(iso, opts = {}) {
462
+ const when = parsedDate(iso);
463
+ if (!when) return UNKNOWN_TIME;
454
464
  const tz = resolveTz(opts.tz);
455
465
  const locale = safeLocale(opts.locale);
456
466
  return new Intl.DateTimeFormat(locale, {
@@ -460,18 +470,22 @@ function formatKickoff(iso, opts = {}) {
460
470
  minute: "2-digit",
461
471
  hour12: false,
462
472
  timeZone: tz
463
- }).format(new Date(iso));
473
+ }).format(when);
464
474
  }
465
475
  function formatDate(iso, opts = {}) {
476
+ const when = parsedDate(iso);
477
+ if (!when) return UNKNOWN_TIME;
466
478
  const tz = resolveTz(opts.tz);
467
479
  const locale = safeLocale(opts.locale);
468
480
  return new Intl.DateTimeFormat(locale, {
469
481
  month: "short",
470
482
  day: "numeric",
471
483
  timeZone: tz
472
- }).format(new Date(iso));
484
+ }).format(when);
473
485
  }
474
486
  function formatTime(iso, opts = {}) {
487
+ const when = parsedDate(iso);
488
+ if (!when) return UNKNOWN_TIME;
475
489
  const tz = resolveTz(opts.tz);
476
490
  const locale = safeLocale(opts.locale);
477
491
  return new Intl.DateTimeFormat(locale, {
@@ -479,10 +493,12 @@ function formatTime(iso, opts = {}) {
479
493
  minute: "2-digit",
480
494
  hour12: false,
481
495
  timeZone: tz
482
- }).format(new Date(iso));
496
+ }).format(when);
483
497
  }
484
498
  function countdown(iso, from = /* @__PURE__ */ new Date()) {
485
- const ms = new Date(iso).getTime() - from.getTime();
499
+ const when = parsedDate(iso);
500
+ if (!when) return UNKNOWN_TIME;
501
+ const ms = when.getTime() - from.getTime();
486
502
  if (ms <= 0) return "now";
487
503
  const totalMin = Math.floor(ms / 6e4);
488
504
  const days = Math.floor(totalMin / 1440);
@@ -493,41 +509,43 @@ function countdown(iso, from = /* @__PURE__ */ new Date()) {
493
509
  return `${mins}m`;
494
510
  }
495
511
  function localDate(iso, tz) {
512
+ const when = parsedDate(iso);
513
+ if (!when) return "";
496
514
  const zone = resolveTz(tz);
497
515
  return new Intl.DateTimeFormat("en-CA", {
498
516
  year: "numeric",
499
517
  month: "2-digit",
500
518
  day: "2-digit",
501
519
  timeZone: zone
502
- }).format(new Date(iso));
520
+ }).format(when);
503
521
  }
504
522
  function shiftUtcDate(dateISO, days) {
505
523
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
506
524
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
507
525
  }
508
- var FEED_TEXT_MAX = 100;
509
- function sanitizeFeedText(value, max = FEED_TEXT_MAX) {
510
- let out = "";
511
- let count = 0;
512
- for (const ch of String(value)) {
513
- const cp = ch.codePointAt(0) ?? 0;
514
- const isWhitespaceControl = cp === 9 || cp === 10 || cp === 13;
515
- if ((cp <= 31 || cp >= 127 && cp <= 159) && !isWhitespaceControl) continue;
516
- if (count >= max) break;
517
- out += isWhitespaceControl ? " " : ch;
518
- count++;
519
- }
520
- return out;
521
- }
522
526
  var segmenter = new Intl.Segmenter();
523
527
  var WIDE_CLUSTER = new RegExp("^(?:\\p{Regional_Indicator}|\\p{Extended_Pictographic})", "u");
528
+ var WIDE_BASE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏ꥠ-꥿가-힣豈-﫿︐-︙︰-﹯＀-⦆¢-₩\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}]/u;
529
+ var KEYCAP = /\u{20E3}/u;
530
+ var ZERO_WIDTH_BASE = /[\p{Mn}\p{Me}\p{Cf}\p{Cc}]/u;
531
+ function clusterWidth(segment) {
532
+ if (WIDE_CLUSTER.test(segment) || KEYCAP.test(segment)) return 2;
533
+ const first = segment.codePointAt(0);
534
+ if (first === void 0) return 0;
535
+ const base = String.fromCodePoint(first);
536
+ if (ZERO_WIDTH_BASE.test(base)) return 0;
537
+ return WIDE_BASE.test(base) ? 2 : 1;
538
+ }
524
539
  function displayWidth(s) {
525
540
  let w = 0;
526
541
  for (const { segment } of segmenter.segment(s)) {
527
- w += WIDE_CLUSTER.test(segment) ? 2 : 1;
542
+ w += clusterWidth(segment);
528
543
  }
529
544
  return w;
530
545
  }
546
+ function* graphemes(s) {
547
+ for (const { segment } of segmenter.segment(s)) yield segment;
548
+ }
531
549
  function padVisible(s, width) {
532
550
  const w = displayWidth(s);
533
551
  return w >= width ? s : s + " ".repeat(width - w);
@@ -2906,47 +2924,291 @@ function rosterAtZero(matches) {
2906
2924
  }
2907
2925
  return [...teams.values()].sort((a, b) => a.name.localeCompare(b.name)).map(blankRow);
2908
2926
  }
2909
- var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
2910
- var DEFAULT_COMPETITION = "fifa.world";
2911
- var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
2912
- var USER_AGENT = `claudinho/${"0.9.3"} (+https://github.com/arturogarrido/claudinho)`;
2913
- var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
2914
- function competitionBase(slug) {
2915
- return `${ESPN_SOCCER}/${slug}`;
2927
+ function bounded(items, max, complete = true) {
2928
+ const kept = items.length > max ? items.slice(0, max) : items;
2929
+ return {
2930
+ items: kept,
2931
+ total: items.length,
2932
+ shown: kept.length,
2933
+ truncated: items.length > kept.length,
2934
+ complete
2935
+ };
2916
2936
  }
2917
- var DEFAULT_TIMEOUT_MS = 6e3;
2918
- var STANDINGS_SHARE_MS = 3e4;
2919
- var ProviderError = class extends Error {
2920
- kind;
2921
- status;
2922
- constructor(message, kind, status) {
2923
- super(message);
2924
- this.name = "ProviderError";
2925
- this.kind = kind;
2926
- this.status = status;
2937
+ function takeBounded(value, max) {
2938
+ if (!Array.isArray(value)) return [];
2939
+ return value.length > max ? value.slice(0, max) : value;
2940
+ }
2941
+ var valid = (value) => ({ kind: "valid", value });
2942
+ var definitiveNone = (reason) => ({
2943
+ kind: "definitive-none",
2944
+ reason
2945
+ });
2946
+ var malformed = (reason) => ({ kind: "malformed", reason });
2947
+ var ambiguous = (reason) => ({ kind: "ambiguous", reason });
2948
+ var unresolved = (reason) => ({ kind: "unresolved", reason });
2949
+ function parsedValue(r) {
2950
+ return r.kind === "valid" ? r.value : void 0;
2951
+ }
2952
+ function isCacheable(r) {
2953
+ return r.kind === "valid" || r.kind === "definitive-none" || r.kind === "ambiguous";
2954
+ }
2955
+ 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");
2956
+ var EMOJI_IN_LABEL = new RegExp("\\p{Extended_Pictographic}|\\p{Regional_Indicator}|\\p{Emoji_Modifier}|\\u{20E3}", "u");
2957
+ var MAX_LABEL_INPUT_UNITS = 4096;
2958
+ var MAX_LABEL_COLUMNS = 100;
2959
+ function humanLabel(value, maxColumns = MAX_LABEL_COLUMNS) {
2960
+ if (typeof value !== "string" || value === "") return "";
2961
+ const capped = value.length > MAX_LABEL_INPUT_UNITS ? value.slice(0, MAX_LABEL_INPUT_UNITS) : value;
2962
+ return visible(runToFixedPoint(capped, maxColumns));
2963
+ }
2964
+ function visible(label) {
2965
+ return label !== "" && displayWidth(label) === 0 ? "" : label;
2966
+ }
2967
+ function runToFixedPoint(capped, maxColumns) {
2968
+ const first = sealLabelOnce(capped, maxColumns);
2969
+ if (!lastPassDropped) return first;
2970
+ let out = first;
2971
+ for (let pass = 0; pass < 3; pass++) {
2972
+ const again = sealLabelOnce(out, maxColumns);
2973
+ if (again === out) return out;
2974
+ out = again;
2927
2975
  }
2928
- /** 429/403 the upstream is refusing us; retrying at the live cadence makes it worse. */
2929
- get throttled() {
2930
- return this.kind === "http" && (this.status === 429 || this.status === 403);
2976
+ return sealLabelOnce(out, maxColumns) === out ? out : "";
2977
+ }
2978
+ var lastPassDropped = false;
2979
+ function sealLabelOnce(value, maxColumns) {
2980
+ lastPassDropped = false;
2981
+ let normalized;
2982
+ try {
2983
+ normalized = value.normalize("NFC");
2984
+ } catch {
2985
+ return "";
2931
2986
  }
2932
- };
2987
+ const maxCodePoints = Math.max(16, maxColumns * 4);
2988
+ let out = "";
2989
+ let width = 0;
2990
+ let points = 0;
2991
+ for (const cluster of graphemes(normalized)) {
2992
+ if ([...cluster].length > 8) {
2993
+ lastPassDropped = true;
2994
+ continue;
2995
+ }
2996
+ if (EMOJI_IN_LABEL.test(cluster)) {
2997
+ lastPassDropped = true;
2998
+ continue;
2999
+ }
3000
+ let piece = "";
3001
+ for (const ch of cluster) {
3002
+ const cp = ch.codePointAt(0) ?? 0;
3003
+ if (cp === 9 || cp === 10 || cp === 13) {
3004
+ piece += " ";
3005
+ continue;
3006
+ }
3007
+ if (FORBIDDEN_IN_LABEL.test(ch)) {
3008
+ lastPassDropped = true;
3009
+ continue;
3010
+ }
3011
+ piece += ch;
3012
+ }
3013
+ if (!piece) {
3014
+ lastPassDropped = true;
3015
+ continue;
3016
+ }
3017
+ const w = displayWidth(piece);
3018
+ const cps = [...piece].length;
3019
+ if (width + w > maxColumns || points + cps > maxCodePoints) break;
3020
+ out += piece;
3021
+ width += w;
3022
+ points += cps;
3023
+ }
3024
+ try {
3025
+ return out.normalize("NFC").trim();
3026
+ } catch {
3027
+ return out.trim();
3028
+ }
3029
+ }
3030
+ function opaqueId(value, grammar) {
3031
+ return typeof value === "string" && grammar.test(value) ? value : void 0;
3032
+ }
3033
+ var ESPN_ID = /^[0-9]{1,20}$/;
3034
+ var ISO_INSTANT = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:[Zz]|[+-]\d{2}(?::?\d{2})?)$/;
3035
+ var ISO_CANONICAL = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
3036
+ function calendarValid(iso) {
3037
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
3038
+ if (!m) return false;
3039
+ const year = Number(m[1]);
3040
+ const month = Number(m[2]);
3041
+ const day = Number(m[3]);
3042
+ if (month < 1 || month > 12 || day < 1) return false;
3043
+ return day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
3044
+ }
3045
+ function canonicalTimestamp(value) {
3046
+ if (typeof value !== "string" || !ISO_INSTANT.test(value)) return void 0;
3047
+ if (!calendarValid(value)) return void 0;
3048
+ const t2 = Date.parse(value);
3049
+ if (!Number.isFinite(t2)) return void 0;
3050
+ const out = new Date(t2).toISOString();
3051
+ return ISO_CANONICAL.test(out) ? out : void 0;
3052
+ }
3053
+ function productFlag(nameOrCode) {
3054
+ return nationToFlag(nameOrCode);
3055
+ }
3056
+ function count(value, max) {
3057
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= max ? value : void 0;
3058
+ }
3059
+ function quantity(value) {
3060
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
3061
+ }
3062
+ function probability(value) {
3063
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
3064
+ }
3065
+ function member(value, allowed) {
3066
+ return typeof value === "string" && allowed.has(value) ? value : void 0;
3067
+ }
3068
+ function flag(value) {
3069
+ return typeof value === "boolean" ? value : void 0;
3070
+ }
3071
+ var MAX_GOALS = 99;
3072
+ var MAX_MINUTE = 200;
3073
+ var MAX_MATCH_EVENTS = 128;
3074
+ var TEAM_CODE_COLUMNS = 8;
3075
+ var STAGES = /* @__PURE__ */ new Set(["GROUP", "R32", "R16", "QF", "SF", "3P", "F", "FRIENDLY"]);
3076
+ var STATUSES = /* @__PURE__ */ new Set(["SCHEDULED", "LIVE", "HT", "FT", "POSTPONED", "CANCELLED"]);
3077
+ var EVENT_TYPES = /* @__PURE__ */ new Set(["GOAL", "OWN_GOAL", "PEN", "YELLOW", "RED", "SUB"]);
3078
+ function teamCode(raw, fallbackName) {
3079
+ const upper = typeof raw === "string" ? raw.toUpperCase() : raw;
3080
+ const stated = humanLabel(upper, TEAM_CODE_COLUMNS);
3081
+ if (stated) return stated;
3082
+ return humanLabel([...fallbackName].slice(0, 3).join("").toUpperCase(), TEAM_CODE_COLUMNS);
3083
+ }
3084
+ function sealTeam(raw) {
3085
+ if (!raw || typeof raw !== "object") return void 0;
3086
+ const t2 = raw;
3087
+ const name = humanLabel(t2.name);
3088
+ if (!name) return void 0;
3089
+ const code = teamCode(t2.code, name);
3090
+ return { code, name, flag: productFlag(name) };
3091
+ }
3092
+ function sealScorePair(raw) {
3093
+ if (!raw || typeof raw !== "object") return void 0;
3094
+ const v = raw;
3095
+ const home = count(v.home, MAX_GOALS);
3096
+ const away = count(v.away, MAX_GOALS);
3097
+ return home !== void 0 && away !== void 0 ? { home, away } : void 0;
3098
+ }
3099
+ function sealEvent(raw) {
3100
+ if (!raw || typeof raw !== "object") return void 0;
3101
+ const e = raw;
3102
+ const type = member(e.type, EVENT_TYPES);
3103
+ if (!type) return void 0;
3104
+ const minute = count(e.minute, MAX_MINUTE);
3105
+ if (minute === void 0) return void 0;
3106
+ const out = { type, minute, teamCode: humanLabel(e.teamCode, TEAM_CODE_COLUMNS) };
3107
+ const player = humanLabel(e.player);
3108
+ if (player) out.player = player;
3109
+ return out;
3110
+ }
3111
+ function sealMatch(parts, opts = {}) {
3112
+ if (!parts || typeof parts !== "object") return malformed("match is not an object");
3113
+ const id = opaqueId(parts.id, ESPN_ID);
3114
+ if (!id) return malformed("match id is absent or not an identifier");
3115
+ const kickoff = canonicalTimestamp(parts.kickoff);
3116
+ if (!kickoff) return malformed("match kickoff is absent or not one instant");
3117
+ const stage = member(parts.stage, STAGES);
3118
+ if (!stage) return malformed("match stage is not a known stage");
3119
+ const status = member(parts.status, STATUSES);
3120
+ if (!status) return malformed("match status is not a known status");
3121
+ const home = sealTeam(parts.home);
3122
+ const away = sealTeam(parts.away);
3123
+ if (!home || !away) return malformed("match does not name both teams");
3124
+ if (home.code === away.code && home.name === away.name) {
3125
+ return definitiveNone("both competitors are the same team");
3126
+ }
3127
+ const group = humanLabel(parts.group) || void 0;
3128
+ const city = humanLabel(parts.city) || void 0;
3129
+ const country = humanLabel(parts.country) || void 0;
3130
+ const score = status === "SCHEDULED" ? void 0 : sealScorePair(parts.score);
3131
+ if ((status === "LIVE" || status === "HT" || status === "FT") && !score) {
3132
+ return malformed("match claims an unreadable score");
3133
+ }
3134
+ const finished = status === "FT";
3135
+ const canGoToPenalties = stage !== "GROUP";
3136
+ const shootoutPresent = parts.shootout !== void 0 && parts.shootout !== null;
3137
+ const parsedShootout = shootoutPresent ? sealScorePair(parts.shootout) : void 0;
3138
+ const shootoutStatus = status === "LIVE" || status === "FT";
3139
+ const shootout = parsedShootout && score && canGoToPenalties && shootoutStatus && !(finished && parsedShootout.home === parsedShootout.away) ? parsedShootout : void 0;
3140
+ const claimedWinner = teamCode(parts.winnerCode, "");
3141
+ const claimedSide = claimedWinner === home.code ? "home" : claimedWinner === away.code ? "away" : void 0;
3142
+ let winnerCode;
3143
+ const unusableShootoutCouldDecide = canGoToPenalties && shootoutPresent && !shootout;
3144
+ if (claimedSide && score && finished && !unusableShootoutCouldDecide) {
3145
+ const level = score.home === score.away;
3146
+ const decider = shootout ?? score;
3147
+ if (decider && decider.home !== decider.away) {
3148
+ if ((decider.home > decider.away ? "home" : "away") === claimedSide) winnerCode = claimedWinner;
3149
+ } else if (level && !shootoutPresent && canGoToPenalties) {
3150
+ winnerCode = claimedWinner;
3151
+ }
3152
+ }
3153
+ const events = opts.events === false ? [] : takeBounded(parts.events, MAX_MATCH_EVENTS).map(sealEvent).filter((e) => !!e);
3154
+ const out = { id, stage };
3155
+ if (group) out.group = group;
3156
+ out.kickoff = kickoff;
3157
+ out.venue = humanLabel(parts.venue);
3158
+ if (city) out.city = city;
3159
+ if (country) out.country = country;
3160
+ out.home = home;
3161
+ out.away = away;
3162
+ if (score) out.score = score;
3163
+ if (shootout) out.shootout = shootout;
3164
+ const minute = count(parts.minute, MAX_MINUTE);
3165
+ if (minute !== void 0) out.minute = minute;
3166
+ out.status = status;
3167
+ if (events.length) out.events = events;
3168
+ if (winnerCode) out.winnerCode = winnerCode;
3169
+ out.updatedAt = canonicalTimestamp(parts.updatedAt) ?? "";
3170
+ return valid(out);
3171
+ }
3172
+ var MAX_EVENTS = 300;
3173
+ var MAX_GROUPS = 16;
3174
+ var MAX_GROUP_ROWS = 32;
3175
+ var STAGES2 = /* @__PURE__ */ new Set(["GROUP", "R32", "R16", "QF", "SF", "3P", "F", "FRIENDLY"]);
3176
+ function teamNames(t2) {
3177
+ return [t2?.displayName, t2?.name, t2?.location, t2?.shortDisplayName, t2?.abbreviation].map((v) => humanLabel(v)).filter((v) => v !== "");
3178
+ }
3179
+ function toParticipant(raw) {
3180
+ if (!raw || typeof raw !== "object") return malformed("competitor is not an object");
3181
+ const names = teamNames(raw.team);
3182
+ if (names.length === 0) return malformed("competitor names no team");
3183
+ const name = names[0];
3184
+ const code = teamCode(raw.team?.abbreviation, name);
3185
+ const team = { code, name, flag: productFlag(name) };
3186
+ const providerId = opaqueId(raw.team?.id, ESPN_ID);
3187
+ const known = productFlag(name) !== nationToFlag("");
3188
+ return valid(
3189
+ providerId && known ? { kind: "team", providerId, team } : { kind: "slot", team }
3190
+ );
3191
+ }
2933
3192
  function mapStatus(st) {
2934
- const name = (st?.type?.name ?? "").toUpperCase();
2935
- const state = st?.type?.state ?? "";
3193
+ const type = st?.type;
3194
+ const name = typeof type?.name === "string" ? type.name.toUpperCase() : "";
3195
+ const state = typeof type?.state === "string" ? type.state : "";
2936
3196
  if (name.includes("HALFTIME")) return "HT";
2937
3197
  if (name.includes("POSTPONED")) return "POSTPONED";
2938
3198
  if (name.includes("CANCEL")) return "CANCELLED";
2939
3199
  if (state === "pre") return "SCHEDULED";
2940
3200
  if (state === "post") return "FT";
2941
3201
  if (state === "in") return "LIVE";
2942
- return "SCHEDULED";
3202
+ return void 0;
2943
3203
  }
2944
3204
  function parseMinute(st) {
2945
- if (st?.type?.state !== "in") return void 0;
2946
- const dc = st.displayClock?.match(/(\d+)/);
2947
- if (dc) return parseInt(dc[1], 10);
2948
- if (typeof st.clock === "number" && st.clock > 0) {
2949
- return Math.floor(st.clock / 60) || void 0;
3205
+ const s = st;
3206
+ if (s?.type?.state !== "in") return void 0;
3207
+ const dc = typeof s.displayClock === "string" ? s.displayClock.match(/(\d+)/) : null;
3208
+ if (dc) return count(Number.parseInt(dc[1], 10), MAX_MINUTE);
3209
+ if (typeof s.clock === "number" && s.clock > 0) {
3210
+ const n = Math.floor(s.clock / 60);
3211
+ return n > 0 ? count(n, MAX_MINUTE) : void 0;
2950
3212
  }
2951
3213
  return void 0;
2952
3214
  }
@@ -2960,58 +3222,74 @@ var SLUG_TO_STAGE = {
2960
3222
  final: "F"
2961
3223
  };
2962
3224
  function stageFromSlug(slug) {
2963
- if (slug && SLUG_TO_STAGE[slug]) return SLUG_TO_STAGE[slug];
2964
- if (!slug) return "GROUP";
3225
+ if (slug == null || slug === "") return "GROUP";
3226
+ if (typeof slug === "string" && Object.hasOwn(SLUG_TO_STAGE, slug)) {
3227
+ const mapped = SLUG_TO_STAGE[slug];
3228
+ if (mapped) return mapped;
3229
+ }
2965
3230
  return "FRIENDLY";
2966
3231
  }
2967
- function toInt(s) {
2968
- if (s == null || s === "") return void 0;
2969
- const n = parseInt(String(s), 10);
2970
- return Number.isFinite(n) ? n : void 0;
2971
- }
2972
- function toTeam(t2) {
2973
- const name = sanitizeFeedText(
2974
- t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD"
2975
- );
2976
- const code = sanitizeFeedText(t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
2977
- return {
2978
- code,
2979
- name,
2980
- flag: nationToFlag(sanitizeFeedText(t2?.displayName ?? t2?.abbreviation ?? name))
2981
- };
2982
- }
2983
- function mapEspnEvent(ev, ctx = {}) {
2984
- const comp = ev.competitions?.[0];
2985
- const competitors = comp?.competitors ?? [];
2986
- const homeC = competitors.find((c) => c.homeAway === "home") ?? competitors[0];
2987
- const awayC = competitors.find((c) => c.homeAway === "away") ?? competitors[1];
3232
+ function toGoals(v) {
3233
+ if (typeof v === "number") return count(v, MAX_GOALS);
3234
+ if (typeof v !== "string" || v.trim() === "") return void 0;
3235
+ return /^-?\d{1,9}$/.test(v.trim()) ? count(Number(v.trim()), MAX_GOALS) : void 0;
3236
+ }
3237
+ function parseEspnEvent(raw, ctx = {}) {
3238
+ if (!raw || typeof raw !== "object") return malformed("event is not an object");
3239
+ const ev = raw;
3240
+ const id = opaqueId(ev.id, ESPN_ID);
3241
+ if (!id) return malformed("event id is absent or not an identifier");
3242
+ const kickoff = canonicalTimestamp(ev.date);
3243
+ if (!kickoff) return malformed("event date is absent or not one instant");
3244
+ const comp = Array.isArray(ev.competitions) ? ev.competitions[0] : void 0;
3245
+ const rawCompetitors = takeBounded(comp?.competitors, 4);
3246
+ if (rawCompetitors.length !== 2) {
3247
+ return definitiveNone(`expected 2 competitors, found ${rawCompetitors.length}`);
3248
+ }
3249
+ const homeRaw = rawCompetitors.find((c) => c?.homeAway === "home");
3250
+ const awayRaw = rawCompetitors.find((c) => c?.homeAway === "away");
3251
+ if (!homeRaw || !awayRaw) return definitiveNone("competitors do not state home and away");
3252
+ const homeP = toParticipant(homeRaw);
3253
+ const awayP = toParticipant(awayRaw);
3254
+ if (homeP.kind !== "valid") return homeP;
3255
+ if (awayP.kind !== "valid") return awayP;
3256
+ const h = homeP.value;
3257
+ const a = awayP.value;
3258
+ if (h.kind === "team" && a.kind === "team" && h.providerId === a.providerId) {
3259
+ return definitiveNone("both competitors are the same team");
3260
+ }
3261
+ const home = homeP.value.team;
3262
+ const away = awayP.value.team;
2988
3263
  const status = mapStatus(ev.status ?? comp?.status);
2989
- const stage = stageFromSlug(ev.season?.slug);
2990
- const home = toTeam(homeC?.team);
2991
- const away = toTeam(awayC?.team);
3264
+ if (!status) return malformed("event status is not a status we recognize");
3265
+ const stage = member(stageFromSlug(ev.season?.slug), STAGES2) ?? "FRIENDLY";
2992
3266
  let group;
2993
3267
  if (stage === "GROUP" && ctx.groupByTeam) {
2994
3268
  group = ctx.groupByTeam[home.code] ?? ctx.groupByTeam[away.code];
2995
3269
  }
2996
- const hs = toInt(homeC?.score);
2997
- const as = toInt(awayC?.score);
2998
- const hasScore = status !== "SCHEDULED" && hs !== void 0 && as !== void 0;
3270
+ const hs = toGoals(homeRaw.score);
3271
+ const as = toGoals(awayRaw.score);
3272
+ const scoreExpected = status === "LIVE" || status === "HT" || status === "FT";
3273
+ const hasScore = scoreExpected && hs !== void 0 && as !== void 0;
3274
+ if (scoreExpected && !hasScore) return malformed("event score is absent or unreadable");
3275
+ const hShoot = toGoals(homeRaw.shootoutScore);
3276
+ const aShoot = toGoals(awayRaw.shootoutScore);
3277
+ const shootoutPresent = homeRaw.shootoutScore !== void 0 || awayRaw.shootoutScore !== void 0;
3278
+ const shootout = shootoutPresent ? { home: hShoot, away: aShoot } : void 0;
2999
3279
  let winnerCode;
3000
3280
  if (isFinished(status)) {
3001
- if (homeC?.winner) winnerCode = home.code;
3002
- else if (awayC?.winner) winnerCode = away.code;
3281
+ const winners = [homeRaw, awayRaw].filter((c) => flag(c.winner) === true);
3282
+ if (winners.length === 1) winnerCode = winners[0] === homeRaw ? home.code : away.code;
3003
3283
  }
3004
- const hShoot = toInt(homeC?.shootoutScore);
3005
- const aShoot = toInt(awayC?.shootoutScore);
3006
- const shootout = hasScore && hShoot !== void 0 && aShoot !== void 0 ? { home: hShoot, away: aShoot } : void 0;
3007
- return {
3008
- id: ev.id,
3284
+ const venue = comp?.venue;
3285
+ return sealMatch({
3286
+ id,
3009
3287
  stage,
3010
3288
  group,
3011
- kickoff: ev.date,
3012
- venue: sanitizeFeedText(comp?.venue?.fullName ?? ""),
3013
- city: sanitizeFeedText(comp?.venue?.address?.city ?? "") || void 0,
3014
- country: sanitizeFeedText(comp?.venue?.address?.country ?? "") || void 0,
3289
+ kickoff,
3290
+ venue: venue?.fullName,
3291
+ city: venue?.address?.city,
3292
+ country: venue?.address?.country,
3015
3293
  home,
3016
3294
  away,
3017
3295
  score: hasScore ? { home: hs, away: as } : void 0,
@@ -3019,57 +3297,224 @@ function mapEspnEvent(ev, ctx = {}) {
3019
3297
  minute: parseMinute(ev.status ?? comp?.status),
3020
3298
  status,
3021
3299
  winnerCode,
3022
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3023
- };
3300
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3301
+ events: ev.events
3302
+ });
3024
3303
  }
3025
- function toEspnDate(d) {
3026
- return d.replace(/\D/g, "").slice(0, 8);
3304
+ function parseEspnEvents(raw, ctx = {}) {
3305
+ const all = raw?.events;
3306
+ const readable = Array.isArray(all);
3307
+ const total = readable ? all.length : 0;
3308
+ const considered = takeBounded(all, MAX_EVENTS);
3309
+ const items = [];
3310
+ const seenIds = /* @__PURE__ */ new Set();
3311
+ let complete = readable && considered.length === total;
3312
+ for (const event of considered) {
3313
+ const parsed = parseEspnEvent(event, ctx);
3314
+ if (parsed.kind !== "valid") {
3315
+ if (parsed.kind !== "definitive-none") complete = false;
3316
+ continue;
3317
+ }
3318
+ if (seenIds.has(parsed.value.id)) {
3319
+ complete = false;
3320
+ continue;
3321
+ }
3322
+ seenIds.add(parsed.value.id);
3323
+ items.push(parsed.value);
3324
+ }
3325
+ return {
3326
+ items,
3327
+ total,
3328
+ shown: items.length,
3329
+ truncated: total > considered.length,
3330
+ // Some record was unreadable, or the window did not cover the payload, or
3331
+ // the envelope itself was not a list — none of those is a complete account
3332
+ // of what the provider sent.
3333
+ complete
3334
+ };
3027
3335
  }
3028
- function statVal(stats, name) {
3029
- const v = stats?.find((s) => s.name === name)?.value;
3030
- return typeof v === "number" && Number.isFinite(v) ? Math.round(v) : 0;
3336
+ function statVal(stats, name, signed = false) {
3337
+ if (!Array.isArray(stats) || stats.length > 64) return void 0;
3338
+ const matches = takeBounded(stats, 64).filter(
3339
+ (s) => s?.name === name
3340
+ );
3341
+ if (matches.length !== 1) return void 0;
3342
+ const v = matches[0]?.value;
3343
+ if (typeof v !== "number" || !Number.isFinite(v) || !Number.isInteger(v)) return void 0;
3344
+ const limit = 1e3;
3345
+ return v > limit || v < (signed ? -limit : 0) ? void 0 : v;
3346
+ }
3347
+ function optionalStatVal(stats, name) {
3348
+ if (!Array.isArray(stats) || stats.length > 64) return void 0;
3349
+ const matches = takeBounded(stats, 64).filter(
3350
+ (s) => s?.name === name
3351
+ );
3352
+ if (matches.length === 0) return 0;
3353
+ if (matches.length !== 1) return void 0;
3354
+ const v = matches[0]?.value;
3355
+ return typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e3 ? v : void 0;
3031
3356
  }
3032
3357
  function entryToRow(e) {
3033
- return {
3034
- team: toTeam(e.team),
3035
- played: statVal(e.stats, "gamesPlayed"),
3036
- won: statVal(e.stats, "wins"),
3037
- drawn: statVal(e.stats, "ties"),
3038
- lost: statVal(e.stats, "losses"),
3039
- goalsFor: statVal(e.stats, "pointsFor"),
3040
- goalsAgainst: statVal(e.stats, "pointsAgainst"),
3041
- goalDiff: statVal(e.stats, "pointDifferential"),
3042
- points: statVal(e.stats, "points")
3043
- };
3358
+ const names = teamNames(e?.team);
3359
+ if (names.length === 0) return definitiveNone("standings entry names no team");
3360
+ const name = names[0];
3361
+ const code = teamCode(e?.team?.abbreviation, name);
3362
+ const played = statVal(e.stats, "gamesPlayed");
3363
+ const won = statVal(e.stats, "wins");
3364
+ const drawn = statVal(e.stats, "ties");
3365
+ const lost = statVal(e.stats, "losses");
3366
+ const goalsFor = statVal(e.stats, "pointsFor");
3367
+ const goalsAgainst = statVal(e.stats, "pointsAgainst");
3368
+ const goalDiff = statVal(e.stats, "pointDifferential", true);
3369
+ const points = statVal(e.stats, "points", true);
3370
+ const deductions = optionalStatVal(e.stats, "deductions");
3371
+ const providerRank = statVal(e.stats, "rank");
3372
+ 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) {
3373
+ return malformed("standings entry has missing or invalid statistics");
3374
+ }
3375
+ if (played !== won + drawn + lost) {
3376
+ return malformed("standings entry games do not add up");
3377
+ }
3378
+ if (goalDiff !== goalsFor - goalsAgainst) {
3379
+ return malformed("standings entry goal difference does not add up");
3380
+ }
3381
+ if (points !== won * 3 + drawn - deductions) {
3382
+ return malformed("standings entry points do not add up");
3383
+ }
3384
+ return valid({
3385
+ team: { code, name, flag: productFlag(name) },
3386
+ played,
3387
+ won,
3388
+ drawn,
3389
+ lost,
3390
+ goalsFor,
3391
+ goalsAgainst,
3392
+ goalDiff,
3393
+ points,
3394
+ providerId: opaqueId(e?.team?.id, ESPN_ID),
3395
+ providerRank
3396
+ });
3044
3397
  }
3045
- function parseStandings(data) {
3398
+ function parseEspnStandings(raw) {
3399
+ const rawChildren = raw?.children;
3400
+ const readable = Array.isArray(rawChildren);
3401
+ const rawCount = readable ? rawChildren.length : 0;
3402
+ const children = takeBounded(rawChildren, MAX_GROUPS * 4);
3403
+ const sawAllChildren = rawCount === children.length;
3404
+ let rowsTruncated = false;
3405
+ let complete = readable && sawAllChildren;
3046
3406
  const out = [];
3047
- for (const child of data.children ?? []) {
3048
- const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
3407
+ const seenGroups = /* @__PURE__ */ new Set();
3408
+ const seenProviderIds = /* @__PURE__ */ new Set();
3409
+ for (const child of children) {
3410
+ const label = humanLabel(child?.name ?? child?.abbreviation);
3411
+ const letter = label.match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
3049
3412
  if (!letter) continue;
3050
- const ranked = (child.standings?.entries ?? []).map((e) => ({
3051
- row: entryToRow(e),
3052
- rank: statVal(e.stats, "rank")
3053
- }));
3413
+ if (seenGroups.has(letter)) {
3414
+ complete = false;
3415
+ continue;
3416
+ }
3417
+ seenGroups.add(letter);
3418
+ const rawEntries = child?.standings?.entries;
3419
+ if (!Array.isArray(rawEntries) || rawEntries.length === 0) {
3420
+ complete = false;
3421
+ continue;
3422
+ }
3423
+ if (rawEntries.length > MAX_GROUP_ROWS) {
3424
+ rowsTruncated = true;
3425
+ complete = false;
3426
+ }
3427
+ const entries = takeBounded(rawEntries, MAX_GROUP_ROWS);
3428
+ const seenCodes = /* @__PURE__ */ new Map();
3429
+ const seenRanks = /* @__PURE__ */ new Set();
3430
+ const ranked = [];
3431
+ for (const e of entries) {
3432
+ const r = entryToRow(e);
3433
+ if (r.kind !== "valid") {
3434
+ if (r.kind !== "definitive-none") complete = false;
3435
+ continue;
3436
+ }
3437
+ const { providerId, providerRank } = r.value;
3438
+ const code = r.value.team.code;
3439
+ const priorHadId = seenCodes.get(code);
3440
+ const codeCollision = priorHadId !== void 0 && (providerId === void 0 || priorHadId === false);
3441
+ if (codeCollision || seenRanks.has(providerRank) || providerId !== void 0 && seenProviderIds.has(providerId)) {
3442
+ complete = false;
3443
+ continue;
3444
+ }
3445
+ seenCodes.set(code, providerId !== void 0);
3446
+ seenRanks.add(providerRank);
3447
+ if (providerId !== void 0) seenProviderIds.add(providerId);
3448
+ const { providerId: _dropId, providerRank: rank, ...row } = r.value;
3449
+ ranked.push({ row, rank });
3450
+ }
3054
3451
  ranked.sort((a, b) => {
3055
3452
  if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
3056
- const r = a.row;
3057
- const s = b.row;
3058
- return s.points - r.points || s.goalDiff - r.goalDiff || s.goalsFor - r.goalsFor || r.team.code.localeCompare(s.team.code);
3453
+ if (b.row.points !== a.row.points) return b.row.points - a.row.points;
3454
+ if (b.row.goalDiff !== a.row.goalDiff) return b.row.goalDiff - a.row.goalDiff;
3455
+ return b.row.goalsFor - a.row.goalsFor;
3059
3456
  });
3457
+ if (ranked.length === 0) {
3458
+ complete = false;
3459
+ continue;
3460
+ }
3060
3461
  out.push({ group: letter, rows: ranked.map((x) => x.row) });
3061
3462
  }
3062
- out.sort((a, b) => a.group.localeCompare(b.group));
3063
- return out;
3463
+ return {
3464
+ items: out,
3465
+ total: seenGroups.size,
3466
+ shown: out.length,
3467
+ // We stopped early if the child list or any single group's rows were cut.
3468
+ truncated: !sawAllChildren || rowsTruncated,
3469
+ complete: complete && !rowsTruncated
3470
+ };
3471
+ }
3472
+ var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3473
+ var DEFAULT_COMPETITION = "fifa.world";
3474
+ var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3475
+ var USER_AGENT = `claudinho/${"0.10.0"} (+https://github.com/arturogarrido/claudinho)`;
3476
+ var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
3477
+ function competitionBase(slug) {
3478
+ return `${ESPN_SOCCER}/${slug}`;
3479
+ }
3480
+ var DEFAULT_TIMEOUT_MS = 6e3;
3481
+ var STANDINGS_SHARE_MS = 3e4;
3482
+ var ProviderError = class extends Error {
3483
+ kind;
3484
+ status;
3485
+ constructor(message, kind, status) {
3486
+ super(message);
3487
+ this.name = "ProviderError";
3488
+ this.kind = kind;
3489
+ this.status = status;
3490
+ }
3491
+ /** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
3492
+ get throttled() {
3493
+ return this.kind === "http" && (this.status === 429 || this.status === 403);
3494
+ }
3495
+ };
3496
+ function toEspnDate(d) {
3497
+ return d.replace(/\D/g, "").slice(0, 8);
3498
+ }
3499
+ function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length > 0) {
3500
+ if (!hasUsableRecord && (!parsed.complete || parsed.total > 0)) {
3501
+ throw new ProviderError(`ESPN ${kind} payload had no readable records`, "parse");
3502
+ }
3503
+ return [...parsed.items];
3064
3504
  }
3065
3505
  var EspnAdapter = class {
3066
3506
  constructor(opts = {}) {
3067
3507
  this.opts = opts;
3508
+ const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
3509
+ this.expectedStandingsGroups = expected ? [...expected] : void 0;
3510
+ this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
3068
3511
  }
3069
3512
  opts;
3070
3513
  name = "espn";
3071
3514
  capabilities = { push: false, latencyHintSec: 45 };
3072
- /** Cached team-code -> group-letter map (built lazily from standings). */
3515
+ expectedStandingsGroups;
3516
+ standingsFallbackGroups;
3517
+ /** Short-lived team-code -> group-letter map (built lazily from standings). */
3073
3518
  groupMap;
3074
3519
  /**
3075
3520
  * One in-flight/recent standings fetch shared by fetchStandings and
@@ -3112,38 +3557,48 @@ var EspnAdapter = class {
3112
3557
  if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
3113
3558
  return this.standingsShared.promise;
3114
3559
  }
3115
- const promise = this.get(this.standingsUrl()).then(
3116
- (d) => parseStandings(d)
3117
- );
3560
+ const promise = this.get(this.standingsUrl()).then((d) => {
3561
+ const parsed = parseEspnStandings(d);
3562
+ return usableProviderItems(
3563
+ "standings",
3564
+ parsed,
3565
+ parsed.items.some((table) => table.rows.length > 0)
3566
+ );
3567
+ });
3118
3568
  this.standingsShared = { at: now, promise };
3119
- promise.catch(() => {
3569
+ void promise.catch(() => {
3120
3570
  if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
3121
3571
  });
3122
3572
  return promise;
3123
3573
  }
3124
3574
  /**
3125
3575
  * Authoritative, cumulative group tables from the standings endpoint. Throws
3126
- * on fetch/parse failure (the caller decides the fallback). Group-stage only:
3127
- * non-group `children` are filtered out by {@link parseStandings}.
3576
+ * on fetch failure. Group-stage only: non-group `children` are filtered out
3577
+ * by {@link parseStandings}; malformed rows are omitted without hiding their
3578
+ * readable siblings.
3128
3579
  */
3129
3580
  async fetchStandings() {
3130
3581
  return this.sharedStandings();
3131
3582
  }
3132
3583
  /**
3133
- * Build (and cache) a team-code -> group-letter map from the standings
3584
+ * Build (and briefly cache) a team-code -> group-letter map from the standings
3134
3585
  * endpoint. Best-effort: returns {} if standings are unavailable — but a
3135
- * transient failure is NOT cached (only a successful parse pins the map), so
3136
- * one blip can't silently drop group letters for the adapter's lifetime.
3586
+ * transient failure is NOT cached, and a partial successful parse expires at
3587
+ * the standings TTL, so neither can silently drop group letters for the
3588
+ * adapter's lifetime.
3137
3589
  * Reuses the same parse/fetch as {@link fetchStandings}, so the two never
3138
3590
  * drift and one command never fetches standings twice.
3139
3591
  */
3140
3592
  async fetchGroupMap(force = false) {
3141
- if (this.groupMap && !force) return this.groupMap;
3593
+ const now = Date.now();
3594
+ if (!force && this.groupMap && now - this.groupMap.at < STANDINGS_SHARE_MS) {
3595
+ return this.groupMap.value;
3596
+ }
3142
3597
  try {
3143
3598
  const tables = await this.sharedStandings();
3144
3599
  const map = {};
3145
3600
  for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
3146
- this.groupMap = map;
3601
+ this.groupMap = { at: Date.now(), value: map };
3147
3602
  return map;
3148
3603
  } catch {
3149
3604
  return {};
@@ -3158,7 +3613,8 @@ var EspnAdapter = class {
3158
3613
  this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
3159
3614
  this.get(url.toString())
3160
3615
  ]);
3161
- return (data.events ?? []).map((ev) => mapEspnEvent(ev, { groupByTeam }));
3616
+ const parsed = parseEspnEvents(data, { groupByTeam });
3617
+ return usableProviderItems("scoreboard", parsed);
3162
3618
  }
3163
3619
  async get(url) {
3164
3620
  const doFetch = this.opts.fetchImpl ?? fetch;
@@ -3934,12 +4390,12 @@ function resolveCompetition(explicit) {
3934
4390
  return DEFAULT_COMPETITION;
3935
4391
  }
3936
4392
  var KNOWN_SOURCES = ["espn"];
3937
- function makeAdapter(source = "espn") {
4393
+ function makeAdapter(source = "espn", opts = {}) {
3938
4394
  switch (source) {
3939
4395
  case "espn": {
3940
4396
  const competition = resolveCompetition();
3941
4397
  const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
3942
- return new EspnAdapter({ baseUrl });
4398
+ return new EspnAdapter({ baseUrl, enrichGroups: opts.enrichGroups });
3943
4399
  }
3944
4400
  default:
3945
4401
  throw new Error(
@@ -3968,17 +4424,26 @@ async function getMatchesForDate(adapter, dateISO) {
3968
4424
  }
3969
4425
  async function getStandings(adapter, group) {
3970
4426
  const want = group?.toUpperCase();
4427
+ const expected = adapter.expectedStandingsGroups;
4428
+ if (want && expected && !expected.includes(want)) {
4429
+ return { tables: [], degraded: false };
4430
+ }
3971
4431
  if (adapter.fetchStandings) {
3972
4432
  try {
3973
4433
  const all = await adapter.fetchStandings();
3974
4434
  const tables2 = (want ? all.filter((t2) => t2.group === want) : all).sort(
3975
4435
  (a, b) => a.group.localeCompare(b.group)
3976
4436
  );
3977
- return { tables: tables2, degraded: false, source: adapter.name };
4437
+ const availableGroups = new Set(tables2.map((table) => table.group));
4438
+ const expectedGroupWasOmitted = want ? (expected?.includes(want) ?? false) && tables2.length === 0 : expected?.some((group2) => !availableGroups.has(group2)) ?? false;
4439
+ if (!expectedGroupWasOmitted) {
4440
+ return { tables: tables2, degraded: false, source: adapter.name };
4441
+ }
3978
4442
  } catch {
3979
4443
  }
3980
4444
  }
3981
- const letters = want ? [want] : groups();
4445
+ const fallbackGroups = adapter.standingsFallbackGroups;
4446
+ const letters = fallbackGroups ? want ? fallbackGroups.includes(want) ? [want] : [] : [...new Set(fallbackGroups)].sort((a, b) => a.localeCompare(b)) : [];
3982
4447
  const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
3983
4448
  return { tables, degraded: true };
3984
4449
  }
@@ -4035,7 +4500,10 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
4035
4500
  try {
4036
4501
  const win = knockoutWindow();
4037
4502
  if (adapter.fetchWindow && win) {
4038
- fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
4503
+ fixtures = mergeLive(
4504
+ fixtures,
4505
+ await adapter.fetchWindow(win.start, win.end)
4506
+ );
4039
4507
  }
4040
4508
  } catch {
4041
4509
  overlayFailed = true;
@@ -4084,14 +4552,141 @@ async function getMatchById(adapter, id) {
4084
4552
  async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
4085
4553
  try {
4086
4554
  const day = now.toISOString().slice(0, 10);
4087
- const matches = adapter.fetchWindow ? (await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1))).filter(
4088
- (m) => isLive(m.status)
4089
- ) : await adapter.fetchLive();
4555
+ const matches = (adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchLive()).filter((m) => isLive(m.status));
4090
4556
  return { matches, degraded: false, source: adapter.name };
4091
4557
  } catch {
4092
4558
  return { matches: [], degraded: true };
4093
4559
  }
4094
4560
  }
4561
+ function pct(p) {
4562
+ return Math.round(p * 100);
4563
+ }
4564
+ var KNOWN_MARKET_SOURCES = ["polymarket", "fake"];
4565
+ function marketSourceLabel(source) {
4566
+ if (source === "polymarket") return "Polymarket";
4567
+ if (source === "fake") return "demo data";
4568
+ return source.charAt(0).toUpperCase() + source.slice(1);
4569
+ }
4570
+ function outcomeLabel(o, match) {
4571
+ if (o.kind === "home") return match.home.name;
4572
+ if (o.kind === "away") return match.away.name;
4573
+ if (o.kind === "draw") return "Draw";
4574
+ return o.label;
4575
+ }
4576
+ function utcHhmm(iso) {
4577
+ const t2 = Date.parse(iso);
4578
+ if (!Number.isFinite(t2)) return "";
4579
+ return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
4580
+ }
4581
+ function marketFavoriteText(signal, match) {
4582
+ const fav = signal.favorite;
4583
+ if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
4584
+ if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
4585
+ const name = fav.kind === "home" ? match.home.name : match.away.name;
4586
+ return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
4587
+ }
4588
+ function marketProbabilityText(signal, match) {
4589
+ const order = ["home", "draw", "away"];
4590
+ const parts = [];
4591
+ for (const kind of order) {
4592
+ const o = signal.outcomes.find((x) => x.kind === kind);
4593
+ if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4594
+ }
4595
+ for (const o of signal.outcomes) {
4596
+ if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4597
+ }
4598
+ return parts.join(" \xB7 ");
4599
+ }
4600
+ function marketAttributionText(signal) {
4601
+ const time = utcHhmm(signal.asOf);
4602
+ const src2 = `Source: ${marketSourceLabel(signal.source)}`;
4603
+ return time ? `${src2} \xB7 updated ${time}` : src2;
4604
+ }
4605
+ function marketLine(signal, match) {
4606
+ return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
4607
+ signal.source
4608
+ )} \xB7 informational only`;
4609
+ }
4610
+ function marketBlock(signal, match) {
4611
+ const lines = [];
4612
+ if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
4613
+ lines.push(marketFavoriteText(signal, match));
4614
+ lines.push(marketProbabilityText(signal, match));
4615
+ lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
4616
+ return lines;
4617
+ }
4618
+ var MAX_OUTCOMES = 128;
4619
+ var MATCH_ID = /^[0-9]{1,20}$/;
4620
+ var MARKET_ID = /^(?:[0-9]{1,32}|fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2})$/;
4621
+ var OUTCOME_KINDS = /* @__PURE__ */ new Set(["home", "draw", "away", "other"]);
4622
+ var TEAM_CODE_COLUMNS2 = 8;
4623
+ function sealOutcome(raw) {
4624
+ if (!raw || typeof raw !== "object") return void 0;
4625
+ const o = raw;
4626
+ const kind = member(o.kind, OUTCOME_KINDS);
4627
+ const p = probability(o.probability);
4628
+ if (!kind || p === void 0) return void 0;
4629
+ const out = { kind };
4630
+ if (o.teamCode !== void 0) {
4631
+ if (typeof o.teamCode !== "string") return void 0;
4632
+ out.teamCode = humanLabel(o.teamCode, TEAM_CODE_COLUMNS2);
4633
+ }
4634
+ out.label = humanLabel(o.label);
4635
+ out.probability = p;
4636
+ if ((out.kind === "home" || out.kind === "away") && !out.teamCode) return void 0;
4637
+ return out;
4638
+ }
4639
+ function hasDuplicateKind(outcomes) {
4640
+ const seen = /* @__PURE__ */ new Set();
4641
+ for (const o of outcomes) {
4642
+ if (o.kind === "other") continue;
4643
+ if (seen.has(o.kind)) return true;
4644
+ seen.add(o.kind);
4645
+ }
4646
+ return false;
4647
+ }
4648
+ function sealMarketSignal(raw, options = {}) {
4649
+ if (!raw || typeof raw !== "object") return malformed("signal is not an object");
4650
+ const s = raw;
4651
+ const matchId = opaqueId(s.matchId, MATCH_ID);
4652
+ if (!matchId) return malformed("signal names no fixture");
4653
+ if (!Array.isArray(s.outcomes) || s.outcomes.length > MAX_OUTCOMES) {
4654
+ return malformed("signal outcomes are absent or exceed the cap");
4655
+ }
4656
+ const outcomes = [];
4657
+ for (const rawOutcome of takeBounded(s.outcomes, MAX_OUTCOMES)) {
4658
+ const outcome = sealOutcome(rawOutcome);
4659
+ if (!outcome) return malformed("signal carries an unreadable outcome");
4660
+ outcomes.push(outcome);
4661
+ }
4662
+ if (hasDuplicateKind(outcomes)) {
4663
+ return ambiguous("two outcomes claim the same result");
4664
+ }
4665
+ const sourceMarketId = opaqueId(s.sourceMarketId, MARKET_ID);
4666
+ const liquidity = quantity(s.liquidity);
4667
+ const volume24h = quantity(s.volume24h);
4668
+ const out = {
4669
+ matchId,
4670
+ // Allow-listed, not merely stripped: this lands in the provider-attribution
4671
+ // slot, where `marketSourceLabel` falls through to the raw string for an
4672
+ // unrecognized provider — attacker prose where the reader expects
4673
+ // "Polymarket".
4674
+ source: member(s.source, new Set(KNOWN_MARKET_SOURCES)) ?? ""
4675
+ };
4676
+ if (sourceMarketId) out.sourceMarketId = sourceMarketId;
4677
+ out.asOf = canonicalTimestamp(s.asOf) ?? "";
4678
+ out.fetchedAt = canonicalTimestamp(s.fetchedAt) ?? "";
4679
+ out.outcomes = outcomes;
4680
+ const isAmbiguous = s.ambiguous !== false;
4681
+ const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
4682
+ if (favorite) out.favorite = favorite;
4683
+ if (liquidity !== void 0) out.liquidity = liquidity;
4684
+ if (volume24h !== void 0) out.volume24h = volume24h;
4685
+ out.stale = s.stale !== false;
4686
+ out.ambiguous = isAmbiguous || out.source === "";
4687
+ out.stale = out.stale || isStaleSignal(out, { now: options.now, maxAgeMs: options.maxAgeMs });
4688
+ return valid(out);
4689
+ }
4095
4690
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
4096
4691
  function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
4097
4692
  if (isLive(match.status)) return true;
@@ -4110,9 +4705,9 @@ function normalizeOutcomes(outcomes) {
4110
4705
  probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
4111
4706
  }));
4112
4707
  }
4113
- function favoriteStrength(probability) {
4114
- if (probability >= 0.65) return "clear";
4115
- if (probability >= 0.52) return "slight";
4708
+ function favoriteStrength(probability2) {
4709
+ if (probability2 >= 0.65) return "clear";
4710
+ if (probability2 >= 0.52) return "slight";
4116
4711
  return "close";
4117
4712
  }
4118
4713
  function deriveFavorite(outcomes) {
@@ -4131,14 +4726,16 @@ function deriveFavorite(outcomes) {
4131
4726
  }
4132
4727
  function mapsCleanly(match, outcomes) {
4133
4728
  if (outcomes.some((o) => o.kind === "other")) return false;
4729
+ const kinds = outcomes.map((o) => o.kind);
4730
+ if (new Set(kinds).size !== kinds.length) return false;
4134
4731
  const home = outcomes.find((o) => o.kind === "home");
4135
4732
  const away = outcomes.find((o) => o.kind === "away");
4136
4733
  const draw = outcomes.find((o) => o.kind === "draw");
4137
4734
  if (!home || !away) return false;
4138
- if (home.teamCode && home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
4735
+ if (!home.teamCode || home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
4139
4736
  return false;
4140
4737
  }
4141
- if (away.teamCode && away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
4738
+ if (!away.teamCode || away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
4142
4739
  return false;
4143
4740
  }
4144
4741
  if (match.stage === "GROUP" && !draw) return false;
@@ -4153,11 +4750,13 @@ function hasSaneDistribution(outcomes) {
4153
4750
  const sum = priced.reduce((s, o) => s + o.probability, 0);
4154
4751
  return sum > 0.97 && sum < 1.03;
4155
4752
  }
4753
+ var FUTURE_SKEW_MS = 6e4;
4156
4754
  function isStaleSignal(signal, options = {}) {
4157
4755
  const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
4158
4756
  const asOf = Date.parse(signal.asOf);
4159
4757
  if (!Number.isFinite(asOf)) return true;
4160
4758
  const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
4759
+ if (asOf - now > FUTURE_SKEW_MS) return true;
4161
4760
  return now - asOf > maxAge;
4162
4761
  }
4163
4762
  function isReliableMarketSignal(signal, options = {}) {
@@ -4173,8 +4772,8 @@ function isReliableMarketSignal(signal, options = {}) {
4173
4772
  }
4174
4773
  function buildMarketSignal(input) {
4175
4774
  const outcomes = normalizeOutcomes(input.outcomes);
4176
- const ambiguous = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
4177
- const favorite = ambiguous ? void 0 : deriveFavorite(outcomes);
4775
+ const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
4776
+ const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
4178
4777
  const signal = {
4179
4778
  matchId: input.match.id,
4180
4779
  source: input.source,
@@ -4186,66 +4785,33 @@ function buildMarketSignal(input) {
4186
4785
  liquidity: input.liquidity,
4187
4786
  volume24h: input.volume24h,
4188
4787
  stale: false,
4189
- ambiguous
4788
+ ambiguous: ambiguous2
4190
4789
  };
4191
4790
  signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
4192
- return signal;
4193
- }
4194
- function pct(p) {
4195
- return Math.round(p * 100);
4196
- }
4197
- function marketSourceLabel(source) {
4198
- if (source === "polymarket") return "Polymarket";
4199
- if (source === "fake") return "demo data";
4200
- return source.charAt(0).toUpperCase() + source.slice(1);
4201
- }
4202
- function outcomeLabel(o, match) {
4203
- if (o.kind === "home") return match.home.name;
4204
- if (o.kind === "away") return match.away.name;
4205
- if (o.kind === "draw") return "Draw";
4206
- return o.label;
4207
- }
4208
- function utcHhmm(iso) {
4209
- const t2 = Date.parse(iso);
4210
- if (!Number.isFinite(t2)) return "";
4211
- return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
4212
- }
4213
- function marketFavoriteText(signal, match) {
4214
- const fav = signal.favorite;
4215
- if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
4216
- if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
4217
- const name = fav.kind === "home" ? match.home.name : match.away.name;
4218
- return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
4219
- }
4220
- function marketProbabilityText(signal, match) {
4221
- const order = ["home", "draw", "away"];
4222
- const parts = [];
4223
- for (const kind of order) {
4224
- const o = signal.outcomes.find((x) => x.kind === kind);
4225
- if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4226
- }
4227
- for (const o of signal.outcomes) {
4228
- if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4791
+ const sealed = sealMarketSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
4792
+ if (sealed.kind !== "valid") {
4793
+ return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
4229
4794
  }
4230
- return parts.join(" \xB7 ");
4795
+ return { ...sealed.value, ambiguous: sealed.value.ambiguous || ambiguous2 };
4231
4796
  }
4232
- function marketAttributionText(signal) {
4233
- const time = utcHhmm(signal.asOf);
4234
- const src2 = `Source: ${marketSourceLabel(signal.source)}`;
4235
- return time ? `${src2} \xB7 updated ${time}` : src2;
4797
+ var NONE = { kind: "none" };
4798
+ function selectOne(candidates) {
4799
+ if (candidates.length === 1) return { kind: "one", value: candidates[0] };
4800
+ if (candidates.length === 0) return NONE;
4801
+ return { kind: "ambiguous", count: candidates.length };
4236
4802
  }
4237
- function marketLine(signal, match) {
4238
- return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
4239
- signal.source
4240
- )} \xB7 informational only`;
4803
+ function resolvedValues(batch) {
4804
+ const out = /* @__PURE__ */ new Map();
4805
+ for (const [key, r] of batch.results) if (r.kind === "valid") out.set(key, r.value);
4806
+ return out;
4241
4807
  }
4242
- function marketBlock(signal, match) {
4243
- const lines = [];
4244
- if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
4245
- lines.push(marketFavoriteText(signal, match));
4246
- lines.push(marketProbabilityText(signal, match));
4247
- lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
4248
- return lines;
4808
+ function cacheableKeys(batch) {
4809
+ const out = /* @__PURE__ */ new Set();
4810
+ for (const [key, r] of batch.results) if (isCacheable(r)) out.add(key);
4811
+ return out;
4812
+ }
4813
+ function emptyBatch() {
4814
+ return { results: /* @__PURE__ */ new Map(), complete: false };
4249
4815
  }
4250
4816
  var FakeMarketProvider = class {
4251
4817
  constructor(opts = {}) {
@@ -4260,14 +4826,12 @@ var FakeMarketProvider = class {
4260
4826
  return void 0;
4261
4827
  }
4262
4828
  async findSignals(matches, options) {
4263
- const signals = /* @__PURE__ */ new Map();
4264
- const checked = /* @__PURE__ */ new Set();
4829
+ const results = /* @__PURE__ */ new Map();
4265
4830
  for (const m of matches) {
4266
- checked.add(m.id);
4267
4831
  const s = await this.findSignal(m, options);
4268
- if (s) signals.set(m.id, s);
4832
+ results.set(m.id, s ? valid(s) : definitiveNone("fake provider has no signal"));
4269
4833
  }
4270
- return { signals, checked };
4834
+ return { results, complete: true };
4271
4835
  }
4272
4836
  synthesize(match, options) {
4273
4837
  const seed = hash(`${match.home.code}-${match.away.code}`);
@@ -4284,7 +4848,9 @@ var FakeMarketProvider = class {
4284
4848
  return buildMarketSignal({
4285
4849
  match,
4286
4850
  source: "fake",
4287
- sourceMarketId: `fake-${match.id}`,
4851
+ // Must satisfy the boundary's opaque-id grammar, like a real one:
4852
+ // a source id that only the live path accepts is the asymmetry itself.
4853
+ sourceMarketId: match.id,
4288
4854
  asOf,
4289
4855
  fetchedAt: now.toISOString(),
4290
4856
  outcomes,
@@ -4308,6 +4874,8 @@ var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
4308
4874
  var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
4309
4875
  var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
4310
4876
  var DEFAULT_TIMEOUT_MS2 = 8e3;
4877
+ var MAX_EVENT_MARKETS = 256;
4878
+ var DEFAULT_DEADLINE_MS = 15e3;
4311
4879
  var WC_SERIES_SLUG = "soccer-fifwc";
4312
4880
  var WC_SPORT = "fifwc";
4313
4881
  var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
@@ -4320,49 +4888,81 @@ var PolymarketProvider = class {
4320
4888
  opts;
4321
4889
  name = "polymarket";
4322
4890
  async findSignal(match, options) {
4323
- const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
4324
- return (await this.resolveOne(match, options, deadline)).signal;
4891
+ const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
4892
+ return parsedValue(await this.resolveOne(match, options, deadline));
4325
4893
  }
4326
4894
  async findSignals(matches, options) {
4327
- const signals = /* @__PURE__ */ new Map();
4328
- const checked = /* @__PURE__ */ new Set();
4329
- const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
4895
+ const results = /* @__PURE__ */ new Map();
4896
+ const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
4897
+ let complete = true;
4330
4898
  for (const m of matches) {
4331
- if (Date.now() >= deadline) break;
4899
+ if (Date.now() >= deadline) {
4900
+ results.set(m.id, unresolved("enrichment deadline expired"));
4901
+ complete = false;
4902
+ continue;
4903
+ }
4332
4904
  const r = await this.resolveOne(m, options, deadline);
4333
- if (r.checked) checked.add(m.id);
4334
- if (r.signal) signals.set(m.id, r.signal);
4905
+ if (r.kind === "unresolved" || r.kind === "malformed") complete = false;
4906
+ results.set(m.id, r);
4335
4907
  }
4336
- return { signals, checked };
4908
+ return { results, complete };
4337
4909
  }
4338
4910
  /**
4339
- * Resolve one match. `checked` distinguishes a DEFINITIVE result (reached the
4340
- * source and found no usable market, or the fixture is unmappable) from a
4341
- * provider/network error so transient failures are retried, not
4342
- * negative-cached.
4911
+ * Resolve one match into a verdict.
4912
+ *
4913
+ * Every exit says which KIND of non-answer it is, because that decides
4914
+ * whether it may be remembered — see `isCacheable`: a conclusion we drew from
4915
+ * a payload we READ is cacheable (including an ambiguity, which is stable),
4916
+ * while a shape we could not read is not. Previously a single
4917
+ * `checked: boolean` collapsed five distinct situations into two, and the
4918
+ * ones that landed on the wrong side of it — an ambiguous payload, a
4919
+ * two-legged market, an incoherent 1X2 — were negative-cached as the fact
4920
+ * that this fixture has no market.
4343
4921
  */
4344
4922
  async resolveOne(match, options, deadline = Number.POSITIVE_INFINITY) {
4345
- const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4346
- const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4347
- if (slugs.length === 0) return { checked: true };
4348
4923
  const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
4349
4924
  try {
4925
+ const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4926
+ const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4927
+ if (slugs.length === 0) return definitiveNone("fixture has no derivable event slug");
4928
+ const RANK = {
4929
+ "definitive-none": 0,
4930
+ ambiguous: 1,
4931
+ unresolved: 2,
4932
+ malformed: 3
4933
+ };
4934
+ let worst;
4935
+ const keepWorst = (r) => {
4936
+ if (r.kind === "definitive-none" || r.kind === "valid") return;
4937
+ if (!worst || (RANK[r.kind] ?? 0) > (RANK[worst.kind] ?? 0)) worst = r;
4938
+ };
4350
4939
  for (const slug of slugs) {
4351
4940
  const remaining = deadline - Date.now();
4352
- if (remaining <= 0) return { checked: false };
4353
- const event = await this.fetchEvent(slug, Math.min(configured, remaining));
4354
- const signal = event ? this.toSignal(match, slug, event, options) : void 0;
4355
- if (signal) return { signal, checked: true };
4941
+ if (remaining <= 0) return unresolved("deadline expired between candidate slugs");
4942
+ let found;
4943
+ try {
4944
+ found = await this.fetchEvent(slug, Math.min(configured, remaining));
4945
+ } catch {
4946
+ keepWorst(malformed("candidate request failed"));
4947
+ continue;
4948
+ }
4949
+ if (found.kind !== "valid") {
4950
+ keepWorst(found);
4951
+ continue;
4952
+ }
4953
+ const r = this.toSignal(match, slug, found.value, options);
4954
+ if (r.kind === "valid") return r;
4955
+ keepWorst(r);
4356
4956
  }
4357
- return { checked: true };
4957
+ return worst ?? definitiveNone("no candidate slug yielded a usable market");
4358
4958
  } catch {
4359
- return { checked: false };
4959
+ return malformed("provider request failed");
4360
4960
  }
4361
4961
  }
4362
4962
  async fetchEvent(slug, timeoutMs) {
4363
4963
  const base = this.opts.baseUrl ?? DEFAULT_BASE2;
4364
4964
  assertAllowedHost(base);
4365
- const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
4965
+ const url = `${base}/events/slug/${encodeURIComponent(slug)}`;
4366
4966
  const doFetch = this.opts.fetchImpl ?? fetch;
4367
4967
  const res = await doFetch(url, {
4368
4968
  signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
@@ -4371,7 +4971,7 @@ var PolymarketProvider = class {
4371
4971
  redirect: "error",
4372
4972
  headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
4373
4973
  });
4374
- if (res.status === 404) return void 0;
4974
+ if (res.status === 404) return definitiveNone("slug returns 404");
4375
4975
  if (!res.ok) {
4376
4976
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4377
4977
  }
@@ -4380,63 +4980,148 @@ var PolymarketProvider = class {
4380
4980
  throw new Error(`Polymarket response too large: ${length} bytes`);
4381
4981
  }
4382
4982
  const data = await res.json();
4983
+ if (Array.isArray(data) && data.length > 1) {
4984
+ return ambiguous("slug returned more than one event");
4985
+ }
4986
+ if (Array.isArray(data) && data.length === 0) return definitiveNone("slug returns no event");
4383
4987
  const event = Array.isArray(data) ? data[0] : data;
4384
- return event && typeof event === "object" ? event : void 0;
4988
+ if (!event || typeof event !== "object") return malformed("event body is not an object");
4989
+ return valid(event);
4385
4990
  }
4386
4991
  toSignal(match, eventSlug, event, options) {
4387
- if (event.active === false || event.closed === true) return void 0;
4388
- if (event.seriesSlug != null && event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
4389
- return void 0;
4992
+ if (typeof event.active !== "boolean" || typeof event.closed !== "boolean") {
4993
+ return malformed("event active/closed is not a boolean");
4994
+ }
4995
+ if (event.active === false || event.closed === true) {
4996
+ return definitiveNone("event is closed or inactive");
4390
4997
  }
4391
- if (event.slug != null && event.slug !== eventSlug) return void 0;
4392
- const start = event.startTime ? Date.parse(event.startTime) : Number.NaN;
4998
+ if (event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
4999
+ return definitiveNone("event is not in this competition");
5000
+ }
5001
+ if (typeof event.slug !== "string") {
5002
+ return malformed("event states no slug");
5003
+ }
5004
+ if (event.slug !== eventSlug) return definitiveNone("event is not the one requested");
5005
+ if (typeof event.startTime !== "string" || !canonicalTimestamp(event.startTime)) {
5006
+ return malformed("event startTime missing or unparseable");
5007
+ }
5008
+ const start = Date.parse(event.startTime);
4393
5009
  const kick = Date.parse(match.kickoff);
4394
- if (Number.isFinite(start) && Number.isFinite(kick) && Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
4395
- return void 0;
5010
+ if (!Number.isFinite(start) || !Number.isFinite(kick)) {
5011
+ return malformed("event or fixture kickoff is unreadable");
5012
+ }
5013
+ if (Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
5014
+ return definitiveNone("event kickoff does not match the fixture");
5015
+ }
5016
+ if (!Array.isArray(event.markets)) {
5017
+ return malformed("event markets is not an array");
4396
5018
  }
4397
- const moneyline = (event.markets ?? []).filter(
4398
- (m) => (m.sportsMarketType ?? "moneyline") === "moneyline"
5019
+ const marketsTruncated = Array.isArray(event.markets) && event.markets.length > MAX_EVENT_MARKETS;
5020
+ if (marketsTruncated) {
5021
+ return malformed("event market list exceeded the cap");
5022
+ }
5023
+ const marketList = takeBounded(event.markets, MAX_EVENT_MARKETS);
5024
+ if (marketList.some(
5025
+ (market) => !market || typeof market !== "object" || typeof market.sportsMarketType !== "string"
5026
+ )) {
5027
+ return malformed("event market is missing its market-type discriminator");
5028
+ }
5029
+ const moneyline = marketList.filter(
5030
+ (m) => m?.sportsMarketType === "moneyline"
4399
5031
  );
4400
- const homeMarket = pickMarket(moneyline, match.home.code, match.home.name);
4401
- const awayMarket = pickMarket(moneyline, match.away.code, match.away.name);
4402
- const drawMarket = pickDraw(moneyline);
4403
- if (!homeMarket || !awayMarket) return void 0;
5032
+ const homeSel = pickMarket(moneyline, match.home.code, match.home.name);
5033
+ const awaySel = pickMarket(moneyline, match.away.code, match.away.name);
5034
+ const drawSel = pickDraw(moneyline);
5035
+ for (const [side, sel] of [
5036
+ ["home", homeSel],
5037
+ ["away", awaySel],
5038
+ ["draw", drawSel]
5039
+ ]) {
5040
+ if (sel.kind === "ambiguous") {
5041
+ return ambiguous(`${sel.count} markets claim the ${side} outcome`);
5042
+ }
5043
+ }
5044
+ if (homeSel.kind !== "one" || awaySel.kind !== "one" || drawSel.kind !== "one") {
5045
+ return definitiveNone("event does not carry all three 1X2 legs");
5046
+ }
5047
+ const homeMarket = homeSel.value;
5048
+ const awayMarket = awaySel.value;
5049
+ const drawMarket = drawSel.value;
4404
5050
  const legIds = [homeMarket, awayMarket, drawMarket].filter((m) => m != null).map((m) => m.id ?? m.slug ?? "");
4405
- if (new Set(legIds).size !== legIds.length) return void 0;
5051
+ if (new Set(legIds).size !== legIds.length) {
5052
+ return ambiguous("two outcome legs are the same market");
5053
+ }
4406
5054
  const legs = [
4407
5055
  ["home", homeMarket, match.home.code, match.home.name],
4408
5056
  ["draw", drawMarket, void 0, "Draw"],
4409
5057
  ["away", awayMarket, match.away.code, match.away.name]
4410
5058
  ];
4411
5059
  const outcomes = [];
4412
- let asOf = event.updatedAt;
5060
+ let asOf = canonicalTimestamp(event.updatedAt);
4413
5061
  let liquidity;
4414
- for (const [kind, market, teamCode, label] of legs) {
5062
+ for (const [kind, market, teamCode2, label] of legs) {
4415
5063
  if (!market) continue;
4416
- if (market.closed === true || market.active === false) return void 0;
4417
- if (market.description && NON_REGULAR_TIME.test(market.description)) return void 0;
5064
+ if (typeof market.closed !== "boolean" || typeof market.active !== "boolean") {
5065
+ return malformed("market active/closed is not a boolean");
5066
+ }
5067
+ if (market.closed === true || market.active === false) {
5068
+ return definitiveNone("an outcome leg is closed or inactive");
5069
+ }
5070
+ if (market.description && NON_REGULAR_TIME.test(market.description)) {
5071
+ return definitiveNone("an outcome leg is not a regular-time market");
5072
+ }
4418
5073
  const yes = yesPrice(market);
4419
- if (yes == null) return void 0;
4420
- outcomes.push({ kind, teamCode, label, probability: yes });
4421
- if (market.updatedAt && (!asOf || market.updatedAt < asOf)) asOf = market.updatedAt;
4422
- const liq = numberish(market.liquidityNum ?? market.liquidity);
5074
+ if (yes == null) return malformed("market is not a readable Yes/No binary");
5075
+ outcomes.push({ kind, teamCode: teamCode2, label, probability: yes });
5076
+ const marketAsOf = canonicalTimestamp(market.updatedAt);
5077
+ if (!marketAsOf) {
5078
+ return malformed("market updatedAt missing or unparseable");
5079
+ }
5080
+ const nowMs = (options?.now ?? this.opts.now ?? /* @__PURE__ */ new Date()).getTime();
5081
+ if (Date.parse(marketAsOf) - nowMs > FUTURE_SKEW_MS) {
5082
+ return malformed("market updatedAt is dated forward");
5083
+ }
5084
+ if (!asOf || Date.parse(marketAsOf) < Date.parse(asOf)) asOf = marketAsOf;
5085
+ const rawLiq = market.liquidityNum ?? market.liquidity;
5086
+ const liq = numberish(rawLiq);
5087
+ if (rawLiq != null && liq == null) {
5088
+ return malformed("market liquidity is unreadable");
5089
+ }
4423
5090
  if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
4424
5091
  }
4425
5092
  const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
4426
- if (rawSum < 0.9 || rawSum > 1.15) return void 0;
5093
+ if (rawSum < 0.9 || rawSum > 1.15) {
5094
+ return ambiguous("outcome probabilities do not form a coherent 1X2");
5095
+ }
5096
+ if (!asOf) return malformed("no usable timestamp on the event or its markets");
4427
5097
  const signal = buildMarketSignal({
4428
5098
  match,
4429
5099
  source: "polymarket",
4430
- sourceMarketId: event.id ?? eventSlug,
4431
- asOf: asOf ?? (/* @__PURE__ */ new Date()).toISOString(),
5100
+ // Echoed into MCP structured content (tools.ts `market.id`), i.e. straight
5101
+ // into an agent's context. Stripping control characters is NOT sufficient
5102
+ // there: printable prose ("IGNORE PREVIOUS INSTRUCTIONS") survives that and
5103
+ // is precisely what matters for a model reading it. Gamma ids are short
5104
+ // opaque tokens, so validate that GRAMMAR and otherwise fall back to the
5105
+ // slug we derived ourselves.
5106
+ // The fallback is grammar-checked too. It is normally a slug we derived
5107
+ // ourselves, but `mapping.2026.json` can override it, so echoing it raw
5108
+ // was the one path around the agent-facing filter this line exists for.
5109
+ sourceMarketId: safeMarketId(event.id) ?? safeDerivedSlug(eventSlug),
5110
+ asOf,
4432
5111
  outcomes,
4433
5112
  liquidity,
4434
5113
  now: options?.now ?? this.opts.now,
4435
5114
  maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
4436
5115
  });
4437
- return signal.ambiguous ? void 0 : signal;
5116
+ return signal.ambiguous ? ambiguous("signal does not map cleanly onto this fixture") : valid(signal);
4438
5117
  }
4439
5118
  };
5119
+ function safeMarketId(id) {
5120
+ return typeof id === "string" && /^[0-9]{1,32}$/.test(id) ? id : void 0;
5121
+ }
5122
+ function safeDerivedSlug(slug) {
5123
+ return typeof slug === "string" && /^fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2}$/.test(slug) ? slug : void 0;
5124
+ }
4440
5125
  var POLYMARKET_TOKEN = {
4441
5126
  SUI: "che",
4442
5127
  // Switzerland
@@ -4450,8 +5135,16 @@ var POLYMARKET_TOKEN = {
4450
5135
  // Croatia
4451
5136
  COD: "cdr",
4452
5137
  // DR Congo
4453
- CPV: "cvi"
5138
+ CPV: "cvi",
4454
5139
  // Cabo Verde
5140
+ // TWO letters, not three — the one entry that is not ISO alpha-3. Verified
5141
+ // live: `fifwc-kor-cze-2026-06-11` is a 404, `fifwc-kr-cze-2026-06-11`
5142
+ // resolves to "Korea Republic vs. Czechia". Korea's three group fixtures
5143
+ // therefore had no market line at all. The `^[a-z]{3}$` guard in
5144
+ // `deriveEventSlugs` validates the FIFA CODE, not the token, so a two-letter
5145
+ // alias passes through it unharmed.
5146
+ KOR: "kr"
5147
+ // Korea Republic
4455
5148
  };
4456
5149
  function pmTokens(code) {
4457
5150
  const c = code.toLowerCase();
@@ -4482,18 +5175,21 @@ function slugToken(m) {
4482
5175
  return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
4483
5176
  }
4484
5177
  function isDrawMarket(m) {
4485
- return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
5178
+ const title = (m.groupItemTitle ?? "").trim().toLowerCase();
5179
+ return slugToken(m) === "draw" || title === "draw" || /^draw\s*\(/.test(title);
4486
5180
  }
4487
- function pickMarket(markets, teamCode, teamName) {
4488
- const tokens = pmTokens(teamCode);
5181
+ function pickMarket(markets, teamCode2, teamName) {
5182
+ const tokens = pmTokens(teamCode2);
4489
5183
  const name = teamName.trim().toLowerCase();
4490
5184
  const teamMarkets = markets.filter((m) => !isDrawMarket(m));
4491
- const bySlug = teamMarkets.find((m) => tokens.includes(slugToken(m)));
4492
- if (bySlug) return bySlug;
4493
- return teamMarkets.find((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name);
5185
+ const bySlug = teamMarkets.filter((m) => tokens.includes(slugToken(m)));
5186
+ if (bySlug.length > 1) return { kind: "ambiguous", count: bySlug.length };
5187
+ const byTitle = name ? teamMarkets.filter((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name) : [];
5188
+ if (byTitle.length > 1) return { kind: "ambiguous", count: byTitle.length };
5189
+ return selectOne([.../* @__PURE__ */ new Set([...bySlug, ...byTitle])]);
4494
5190
  }
4495
5191
  function pickDraw(markets) {
4496
- return markets.find(isDrawMarket);
5192
+ return selectOne(markets.filter(isDrawMarket));
4497
5193
  }
4498
5194
  function assertAllowedHost(base) {
4499
5195
  let host;
@@ -4507,20 +5203,29 @@ function assertAllowedHost(base) {
4507
5203
  }
4508
5204
  }
4509
5205
  function yesPrice(market) {
4510
- const labels = parseJsonArray(market.outcomes);
4511
- const prices = parseJsonArray(market.outcomePrices).map((p2) => Number(p2));
4512
- if (labels.length === 0 || labels.length !== prices.length) return void 0;
4513
- const i = labels.findIndex((l) => l.trim().toLowerCase() === "yes");
4514
- if (i < 0) return void 0;
4515
- const p = prices[i];
4516
- return typeof p === "number" && Number.isFinite(p) && p > 0 && p <= 1 ? p : void 0;
5206
+ const labels = parseJsonArray(market.outcomes).map((l) => l.trim().toLowerCase());
5207
+ const raw = parseJsonArray(market.outcomePrices);
5208
+ if (raw.some((v) => v.trim() === "" || !Number.isFinite(Number(v)))) return void 0;
5209
+ const prices = raw.map((v) => Number(v));
5210
+ if (labels.length !== 2 || prices.length !== 2) return void 0;
5211
+ const i = labels.indexOf("yes");
5212
+ const j = labels.indexOf("no");
5213
+ if (i < 0 || j < 0) return void 0;
5214
+ const yes = prices[i];
5215
+ const no = prices[j];
5216
+ if (![yes, no].every((v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1)) {
5217
+ return void 0;
5218
+ }
5219
+ if (Math.abs(yes + no - 1) > 0.05) return void 0;
5220
+ return yes > 0 ? yes : void 0;
4517
5221
  }
4518
5222
  function parseJsonArray(v) {
4519
- if (Array.isArray(v)) return v.map((x) => String(x));
5223
+ const asText = (x) => typeof x === "string" || typeof x === "number" ? String(x) : "";
5224
+ if (Array.isArray(v)) return v.map(asText);
4520
5225
  if (typeof v === "string") {
4521
5226
  try {
4522
5227
  const parsed = JSON.parse(v);
4523
- return Array.isArray(parsed) ? parsed.map((x) => String(x)) : [];
5228
+ return Array.isArray(parsed) ? parsed.map(asText) : [];
4524
5229
  } catch {
4525
5230
  return [];
4526
5231
  }
@@ -4528,13 +5233,17 @@ function parseJsonArray(v) {
4528
5233
  return [];
4529
5234
  }
4530
5235
  function numberish(v) {
4531
- if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
5236
+ if (typeof v === "number") return Number.isFinite(v) && v >= 0 ? v : void 0;
4532
5237
  if (typeof v === "string") {
4533
5238
  const n = Number(v);
4534
- return Number.isFinite(n) ? n : void 0;
5239
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
4535
5240
  }
4536
5241
  return void 0;
4537
5242
  }
5243
+ var MARKET_COMPETITIONS = /* @__PURE__ */ new Set([DEFAULT_COMPETITION]);
5244
+ function marketsCoverCompetition(competition = resolveCompetition()) {
5245
+ return MARKET_COMPETITIONS.has(competition);
5246
+ }
4538
5247
  function resolveMarketSource(explicit) {
4539
5248
  if (explicit) return explicit;
4540
5249
  if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
@@ -4551,21 +5260,15 @@ function makeMarketProvider(source) {
4551
5260
  return new FakeMarketProvider();
4552
5261
  // no synth → yields no signals, no network
4553
5262
  default:
5263
+ if (!marketsCoverCompetition()) return new FakeMarketProvider();
4554
5264
  return new PolymarketProvider();
4555
5265
  }
4556
5266
  }
4557
- async function getMarketSignal(provider, match, options) {
4558
- try {
4559
- return await provider.findSignal(match, options);
4560
- } catch {
4561
- return void 0;
4562
- }
4563
- }
4564
5267
  async function getMarketSignals(provider, matches, options) {
4565
5268
  try {
4566
5269
  return await provider.findSignals(matches, options);
4567
5270
  } catch {
4568
- return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
5271
+ return emptyBatch();
4569
5272
  }
4570
5273
  }
4571
5274
  var SHARE_HASHTAG = "#VibingLaVidaLoca";
@@ -4646,6 +5349,9 @@ function formatShareSnippet(input, options = {}) {
4646
5349
  if (input.degraded && input.matches.length > 0) {
4647
5350
  blocks.push("(Live data unavailable \u2014 showing the bundled schedule, not live scores.)");
4648
5351
  }
5352
+ if (includeMarkets && input.marketComplete === false) {
5353
+ blocks.push("(Market data unavailable or incomplete \u2014 not all fixtures were checked.)");
5354
+ }
4649
5355
  blocks.push(
4650
5356
  shareFooter({
4651
5357
  source: input.source,
@@ -4676,7 +5382,9 @@ function formatShareTable(input, options = {}) {
4676
5382
  const includeInstall = options.includeInstallLine !== false;
4677
5383
  const blocks = [];
4678
5384
  if (input.tables.length === 0) {
4679
- blocks.push(input.emptyNote ?? "No standings available.");
5385
+ blocks.push(
5386
+ input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
5387
+ );
4680
5388
  } else {
4681
5389
  for (const { group, rows } of input.tables) {
4682
5390
  blocks.push(
@@ -4815,9 +5523,21 @@ function matchLine(m, opts = {}) {
4815
5523
  const base = `${head} \u2014 ${tail} \xB7 ${stage} \xB7 ${matchLocation(m)}`;
4816
5524
  return (flair ? `${base} \u2014 ${flair}` : base).trimEnd();
4817
5525
  }
5526
+ var MAX_LIST_MATCHES = 40;
5527
+ function boundedRecords(rows, max = MAX_LIST_MATCHES) {
5528
+ return bounded(rows, max);
5529
+ }
5530
+ function truncationNote(list) {
5531
+ return list.truncated ? `
5532
+ (showing ${list.shown} of ${list.total} \u2014 list truncated)` : "";
5533
+ }
4818
5534
  function matchList(matches, empty, opts = {}) {
4819
5535
  if (matches.length === 0) return empty;
4820
- return matches.map((m) => `\u2022 ${matchLine(m, opts)}`).join("\n");
5536
+ const shown = matches.slice(0, MAX_LIST_MATCHES);
5537
+ const lines = shown.map((m) => `\u2022 ${matchLine(m, opts)}`).join("\n");
5538
+ const overflow = matches.length - shown.length;
5539
+ return overflow > 0 ? `${lines}
5540
+ \u2022 (list truncated \u2014 ${overflow} more not shown)` : lines;
4821
5541
  }
4822
5542
  function standingsTable(group, rows) {
4823
5543
  const header = `Group ${group}`;
@@ -4837,6 +5557,14 @@ function standingsTable(group, rows) {
4837
5557
  return [header, cols, ...lines].join("\n");
4838
5558
  }
4839
5559
  var DISCLAIMER = "Claudinho is an independent fan project \u2014 not affiliated with or endorsed by FIFA or Anthropic.";
5560
+ function capSignals(signals, kept) {
5561
+ const ids = new Set(kept.map((m) => m.id));
5562
+ const out = {};
5563
+ for (const [id, v] of Object.entries(signals)) {
5564
+ if (ids.has(id)) out[id] = v;
5565
+ }
5566
+ return out;
5567
+ }
4840
5568
 
4841
5569
  // src/tools.ts
4842
5570
  var adapters = /* @__PURE__ */ new Map();
@@ -4864,7 +5592,9 @@ function marketText(m, sig, args) {
4864
5592
  return `${marketHeader(m, args)}
4865
5593
  ${marketBlock(sig, m).join("\n")}`;
4866
5594
  }
5595
+ var MARKETS_SCOPE_NOTE = "Market signals cover the World Cup only; none are read for this competition.";
4867
5596
  function noSignalText(m, args, now) {
5597
+ if (!marketsCoverCompetition()) return `${marketHeader(m, args)} \u2014 ${MARKETS_SCOPE_NOTE}`;
4868
5598
  if (marketRelevant(m, now)) return `No reliable market signal for ${marketHeader(m, args)}.`;
4869
5599
  const verb = isFinished(m.status) ? "has finished" : "appears to have finished";
4870
5600
  return `${marketHeader(m, args)} ${verb} \u2014 market signals are pre-match and in-play reads.`;
@@ -4895,11 +5625,19 @@ var MARKETS_TOOL_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
4895
5625
  function memKey(competition, id) {
4896
5626
  return `polymarket:${competition}:${id}`;
4897
5627
  }
4898
- async function cachedMarketSignals(args, matches) {
4899
- if (args.marketProvider) return (await getMarketSignals(args.marketProvider, matches)).signals;
5628
+ async function cachedMarketSignals(args, matches, providerFactory = makeMarketProvider) {
5629
+ if (args.marketProvider) {
5630
+ const batch = await getMarketSignals(args.marketProvider, matches);
5631
+ return { signals: resolvedValues(batch), complete: batch.complete };
5632
+ }
4900
5633
  const source = resolveMarketSource();
4901
5634
  if (source !== "polymarket") {
4902
- return (await getMarketSignals(makeMarketProvider(source), matches, DEFAULT_ON_MARKET_OPTS)).signals;
5635
+ const batch = await getMarketSignals(
5636
+ providerFactory(source),
5637
+ matches,
5638
+ DEFAULT_ON_MARKET_OPTS
5639
+ );
5640
+ return { signals: resolvedValues(batch), complete: batch.complete };
4903
5641
  }
4904
5642
  const competition = resolveCompetition();
4905
5643
  const now = Date.now();
@@ -4908,39 +5646,46 @@ async function cachedMarketSignals(args, matches) {
4908
5646
  for (const m of matches) {
4909
5647
  const e = marketMem.get(memKey(competition, m.id));
4910
5648
  const ttl = e?.signal ? MEM_POSITIVE_TTL : MEM_NEGATIVE_TTL;
4911
- if (e && now - e.at <= ttl) {
5649
+ const fresh = e && now - e.at <= ttl;
5650
+ if (fresh && (!e.signal || marketSignalRendersFor(m, e.signal))) {
4912
5651
  if (e.signal) result.set(m.id, e.signal);
4913
5652
  } else {
4914
5653
  miss.push(m);
4915
5654
  }
4916
5655
  }
5656
+ let complete = true;
4917
5657
  if (miss.length > 0) {
4918
- const { signals: fetched, checked } = await getMarketSignals(
4919
- makeMarketProvider("polymarket"),
5658
+ const batch = await getMarketSignals(
5659
+ providerFactory("polymarket"),
4920
5660
  miss,
4921
5661
  DEFAULT_ON_MARKET_OPTS
4922
5662
  );
4923
- for (const id of checked) {
5663
+ const fetched = resolvedValues(batch);
5664
+ complete = batch.complete;
5665
+ for (const id of cacheableKeys(batch)) {
4924
5666
  marketMem.set(memKey(competition, id), { at: now, signal: fetched.get(id) ?? null });
4925
5667
  }
4926
5668
  for (const [id, s] of fetched) result.set(id, s);
4927
5669
  }
4928
- return result;
5670
+ return { signals: result, complete };
4929
5671
  }
4930
5672
  async function reliableMarketData(args, matches) {
4931
- if (!marketsEnabled()) return void 0;
5673
+ if (!marketsEnabled()) return { data: void 0, complete: true };
4932
5674
  const now = args.now ?? /* @__PURE__ */ new Date();
4933
5675
  const relevant = matches.filter((m) => marketRelevant(m, now));
4934
- if (relevant.length === 0) return void 0;
4935
- const signals = await cachedMarketSignals(args, relevant);
5676
+ if (relevant.length === 0) return { data: void 0, complete: true };
5677
+ const result = await cachedMarketSignals(args, relevant);
4936
5678
  const out = {};
4937
5679
  for (const m of relevant) {
4938
- const s = signals.get(m.id);
5680
+ const s = result.signals.get(m.id);
4939
5681
  if (s && isReliableMarketSignal(s, { now }) && marketSignalRendersFor(m, s)) {
4940
5682
  out[m.id] = marketData(s);
4941
5683
  }
4942
5684
  }
4943
- return Object.keys(out).length > 0 ? out : void 0;
5685
+ return {
5686
+ data: Object.keys(out).length > 0 ? out : void 0,
5687
+ complete: result.complete
5688
+ };
4944
5689
  }
4945
5690
  function fmtOpts(args) {
4946
5691
  return {
@@ -4965,16 +5710,28 @@ async function toolGetToday(args) {
4965
5710
  let text = `Matches on ${date}:
4966
5711
  ${matchList(todays, "No matches scheduled.", opts)}`;
4967
5712
  if (degraded) text += "\n\n(Live scores unavailable \u2014 showing the bundled schedule.)";
4968
- const marketSignals = await reliableMarketData(args, todays);
5713
+ const market = await reliableMarketData(args, todays);
5714
+ if (!market.complete) {
5715
+ text += "\n\n(Market data unavailable or incomplete \u2014 not all fixtures were checked.)";
5716
+ }
5717
+ const shownToday = boundedRecords(todays);
4969
5718
  return {
4970
5719
  text: withDisclaimer(text, source, args.lang),
4971
5720
  data: {
4972
5721
  date,
4973
5722
  degraded,
4974
5723
  source: source ?? null,
4975
- count: todays.length,
4976
- matches: todays,
4977
- ...marketSignals ? { marketSignals } : {}
5724
+ // ONE bounded view, so `count`, `matches` and the signal set cannot
5725
+ // disagree about the same payload. `count` is the TRUE total; bounding
5726
+ // only the TEXT would leave structuredContent unbounded, and that is
5727
+ // model context too — a repeated-record payload measured ~5 MB there.
5728
+ count: shownToday.total,
5729
+ truncated: shownToday.truncated,
5730
+ matches: shownToday.items,
5731
+ marketComplete: market.complete,
5732
+ // Capped in step with `matches`: a signal keyed to a match that is no
5733
+ // longer in the payload is dead weight in model context.
5734
+ ...market.data ? { marketSignals: capSignals(market.data, shownToday.items) } : {}
4978
5735
  }
4979
5736
  };
4980
5737
  }
@@ -4984,9 +5741,16 @@ async function toolGetLive(args = {}) {
4984
5741
  const opts = fmtOpts(args);
4985
5742
  const text = degraded ? "Live scores unavailable right now \u2014 could not reach the data provider." : `Live now:
4986
5743
  ${matchList(matches, "No matches in play right now.", opts)}`;
5744
+ const shownLive = boundedRecords(matches);
4987
5745
  return {
4988
5746
  text: withDisclaimer(text, source, args.lang),
4989
- data: { degraded, source: source ?? null, count: matches.length, matches }
5747
+ data: {
5748
+ degraded,
5749
+ source: source ?? null,
5750
+ count: shownLive.total,
5751
+ truncated: shownLive.truncated,
5752
+ matches: shownLive.items
5753
+ }
4990
5754
  };
4991
5755
  }
4992
5756
  async function toolGetMatch(args) {
@@ -4997,36 +5761,45 @@ async function toolGetMatch(args) {
4997
5761
  const opts = fmtOpts(args);
4998
5762
  const now = args.now ?? /* @__PURE__ */ new Date();
4999
5763
  let marketSignal;
5764
+ let marketComplete = true;
5000
5765
  if (marketsEnabled() && marketRelevant(match, now)) {
5001
- const s = (await cachedMarketSignals(args, [match])).get(match.id);
5766
+ const market = await cachedMarketSignals(args, [match]);
5767
+ marketComplete = market.complete;
5768
+ const s = market.signals.get(match.id);
5002
5769
  if (s && isReliableMarketSignal(s, { now }) && marketSignalRendersFor(match, s)) marketSignal = s;
5003
5770
  }
5004
5771
  const base = matchLine(match, opts);
5005
5772
  let text = marketSignal ? `${base}
5006
5773
  ${marketBlock(marketSignal, match).join("\n")}` : base;
5007
5774
  if (degraded) text += "\n\n(Live state unavailable \u2014 showing the scheduled fixture.)";
5775
+ if (!marketComplete) {
5776
+ text += "\n\n(Market data unavailable or incomplete \u2014 this match was not checked.)";
5777
+ }
5008
5778
  return {
5009
5779
  text: withDisclaimer(text, liveSource, args.lang),
5010
5780
  data: {
5011
5781
  degraded,
5012
5782
  source: liveSource ?? null,
5013
5783
  match,
5784
+ marketComplete,
5014
5785
  marketSignal: marketSignal ? marketData(marketSignal) : null
5015
5786
  }
5016
5787
  };
5017
5788
  }
5018
5789
  async function toolGetStandings(args) {
5019
5790
  const { tables, degraded, source } = await getStandings(resolveAdapter(args), args.group);
5020
- const shaped = tables.map((tb) => ({ group: tb.group, standings: tb.rows }));
5791
+ const boundedTables = boundedRecords(tables);
5792
+ const shaped = boundedTables.items.map((tb) => ({ group: tb.group, standings: tb.rows }));
5021
5793
  if (shaped.length === 0) {
5022
5794
  const g = args.group?.toUpperCase();
5023
- const msg = g ? `No group "${g}". Groups are A\u2013L.` : "No standings available.";
5795
+ const msg = degraded ? t(args.lang, "standings.unavailable") : g ? `No group "${g}".` : "No standings available.";
5024
5796
  return {
5025
- text: withDisclaimer(degraded ? `${msg} (Live standings unavailable.)` : msg, source, args.lang),
5797
+ text: withDisclaimer(msg, source, args.lang),
5026
5798
  data: { degraded, source: source ?? null, tables: args.group ? null : [] }
5027
5799
  };
5028
5800
  }
5029
5801
  let text = shaped.map((t2) => standingsTable(t2.group, t2.standings)).join("\n\n");
5802
+ text += truncationNote(boundedTables);
5030
5803
  if (degraded) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
5031
5804
  return {
5032
5805
  text: withDisclaimer(text, source, args.lang),
@@ -5069,7 +5842,7 @@ async function standingsResourceText(group, adapter) {
5069
5842
  const g = group.toUpperCase();
5070
5843
  const { tables, degraded, source } = await getStandings(adapter, g);
5071
5844
  const tb = tables[0];
5072
- let text = tb ? standingsTable(tb.group, tb.rows) : `No group ${g}.`;
5845
+ let text = tb ? standingsTable(tb.group, tb.rows) : degraded ? "Live standings unavailable." : `No group ${g}.`;
5073
5846
  if (degraded && tb) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
5074
5847
  return withDisclaimer(text, source);
5075
5848
  }
@@ -5115,14 +5888,16 @@ async function toolGetMarketSignal(args) {
5115
5888
  if (args.matchId) {
5116
5889
  const { match } = await getMatchById(resolveAdapter(args), args.matchId);
5117
5890
  const relevant = match ? marketRelevant(match, now) : false;
5118
- const sig = match && relevant ? await getMarketSignal(provider, match) : void 0;
5119
- const shown2 = match && sig && marketDisplayable(match, sig) ? sig : void 0;
5120
- const text2 = !match ? `No match found with id ${args.matchId}.` : shown2 ? marketText(match, shown2, args) : noSignalText(match, args, now);
5891
+ const batch2 = match && relevant ? await getMarketSignals(provider, [match], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
5892
+ const sig = match ? resolvedValues(batch2).get(match.id) : void 0;
5893
+ const shown2 = batch2.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
5894
+ const text2 = !match ? `No match found with id ${args.matchId}.` : !batch2.complete ? `Market data unavailable or incomplete for ${marketHeader(match, args)} \u2014 this match could not be checked.` : shown2 ? marketText(match, shown2, args) : noSignalText(match, args, now);
5121
5895
  return {
5122
5896
  text: withDisclaimer(text2),
5123
5897
  data: {
5124
5898
  matchId: args.matchId,
5125
5899
  informationalOnly: true,
5900
+ complete: batch2.complete,
5126
5901
  signal: shown2 ? marketData(shown2) : null
5127
5902
  }
5128
5903
  };
@@ -5131,9 +5906,10 @@ async function toolGetMarketSignal(args) {
5131
5906
  const code = args.team.toUpperCase();
5132
5907
  const { match: fixture, degraded } = await marketFixtureForTeam(resolveAdapter(args), code, now);
5133
5908
  const relevant = fixture ? marketRelevant(fixture, now) : false;
5134
- const sig = fixture && relevant ? await getMarketSignal(provider, fixture) : void 0;
5135
- const shown2 = fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
5136
- const text2 = !fixture ? degraded ? `Live feed unavailable \u2014 can't resolve ${code}'s next fixture right now.` : `No upcoming fixture found for ${code}.` : shown2 ? marketText(fixture, shown2, args) : noSignalText(fixture, args, now);
5909
+ const batch2 = fixture && relevant ? await getMarketSignals(provider, [fixture], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
5910
+ const sig = fixture ? resolvedValues(batch2).get(fixture.id) : void 0;
5911
+ const shown2 = batch2.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
5912
+ const text2 = !fixture ? degraded ? `Live feed unavailable \u2014 can't resolve ${code}'s next fixture right now.` : `No upcoming fixture found for ${code}.` : !batch2.complete ? `Market data unavailable or incomplete for ${marketHeader(fixture, args)} \u2014 this match could not be checked.` : shown2 ? marketText(fixture, shown2, args) : noSignalText(fixture, args, now);
5137
5913
  return {
5138
5914
  text: withDisclaimer(text2),
5139
5915
  data: {
@@ -5141,6 +5917,7 @@ async function toolGetMarketSignal(args) {
5141
5917
  matchId: fixture?.id ?? null,
5142
5918
  degraded,
5143
5919
  informationalOnly: true,
5920
+ complete: batch2.complete,
5144
5921
  signal: shown2 ? marketData(shown2) : null
5145
5922
  }
5146
5923
  };
@@ -5148,33 +5925,56 @@ async function toolGetMarketSignal(args) {
5148
5925
  const date = args.date ?? localDate(now.toISOString(), args.tz);
5149
5926
  const { matches } = await getMatchesForDate(resolveAdapter(args), date);
5150
5927
  const todays = fixturesByDate(date, matches, args.tz).filter((m) => marketRelevant(m, now));
5151
- const { signals } = await getMarketSignals(provider, todays, MARKETS_TOOL_OPTS);
5152
- const shown = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
5928
+ const batch = await getMarketSignals(provider, todays, MARKETS_TOOL_OPTS);
5929
+ const signals = resolvedValues(batch);
5930
+ const all = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
5153
5931
  (r) => !!r.signal && marketDisplayable(r.match, r.signal)
5154
5932
  );
5155
- const text = shown.length ? `Market signals on ${date}:
5156
- ${shown.map(({ match, signal }) => marketText(match, signal, args)).join("\n\n")}` : `No reliable market signals on ${date}.`;
5933
+ const shown = boundedRecords(all);
5934
+ let text = shown.shown ? `Market signals on ${date}:${truncationNote(shown)}
5935
+ ${shown.items.map(({ match, signal }) => marketText(match, signal, args)).join("\n\n")}` : (
5936
+ // An empty result and an INCOMPLETE one are different answers. The batch
5937
+ // knows which it was — a provider outage or an expired deadline leaves it
5938
+ // `complete: false` — and collapsing to `resolvedValues` threw that away,
5939
+ // so "we could not reach the market data" rendered as the confident
5940
+ // "there is none", which is the failure this project refuses everywhere
5941
+ // else.
5942
+ !marketsCoverCompetition() ? `${MARKETS_SCOPE_NOTE} (${date})` : batch.complete ? `No reliable market signals on ${date}.` : `Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`
5943
+ );
5944
+ if (shown.shown > 0 && !batch.complete) {
5945
+ text += `
5946
+
5947
+ Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`;
5948
+ }
5157
5949
  return {
5158
5950
  text: withDisclaimer(text),
5159
5951
  data: {
5160
5952
  date,
5161
5953
  informationalOnly: true,
5162
- signals: shown.map(({ signal }) => marketData(signal))
5954
+ // Self-describing: the prose says it was truncated, and so does the
5955
+ // structured payload — a consumer reading only `data` could not otherwise
5956
+ // tell 40 signals from all of them.
5957
+ count: shown.total,
5958
+ truncated: shown.truncated,
5959
+ // Stated, so a consumer reading only `data` can tell "none" from
5960
+ // "we could not check them all".
5961
+ complete: batch.complete,
5962
+ signals: shown.items.map(({ signal }) => marketData(signal))
5163
5963
  }
5164
5964
  };
5165
5965
  }
5166
5966
  async function reliableSignalMap(args, matches) {
5167
- if (!marketsEnabled()) return /* @__PURE__ */ new Map();
5967
+ if (!marketsEnabled()) return { signals: /* @__PURE__ */ new Map(), complete: true };
5168
5968
  const now = args.now ?? /* @__PURE__ */ new Date();
5169
5969
  const relevant = matches.filter((m) => marketRelevant(m, now));
5170
- if (relevant.length === 0) return /* @__PURE__ */ new Map();
5171
- const signals = await cachedMarketSignals(args, relevant);
5970
+ if (relevant.length === 0) return { signals: /* @__PURE__ */ new Map(), complete: true };
5971
+ const result = await cachedMarketSignals(args, relevant);
5172
5972
  const out = /* @__PURE__ */ new Map();
5173
5973
  for (const m of relevant) {
5174
- const s = signals.get(m.id);
5974
+ const s = result.signals.get(m.id);
5175
5975
  if (s && isReliableMarketSignal(s, { now }) && marketDisplayable(m, s)) out.set(m.id, s);
5176
5976
  }
5177
- return out;
5977
+ return { signals: out, complete: result.complete };
5178
5978
  }
5179
5979
  function shareOptions(args) {
5180
5980
  return {
@@ -5184,7 +5984,7 @@ function shareOptions(args) {
5184
5984
  includeInstallLine: args.includeInstallLine !== false
5185
5985
  };
5186
5986
  }
5187
- function shareResult(kind, target, team, input, options) {
5987
+ function shareResult(kind, target, team, input, options, total = input.matches.length) {
5188
5988
  const snippet = formatShareSnippet(input, options);
5189
5989
  return {
5190
5990
  // The snippet is self-contained: it carries its own non-affiliation
@@ -5201,28 +6001,34 @@ function shareResult(kind, target, team, input, options) {
5201
6001
  informationalOnly: true,
5202
6002
  style: options.style ?? "social",
5203
6003
  snippet,
6004
+ count: total,
6005
+ truncated: total > input.matches.length,
5204
6006
  matches: input.matches,
5205
6007
  marketSignals: Object.fromEntries(
5206
6008
  [...input.marketSignals ?? /* @__PURE__ */ new Map()].map(([id, s]) => [
5207
6009
  id,
5208
6010
  marketData(s)
5209
6011
  ])
5210
- )
6012
+ ),
6013
+ marketComplete: input.marketComplete ?? true
5211
6014
  }
5212
6015
  };
5213
6016
  }
5214
6017
  async function toolGetShareSnippet(args) {
5215
6018
  const options = shareOptions(args);
5216
- const signalsFor = (ms) => args.includeMarkets === false ? Promise.resolve(/* @__PURE__ */ new Map()) : reliableSignalMap(args, ms);
6019
+ const signalsFor = (ms) => args.includeMarkets === false ? Promise.resolve({ signals: /* @__PURE__ */ new Map(), complete: true }) : reliableSignalMap(args, ms);
5217
6020
  if (args.live) {
5218
6021
  const { matches, degraded: degraded2, source: source2 } = await getLiveMatches(resolveAdapter(args));
6022
+ const shownLive = boundedRecords(matches);
5219
6023
  return shareResult(
5220
6024
  "live",
5221
6025
  "live",
5222
6026
  void 0,
5223
6027
  {
5224
- title: "Live match pulse",
5225
- matches,
6028
+ title: `Live match pulse${truncationNote(shownLive)}`,
6029
+ // Bounded like the date branch: a share card is returned through MCP
6030
+ // before a human ever sees it. The count is STATED, not silently lost.
6031
+ matches: shownLive.items,
5226
6032
  source: source2,
5227
6033
  degraded: degraded2,
5228
6034
  // Feed down ⇒ don't let an empty card read as "nothing is on".
@@ -5231,7 +6037,8 @@ async function toolGetShareSnippet(args) {
5231
6037
  tz: args.tz,
5232
6038
  locale: args.lang
5233
6039
  },
5234
- { ...options, includeMarkets: false }
6040
+ { ...options, includeMarkets: false },
6041
+ matches.length
5235
6042
  );
5236
6043
  }
5237
6044
  if (args.group) {
@@ -5239,12 +6046,15 @@ async function toolGetShareSnippet(args) {
5239
6046
  const { tables, degraded: degraded2, source: source2 } = await getStandings(resolveAdapter(args), group);
5240
6047
  const snippet = formatShareTable(
5241
6048
  {
5242
- tables,
5243
- // Degraded static roster, no live provider: don't attribute one, and
5244
- // surface the not-live notice (the card gets pasted publicly).
6049
+ // Capped like the structured payload beside it. Bounding `data.tables`
6050
+ // while the rendered SNIPPET came from the full list meant the surface a
6051
+ // reader actually sees was the unbounded one.
6052
+ tables: boundedRecords(tables).items,
6053
+ // Degraded ⇒ no live provider: don't attribute one. An open-scope
6054
+ // outage has no compatible bundled roster, so name that empty state.
5245
6055
  source: degraded2 ? void 0 : source2,
5246
6056
  installLine: `npx @claudinho/cli table ${group}`,
5247
- emptyNote: `No group ${group}.`,
6057
+ emptyNote: degraded2 ? "Live standings unavailable." : `No group ${group}.`,
5248
6058
  degraded: degraded2
5249
6059
  },
5250
6060
  options
@@ -5259,7 +6069,7 @@ async function toolGetShareSnippet(args) {
5259
6069
  degraded: degraded2,
5260
6070
  informationalOnly: true,
5261
6071
  snippet,
5262
- tables: tables.map((tb) => ({ group: tb.group, standings: tb.rows }))
6072
+ tables: boundedRecords(tables).items.map((tb) => ({ group: tb.group, standings: tb.rows }))
5263
6073
  }
5264
6074
  };
5265
6075
  }
@@ -5305,6 +6115,7 @@ async function toolGetShareSnippet(args) {
5305
6115
  if (args.matchId) {
5306
6116
  const { match, degraded: degraded2, source: source2 } = await getMatchById(resolveAdapter(args), args.matchId);
5307
6117
  const matches = match ? [match] : [];
6118
+ const market2 = await signalsFor(matches);
5308
6119
  return shareResult(
5309
6120
  "match",
5310
6121
  args.matchId,
@@ -5312,7 +6123,8 @@ async function toolGetShareSnippet(args) {
5312
6123
  {
5313
6124
  title: "Match pulse",
5314
6125
  matches,
5315
- marketSignals: await signalsFor(matches),
6126
+ marketSignals: market2.signals,
6127
+ marketComplete: market2.complete,
5316
6128
  source: source2,
5317
6129
  degraded: degraded2,
5318
6130
  emptyNote: `No match found with id ${args.matchId}.`,
@@ -5332,6 +6144,7 @@ async function toolGetShareSnippet(args) {
5332
6144
  );
5333
6145
  const matches = fixture ? [fixture] : [];
5334
6146
  const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
6147
+ const market2 = await signalsFor(matches);
5335
6148
  return shareResult(
5336
6149
  "next",
5337
6150
  "next",
@@ -5339,7 +6152,8 @@ async function toolGetShareSnippet(args) {
5339
6152
  {
5340
6153
  title: `Next up for ${teamName}`,
5341
6154
  matches,
5342
- marketSignals: await signalsFor(matches),
6155
+ marketSignals: market2.signals,
6156
+ marketComplete: market2.complete,
5343
6157
  // Attribute the provider only when the overlay resolved the tie; parity
5344
6158
  // with get_next_fixture (a static group fixture carries no source).
5345
6159
  source: source2,
@@ -5356,14 +6170,19 @@ async function toolGetShareSnippet(args) {
5356
6170
  const { matches: all, degraded, source } = await getMatchesForDate(resolveAdapter(args), date);
5357
6171
  const todays = fixturesByDate(date, all, args.tz);
5358
6172
  const human = formatDate(`${date}T12:00:00.000Z`, { tz: args.tz, locale: args.lang });
6173
+ const shownToday = boundedRecords(todays);
6174
+ const market = await signalsFor(shownToday.items);
5359
6175
  return shareResult(
5360
6176
  "today",
5361
6177
  date,
5362
6178
  void 0,
5363
6179
  {
5364
- title: args.date ? `Matches \xB7 ${human}` : `Today's matches \xB7 ${human}`,
5365
- matches: todays,
5366
- marketSignals: await signalsFor(todays),
6180
+ title: (args.date ? `Matches \xB7 ${human}` : `Today's matches \xB7 ${human}`) + truncationNote(shownToday),
6181
+ // Bounded like every other model-facing payload — a share card is
6182
+ // returned through MCP before a human ever sees it.
6183
+ matches: shownToday.items,
6184
+ marketSignals: market.signals,
6185
+ marketComplete: market.complete,
5367
6186
  source,
5368
6187
  degraded,
5369
6188
  emptyNote: `No matches scheduled for ${human}.`,
@@ -5371,13 +6190,14 @@ async function toolGetShareSnippet(args) {
5371
6190
  tz: args.tz,
5372
6191
  locale: args.lang
5373
6192
  },
5374
- options
6193
+ options,
6194
+ todays.length
5375
6195
  );
5376
6196
  }
5377
6197
 
5378
6198
  // src/server.ts
5379
6199
  var SERVER_NAME = "claudinho";
5380
- var SERVER_VERSION = "0.9.3";
6200
+ var SERVER_VERSION = "0.10.0";
5381
6201
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
5382
6202
  Voice: when relaying scores, narrate with lively, regionally-appropriate football-commentary energy in the user's language. Each match line may end with a short exclamation ("\u2014 \xA1GOOOOL!") \u2014 use it as a tone cue. Keep every fact exact; never invent details and never impersonate or name a real commentator.`;
5383
6203
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
@@ -5415,37 +6235,59 @@ var matchOut = z.object({
5415
6235
  }).passthrough();
5416
6236
  var anyObj = z.object({}).passthrough();
5417
6237
  var src = z.string().nullable();
6238
+ var responseMeta = {
6239
+ responseTruncated: z.boolean().optional(),
6240
+ responseTruncation: z.string().optional()
6241
+ };
5418
6242
  var todayOut = {
5419
6243
  date: z.string(),
5420
6244
  degraded: z.boolean(),
5421
6245
  source: src,
6246
+ // `count` is the TRUE total and `matches` may be a bounded view of it, so the
6247
+ // payload states whether it was cut rather than leaving a consumer to infer
6248
+ // it from two numbers.
5422
6249
  count: z.number(),
6250
+ truncated: z.boolean(),
5423
6251
  matches: z.array(matchOut),
5424
- marketSignals: z.record(anyObj).optional()
6252
+ marketSignals: z.record(anyObj).optional(),
6253
+ marketComplete: z.boolean().optional().describe("False when optional market enrichment did not check every relevant fixture"),
6254
+ ...responseMeta
6255
+ };
6256
+ var liveOut = {
6257
+ degraded: z.boolean(),
6258
+ source: src,
6259
+ count: z.number(),
6260
+ truncated: z.boolean(),
6261
+ matches: z.array(matchOut),
6262
+ ...responseMeta
5425
6263
  };
5426
- var liveOut = { degraded: z.boolean(), source: src, count: z.number(), matches: z.array(matchOut) };
5427
6264
  var matchDetailOut = {
5428
6265
  match: matchOut.nullable(),
5429
6266
  degraded: z.boolean().optional(),
5430
6267
  source: src.optional(),
5431
- marketSignal: anyObj.nullable().optional()
6268
+ marketSignal: anyObj.nullable().optional(),
6269
+ marketComplete: z.boolean().optional().describe("False when optional market enrichment did not check this fixture"),
6270
+ ...responseMeta
5432
6271
  };
5433
6272
  var standingsOut = {
5434
6273
  degraded: z.boolean(),
5435
6274
  source: src,
5436
- tables: z.union([anyObj, z.array(anyObj), z.null()])
6275
+ tables: z.union([anyObj, z.array(anyObj), z.null()]),
6276
+ ...responseMeta
5437
6277
  };
5438
6278
  var bracketOut = {
5439
6279
  view: anyObj.nullable(),
5440
6280
  degraded: z.boolean().optional(),
5441
6281
  standingsDegraded: z.boolean().optional(),
5442
- source: src.optional()
6282
+ source: src.optional(),
6283
+ ...responseMeta
5443
6284
  };
5444
6285
  var nextOut = {
5445
6286
  team: z.string(),
5446
6287
  fixture: matchOut.nullable(),
5447
6288
  degraded: z.boolean(),
5448
- source: src
6289
+ source: src,
6290
+ ...responseMeta
5449
6291
  };
5450
6292
  var marketOut = {
5451
6293
  matchId: z.string().nullable().optional(),
@@ -5454,7 +6296,14 @@ var marketOut = {
5454
6296
  degraded: z.boolean().optional(),
5455
6297
  informationalOnly: z.boolean(),
5456
6298
  signal: anyObj.nullable().optional(),
5457
- signals: z.array(anyObj).optional()
6299
+ signals: z.array(anyObj).optional(),
6300
+ // Present on the list-shaped branches: the TRUE total and whether the array
6301
+ // beside it was capped, so a consumer reading only `structuredContent` can
6302
+ // tell a complete list from a truncated one.
6303
+ count: z.number().optional(),
6304
+ truncated: z.boolean().optional(),
6305
+ complete: z.boolean().optional().describe("False when the market provider did not complete every relevant read"),
6306
+ ...responseMeta
5458
6307
  };
5459
6308
  var shareOut = {
5460
6309
  kind: z.string(),
@@ -5471,23 +6320,183 @@ var shareOut = {
5471
6320
  tables: z.union([anyObj, z.array(anyObj), z.null()]).optional(),
5472
6321
  view: anyObj.nullable().optional(),
5473
6322
  matches: z.array(matchOut).optional(),
5474
- marketSignals: z.record(anyObj).optional()
6323
+ marketSignals: z.record(anyObj).optional(),
6324
+ marketComplete: z.boolean().optional().describe("False when optional market enrichment did not check every relevant fixture"),
6325
+ count: z.number().optional(),
6326
+ truncated: z.boolean().optional(),
6327
+ ...responseMeta
5475
6328
  };
5476
6329
  var teamInfo = z.object({ code: z.string(), name: z.string(), flag: z.string(), group: z.string() }).partial().passthrough();
5477
6330
  var teamOut = {
5478
6331
  query: z.string(),
5479
6332
  team: teamInfo.nullable(),
5480
6333
  matches: z.array(teamInfo),
5481
- count: z.number()
6334
+ count: z.number(),
6335
+ ...responseMeta
5482
6336
  };
6337
+ var MAX_RESPONSE_CHARS = 128e3;
6338
+ var RESPONSE_TRUNCATION = "Optional response detail was truncated to stay within the MCP context limit.";
6339
+ var SHRINK_FAILED = /* @__PURE__ */ Symbol("shrink-failed");
6340
+ var MAX_INSPECTION_DEPTH = 64;
6341
+ var MAX_INSPECTION_ARRAY_LENGTH = 4096;
6342
+ var MAX_INSPECTION_KEYS_PER_OBJECT = 1024;
6343
+ var MAX_INSPECTION_ENTRIES = 65536;
6344
+ var MAX_INSPECTION_CONTAINERS = 16384;
6345
+ var MAX_INSPECTION_CHARS = 2e6;
6346
+ function withinInspectionBudget(root) {
6347
+ const stack = [{ value: root, depth: 0 }];
6348
+ const ancestors = /* @__PURE__ */ new WeakSet();
6349
+ let containers = 0;
6350
+ let entries = 0;
6351
+ let chars = 0;
6352
+ try {
6353
+ while (stack.length > 0) {
6354
+ const frame = stack.pop();
6355
+ if (frame.exit) {
6356
+ ancestors.delete(frame.value);
6357
+ continue;
6358
+ }
6359
+ const { value, depth } = frame;
6360
+ if (typeof value === "string") {
6361
+ chars += value.length;
6362
+ if (chars > MAX_INSPECTION_CHARS) return false;
6363
+ continue;
6364
+ }
6365
+ if (!value || typeof value !== "object") continue;
6366
+ if (depth >= MAX_INSPECTION_DEPTH || ancestors.has(value)) return false;
6367
+ if (utilTypes.isProxy(value)) return false;
6368
+ containers += 1;
6369
+ if (containers > MAX_INSPECTION_CONTAINERS) return false;
6370
+ const proto = Object.getPrototypeOf(value);
6371
+ const expectedProto = Array.isArray(value) ? Array.prototype : Object.prototype;
6372
+ if (proto !== expectedProto && proto !== null) return false;
6373
+ if (Object.getOwnPropertyDescriptor(value, "toJSON") || proto && Object.getOwnPropertyDescriptor(proto, "toJSON")) {
6374
+ return false;
6375
+ }
6376
+ ancestors.add(value);
6377
+ stack.push({ value, depth, exit: true });
6378
+ if (Array.isArray(value)) {
6379
+ if (value.length > MAX_INSPECTION_ARRAY_LENGTH) return false;
6380
+ entries += value.length;
6381
+ if (entries > MAX_INSPECTION_ENTRIES) return false;
6382
+ for (let i = 0; i < value.length; i++) {
6383
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
6384
+ if (!descriptor) continue;
6385
+ if (!("value" in descriptor)) return false;
6386
+ stack.push({ value: descriptor.value, depth: depth + 1 });
6387
+ }
6388
+ continue;
6389
+ }
6390
+ let keys = 0;
6391
+ for (const key in value) {
6392
+ keys += 1;
6393
+ if (keys > MAX_INSPECTION_KEYS_PER_OBJECT) return false;
6394
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
6395
+ if (!descriptor?.enumerable) continue;
6396
+ if (!("value" in descriptor)) return false;
6397
+ entries += 1;
6398
+ chars += key.length;
6399
+ if (entries > MAX_INSPECTION_ENTRIES || chars > MAX_INSPECTION_CHARS) return false;
6400
+ stack.push({ value: descriptor.value, depth: depth + 1 });
6401
+ }
6402
+ }
6403
+ return true;
6404
+ } catch {
6405
+ return false;
6406
+ }
6407
+ }
6408
+ function shrink(value, pass, depth = 0, ancestors = /* @__PURE__ */ new WeakSet()) {
6409
+ if (depth >= 64) return SHRINK_FAILED;
6410
+ if (Array.isArray(value)) {
6411
+ if (ancestors.has(value)) return SHRINK_FAILED;
6412
+ ancestors.add(value);
6413
+ const items = pass === 3 ? value.slice(0, 8) : value;
6414
+ const out = [];
6415
+ for (const item of items) {
6416
+ const shrunk = shrink(item, pass, depth + 1, ancestors);
6417
+ if (shrunk === SHRINK_FAILED) return SHRINK_FAILED;
6418
+ out.push(shrunk);
6419
+ }
6420
+ ancestors.delete(value);
6421
+ return out;
6422
+ }
6423
+ if (value && typeof value === "object") {
6424
+ if (ancestors.has(value)) return SHRINK_FAILED;
6425
+ ancestors.add(value);
6426
+ const out = {};
6427
+ for (const [k, v] of Object.entries(value)) {
6428
+ if (k === "events") continue;
6429
+ const shrunk = shrink(v, pass, depth + 1, ancestors);
6430
+ if (shrunk === SHRINK_FAILED) return SHRINK_FAILED;
6431
+ out[k] = shrunk;
6432
+ }
6433
+ ancestors.delete(value);
6434
+ return out;
6435
+ }
6436
+ if (typeof value === "string") {
6437
+ if (pass >= 2 && value.length > 2e3) return `${value.slice(0, 2e3)}\u2026`;
6438
+ }
6439
+ return value;
6440
+ }
6441
+ function size(v) {
6442
+ try {
6443
+ return JSON.stringify(v ?? null)?.length ?? Number.POSITIVE_INFINITY;
6444
+ } catch {
6445
+ return Number.POSITIVE_INFINITY;
6446
+ }
6447
+ }
6448
+ function boundResponse(data) {
6449
+ if (!withinInspectionBudget(data)) return null;
6450
+ if (size(data) <= MAX_RESPONSE_CHARS) return data;
6451
+ if (!data || typeof data !== "object" || Array.isArray(data)) return null;
6452
+ for (const pass of [1, 2, 3]) {
6453
+ const candidate = shrink(data, pass);
6454
+ if (candidate === SHRINK_FAILED || !candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
6455
+ continue;
6456
+ }
6457
+ const marked = {
6458
+ ...candidate,
6459
+ responseTruncated: true,
6460
+ responseTruncation: RESPONSE_TRUNCATION
6461
+ };
6462
+ if (size(marked) <= MAX_RESPONSE_CHARS) return marked;
6463
+ }
6464
+ return null;
6465
+ }
6466
+ var MAX_TEXT_CHARS = 32e3;
6467
+ var DUAL_EMIT_LIMIT = 16e3;
5483
6468
  function toContent(r) {
5484
- return {
5485
- content: [
5486
- { type: "text", text: r.text },
5487
- { type: "text", text: "```json\n" + JSON.stringify(r.data, null, 2) + "\n```" }
5488
- ],
5489
- structuredContent: r.data
5490
- };
6469
+ const data = boundResponse(r.data);
6470
+ const dataTruncated = !!data && typeof data === "object" && data.responseTruncated === true;
6471
+ const marker = dataTruncated ? `
6472
+
6473
+ (${RESPONSE_TRUNCATION})` : "";
6474
+ const room = Math.max(0, MAX_TEXT_CHARS - marker.length - "\n(truncated)".length);
6475
+ const text = r.text.length > room ? `${r.text.slice(0, room)}
6476
+ (truncated)${marker}` : r.text + marker;
6477
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
6478
+ const error = "Response data exceeded the MCP context limit and could not be reduced without violating its output schema.";
6479
+ return {
6480
+ content: [
6481
+ {
6482
+ type: "text",
6483
+ text: `${text.slice(0, Math.max(0, MAX_TEXT_CHARS - error.length - 2))}
6484
+
6485
+ ${error}`
6486
+ }
6487
+ ],
6488
+ isError: true
6489
+ };
6490
+ }
6491
+ const compact = JSON.stringify(data);
6492
+ const content = [{ type: "text", text }];
6493
+ if (compact.length <= DUAL_EMIT_LIMIT) {
6494
+ content.push({
6495
+ type: "text",
6496
+ text: "```json\n" + JSON.stringify(data, null, 2) + "\n```"
6497
+ });
6498
+ }
6499
+ return { content, structuredContent: data };
5491
6500
  }
5492
6501
  function buildServer() {
5493
6502
  const server = new McpServer(
@@ -5498,7 +6507,7 @@ function buildServer() {
5498
6507
  "get_today",
5499
6508
  {
5500
6509
  title: "Today's matches",
5501
- description: "All fixtures for a date (default: today), with live score and minute overlaid on any match in play. Use this for a whole day's card; for only in-play matches use get_live, for one team's match use get_next_fixture, for a single match's detail use get_match. Kickoffs render in tz; lang localizes dates, attribution, and commentary (en/es/pt/fr); flavor sets commentary tone.",
6510
+ description: "All fixtures for a date (default: today), with live score and minute overlaid on any match in play. Optional prediction-market enrichment carries marketComplete; false means the read was incomplete, not that no signal exists. Use this for a whole day's card; for only in-play matches use get_live, for one team's match use get_next_fixture, for a single match's detail use get_match. Kickoffs render in tz; lang localizes dates, attribution, and commentary (en/es/pt/fr); flavor sets commentary tone.",
5502
6511
  inputSchema: {
5503
6512
  date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today)"),
5504
6513
  ...commonArgs
@@ -5524,7 +6533,7 @@ function buildServer() {
5524
6533
  "get_match",
5525
6534
  {
5526
6535
  title: "Match detail",
5527
- description: "One match by its id, with live score/minute overlaid when it's in play. Get the id from get_today or get_live; to find a team's match without an id, use get_next_fixture. tz/lang/flavor affect formatting.",
6536
+ description: "One match by its id, with live score/minute overlaid when it's in play. Optional prediction-market enrichment carries marketComplete; false means the read was incomplete, not that no signal exists. Get the id from get_today or get_live; to find a team's match without an id, use get_next_fixture. tz/lang/flavor affect formatting.",
5528
6537
  inputSchema: { id: z.string().describe("Match id"), ...commonArgs },
5529
6538
  annotations: { readOnlyHint: true, openWorldHint: true },
5530
6539
  outputSchema: matchDetailOut
@@ -5535,7 +6544,7 @@ function buildServer() {
5535
6544
  "get_standings",
5536
6545
  {
5537
6546
  title: "Group standings",
5538
- description: "Live cumulative group standings \u2014 pass a group letter A\u2013L, or omit for all 12. Returns ranked rows (team, played, W/D/L, goal difference, points). Use get_today for fixtures/scores and get_next_fixture for one team. Falls back to a roster at zero (flagged degraded) if live standings are unavailable.",
6547
+ description: "Live cumulative group standings \u2014 pass a group letter A\u2013L, or omit for all 12. Returns ranked rows (team, played, W/D/L, goal difference, points). Use get_today for fixtures/scores and get_next_fixture for one team. If unavailable, the default World Cup scope returns a roster at zero; competitions without a compatible bundled roster return no tables. Both are flagged degraded.",
5539
6548
  inputSchema: {
5540
6549
  group: groupArg.optional().describe("Group letter A\u2013L (omit for all)"),
5541
6550
  ...commonArgs
@@ -5575,7 +6584,7 @@ function buildServer() {
5575
6584
  "get_market_signal",
5576
6585
  {
5577
6586
  title: "Prediction-market signal",
5578
- description: "Read-only prediction-market signals for a match (by id), a team's current-or-next fixture, or a date (default: today). Returns market-implied percentages with attribution. Shown only before and during a match \u2014 finished matches have no market read. Informational only \u2014 relay the numbers factually; do not add betting, trading, or 'value' advice, and do not invent links.",
6587
+ description: "Read-only prediction-market signals for a match (by id), a team's current-or-next fixture, or a date (default: today). Returns market-implied percentages with attribution; complete:false means the provider read was incomplete, not that no signal exists. Shown only before and during a match \u2014 finished matches have no market read. Informational only \u2014 relay the numbers factually; do not add betting, trading, or 'value' advice, and do not invent links.",
5579
6588
  inputSchema: {
5580
6589
  matchId: z.string().optional().describe("Match id (most specific)"),
5581
6590
  team: teamArg.optional().describe(
@@ -5594,7 +6603,7 @@ function buildServer() {
5594
6603
  "get_share_snippet",
5595
6604
  {
5596
6605
  title: "Shareable match snippet",
5597
- description: `A polished, copy-pasteable card (plain text) for a match (matchId), a team's next fixture (team), a group's standings table (group, e.g. "A"), the knockout bracket (bracket: true), a date (default: today), or live matches (live: true). Returns the ready-to-paste snippet plus structured data \u2014 hand the snippet text to the user verbatim. No links; it carries a non-affiliation disclaimer, and any market line stays informational only.`,
6606
+ description: `A polished, copy-pasteable card (plain text) for a match (matchId), a team's next fixture (team), a group's standings table (group, e.g. "A"), the knockout bracket (bracket: true), a date (default: today), or live matches (live: true). Returns the ready-to-paste snippet plus structured data \u2014 hand the snippet text to the user verbatim. marketComplete:false is stated inside the card as an incomplete optional read. No links; it carries a non-affiliation disclaimer, and any market line stays informational only.`,
5598
6607
  inputSchema: {
5599
6608
  matchId: z.string().optional().describe("Match id (most specific)"),
5600
6609
  team: teamArg.optional().describe("3-letter team code for that team's next fixture, e.g. MEX"),