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