@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.
- package/README.md +4 -1
- package/dist/index.js +1372 -373
- 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.
|
|
162
|
-
|
|
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
|
|
259
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
496
|
+
}).format(when);
|
|
483
497
|
}
|
|
484
498
|
function countdown(iso, from = /* @__PURE__ */ new Date()) {
|
|
485
|
-
const
|
|
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(
|
|
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 +=
|
|
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
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
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
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
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
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
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
|
|
2935
|
-
const
|
|
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
|
|
3202
|
+
return void 0;
|
|
2943
3203
|
}
|
|
2944
3204
|
function parseMinute(st) {
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
if (
|
|
2949
|
-
|
|
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
|
|
2964
|
-
if (
|
|
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
|
|
2968
|
-
if (
|
|
2969
|
-
|
|
2970
|
-
return
|
|
2971
|
-
}
|
|
2972
|
-
function
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
);
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
}
|
|
2983
|
-
|
|
2984
|
-
const
|
|
2985
|
-
const
|
|
2986
|
-
|
|
2987
|
-
const
|
|
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
|
-
|
|
2990
|
-
const
|
|
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 =
|
|
2997
|
-
const as =
|
|
2998
|
-
const
|
|
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
|
-
|
|
3002
|
-
|
|
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
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
return {
|
|
3008
|
-
id: ev.id,
|
|
3284
|
+
const venue = comp?.venue;
|
|
3285
|
+
return sealMatch({
|
|
3286
|
+
id,
|
|
3009
3287
|
stage,
|
|
3010
3288
|
group,
|
|
3011
|
-
kickoff
|
|
3012
|
-
venue:
|
|
3013
|
-
city:
|
|
3014
|
-
country:
|
|
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
|
|
3026
|
-
|
|
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
|
-
|
|
3030
|
-
|
|
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
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
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
|
|
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
|
-
|
|
3048
|
-
|
|
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
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
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
|
-
|
|
3057
|
-
|
|
3058
|
-
return
|
|
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
|
-
|
|
3063
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3127
|
-
*
|
|
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
|
|
3136
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 ?
|
|
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(
|
|
4114
|
-
if (
|
|
4115
|
-
if (
|
|
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
|
|
4732
|
+
if (!home.teamCode || home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
|
|
4139
4733
|
return false;
|
|
4140
4734
|
}
|
|
4141
|
-
if (away.teamCode
|
|
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
|
|
4177
|
-
const favorite =
|
|
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
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
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
|
|
4792
|
+
return { ...sealed.value, ambiguous: sealed.value.ambiguous || ambiguous2 };
|
|
4231
4793
|
}
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
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
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
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
|
|
4243
|
-
const
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
return
|
|
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
|
|
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
|
-
|
|
4829
|
+
results.set(m.id, s ? valid(s) : definitiveNone("fake provider has no signal"));
|
|
4269
4830
|
}
|
|
4270
|
-
return {
|
|
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
|
-
|
|
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 =
|
|
4324
|
-
return (await this.resolveOne(match, options, deadline))
|
|
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
|
|
4328
|
-
const
|
|
4329
|
-
|
|
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)
|
|
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.
|
|
4334
|
-
|
|
4902
|
+
if (r.kind === "unresolved" || r.kind === "malformed") complete = false;
|
|
4903
|
+
results.set(m.id, r);
|
|
4335
4904
|
}
|
|
4336
|
-
return {
|
|
4905
|
+
return { results, complete };
|
|
4337
4906
|
}
|
|
4338
4907
|
/**
|
|
4339
|
-
* Resolve one match
|
|
4340
|
-
*
|
|
4341
|
-
*
|
|
4342
|
-
*
|
|
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
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
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
|
|
4954
|
+
return worst ?? definitiveNone("no candidate slug yielded a usable market");
|
|
4358
4955
|
} catch {
|
|
4359
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
4388
|
-
|
|
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.
|
|
4392
|
-
|
|
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)
|
|
4395
|
-
return
|
|
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
|
-
|
|
4398
|
-
(
|
|
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
|
|
4401
|
-
const
|
|
4402
|
-
const
|
|
4403
|
-
|
|
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)
|
|
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,
|
|
5059
|
+
for (const [kind, market, teamCode2, label] of legs) {
|
|
4415
5060
|
if (!market) continue;
|
|
4416
|
-
if (market.closed
|
|
4417
|
-
|
|
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
|
|
4420
|
-
outcomes.push({ kind, teamCode, label, probability: yes });
|
|
4421
|
-
|
|
4422
|
-
|
|
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)
|
|
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
|
-
|
|
4431
|
-
|
|
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 ?
|
|
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
|
-
|
|
5175
|
+
const title = (m.groupItemTitle ?? "").trim().toLowerCase();
|
|
5176
|
+
return slugToken(m) === "draw" || title === "draw" || /^draw\s*\(/.test(title);
|
|
4486
5177
|
}
|
|
4487
|
-
function pickMarket(markets,
|
|
4488
|
-
const tokens = pmTokens(
|
|
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.
|
|
4492
|
-
if (bySlug) return bySlug;
|
|
4493
|
-
|
|
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.
|
|
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
|
|
4512
|
-
if (
|
|
4513
|
-
const
|
|
4514
|
-
if (
|
|
4515
|
-
const
|
|
4516
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4919
|
-
|
|
5648
|
+
const batch = await getMarketSignals(
|
|
5649
|
+
providerFactory("polymarket"),
|
|
4920
5650
|
miss,
|
|
4921
5651
|
DEFAULT_ON_MARKET_OPTS
|
|
4922
5652
|
);
|
|
4923
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4976
|
-
|
|
4977
|
-
|
|
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: {
|
|
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
|
|
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
|
|
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}"
|
|
5785
|
+
const msg = degraded ? t(args.lang, "standings.unavailable") : g ? `No group "${g}".` : "No standings available.";
|
|
5024
5786
|
return {
|
|
5025
|
-
text: withDisclaimer(
|
|
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
|
|
5119
|
-
const
|
|
5120
|
-
const
|
|
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
|
|
5135
|
-
const
|
|
5136
|
-
const
|
|
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
|
|
5152
|
-
const
|
|
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
|
|
5156
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
5225
|
-
|
|
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
|
-
//
|
|
5244
|
-
//
|
|
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:
|
|
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:
|
|
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
|
-
|
|
5366
|
-
|
|
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.
|
|
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
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
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.
|
|
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"),
|