@claudinho/mcp 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -1
  2. package/dist/index.js +1372 -373
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ 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 { types as utilTypes } from "util";
8
9
  import { z } from "zod";
9
10
 
10
11
  // ../core/dist/index.js
@@ -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.2"} (+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,221 @@ 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 seenTeams = /* @__PURE__ */ new Set();
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 key = r.value.providerId ?? r.value.team.code;
3438
+ if (seenTeams.has(key) || seenRanks.has(r.value.providerRank) || r.value.providerId !== void 0 && seenProviderIds.has(r.value.providerId)) {
3439
+ complete = false;
3440
+ continue;
3441
+ }
3442
+ seenTeams.add(key);
3443
+ seenRanks.add(r.value.providerRank);
3444
+ if (r.value.providerId !== void 0) seenProviderIds.add(r.value.providerId);
3445
+ const { providerId: _dropId, providerRank: rank, ...row } = r.value;
3446
+ ranked.push({ row, rank });
3447
+ }
3054
3448
  ranked.sort((a, b) => {
3055
3449
  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);
3450
+ if (b.row.points !== a.row.points) return b.row.points - a.row.points;
3451
+ if (b.row.goalDiff !== a.row.goalDiff) return b.row.goalDiff - a.row.goalDiff;
3452
+ return b.row.goalsFor - a.row.goalsFor;
3059
3453
  });
3454
+ if (ranked.length === 0) {
3455
+ complete = false;
3456
+ continue;
3457
+ }
3060
3458
  out.push({ group: letter, rows: ranked.map((x) => x.row) });
3061
3459
  }
3062
- out.sort((a, b) => a.group.localeCompare(b.group));
3063
- return out;
3460
+ return {
3461
+ items: out,
3462
+ total: seenGroups.size,
3463
+ shown: out.length,
3464
+ // We stopped early if the child list or any single group's rows were cut.
3465
+ truncated: !sawAllChildren || rowsTruncated,
3466
+ complete: complete && !rowsTruncated
3467
+ };
3468
+ }
3469
+ var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3470
+ var DEFAULT_COMPETITION = "fifa.world";
3471
+ var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3472
+ var USER_AGENT = `claudinho/${"0.9.4"} (+https://github.com/arturogarrido/claudinho)`;
3473
+ var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
3474
+ function competitionBase(slug) {
3475
+ return `${ESPN_SOCCER}/${slug}`;
3476
+ }
3477
+ var DEFAULT_TIMEOUT_MS = 6e3;
3478
+ var STANDINGS_SHARE_MS = 3e4;
3479
+ var ProviderError = class extends Error {
3480
+ kind;
3481
+ status;
3482
+ constructor(message, kind, status) {
3483
+ super(message);
3484
+ this.name = "ProviderError";
3485
+ this.kind = kind;
3486
+ this.status = status;
3487
+ }
3488
+ /** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
3489
+ get throttled() {
3490
+ return this.kind === "http" && (this.status === 429 || this.status === 403);
3491
+ }
3492
+ };
3493
+ function toEspnDate(d) {
3494
+ return d.replace(/\D/g, "").slice(0, 8);
3495
+ }
3496
+ function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length > 0) {
3497
+ if (!hasUsableRecord && (!parsed.complete || parsed.total > 0)) {
3498
+ throw new ProviderError(`ESPN ${kind} payload had no readable records`, "parse");
3499
+ }
3500
+ return [...parsed.items];
3064
3501
  }
3065
3502
  var EspnAdapter = class {
3066
3503
  constructor(opts = {}) {
3067
3504
  this.opts = opts;
3505
+ const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
3506
+ this.expectedStandingsGroups = expected ? [...expected] : void 0;
3507
+ this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
3068
3508
  }
3069
3509
  opts;
3070
3510
  name = "espn";
3071
3511
  capabilities = { push: false, latencyHintSec: 45 };
3072
- /** Cached team-code -> group-letter map (built lazily from standings). */
3512
+ expectedStandingsGroups;
3513
+ standingsFallbackGroups;
3514
+ /** Short-lived team-code -> group-letter map (built lazily from standings). */
3073
3515
  groupMap;
3074
3516
  /**
3075
3517
  * One in-flight/recent standings fetch shared by fetchStandings and
@@ -3112,38 +3554,48 @@ var EspnAdapter = class {
3112
3554
  if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
3113
3555
  return this.standingsShared.promise;
3114
3556
  }
3115
- const promise = this.get(this.standingsUrl()).then(
3116
- (d) => parseStandings(d)
3117
- );
3557
+ const promise = this.get(this.standingsUrl()).then((d) => {
3558
+ const parsed = parseEspnStandings(d);
3559
+ return usableProviderItems(
3560
+ "standings",
3561
+ parsed,
3562
+ parsed.items.some((table) => table.rows.length > 0)
3563
+ );
3564
+ });
3118
3565
  this.standingsShared = { at: now, promise };
3119
- promise.catch(() => {
3566
+ void promise.catch(() => {
3120
3567
  if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
3121
3568
  });
3122
3569
  return promise;
3123
3570
  }
3124
3571
  /**
3125
3572
  * 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}.
3573
+ * on fetch failure. Group-stage only: non-group `children` are filtered out
3574
+ * by {@link parseStandings}; malformed rows are omitted without hiding their
3575
+ * readable siblings.
3128
3576
  */
3129
3577
  async fetchStandings() {
3130
3578
  return this.sharedStandings();
3131
3579
  }
3132
3580
  /**
3133
- * Build (and cache) a team-code -> group-letter map from the standings
3581
+ * Build (and briefly cache) a team-code -> group-letter map from the standings
3134
3582
  * 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.
3583
+ * transient failure is NOT cached, and a partial successful parse expires at
3584
+ * the standings TTL, so neither can silently drop group letters for the
3585
+ * adapter's lifetime.
3137
3586
  * Reuses the same parse/fetch as {@link fetchStandings}, so the two never
3138
3587
  * drift and one command never fetches standings twice.
3139
3588
  */
3140
3589
  async fetchGroupMap(force = false) {
3141
- if (this.groupMap && !force) return this.groupMap;
3590
+ const now = Date.now();
3591
+ if (!force && this.groupMap && now - this.groupMap.at < STANDINGS_SHARE_MS) {
3592
+ return this.groupMap.value;
3593
+ }
3142
3594
  try {
3143
3595
  const tables = await this.sharedStandings();
3144
3596
  const map = {};
3145
3597
  for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
3146
- this.groupMap = map;
3598
+ this.groupMap = { at: Date.now(), value: map };
3147
3599
  return map;
3148
3600
  } catch {
3149
3601
  return {};
@@ -3158,7 +3610,8 @@ var EspnAdapter = class {
3158
3610
  this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
3159
3611
  this.get(url.toString())
3160
3612
  ]);
3161
- return (data.events ?? []).map((ev) => mapEspnEvent(ev, { groupByTeam }));
3613
+ const parsed = parseEspnEvents(data, { groupByTeam });
3614
+ return usableProviderItems("scoreboard", parsed);
3162
3615
  }
3163
3616
  async get(url) {
3164
3617
  const doFetch = this.opts.fetchImpl ?? fetch;
@@ -3968,17 +4421,26 @@ async function getMatchesForDate(adapter, dateISO) {
3968
4421
  }
3969
4422
  async function getStandings(adapter, group) {
3970
4423
  const want = group?.toUpperCase();
4424
+ const expected = adapter.expectedStandingsGroups;
4425
+ if (want && expected && !expected.includes(want)) {
4426
+ return { tables: [], degraded: false };
4427
+ }
3971
4428
  if (adapter.fetchStandings) {
3972
4429
  try {
3973
4430
  const all = await adapter.fetchStandings();
3974
4431
  const tables2 = (want ? all.filter((t2) => t2.group === want) : all).sort(
3975
4432
  (a, b) => a.group.localeCompare(b.group)
3976
4433
  );
3977
- return { tables: tables2, degraded: false, source: adapter.name };
4434
+ const availableGroups = new Set(tables2.map((table) => table.group));
4435
+ const expectedGroupWasOmitted = want ? (expected?.includes(want) ?? false) && tables2.length === 0 : expected?.some((group2) => !availableGroups.has(group2)) ?? false;
4436
+ if (!expectedGroupWasOmitted) {
4437
+ return { tables: tables2, degraded: false, source: adapter.name };
4438
+ }
3978
4439
  } catch {
3979
4440
  }
3980
4441
  }
3981
- const letters = want ? [want] : groups();
4442
+ const fallbackGroups = adapter.standingsFallbackGroups;
4443
+ const letters = fallbackGroups ? want ? fallbackGroups.includes(want) ? [want] : [] : [...new Set(fallbackGroups)].sort((a, b) => a.localeCompare(b)) : [];
3982
4444
  const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
3983
4445
  return { tables, degraded: true };
3984
4446
  }
@@ -4035,7 +4497,10 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
4035
4497
  try {
4036
4498
  const win = knockoutWindow();
4037
4499
  if (adapter.fetchWindow && win) {
4038
- fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
4500
+ fixtures = mergeLive(
4501
+ fixtures,
4502
+ await adapter.fetchWindow(win.start, win.end)
4503
+ );
4039
4504
  }
4040
4505
  } catch {
4041
4506
  overlayFailed = true;
@@ -4084,14 +4549,141 @@ async function getMatchById(adapter, id) {
4084
4549
  async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
4085
4550
  try {
4086
4551
  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();
4552
+ const matches = (adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchLive()).filter((m) => isLive(m.status));
4090
4553
  return { matches, degraded: false, source: adapter.name };
4091
4554
  } catch {
4092
4555
  return { matches: [], degraded: true };
4093
4556
  }
4094
4557
  }
4558
+ function pct(p) {
4559
+ return Math.round(p * 100);
4560
+ }
4561
+ var KNOWN_MARKET_SOURCES = ["polymarket", "fake"];
4562
+ function marketSourceLabel(source) {
4563
+ if (source === "polymarket") return "Polymarket";
4564
+ if (source === "fake") return "demo data";
4565
+ return source.charAt(0).toUpperCase() + source.slice(1);
4566
+ }
4567
+ function outcomeLabel(o, match) {
4568
+ if (o.kind === "home") return match.home.name;
4569
+ if (o.kind === "away") return match.away.name;
4570
+ if (o.kind === "draw") return "Draw";
4571
+ return o.label;
4572
+ }
4573
+ function utcHhmm(iso) {
4574
+ const t2 = Date.parse(iso);
4575
+ if (!Number.isFinite(t2)) return "";
4576
+ return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
4577
+ }
4578
+ function marketFavoriteText(signal, match) {
4579
+ const fav = signal.favorite;
4580
+ if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
4581
+ if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
4582
+ const name = fav.kind === "home" ? match.home.name : match.away.name;
4583
+ return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
4584
+ }
4585
+ function marketProbabilityText(signal, match) {
4586
+ const order = ["home", "draw", "away"];
4587
+ const parts = [];
4588
+ for (const kind of order) {
4589
+ const o = signal.outcomes.find((x) => x.kind === kind);
4590
+ if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4591
+ }
4592
+ for (const o of signal.outcomes) {
4593
+ if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
4594
+ }
4595
+ return parts.join(" \xB7 ");
4596
+ }
4597
+ function marketAttributionText(signal) {
4598
+ const time = utcHhmm(signal.asOf);
4599
+ const src2 = `Source: ${marketSourceLabel(signal.source)}`;
4600
+ return time ? `${src2} \xB7 updated ${time}` : src2;
4601
+ }
4602
+ function marketLine(signal, match) {
4603
+ return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
4604
+ signal.source
4605
+ )} \xB7 informational only`;
4606
+ }
4607
+ function marketBlock(signal, match) {
4608
+ const lines = [];
4609
+ if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
4610
+ lines.push(marketFavoriteText(signal, match));
4611
+ lines.push(marketProbabilityText(signal, match));
4612
+ lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
4613
+ return lines;
4614
+ }
4615
+ var MAX_OUTCOMES = 128;
4616
+ var MATCH_ID = /^[0-9]{1,20}$/;
4617
+ var MARKET_ID = /^(?:[0-9]{1,32}|fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2})$/;
4618
+ var OUTCOME_KINDS = /* @__PURE__ */ new Set(["home", "draw", "away", "other"]);
4619
+ var TEAM_CODE_COLUMNS2 = 8;
4620
+ function sealOutcome(raw) {
4621
+ if (!raw || typeof raw !== "object") return void 0;
4622
+ const o = raw;
4623
+ const kind = member(o.kind, OUTCOME_KINDS);
4624
+ const p = probability(o.probability);
4625
+ if (!kind || p === void 0) return void 0;
4626
+ const out = { kind };
4627
+ if (o.teamCode !== void 0) {
4628
+ if (typeof o.teamCode !== "string") return void 0;
4629
+ out.teamCode = humanLabel(o.teamCode, TEAM_CODE_COLUMNS2);
4630
+ }
4631
+ out.label = humanLabel(o.label);
4632
+ out.probability = p;
4633
+ if ((out.kind === "home" || out.kind === "away") && !out.teamCode) return void 0;
4634
+ return out;
4635
+ }
4636
+ function hasDuplicateKind(outcomes) {
4637
+ const seen = /* @__PURE__ */ new Set();
4638
+ for (const o of outcomes) {
4639
+ if (o.kind === "other") continue;
4640
+ if (seen.has(o.kind)) return true;
4641
+ seen.add(o.kind);
4642
+ }
4643
+ return false;
4644
+ }
4645
+ function sealMarketSignal(raw, options = {}) {
4646
+ if (!raw || typeof raw !== "object") return malformed("signal is not an object");
4647
+ const s = raw;
4648
+ const matchId = opaqueId(s.matchId, MATCH_ID);
4649
+ if (!matchId) return malformed("signal names no fixture");
4650
+ if (!Array.isArray(s.outcomes) || s.outcomes.length > MAX_OUTCOMES) {
4651
+ return malformed("signal outcomes are absent or exceed the cap");
4652
+ }
4653
+ const outcomes = [];
4654
+ for (const rawOutcome of takeBounded(s.outcomes, MAX_OUTCOMES)) {
4655
+ const outcome = sealOutcome(rawOutcome);
4656
+ if (!outcome) return malformed("signal carries an unreadable outcome");
4657
+ outcomes.push(outcome);
4658
+ }
4659
+ if (hasDuplicateKind(outcomes)) {
4660
+ return ambiguous("two outcomes claim the same result");
4661
+ }
4662
+ const sourceMarketId = opaqueId(s.sourceMarketId, MARKET_ID);
4663
+ const liquidity = quantity(s.liquidity);
4664
+ const volume24h = quantity(s.volume24h);
4665
+ const out = {
4666
+ matchId,
4667
+ // Allow-listed, not merely stripped: this lands in the provider-attribution
4668
+ // slot, where `marketSourceLabel` falls through to the raw string for an
4669
+ // unrecognized provider — attacker prose where the reader expects
4670
+ // "Polymarket".
4671
+ source: member(s.source, new Set(KNOWN_MARKET_SOURCES)) ?? ""
4672
+ };
4673
+ if (sourceMarketId) out.sourceMarketId = sourceMarketId;
4674
+ out.asOf = canonicalTimestamp(s.asOf) ?? "";
4675
+ out.fetchedAt = canonicalTimestamp(s.fetchedAt) ?? "";
4676
+ out.outcomes = outcomes;
4677
+ const isAmbiguous = s.ambiguous !== false;
4678
+ const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
4679
+ if (favorite) out.favorite = favorite;
4680
+ if (liquidity !== void 0) out.liquidity = liquidity;
4681
+ if (volume24h !== void 0) out.volume24h = volume24h;
4682
+ out.stale = s.stale !== false;
4683
+ out.ambiguous = isAmbiguous || out.source === "";
4684
+ out.stale = out.stale || isStaleSignal(out, { now: options.now, maxAgeMs: options.maxAgeMs });
4685
+ return valid(out);
4686
+ }
4095
4687
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
4096
4688
  function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
4097
4689
  if (isLive(match.status)) return true;
@@ -4110,9 +4702,9 @@ function normalizeOutcomes(outcomes) {
4110
4702
  probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
4111
4703
  }));
4112
4704
  }
4113
- function favoriteStrength(probability) {
4114
- if (probability >= 0.65) return "clear";
4115
- if (probability >= 0.52) return "slight";
4705
+ function favoriteStrength(probability2) {
4706
+ if (probability2 >= 0.65) return "clear";
4707
+ if (probability2 >= 0.52) return "slight";
4116
4708
  return "close";
4117
4709
  }
4118
4710
  function deriveFavorite(outcomes) {
@@ -4131,14 +4723,16 @@ function deriveFavorite(outcomes) {
4131
4723
  }
4132
4724
  function mapsCleanly(match, outcomes) {
4133
4725
  if (outcomes.some((o) => o.kind === "other")) return false;
4726
+ const kinds = outcomes.map((o) => o.kind);
4727
+ if (new Set(kinds).size !== kinds.length) return false;
4134
4728
  const home = outcomes.find((o) => o.kind === "home");
4135
4729
  const away = outcomes.find((o) => o.kind === "away");
4136
4730
  const draw = outcomes.find((o) => o.kind === "draw");
4137
4731
  if (!home || !away) return false;
4138
- if (home.teamCode && home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
4732
+ if (!home.teamCode || home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
4139
4733
  return false;
4140
4734
  }
4141
- if (away.teamCode && away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
4735
+ if (!away.teamCode || away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
4142
4736
  return false;
4143
4737
  }
4144
4738
  if (match.stage === "GROUP" && !draw) return false;
@@ -4153,11 +4747,13 @@ function hasSaneDistribution(outcomes) {
4153
4747
  const sum = priced.reduce((s, o) => s + o.probability, 0);
4154
4748
  return sum > 0.97 && sum < 1.03;
4155
4749
  }
4750
+ var FUTURE_SKEW_MS = 6e4;
4156
4751
  function isStaleSignal(signal, options = {}) {
4157
4752
  const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
4158
4753
  const asOf = Date.parse(signal.asOf);
4159
4754
  if (!Number.isFinite(asOf)) return true;
4160
4755
  const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
4756
+ if (asOf - now > FUTURE_SKEW_MS) return true;
4161
4757
  return now - asOf > maxAge;
4162
4758
  }
4163
4759
  function isReliableMarketSignal(signal, options = {}) {
@@ -4173,8 +4769,8 @@ function isReliableMarketSignal(signal, options = {}) {
4173
4769
  }
4174
4770
  function buildMarketSignal(input) {
4175
4771
  const outcomes = normalizeOutcomes(input.outcomes);
4176
- const ambiguous = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
4177
- const favorite = ambiguous ? void 0 : deriveFavorite(outcomes);
4772
+ const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
4773
+ const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
4178
4774
  const signal = {
4179
4775
  matchId: input.match.id,
4180
4776
  source: input.source,
@@ -4186,66 +4782,33 @@ function buildMarketSignal(input) {
4186
4782
  liquidity: input.liquidity,
4187
4783
  volume24h: input.volume24h,
4188
4784
  stale: false,
4189
- ambiguous
4785
+ ambiguous: ambiguous2
4190
4786
  };
4191
4787
  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)}%`);
4788
+ const sealed = sealMarketSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
4789
+ if (sealed.kind !== "valid") {
4790
+ return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
4229
4791
  }
4230
- return parts.join(" \xB7 ");
4792
+ return { ...sealed.value, ambiguous: sealed.value.ambiguous || ambiguous2 };
4231
4793
  }
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;
4794
+ var NONE = { kind: "none" };
4795
+ function selectOne(candidates) {
4796
+ if (candidates.length === 1) return { kind: "one", value: candidates[0] };
4797
+ if (candidates.length === 0) return NONE;
4798
+ return { kind: "ambiguous", count: candidates.length };
4236
4799
  }
4237
- function marketLine(signal, match) {
4238
- return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
4239
- signal.source
4240
- )} \xB7 informational only`;
4800
+ function resolvedValues(batch) {
4801
+ const out = /* @__PURE__ */ new Map();
4802
+ for (const [key, r] of batch.results) if (r.kind === "valid") out.set(key, r.value);
4803
+ return out;
4241
4804
  }
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;
4805
+ function cacheableKeys(batch) {
4806
+ const out = /* @__PURE__ */ new Set();
4807
+ for (const [key, r] of batch.results) if (isCacheable(r)) out.add(key);
4808
+ return out;
4809
+ }
4810
+ function emptyBatch() {
4811
+ return { results: /* @__PURE__ */ new Map(), complete: false };
4249
4812
  }
4250
4813
  var FakeMarketProvider = class {
4251
4814
  constructor(opts = {}) {
@@ -4260,14 +4823,12 @@ var FakeMarketProvider = class {
4260
4823
  return void 0;
4261
4824
  }
4262
4825
  async findSignals(matches, options) {
4263
- const signals = /* @__PURE__ */ new Map();
4264
- const checked = /* @__PURE__ */ new Set();
4826
+ const results = /* @__PURE__ */ new Map();
4265
4827
  for (const m of matches) {
4266
- checked.add(m.id);
4267
4828
  const s = await this.findSignal(m, options);
4268
- if (s) signals.set(m.id, s);
4829
+ results.set(m.id, s ? valid(s) : definitiveNone("fake provider has no signal"));
4269
4830
  }
4270
- return { signals, checked };
4831
+ return { results, complete: true };
4271
4832
  }
4272
4833
  synthesize(match, options) {
4273
4834
  const seed = hash(`${match.home.code}-${match.away.code}`);
@@ -4284,7 +4845,9 @@ var FakeMarketProvider = class {
4284
4845
  return buildMarketSignal({
4285
4846
  match,
4286
4847
  source: "fake",
4287
- sourceMarketId: `fake-${match.id}`,
4848
+ // Must satisfy the boundary's opaque-id grammar, like a real one:
4849
+ // a source id that only the live path accepts is the asymmetry itself.
4850
+ sourceMarketId: match.id,
4288
4851
  asOf,
4289
4852
  fetchedAt: now.toISOString(),
4290
4853
  outcomes,
@@ -4308,6 +4871,8 @@ var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
4308
4871
  var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
4309
4872
  var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
4310
4873
  var DEFAULT_TIMEOUT_MS2 = 8e3;
4874
+ var MAX_EVENT_MARKETS = 256;
4875
+ var DEFAULT_DEADLINE_MS = 15e3;
4311
4876
  var WC_SERIES_SLUG = "soccer-fifwc";
4312
4877
  var WC_SPORT = "fifwc";
4313
4878
  var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
@@ -4320,49 +4885,81 @@ var PolymarketProvider = class {
4320
4885
  opts;
4321
4886
  name = "polymarket";
4322
4887
  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;
4888
+ const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
4889
+ return parsedValue(await this.resolveOne(match, options, deadline));
4325
4890
  }
4326
4891
  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;
4892
+ const results = /* @__PURE__ */ new Map();
4893
+ const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
4894
+ let complete = true;
4330
4895
  for (const m of matches) {
4331
- if (Date.now() >= deadline) break;
4896
+ if (Date.now() >= deadline) {
4897
+ results.set(m.id, unresolved("enrichment deadline expired"));
4898
+ complete = false;
4899
+ continue;
4900
+ }
4332
4901
  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);
4902
+ if (r.kind === "unresolved" || r.kind === "malformed") complete = false;
4903
+ results.set(m.id, r);
4335
4904
  }
4336
- return { signals, checked };
4905
+ return { results, complete };
4337
4906
  }
4338
4907
  /**
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.
4908
+ * Resolve one match into a verdict.
4909
+ *
4910
+ * Every exit says which KIND of non-answer it is, because that decides
4911
+ * whether it may be remembered — see `isCacheable`: a conclusion we drew from
4912
+ * a payload we READ is cacheable (including an ambiguity, which is stable),
4913
+ * while a shape we could not read is not. Previously a single
4914
+ * `checked: boolean` collapsed five distinct situations into two, and the
4915
+ * ones that landed on the wrong side of it — an ambiguous payload, a
4916
+ * two-legged market, an incoherent 1X2 — were negative-cached as the fact
4917
+ * that this fixture has no market.
4343
4918
  */
4344
4919
  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
4920
  const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
4349
4921
  try {
4922
+ const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4923
+ const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4924
+ if (slugs.length === 0) return definitiveNone("fixture has no derivable event slug");
4925
+ const RANK = {
4926
+ "definitive-none": 0,
4927
+ ambiguous: 1,
4928
+ unresolved: 2,
4929
+ malformed: 3
4930
+ };
4931
+ let worst;
4932
+ const keepWorst = (r) => {
4933
+ if (r.kind === "definitive-none" || r.kind === "valid") return;
4934
+ if (!worst || (RANK[r.kind] ?? 0) > (RANK[worst.kind] ?? 0)) worst = r;
4935
+ };
4350
4936
  for (const slug of slugs) {
4351
4937
  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 };
4938
+ if (remaining <= 0) return unresolved("deadline expired between candidate slugs");
4939
+ let found;
4940
+ try {
4941
+ found = await this.fetchEvent(slug, Math.min(configured, remaining));
4942
+ } catch {
4943
+ keepWorst(malformed("candidate request failed"));
4944
+ continue;
4945
+ }
4946
+ if (found.kind !== "valid") {
4947
+ keepWorst(found);
4948
+ continue;
4949
+ }
4950
+ const r = this.toSignal(match, slug, found.value, options);
4951
+ if (r.kind === "valid") return r;
4952
+ keepWorst(r);
4356
4953
  }
4357
- return { checked: true };
4954
+ return worst ?? definitiveNone("no candidate slug yielded a usable market");
4358
4955
  } catch {
4359
- return { checked: false };
4956
+ return malformed("provider request failed");
4360
4957
  }
4361
4958
  }
4362
4959
  async fetchEvent(slug, timeoutMs) {
4363
4960
  const base = this.opts.baseUrl ?? DEFAULT_BASE2;
4364
4961
  assertAllowedHost(base);
4365
- const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
4962
+ const url = `${base}/events/slug/${encodeURIComponent(slug)}`;
4366
4963
  const doFetch = this.opts.fetchImpl ?? fetch;
4367
4964
  const res = await doFetch(url, {
4368
4965
  signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
@@ -4371,7 +4968,7 @@ var PolymarketProvider = class {
4371
4968
  redirect: "error",
4372
4969
  headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
4373
4970
  });
4374
- if (res.status === 404) return void 0;
4971
+ if (res.status === 404) return definitiveNone("slug returns 404");
4375
4972
  if (!res.ok) {
4376
4973
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4377
4974
  }
@@ -4380,63 +4977,148 @@ var PolymarketProvider = class {
4380
4977
  throw new Error(`Polymarket response too large: ${length} bytes`);
4381
4978
  }
4382
4979
  const data = await res.json();
4980
+ if (Array.isArray(data) && data.length > 1) {
4981
+ return ambiguous("slug returned more than one event");
4982
+ }
4983
+ if (Array.isArray(data) && data.length === 0) return definitiveNone("slug returns no event");
4383
4984
  const event = Array.isArray(data) ? data[0] : data;
4384
- return event && typeof event === "object" ? event : void 0;
4985
+ if (!event || typeof event !== "object") return malformed("event body is not an object");
4986
+ return valid(event);
4385
4987
  }
4386
4988
  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;
4989
+ if (typeof event.active !== "boolean" || typeof event.closed !== "boolean") {
4990
+ return malformed("event active/closed is not a boolean");
4390
4991
  }
4391
- if (event.slug != null && event.slug !== eventSlug) return void 0;
4392
- const start = event.startTime ? Date.parse(event.startTime) : Number.NaN;
4992
+ if (event.active === false || event.closed === true) {
4993
+ return definitiveNone("event is closed or inactive");
4994
+ }
4995
+ if (event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
4996
+ return definitiveNone("event is not in this competition");
4997
+ }
4998
+ if (typeof event.slug !== "string") {
4999
+ return malformed("event states no slug");
5000
+ }
5001
+ if (event.slug !== eventSlug) return definitiveNone("event is not the one requested");
5002
+ if (typeof event.startTime !== "string" || !canonicalTimestamp(event.startTime)) {
5003
+ return malformed("event startTime missing or unparseable");
5004
+ }
5005
+ const start = Date.parse(event.startTime);
4393
5006
  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;
5007
+ if (!Number.isFinite(start) || !Number.isFinite(kick)) {
5008
+ return malformed("event or fixture kickoff is unreadable");
5009
+ }
5010
+ if (Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
5011
+ return definitiveNone("event kickoff does not match the fixture");
4396
5012
  }
4397
- const moneyline = (event.markets ?? []).filter(
4398
- (m) => (m.sportsMarketType ?? "moneyline") === "moneyline"
5013
+ if (!Array.isArray(event.markets)) {
5014
+ return malformed("event markets is not an array");
5015
+ }
5016
+ const marketsTruncated = Array.isArray(event.markets) && event.markets.length > MAX_EVENT_MARKETS;
5017
+ if (marketsTruncated) {
5018
+ return malformed("event market list exceeded the cap");
5019
+ }
5020
+ const marketList = takeBounded(event.markets, MAX_EVENT_MARKETS);
5021
+ if (marketList.some(
5022
+ (market) => !market || typeof market !== "object" || typeof market.sportsMarketType !== "string"
5023
+ )) {
5024
+ return malformed("event market is missing its market-type discriminator");
5025
+ }
5026
+ const moneyline = marketList.filter(
5027
+ (m) => m?.sportsMarketType === "moneyline"
4399
5028
  );
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;
5029
+ const homeSel = pickMarket(moneyline, match.home.code, match.home.name);
5030
+ const awaySel = pickMarket(moneyline, match.away.code, match.away.name);
5031
+ const drawSel = pickDraw(moneyline);
5032
+ for (const [side, sel] of [
5033
+ ["home", homeSel],
5034
+ ["away", awaySel],
5035
+ ["draw", drawSel]
5036
+ ]) {
5037
+ if (sel.kind === "ambiguous") {
5038
+ return ambiguous(`${sel.count} markets claim the ${side} outcome`);
5039
+ }
5040
+ }
5041
+ if (homeSel.kind !== "one" || awaySel.kind !== "one" || drawSel.kind !== "one") {
5042
+ return definitiveNone("event does not carry all three 1X2 legs");
5043
+ }
5044
+ const homeMarket = homeSel.value;
5045
+ const awayMarket = awaySel.value;
5046
+ const drawMarket = drawSel.value;
4404
5047
  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;
5048
+ if (new Set(legIds).size !== legIds.length) {
5049
+ return ambiguous("two outcome legs are the same market");
5050
+ }
4406
5051
  const legs = [
4407
5052
  ["home", homeMarket, match.home.code, match.home.name],
4408
5053
  ["draw", drawMarket, void 0, "Draw"],
4409
5054
  ["away", awayMarket, match.away.code, match.away.name]
4410
5055
  ];
4411
5056
  const outcomes = [];
4412
- let asOf = event.updatedAt;
5057
+ let asOf = canonicalTimestamp(event.updatedAt);
4413
5058
  let liquidity;
4414
- for (const [kind, market, teamCode, label] of legs) {
5059
+ for (const [kind, market, teamCode2, label] of legs) {
4415
5060
  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;
5061
+ if (typeof market.closed !== "boolean" || typeof market.active !== "boolean") {
5062
+ return malformed("market active/closed is not a boolean");
5063
+ }
5064
+ if (market.closed === true || market.active === false) {
5065
+ return definitiveNone("an outcome leg is closed or inactive");
5066
+ }
5067
+ if (market.description && NON_REGULAR_TIME.test(market.description)) {
5068
+ return definitiveNone("an outcome leg is not a regular-time market");
5069
+ }
4418
5070
  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);
5071
+ if (yes == null) return malformed("market is not a readable Yes/No binary");
5072
+ outcomes.push({ kind, teamCode: teamCode2, label, probability: yes });
5073
+ const marketAsOf = canonicalTimestamp(market.updatedAt);
5074
+ if (!marketAsOf) {
5075
+ return malformed("market updatedAt missing or unparseable");
5076
+ }
5077
+ const nowMs = (options?.now ?? this.opts.now ?? /* @__PURE__ */ new Date()).getTime();
5078
+ if (Date.parse(marketAsOf) - nowMs > FUTURE_SKEW_MS) {
5079
+ return malformed("market updatedAt is dated forward");
5080
+ }
5081
+ if (!asOf || Date.parse(marketAsOf) < Date.parse(asOf)) asOf = marketAsOf;
5082
+ const rawLiq = market.liquidityNum ?? market.liquidity;
5083
+ const liq = numberish(rawLiq);
5084
+ if (rawLiq != null && liq == null) {
5085
+ return malformed("market liquidity is unreadable");
5086
+ }
4423
5087
  if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
4424
5088
  }
4425
5089
  const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
4426
- if (rawSum < 0.9 || rawSum > 1.15) return void 0;
5090
+ if (rawSum < 0.9 || rawSum > 1.15) {
5091
+ return ambiguous("outcome probabilities do not form a coherent 1X2");
5092
+ }
5093
+ if (!asOf) return malformed("no usable timestamp on the event or its markets");
4427
5094
  const signal = buildMarketSignal({
4428
5095
  match,
4429
5096
  source: "polymarket",
4430
- sourceMarketId: event.id ?? eventSlug,
4431
- asOf: asOf ?? (/* @__PURE__ */ new Date()).toISOString(),
5097
+ // Echoed into MCP structured content (tools.ts `market.id`), i.e. straight
5098
+ // into an agent's context. Stripping control characters is NOT sufficient
5099
+ // there: printable prose ("IGNORE PREVIOUS INSTRUCTIONS") survives that and
5100
+ // is precisely what matters for a model reading it. Gamma ids are short
5101
+ // opaque tokens, so validate that GRAMMAR and otherwise fall back to the
5102
+ // slug we derived ourselves.
5103
+ // The fallback is grammar-checked too. It is normally a slug we derived
5104
+ // ourselves, but `mapping.2026.json` can override it, so echoing it raw
5105
+ // was the one path around the agent-facing filter this line exists for.
5106
+ sourceMarketId: safeMarketId(event.id) ?? safeDerivedSlug(eventSlug),
5107
+ asOf,
4432
5108
  outcomes,
4433
5109
  liquidity,
4434
5110
  now: options?.now ?? this.opts.now,
4435
5111
  maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
4436
5112
  });
4437
- return signal.ambiguous ? void 0 : signal;
5113
+ return signal.ambiguous ? ambiguous("signal does not map cleanly onto this fixture") : valid(signal);
4438
5114
  }
4439
5115
  };
5116
+ function safeMarketId(id) {
5117
+ return typeof id === "string" && /^[0-9]{1,32}$/.test(id) ? id : void 0;
5118
+ }
5119
+ function safeDerivedSlug(slug) {
5120
+ return typeof slug === "string" && /^fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2}$/.test(slug) ? slug : void 0;
5121
+ }
4440
5122
  var POLYMARKET_TOKEN = {
4441
5123
  SUI: "che",
4442
5124
  // Switzerland
@@ -4450,8 +5132,16 @@ var POLYMARKET_TOKEN = {
4450
5132
  // Croatia
4451
5133
  COD: "cdr",
4452
5134
  // DR Congo
4453
- CPV: "cvi"
5135
+ CPV: "cvi",
4454
5136
  // Cabo Verde
5137
+ // TWO letters, not three — the one entry that is not ISO alpha-3. Verified
5138
+ // live: `fifwc-kor-cze-2026-06-11` is a 404, `fifwc-kr-cze-2026-06-11`
5139
+ // resolves to "Korea Republic vs. Czechia". Korea's three group fixtures
5140
+ // therefore had no market line at all. The `^[a-z]{3}$` guard in
5141
+ // `deriveEventSlugs` validates the FIFA CODE, not the token, so a two-letter
5142
+ // alias passes through it unharmed.
5143
+ KOR: "kr"
5144
+ // Korea Republic
4455
5145
  };
4456
5146
  function pmTokens(code) {
4457
5147
  const c = code.toLowerCase();
@@ -4482,18 +5172,21 @@ function slugToken(m) {
4482
5172
  return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
4483
5173
  }
4484
5174
  function isDrawMarket(m) {
4485
- return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
5175
+ const title = (m.groupItemTitle ?? "").trim().toLowerCase();
5176
+ return slugToken(m) === "draw" || title === "draw" || /^draw\s*\(/.test(title);
4486
5177
  }
4487
- function pickMarket(markets, teamCode, teamName) {
4488
- const tokens = pmTokens(teamCode);
5178
+ function pickMarket(markets, teamCode2, teamName) {
5179
+ const tokens = pmTokens(teamCode2);
4489
5180
  const name = teamName.trim().toLowerCase();
4490
5181
  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);
5182
+ const bySlug = teamMarkets.filter((m) => tokens.includes(slugToken(m)));
5183
+ if (bySlug.length > 1) return { kind: "ambiguous", count: bySlug.length };
5184
+ const byTitle = name ? teamMarkets.filter((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name) : [];
5185
+ if (byTitle.length > 1) return { kind: "ambiguous", count: byTitle.length };
5186
+ return selectOne([.../* @__PURE__ */ new Set([...bySlug, ...byTitle])]);
4494
5187
  }
4495
5188
  function pickDraw(markets) {
4496
- return markets.find(isDrawMarket);
5189
+ return selectOne(markets.filter(isDrawMarket));
4497
5190
  }
4498
5191
  function assertAllowedHost(base) {
4499
5192
  let host;
@@ -4507,20 +5200,29 @@ function assertAllowedHost(base) {
4507
5200
  }
4508
5201
  }
4509
5202
  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;
5203
+ const labels = parseJsonArray(market.outcomes).map((l) => l.trim().toLowerCase());
5204
+ const raw = parseJsonArray(market.outcomePrices);
5205
+ if (raw.some((v) => v.trim() === "" || !Number.isFinite(Number(v)))) return void 0;
5206
+ const prices = raw.map((v) => Number(v));
5207
+ if (labels.length !== 2 || prices.length !== 2) return void 0;
5208
+ const i = labels.indexOf("yes");
5209
+ const j = labels.indexOf("no");
5210
+ if (i < 0 || j < 0) return void 0;
5211
+ const yes = prices[i];
5212
+ const no = prices[j];
5213
+ if (![yes, no].every((v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1)) {
5214
+ return void 0;
5215
+ }
5216
+ if (Math.abs(yes + no - 1) > 0.05) return void 0;
5217
+ return yes > 0 ? yes : void 0;
4517
5218
  }
4518
5219
  function parseJsonArray(v) {
4519
- if (Array.isArray(v)) return v.map((x) => String(x));
5220
+ const asText = (x) => typeof x === "string" || typeof x === "number" ? String(x) : "";
5221
+ if (Array.isArray(v)) return v.map(asText);
4520
5222
  if (typeof v === "string") {
4521
5223
  try {
4522
5224
  const parsed = JSON.parse(v);
4523
- return Array.isArray(parsed) ? parsed.map((x) => String(x)) : [];
5225
+ return Array.isArray(parsed) ? parsed.map(asText) : [];
4524
5226
  } catch {
4525
5227
  return [];
4526
5228
  }
@@ -4528,10 +5230,10 @@ function parseJsonArray(v) {
4528
5230
  return [];
4529
5231
  }
4530
5232
  function numberish(v) {
4531
- if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
5233
+ if (typeof v === "number") return Number.isFinite(v) && v >= 0 ? v : void 0;
4532
5234
  if (typeof v === "string") {
4533
5235
  const n = Number(v);
4534
- return Number.isFinite(n) ? n : void 0;
5236
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
4535
5237
  }
4536
5238
  return void 0;
4537
5239
  }
@@ -4554,18 +5256,11 @@ function makeMarketProvider(source) {
4554
5256
  return new PolymarketProvider();
4555
5257
  }
4556
5258
  }
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
5259
  async function getMarketSignals(provider, matches, options) {
4565
5260
  try {
4566
5261
  return await provider.findSignals(matches, options);
4567
5262
  } catch {
4568
- return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
5263
+ return emptyBatch();
4569
5264
  }
4570
5265
  }
4571
5266
  var SHARE_HASHTAG = "#VibingLaVidaLoca";
@@ -4646,6 +5341,9 @@ function formatShareSnippet(input, options = {}) {
4646
5341
  if (input.degraded && input.matches.length > 0) {
4647
5342
  blocks.push("(Live data unavailable \u2014 showing the bundled schedule, not live scores.)");
4648
5343
  }
5344
+ if (includeMarkets && input.marketComplete === false) {
5345
+ blocks.push("(Market data unavailable or incomplete \u2014 not all fixtures were checked.)");
5346
+ }
4649
5347
  blocks.push(
4650
5348
  shareFooter({
4651
5349
  source: input.source,
@@ -4676,7 +5374,9 @@ function formatShareTable(input, options = {}) {
4676
5374
  const includeInstall = options.includeInstallLine !== false;
4677
5375
  const blocks = [];
4678
5376
  if (input.tables.length === 0) {
4679
- blocks.push(input.emptyNote ?? "No standings available.");
5377
+ blocks.push(
5378
+ input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
5379
+ );
4680
5380
  } else {
4681
5381
  for (const { group, rows } of input.tables) {
4682
5382
  blocks.push(
@@ -4815,9 +5515,21 @@ function matchLine(m, opts = {}) {
4815
5515
  const base = `${head} \u2014 ${tail} \xB7 ${stage} \xB7 ${matchLocation(m)}`;
4816
5516
  return (flair ? `${base} \u2014 ${flair}` : base).trimEnd();
4817
5517
  }
5518
+ var MAX_LIST_MATCHES = 40;
5519
+ function boundedRecords(rows, max = MAX_LIST_MATCHES) {
5520
+ return bounded(rows, max);
5521
+ }
5522
+ function truncationNote(list) {
5523
+ return list.truncated ? `
5524
+ (showing ${list.shown} of ${list.total} \u2014 list truncated)` : "";
5525
+ }
4818
5526
  function matchList(matches, empty, opts = {}) {
4819
5527
  if (matches.length === 0) return empty;
4820
- return matches.map((m) => `\u2022 ${matchLine(m, opts)}`).join("\n");
5528
+ const shown = matches.slice(0, MAX_LIST_MATCHES);
5529
+ const lines = shown.map((m) => `\u2022 ${matchLine(m, opts)}`).join("\n");
5530
+ const overflow = matches.length - shown.length;
5531
+ return overflow > 0 ? `${lines}
5532
+ \u2022 (list truncated \u2014 ${overflow} more not shown)` : lines;
4821
5533
  }
4822
5534
  function standingsTable(group, rows) {
4823
5535
  const header = `Group ${group}`;
@@ -4837,6 +5549,14 @@ function standingsTable(group, rows) {
4837
5549
  return [header, cols, ...lines].join("\n");
4838
5550
  }
4839
5551
  var DISCLAIMER = "Claudinho is an independent fan project \u2014 not affiliated with or endorsed by FIFA or Anthropic.";
5552
+ function capSignals(signals, kept) {
5553
+ const ids = new Set(kept.map((m) => m.id));
5554
+ const out = {};
5555
+ for (const [id, v] of Object.entries(signals)) {
5556
+ if (ids.has(id)) out[id] = v;
5557
+ }
5558
+ return out;
5559
+ }
4840
5560
 
4841
5561
  // src/tools.ts
4842
5562
  var adapters = /* @__PURE__ */ new Map();
@@ -4895,11 +5615,19 @@ var MARKETS_TOOL_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
4895
5615
  function memKey(competition, id) {
4896
5616
  return `polymarket:${competition}:${id}`;
4897
5617
  }
4898
- async function cachedMarketSignals(args, matches) {
4899
- if (args.marketProvider) return (await getMarketSignals(args.marketProvider, matches)).signals;
5618
+ async function cachedMarketSignals(args, matches, providerFactory = makeMarketProvider) {
5619
+ if (args.marketProvider) {
5620
+ const batch = await getMarketSignals(args.marketProvider, matches);
5621
+ return { signals: resolvedValues(batch), complete: batch.complete };
5622
+ }
4900
5623
  const source = resolveMarketSource();
4901
5624
  if (source !== "polymarket") {
4902
- return (await getMarketSignals(makeMarketProvider(source), matches, DEFAULT_ON_MARKET_OPTS)).signals;
5625
+ const batch = await getMarketSignals(
5626
+ providerFactory(source),
5627
+ matches,
5628
+ DEFAULT_ON_MARKET_OPTS
5629
+ );
5630
+ return { signals: resolvedValues(batch), complete: batch.complete };
4903
5631
  }
4904
5632
  const competition = resolveCompetition();
4905
5633
  const now = Date.now();
@@ -4908,39 +5636,46 @@ async function cachedMarketSignals(args, matches) {
4908
5636
  for (const m of matches) {
4909
5637
  const e = marketMem.get(memKey(competition, m.id));
4910
5638
  const ttl = e?.signal ? MEM_POSITIVE_TTL : MEM_NEGATIVE_TTL;
4911
- if (e && now - e.at <= ttl) {
5639
+ const fresh = e && now - e.at <= ttl;
5640
+ if (fresh && (!e.signal || marketSignalRendersFor(m, e.signal))) {
4912
5641
  if (e.signal) result.set(m.id, e.signal);
4913
5642
  } else {
4914
5643
  miss.push(m);
4915
5644
  }
4916
5645
  }
5646
+ let complete = true;
4917
5647
  if (miss.length > 0) {
4918
- const { signals: fetched, checked } = await getMarketSignals(
4919
- makeMarketProvider("polymarket"),
5648
+ const batch = await getMarketSignals(
5649
+ providerFactory("polymarket"),
4920
5650
  miss,
4921
5651
  DEFAULT_ON_MARKET_OPTS
4922
5652
  );
4923
- for (const id of checked) {
5653
+ const fetched = resolvedValues(batch);
5654
+ complete = batch.complete;
5655
+ for (const id of cacheableKeys(batch)) {
4924
5656
  marketMem.set(memKey(competition, id), { at: now, signal: fetched.get(id) ?? null });
4925
5657
  }
4926
5658
  for (const [id, s] of fetched) result.set(id, s);
4927
5659
  }
4928
- return result;
5660
+ return { signals: result, complete };
4929
5661
  }
4930
5662
  async function reliableMarketData(args, matches) {
4931
- if (!marketsEnabled()) return void 0;
5663
+ if (!marketsEnabled()) return { data: void 0, complete: true };
4932
5664
  const now = args.now ?? /* @__PURE__ */ new Date();
4933
5665
  const relevant = matches.filter((m) => marketRelevant(m, now));
4934
- if (relevant.length === 0) return void 0;
4935
- const signals = await cachedMarketSignals(args, relevant);
5666
+ if (relevant.length === 0) return { data: void 0, complete: true };
5667
+ const result = await cachedMarketSignals(args, relevant);
4936
5668
  const out = {};
4937
5669
  for (const m of relevant) {
4938
- const s = signals.get(m.id);
5670
+ const s = result.signals.get(m.id);
4939
5671
  if (s && isReliableMarketSignal(s, { now }) && marketSignalRendersFor(m, s)) {
4940
5672
  out[m.id] = marketData(s);
4941
5673
  }
4942
5674
  }
4943
- return Object.keys(out).length > 0 ? out : void 0;
5675
+ return {
5676
+ data: Object.keys(out).length > 0 ? out : void 0,
5677
+ complete: result.complete
5678
+ };
4944
5679
  }
4945
5680
  function fmtOpts(args) {
4946
5681
  return {
@@ -4965,16 +5700,28 @@ async function toolGetToday(args) {
4965
5700
  let text = `Matches on ${date}:
4966
5701
  ${matchList(todays, "No matches scheduled.", opts)}`;
4967
5702
  if (degraded) text += "\n\n(Live scores unavailable \u2014 showing the bundled schedule.)";
4968
- const marketSignals = await reliableMarketData(args, todays);
5703
+ const market = await reliableMarketData(args, todays);
5704
+ if (!market.complete) {
5705
+ text += "\n\n(Market data unavailable or incomplete \u2014 not all fixtures were checked.)";
5706
+ }
5707
+ const shownToday = boundedRecords(todays);
4969
5708
  return {
4970
5709
  text: withDisclaimer(text, source, args.lang),
4971
5710
  data: {
4972
5711
  date,
4973
5712
  degraded,
4974
5713
  source: source ?? null,
4975
- count: todays.length,
4976
- matches: todays,
4977
- ...marketSignals ? { marketSignals } : {}
5714
+ // ONE bounded view, so `count`, `matches` and the signal set cannot
5715
+ // disagree about the same payload. `count` is the TRUE total; bounding
5716
+ // only the TEXT would leave structuredContent unbounded, and that is
5717
+ // model context too — a repeated-record payload measured ~5 MB there.
5718
+ count: shownToday.total,
5719
+ truncated: shownToday.truncated,
5720
+ matches: shownToday.items,
5721
+ marketComplete: market.complete,
5722
+ // Capped in step with `matches`: a signal keyed to a match that is no
5723
+ // longer in the payload is dead weight in model context.
5724
+ ...market.data ? { marketSignals: capSignals(market.data, shownToday.items) } : {}
4978
5725
  }
4979
5726
  };
4980
5727
  }
@@ -4984,9 +5731,16 @@ async function toolGetLive(args = {}) {
4984
5731
  const opts = fmtOpts(args);
4985
5732
  const text = degraded ? "Live scores unavailable right now \u2014 could not reach the data provider." : `Live now:
4986
5733
  ${matchList(matches, "No matches in play right now.", opts)}`;
5734
+ const shownLive = boundedRecords(matches);
4987
5735
  return {
4988
5736
  text: withDisclaimer(text, source, args.lang),
4989
- data: { degraded, source: source ?? null, count: matches.length, matches }
5737
+ data: {
5738
+ degraded,
5739
+ source: source ?? null,
5740
+ count: shownLive.total,
5741
+ truncated: shownLive.truncated,
5742
+ matches: shownLive.items
5743
+ }
4990
5744
  };
4991
5745
  }
4992
5746
  async function toolGetMatch(args) {
@@ -4997,36 +5751,45 @@ async function toolGetMatch(args) {
4997
5751
  const opts = fmtOpts(args);
4998
5752
  const now = args.now ?? /* @__PURE__ */ new Date();
4999
5753
  let marketSignal;
5754
+ let marketComplete = true;
5000
5755
  if (marketsEnabled() && marketRelevant(match, now)) {
5001
- const s = (await cachedMarketSignals(args, [match])).get(match.id);
5756
+ const market = await cachedMarketSignals(args, [match]);
5757
+ marketComplete = market.complete;
5758
+ const s = market.signals.get(match.id);
5002
5759
  if (s && isReliableMarketSignal(s, { now }) && marketSignalRendersFor(match, s)) marketSignal = s;
5003
5760
  }
5004
5761
  const base = matchLine(match, opts);
5005
5762
  let text = marketSignal ? `${base}
5006
5763
  ${marketBlock(marketSignal, match).join("\n")}` : base;
5007
5764
  if (degraded) text += "\n\n(Live state unavailable \u2014 showing the scheduled fixture.)";
5765
+ if (!marketComplete) {
5766
+ text += "\n\n(Market data unavailable or incomplete \u2014 this match was not checked.)";
5767
+ }
5008
5768
  return {
5009
5769
  text: withDisclaimer(text, liveSource, args.lang),
5010
5770
  data: {
5011
5771
  degraded,
5012
5772
  source: liveSource ?? null,
5013
5773
  match,
5774
+ marketComplete,
5014
5775
  marketSignal: marketSignal ? marketData(marketSignal) : null
5015
5776
  }
5016
5777
  };
5017
5778
  }
5018
5779
  async function toolGetStandings(args) {
5019
5780
  const { tables, degraded, source } = await getStandings(resolveAdapter(args), args.group);
5020
- const shaped = tables.map((tb) => ({ group: tb.group, standings: tb.rows }));
5781
+ const boundedTables = boundedRecords(tables);
5782
+ const shaped = boundedTables.items.map((tb) => ({ group: tb.group, standings: tb.rows }));
5021
5783
  if (shaped.length === 0) {
5022
5784
  const g = args.group?.toUpperCase();
5023
- const msg = g ? `No group "${g}". Groups are A\u2013L.` : "No standings available.";
5785
+ const msg = degraded ? t(args.lang, "standings.unavailable") : g ? `No group "${g}".` : "No standings available.";
5024
5786
  return {
5025
- text: withDisclaimer(degraded ? `${msg} (Live standings unavailable.)` : msg, source, args.lang),
5787
+ text: withDisclaimer(msg, source, args.lang),
5026
5788
  data: { degraded, source: source ?? null, tables: args.group ? null : [] }
5027
5789
  };
5028
5790
  }
5029
5791
  let text = shaped.map((t2) => standingsTable(t2.group, t2.standings)).join("\n\n");
5792
+ text += truncationNote(boundedTables);
5030
5793
  if (degraded) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
5031
5794
  return {
5032
5795
  text: withDisclaimer(text, source, args.lang),
@@ -5069,7 +5832,7 @@ async function standingsResourceText(group, adapter) {
5069
5832
  const g = group.toUpperCase();
5070
5833
  const { tables, degraded, source } = await getStandings(adapter, g);
5071
5834
  const tb = tables[0];
5072
- let text = tb ? standingsTable(tb.group, tb.rows) : `No group ${g}.`;
5835
+ let text = tb ? standingsTable(tb.group, tb.rows) : degraded ? "Live standings unavailable." : `No group ${g}.`;
5073
5836
  if (degraded && tb) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
5074
5837
  return withDisclaimer(text, source);
5075
5838
  }
@@ -5115,14 +5878,16 @@ async function toolGetMarketSignal(args) {
5115
5878
  if (args.matchId) {
5116
5879
  const { match } = await getMatchById(resolveAdapter(args), args.matchId);
5117
5880
  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);
5881
+ const batch2 = match && relevant ? await getMarketSignals(provider, [match], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
5882
+ const sig = match ? resolvedValues(batch2).get(match.id) : void 0;
5883
+ const shown2 = batch2.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
5884
+ 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
5885
  return {
5122
5886
  text: withDisclaimer(text2),
5123
5887
  data: {
5124
5888
  matchId: args.matchId,
5125
5889
  informationalOnly: true,
5890
+ complete: batch2.complete,
5126
5891
  signal: shown2 ? marketData(shown2) : null
5127
5892
  }
5128
5893
  };
@@ -5131,9 +5896,10 @@ async function toolGetMarketSignal(args) {
5131
5896
  const code = args.team.toUpperCase();
5132
5897
  const { match: fixture, degraded } = await marketFixtureForTeam(resolveAdapter(args), code, now);
5133
5898
  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);
5899
+ const batch2 = fixture && relevant ? await getMarketSignals(provider, [fixture], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
5900
+ const sig = fixture ? resolvedValues(batch2).get(fixture.id) : void 0;
5901
+ const shown2 = batch2.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
5902
+ 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
5903
  return {
5138
5904
  text: withDisclaimer(text2),
5139
5905
  data: {
@@ -5141,6 +5907,7 @@ async function toolGetMarketSignal(args) {
5141
5907
  matchId: fixture?.id ?? null,
5142
5908
  degraded,
5143
5909
  informationalOnly: true,
5910
+ complete: batch2.complete,
5144
5911
  signal: shown2 ? marketData(shown2) : null
5145
5912
  }
5146
5913
  };
@@ -5148,33 +5915,56 @@ async function toolGetMarketSignal(args) {
5148
5915
  const date = args.date ?? localDate(now.toISOString(), args.tz);
5149
5916
  const { matches } = await getMatchesForDate(resolveAdapter(args), date);
5150
5917
  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(
5918
+ const batch = await getMarketSignals(provider, todays, MARKETS_TOOL_OPTS);
5919
+ const signals = resolvedValues(batch);
5920
+ const all = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
5153
5921
  (r) => !!r.signal && marketDisplayable(r.match, r.signal)
5154
5922
  );
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}.`;
5923
+ const shown = boundedRecords(all);
5924
+ let text = shown.shown ? `Market signals on ${date}:${truncationNote(shown)}
5925
+ ${shown.items.map(({ match, signal }) => marketText(match, signal, args)).join("\n\n")}` : (
5926
+ // An empty result and an INCOMPLETE one are different answers. The batch
5927
+ // knows which it was — a provider outage or an expired deadline leaves it
5928
+ // `complete: false` — and collapsing to `resolvedValues` threw that away,
5929
+ // so "we could not reach the market data" rendered as the confident
5930
+ // "there is none", which is the failure this project refuses everywhere
5931
+ // else.
5932
+ batch.complete ? `No reliable market signals on ${date}.` : `Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`
5933
+ );
5934
+ if (shown.shown > 0 && !batch.complete) {
5935
+ text += `
5936
+
5937
+ Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`;
5938
+ }
5157
5939
  return {
5158
5940
  text: withDisclaimer(text),
5159
5941
  data: {
5160
5942
  date,
5161
5943
  informationalOnly: true,
5162
- signals: shown.map(({ signal }) => marketData(signal))
5944
+ // Self-describing: the prose says it was truncated, and so does the
5945
+ // structured payload — a consumer reading only `data` could not otherwise
5946
+ // tell 40 signals from all of them.
5947
+ count: shown.total,
5948
+ truncated: shown.truncated,
5949
+ // Stated, so a consumer reading only `data` can tell "none" from
5950
+ // "we could not check them all".
5951
+ complete: batch.complete,
5952
+ signals: shown.items.map(({ signal }) => marketData(signal))
5163
5953
  }
5164
5954
  };
5165
5955
  }
5166
5956
  async function reliableSignalMap(args, matches) {
5167
- if (!marketsEnabled()) return /* @__PURE__ */ new Map();
5957
+ if (!marketsEnabled()) return { signals: /* @__PURE__ */ new Map(), complete: true };
5168
5958
  const now = args.now ?? /* @__PURE__ */ new Date();
5169
5959
  const relevant = matches.filter((m) => marketRelevant(m, now));
5170
- if (relevant.length === 0) return /* @__PURE__ */ new Map();
5171
- const signals = await cachedMarketSignals(args, relevant);
5960
+ if (relevant.length === 0) return { signals: /* @__PURE__ */ new Map(), complete: true };
5961
+ const result = await cachedMarketSignals(args, relevant);
5172
5962
  const out = /* @__PURE__ */ new Map();
5173
5963
  for (const m of relevant) {
5174
- const s = signals.get(m.id);
5964
+ const s = result.signals.get(m.id);
5175
5965
  if (s && isReliableMarketSignal(s, { now }) && marketDisplayable(m, s)) out.set(m.id, s);
5176
5966
  }
5177
- return out;
5967
+ return { signals: out, complete: result.complete };
5178
5968
  }
5179
5969
  function shareOptions(args) {
5180
5970
  return {
@@ -5184,7 +5974,7 @@ function shareOptions(args) {
5184
5974
  includeInstallLine: args.includeInstallLine !== false
5185
5975
  };
5186
5976
  }
5187
- function shareResult(kind, target, team, input, options) {
5977
+ function shareResult(kind, target, team, input, options, total = input.matches.length) {
5188
5978
  const snippet = formatShareSnippet(input, options);
5189
5979
  return {
5190
5980
  // The snippet is self-contained: it carries its own non-affiliation
@@ -5201,28 +5991,34 @@ function shareResult(kind, target, team, input, options) {
5201
5991
  informationalOnly: true,
5202
5992
  style: options.style ?? "social",
5203
5993
  snippet,
5994
+ count: total,
5995
+ truncated: total > input.matches.length,
5204
5996
  matches: input.matches,
5205
5997
  marketSignals: Object.fromEntries(
5206
5998
  [...input.marketSignals ?? /* @__PURE__ */ new Map()].map(([id, s]) => [
5207
5999
  id,
5208
6000
  marketData(s)
5209
6001
  ])
5210
- )
6002
+ ),
6003
+ marketComplete: input.marketComplete ?? true
5211
6004
  }
5212
6005
  };
5213
6006
  }
5214
6007
  async function toolGetShareSnippet(args) {
5215
6008
  const options = shareOptions(args);
5216
- const signalsFor = (ms) => args.includeMarkets === false ? Promise.resolve(/* @__PURE__ */ new Map()) : reliableSignalMap(args, ms);
6009
+ const signalsFor = (ms) => args.includeMarkets === false ? Promise.resolve({ signals: /* @__PURE__ */ new Map(), complete: true }) : reliableSignalMap(args, ms);
5217
6010
  if (args.live) {
5218
6011
  const { matches, degraded: degraded2, source: source2 } = await getLiveMatches(resolveAdapter(args));
6012
+ const shownLive = boundedRecords(matches);
5219
6013
  return shareResult(
5220
6014
  "live",
5221
6015
  "live",
5222
6016
  void 0,
5223
6017
  {
5224
- title: "Live match pulse",
5225
- matches,
6018
+ title: `Live match pulse${truncationNote(shownLive)}`,
6019
+ // Bounded like the date branch: a share card is returned through MCP
6020
+ // before a human ever sees it. The count is STATED, not silently lost.
6021
+ matches: shownLive.items,
5226
6022
  source: source2,
5227
6023
  degraded: degraded2,
5228
6024
  // Feed down ⇒ don't let an empty card read as "nothing is on".
@@ -5231,7 +6027,8 @@ async function toolGetShareSnippet(args) {
5231
6027
  tz: args.tz,
5232
6028
  locale: args.lang
5233
6029
  },
5234
- { ...options, includeMarkets: false }
6030
+ { ...options, includeMarkets: false },
6031
+ matches.length
5235
6032
  );
5236
6033
  }
5237
6034
  if (args.group) {
@@ -5239,12 +6036,15 @@ async function toolGetShareSnippet(args) {
5239
6036
  const { tables, degraded: degraded2, source: source2 } = await getStandings(resolveAdapter(args), group);
5240
6037
  const snippet = formatShareTable(
5241
6038
  {
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).
6039
+ // Capped like the structured payload beside it. Bounding `data.tables`
6040
+ // while the rendered SNIPPET came from the full list meant the surface a
6041
+ // reader actually sees was the unbounded one.
6042
+ tables: boundedRecords(tables).items,
6043
+ // Degraded ⇒ no live provider: don't attribute one. An open-scope
6044
+ // outage has no compatible bundled roster, so name that empty state.
5245
6045
  source: degraded2 ? void 0 : source2,
5246
6046
  installLine: `npx @claudinho/cli table ${group}`,
5247
- emptyNote: `No group ${group}.`,
6047
+ emptyNote: degraded2 ? "Live standings unavailable." : `No group ${group}.`,
5248
6048
  degraded: degraded2
5249
6049
  },
5250
6050
  options
@@ -5259,7 +6059,7 @@ async function toolGetShareSnippet(args) {
5259
6059
  degraded: degraded2,
5260
6060
  informationalOnly: true,
5261
6061
  snippet,
5262
- tables: tables.map((tb) => ({ group: tb.group, standings: tb.rows }))
6062
+ tables: boundedRecords(tables).items.map((tb) => ({ group: tb.group, standings: tb.rows }))
5263
6063
  }
5264
6064
  };
5265
6065
  }
@@ -5305,6 +6105,7 @@ async function toolGetShareSnippet(args) {
5305
6105
  if (args.matchId) {
5306
6106
  const { match, degraded: degraded2, source: source2 } = await getMatchById(resolveAdapter(args), args.matchId);
5307
6107
  const matches = match ? [match] : [];
6108
+ const market2 = await signalsFor(matches);
5308
6109
  return shareResult(
5309
6110
  "match",
5310
6111
  args.matchId,
@@ -5312,7 +6113,8 @@ async function toolGetShareSnippet(args) {
5312
6113
  {
5313
6114
  title: "Match pulse",
5314
6115
  matches,
5315
- marketSignals: await signalsFor(matches),
6116
+ marketSignals: market2.signals,
6117
+ marketComplete: market2.complete,
5316
6118
  source: source2,
5317
6119
  degraded: degraded2,
5318
6120
  emptyNote: `No match found with id ${args.matchId}.`,
@@ -5332,6 +6134,7 @@ async function toolGetShareSnippet(args) {
5332
6134
  );
5333
6135
  const matches = fixture ? [fixture] : [];
5334
6136
  const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
6137
+ const market2 = await signalsFor(matches);
5335
6138
  return shareResult(
5336
6139
  "next",
5337
6140
  "next",
@@ -5339,7 +6142,8 @@ async function toolGetShareSnippet(args) {
5339
6142
  {
5340
6143
  title: `Next up for ${teamName}`,
5341
6144
  matches,
5342
- marketSignals: await signalsFor(matches),
6145
+ marketSignals: market2.signals,
6146
+ marketComplete: market2.complete,
5343
6147
  // Attribute the provider only when the overlay resolved the tie; parity
5344
6148
  // with get_next_fixture (a static group fixture carries no source).
5345
6149
  source: source2,
@@ -5356,14 +6160,19 @@ async function toolGetShareSnippet(args) {
5356
6160
  const { matches: all, degraded, source } = await getMatchesForDate(resolveAdapter(args), date);
5357
6161
  const todays = fixturesByDate(date, all, args.tz);
5358
6162
  const human = formatDate(`${date}T12:00:00.000Z`, { tz: args.tz, locale: args.lang });
6163
+ const shownToday = boundedRecords(todays);
6164
+ const market = await signalsFor(shownToday.items);
5359
6165
  return shareResult(
5360
6166
  "today",
5361
6167
  date,
5362
6168
  void 0,
5363
6169
  {
5364
- title: args.date ? `Matches \xB7 ${human}` : `Today's matches \xB7 ${human}`,
5365
- matches: todays,
5366
- marketSignals: await signalsFor(todays),
6170
+ title: (args.date ? `Matches \xB7 ${human}` : `Today's matches \xB7 ${human}`) + truncationNote(shownToday),
6171
+ // Bounded like every other model-facing payload — a share card is
6172
+ // returned through MCP before a human ever sees it.
6173
+ matches: shownToday.items,
6174
+ marketSignals: market.signals,
6175
+ marketComplete: market.complete,
5367
6176
  source,
5368
6177
  degraded,
5369
6178
  emptyNote: `No matches scheduled for ${human}.`,
@@ -5371,13 +6180,14 @@ async function toolGetShareSnippet(args) {
5371
6180
  tz: args.tz,
5372
6181
  locale: args.lang
5373
6182
  },
5374
- options
6183
+ options,
6184
+ todays.length
5375
6185
  );
5376
6186
  }
5377
6187
 
5378
6188
  // src/server.ts
5379
6189
  var SERVER_NAME = "claudinho";
5380
- var SERVER_VERSION = "0.9.2";
6190
+ var SERVER_VERSION = "0.9.4";
5381
6191
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
5382
6192
  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
6193
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
@@ -5415,37 +6225,59 @@ var matchOut = z.object({
5415
6225
  }).passthrough();
5416
6226
  var anyObj = z.object({}).passthrough();
5417
6227
  var src = z.string().nullable();
6228
+ var responseMeta = {
6229
+ responseTruncated: z.boolean().optional(),
6230
+ responseTruncation: z.string().optional()
6231
+ };
5418
6232
  var todayOut = {
5419
6233
  date: z.string(),
5420
6234
  degraded: z.boolean(),
5421
6235
  source: src,
6236
+ // `count` is the TRUE total and `matches` may be a bounded view of it, so the
6237
+ // payload states whether it was cut rather than leaving a consumer to infer
6238
+ // it from two numbers.
5422
6239
  count: z.number(),
6240
+ truncated: z.boolean(),
5423
6241
  matches: z.array(matchOut),
5424
- marketSignals: z.record(anyObj).optional()
6242
+ marketSignals: z.record(anyObj).optional(),
6243
+ marketComplete: z.boolean().optional().describe("False when optional market enrichment did not check every relevant fixture"),
6244
+ ...responseMeta
6245
+ };
6246
+ var liveOut = {
6247
+ degraded: z.boolean(),
6248
+ source: src,
6249
+ count: z.number(),
6250
+ truncated: z.boolean(),
6251
+ matches: z.array(matchOut),
6252
+ ...responseMeta
5425
6253
  };
5426
- var liveOut = { degraded: z.boolean(), source: src, count: z.number(), matches: z.array(matchOut) };
5427
6254
  var matchDetailOut = {
5428
6255
  match: matchOut.nullable(),
5429
6256
  degraded: z.boolean().optional(),
5430
6257
  source: src.optional(),
5431
- marketSignal: anyObj.nullable().optional()
6258
+ marketSignal: anyObj.nullable().optional(),
6259
+ marketComplete: z.boolean().optional().describe("False when optional market enrichment did not check this fixture"),
6260
+ ...responseMeta
5432
6261
  };
5433
6262
  var standingsOut = {
5434
6263
  degraded: z.boolean(),
5435
6264
  source: src,
5436
- tables: z.union([anyObj, z.array(anyObj), z.null()])
6265
+ tables: z.union([anyObj, z.array(anyObj), z.null()]),
6266
+ ...responseMeta
5437
6267
  };
5438
6268
  var bracketOut = {
5439
6269
  view: anyObj.nullable(),
5440
6270
  degraded: z.boolean().optional(),
5441
6271
  standingsDegraded: z.boolean().optional(),
5442
- source: src.optional()
6272
+ source: src.optional(),
6273
+ ...responseMeta
5443
6274
  };
5444
6275
  var nextOut = {
5445
6276
  team: z.string(),
5446
6277
  fixture: matchOut.nullable(),
5447
6278
  degraded: z.boolean(),
5448
- source: src
6279
+ source: src,
6280
+ ...responseMeta
5449
6281
  };
5450
6282
  var marketOut = {
5451
6283
  matchId: z.string().nullable().optional(),
@@ -5454,7 +6286,14 @@ var marketOut = {
5454
6286
  degraded: z.boolean().optional(),
5455
6287
  informationalOnly: z.boolean(),
5456
6288
  signal: anyObj.nullable().optional(),
5457
- signals: z.array(anyObj).optional()
6289
+ signals: z.array(anyObj).optional(),
6290
+ // Present on the list-shaped branches: the TRUE total and whether the array
6291
+ // beside it was capped, so a consumer reading only `structuredContent` can
6292
+ // tell a complete list from a truncated one.
6293
+ count: z.number().optional(),
6294
+ truncated: z.boolean().optional(),
6295
+ complete: z.boolean().optional().describe("False when the market provider did not complete every relevant read"),
6296
+ ...responseMeta
5458
6297
  };
5459
6298
  var shareOut = {
5460
6299
  kind: z.string(),
@@ -5471,23 +6310,183 @@ var shareOut = {
5471
6310
  tables: z.union([anyObj, z.array(anyObj), z.null()]).optional(),
5472
6311
  view: anyObj.nullable().optional(),
5473
6312
  matches: z.array(matchOut).optional(),
5474
- marketSignals: z.record(anyObj).optional()
6313
+ marketSignals: z.record(anyObj).optional(),
6314
+ marketComplete: z.boolean().optional().describe("False when optional market enrichment did not check every relevant fixture"),
6315
+ count: z.number().optional(),
6316
+ truncated: z.boolean().optional(),
6317
+ ...responseMeta
5475
6318
  };
5476
6319
  var teamInfo = z.object({ code: z.string(), name: z.string(), flag: z.string(), group: z.string() }).partial().passthrough();
5477
6320
  var teamOut = {
5478
6321
  query: z.string(),
5479
6322
  team: teamInfo.nullable(),
5480
6323
  matches: z.array(teamInfo),
5481
- count: z.number()
6324
+ count: z.number(),
6325
+ ...responseMeta
5482
6326
  };
6327
+ var MAX_RESPONSE_CHARS = 128e3;
6328
+ var RESPONSE_TRUNCATION = "Optional response detail was truncated to stay within the MCP context limit.";
6329
+ var SHRINK_FAILED = /* @__PURE__ */ Symbol("shrink-failed");
6330
+ var MAX_INSPECTION_DEPTH = 64;
6331
+ var MAX_INSPECTION_ARRAY_LENGTH = 4096;
6332
+ var MAX_INSPECTION_KEYS_PER_OBJECT = 1024;
6333
+ var MAX_INSPECTION_ENTRIES = 65536;
6334
+ var MAX_INSPECTION_CONTAINERS = 16384;
6335
+ var MAX_INSPECTION_CHARS = 2e6;
6336
+ function withinInspectionBudget(root) {
6337
+ const stack = [{ value: root, depth: 0 }];
6338
+ const ancestors = /* @__PURE__ */ new WeakSet();
6339
+ let containers = 0;
6340
+ let entries = 0;
6341
+ let chars = 0;
6342
+ try {
6343
+ while (stack.length > 0) {
6344
+ const frame = stack.pop();
6345
+ if (frame.exit) {
6346
+ ancestors.delete(frame.value);
6347
+ continue;
6348
+ }
6349
+ const { value, depth } = frame;
6350
+ if (typeof value === "string") {
6351
+ chars += value.length;
6352
+ if (chars > MAX_INSPECTION_CHARS) return false;
6353
+ continue;
6354
+ }
6355
+ if (!value || typeof value !== "object") continue;
6356
+ if (depth >= MAX_INSPECTION_DEPTH || ancestors.has(value)) return false;
6357
+ if (utilTypes.isProxy(value)) return false;
6358
+ containers += 1;
6359
+ if (containers > MAX_INSPECTION_CONTAINERS) return false;
6360
+ const proto = Object.getPrototypeOf(value);
6361
+ const expectedProto = Array.isArray(value) ? Array.prototype : Object.prototype;
6362
+ if (proto !== expectedProto && proto !== null) return false;
6363
+ if (Object.getOwnPropertyDescriptor(value, "toJSON") || proto && Object.getOwnPropertyDescriptor(proto, "toJSON")) {
6364
+ return false;
6365
+ }
6366
+ ancestors.add(value);
6367
+ stack.push({ value, depth, exit: true });
6368
+ if (Array.isArray(value)) {
6369
+ if (value.length > MAX_INSPECTION_ARRAY_LENGTH) return false;
6370
+ entries += value.length;
6371
+ if (entries > MAX_INSPECTION_ENTRIES) return false;
6372
+ for (let i = 0; i < value.length; i++) {
6373
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
6374
+ if (!descriptor) continue;
6375
+ if (!("value" in descriptor)) return false;
6376
+ stack.push({ value: descriptor.value, depth: depth + 1 });
6377
+ }
6378
+ continue;
6379
+ }
6380
+ let keys = 0;
6381
+ for (const key in value) {
6382
+ keys += 1;
6383
+ if (keys > MAX_INSPECTION_KEYS_PER_OBJECT) return false;
6384
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
6385
+ if (!descriptor?.enumerable) continue;
6386
+ if (!("value" in descriptor)) return false;
6387
+ entries += 1;
6388
+ chars += key.length;
6389
+ if (entries > MAX_INSPECTION_ENTRIES || chars > MAX_INSPECTION_CHARS) return false;
6390
+ stack.push({ value: descriptor.value, depth: depth + 1 });
6391
+ }
6392
+ }
6393
+ return true;
6394
+ } catch {
6395
+ return false;
6396
+ }
6397
+ }
6398
+ function shrink(value, pass, depth = 0, ancestors = /* @__PURE__ */ new WeakSet()) {
6399
+ if (depth >= 64) return SHRINK_FAILED;
6400
+ if (Array.isArray(value)) {
6401
+ if (ancestors.has(value)) return SHRINK_FAILED;
6402
+ ancestors.add(value);
6403
+ const items = pass === 3 ? value.slice(0, 8) : value;
6404
+ const out = [];
6405
+ for (const item of items) {
6406
+ const shrunk = shrink(item, pass, depth + 1, ancestors);
6407
+ if (shrunk === SHRINK_FAILED) return SHRINK_FAILED;
6408
+ out.push(shrunk);
6409
+ }
6410
+ ancestors.delete(value);
6411
+ return out;
6412
+ }
6413
+ if (value && typeof value === "object") {
6414
+ if (ancestors.has(value)) return SHRINK_FAILED;
6415
+ ancestors.add(value);
6416
+ const out = {};
6417
+ for (const [k, v] of Object.entries(value)) {
6418
+ if (k === "events") continue;
6419
+ const shrunk = shrink(v, pass, depth + 1, ancestors);
6420
+ if (shrunk === SHRINK_FAILED) return SHRINK_FAILED;
6421
+ out[k] = shrunk;
6422
+ }
6423
+ ancestors.delete(value);
6424
+ return out;
6425
+ }
6426
+ if (typeof value === "string") {
6427
+ if (pass >= 2 && value.length > 2e3) return `${value.slice(0, 2e3)}\u2026`;
6428
+ }
6429
+ return value;
6430
+ }
6431
+ function size(v) {
6432
+ try {
6433
+ return JSON.stringify(v ?? null)?.length ?? Number.POSITIVE_INFINITY;
6434
+ } catch {
6435
+ return Number.POSITIVE_INFINITY;
6436
+ }
6437
+ }
6438
+ function boundResponse(data) {
6439
+ if (!withinInspectionBudget(data)) return null;
6440
+ if (size(data) <= MAX_RESPONSE_CHARS) return data;
6441
+ if (!data || typeof data !== "object" || Array.isArray(data)) return null;
6442
+ for (const pass of [1, 2, 3]) {
6443
+ const candidate = shrink(data, pass);
6444
+ if (candidate === SHRINK_FAILED || !candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
6445
+ continue;
6446
+ }
6447
+ const marked = {
6448
+ ...candidate,
6449
+ responseTruncated: true,
6450
+ responseTruncation: RESPONSE_TRUNCATION
6451
+ };
6452
+ if (size(marked) <= MAX_RESPONSE_CHARS) return marked;
6453
+ }
6454
+ return null;
6455
+ }
6456
+ var MAX_TEXT_CHARS = 32e3;
6457
+ var DUAL_EMIT_LIMIT = 16e3;
5483
6458
  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
- };
6459
+ const data = boundResponse(r.data);
6460
+ const dataTruncated = !!data && typeof data === "object" && data.responseTruncated === true;
6461
+ const marker = dataTruncated ? `
6462
+
6463
+ (${RESPONSE_TRUNCATION})` : "";
6464
+ const room = Math.max(0, MAX_TEXT_CHARS - marker.length - "\n(truncated)".length);
6465
+ const text = r.text.length > room ? `${r.text.slice(0, room)}
6466
+ (truncated)${marker}` : r.text + marker;
6467
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
6468
+ const error = "Response data exceeded the MCP context limit and could not be reduced without violating its output schema.";
6469
+ return {
6470
+ content: [
6471
+ {
6472
+ type: "text",
6473
+ text: `${text.slice(0, Math.max(0, MAX_TEXT_CHARS - error.length - 2))}
6474
+
6475
+ ${error}`
6476
+ }
6477
+ ],
6478
+ isError: true
6479
+ };
6480
+ }
6481
+ const compact = JSON.stringify(data);
6482
+ const content = [{ type: "text", text }];
6483
+ if (compact.length <= DUAL_EMIT_LIMIT) {
6484
+ content.push({
6485
+ type: "text",
6486
+ text: "```json\n" + JSON.stringify(data, null, 2) + "\n```"
6487
+ });
6488
+ }
6489
+ return { content, structuredContent: data };
5491
6490
  }
5492
6491
  function buildServer() {
5493
6492
  const server = new McpServer(
@@ -5498,7 +6497,7 @@ function buildServer() {
5498
6497
  "get_today",
5499
6498
  {
5500
6499
  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.",
6500
+ 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
6501
  inputSchema: {
5503
6502
  date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today)"),
5504
6503
  ...commonArgs
@@ -5524,7 +6523,7 @@ function buildServer() {
5524
6523
  "get_match",
5525
6524
  {
5526
6525
  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.",
6526
+ 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
6527
  inputSchema: { id: z.string().describe("Match id"), ...commonArgs },
5529
6528
  annotations: { readOnlyHint: true, openWorldHint: true },
5530
6529
  outputSchema: matchDetailOut
@@ -5535,7 +6534,7 @@ function buildServer() {
5535
6534
  "get_standings",
5536
6535
  {
5537
6536
  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.",
6537
+ 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
6538
  inputSchema: {
5540
6539
  group: groupArg.optional().describe("Group letter A\u2013L (omit for all)"),
5541
6540
  ...commonArgs
@@ -5575,7 +6574,7 @@ function buildServer() {
5575
6574
  "get_market_signal",
5576
6575
  {
5577
6576
  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.",
6577
+ 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
6578
  inputSchema: {
5580
6579
  matchId: z.string().optional().describe("Match id (most specific)"),
5581
6580
  team: teamArg.optional().describe(
@@ -5594,7 +6593,7 @@ function buildServer() {
5594
6593
  "get_share_snippet",
5595
6594
  {
5596
6595
  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.`,
6596
+ 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
6597
  inputSchema: {
5599
6598
  matchId: z.string().optional().describe("Match id (most specific)"),
5600
6599
  team: teamArg.optional().describe("3-letter team code for that team's next fixture, e.g. MEX"),