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