@claudinho/cli 0.9.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/index.js +1469 -525
- package/package.json +5 -5
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);
|
|
@@ -2953,47 +2953,285 @@ function rosterAtZero(matches) {
|
|
|
2953
2953
|
}
|
|
2954
2954
|
return [...teams.values()].sort((a, b) => a.name.localeCompare(b.name)).map(blankRow);
|
|
2955
2955
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
var USER_AGENT = `claudinho/${"0.9.3"} (+https://github.com/arturogarrido/claudinho)`;
|
|
2960
|
-
var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
2961
|
-
function competitionBase(slug) {
|
|
2962
|
-
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;
|
|
2963
2959
|
}
|
|
2964
|
-
var
|
|
2965
|
-
var
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
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 "";
|
|
2974
3005
|
}
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
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;
|
|
2978
3042
|
}
|
|
2979
|
-
|
|
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
|
+
}
|
|
2980
3215
|
function mapStatus(st) {
|
|
2981
|
-
const
|
|
2982
|
-
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 : "";
|
|
2983
3219
|
if (name.includes("HALFTIME")) return "HT";
|
|
2984
3220
|
if (name.includes("POSTPONED")) return "POSTPONED";
|
|
2985
3221
|
if (name.includes("CANCEL")) return "CANCELLED";
|
|
2986
3222
|
if (state === "pre") return "SCHEDULED";
|
|
2987
3223
|
if (state === "post") return "FT";
|
|
2988
3224
|
if (state === "in") return "LIVE";
|
|
2989
|
-
return
|
|
3225
|
+
return void 0;
|
|
2990
3226
|
}
|
|
2991
3227
|
function parseMinute(st) {
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
if (
|
|
2996
|
-
|
|
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;
|
|
2997
3235
|
}
|
|
2998
3236
|
return void 0;
|
|
2999
3237
|
}
|
|
@@ -3007,58 +3245,74 @@ var SLUG_TO_STAGE = {
|
|
|
3007
3245
|
final: "F"
|
|
3008
3246
|
};
|
|
3009
3247
|
function stageFromSlug(slug) {
|
|
3010
|
-
if (slug
|
|
3011
|
-
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
|
+
}
|
|
3012
3253
|
return "FRIENDLY";
|
|
3013
3254
|
}
|
|
3014
|
-
function
|
|
3015
|
-
if (
|
|
3016
|
-
|
|
3017
|
-
return
|
|
3018
|
-
}
|
|
3019
|
-
function
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
);
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
}
|
|
3030
|
-
|
|
3031
|
-
const
|
|
3032
|
-
const
|
|
3033
|
-
|
|
3034
|
-
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;
|
|
3035
3286
|
const status = mapStatus(ev.status ?? comp?.status);
|
|
3036
|
-
|
|
3037
|
-
const
|
|
3038
|
-
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";
|
|
3039
3289
|
let group;
|
|
3040
3290
|
if (stage === "GROUP" && ctx.groupByTeam) {
|
|
3041
3291
|
group = ctx.groupByTeam[home.code] ?? ctx.groupByTeam[away.code];
|
|
3042
3292
|
}
|
|
3043
|
-
const hs =
|
|
3044
|
-
const as =
|
|
3045
|
-
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;
|
|
3046
3302
|
let winnerCode;
|
|
3047
3303
|
if (isFinished(status)) {
|
|
3048
|
-
|
|
3049
|
-
|
|
3304
|
+
const winners = [homeRaw, awayRaw].filter((c) => flag(c.winner) === true);
|
|
3305
|
+
if (winners.length === 1) winnerCode = winners[0] === homeRaw ? home.code : away.code;
|
|
3050
3306
|
}
|
|
3051
|
-
const
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
return {
|
|
3055
|
-
id: ev.id,
|
|
3307
|
+
const venue = comp?.venue;
|
|
3308
|
+
return sealMatch({
|
|
3309
|
+
id,
|
|
3056
3310
|
stage,
|
|
3057
3311
|
group,
|
|
3058
|
-
kickoff
|
|
3059
|
-
venue:
|
|
3060
|
-
city:
|
|
3061
|
-
country:
|
|
3312
|
+
kickoff,
|
|
3313
|
+
venue: venue?.fullName,
|
|
3314
|
+
city: venue?.address?.city,
|
|
3315
|
+
country: venue?.address?.country,
|
|
3062
3316
|
home,
|
|
3063
3317
|
away,
|
|
3064
3318
|
score: hasScore ? { home: hs, away: as } : void 0,
|
|
@@ -3066,57 +3320,224 @@ function mapEspnEvent(ev, ctx = {}) {
|
|
|
3066
3320
|
minute: parseMinute(ev.status ?? comp?.status),
|
|
3067
3321
|
status,
|
|
3068
3322
|
winnerCode,
|
|
3069
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3070
|
-
|
|
3323
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3324
|
+
events: ev.events
|
|
3325
|
+
});
|
|
3071
3326
|
}
|
|
3072
|
-
function
|
|
3073
|
-
|
|
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
|
+
};
|
|
3074
3358
|
}
|
|
3075
|
-
function statVal(stats, name) {
|
|
3076
|
-
|
|
3077
|
-
|
|
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;
|
|
3078
3379
|
}
|
|
3079
3380
|
function entryToRow(e) {
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
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
|
+
});
|
|
3091
3420
|
}
|
|
3092
|
-
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;
|
|
3093
3429
|
const out2 = [];
|
|
3094
|
-
|
|
3095
|
-
|
|
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();
|
|
3096
3435
|
if (!letter) continue;
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
}
|
|
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 seenCodes = /* @__PURE__ */ new Map();
|
|
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 { providerId, providerRank } = r.value;
|
|
3461
|
+
const code = r.value.team.code;
|
|
3462
|
+
const priorHadId = seenCodes.get(code);
|
|
3463
|
+
const codeCollision = priorHadId !== void 0 && (providerId === void 0 || priorHadId === false);
|
|
3464
|
+
if (codeCollision || seenRanks.has(providerRank) || providerId !== void 0 && seenProviderIds.has(providerId)) {
|
|
3465
|
+
complete = false;
|
|
3466
|
+
continue;
|
|
3467
|
+
}
|
|
3468
|
+
seenCodes.set(code, providerId !== void 0);
|
|
3469
|
+
seenRanks.add(providerRank);
|
|
3470
|
+
if (providerId !== void 0) seenProviderIds.add(providerId);
|
|
3471
|
+
const { providerId: _dropId, providerRank: rank, ...row } = r.value;
|
|
3472
|
+
ranked.push({ row, rank });
|
|
3473
|
+
}
|
|
3101
3474
|
ranked.sort((a, b) => {
|
|
3102
3475
|
if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
return
|
|
3476
|
+
if (b.row.points !== a.row.points) return b.row.points - a.row.points;
|
|
3477
|
+
if (b.row.goalDiff !== a.row.goalDiff) return b.row.goalDiff - a.row.goalDiff;
|
|
3478
|
+
return b.row.goalsFor - a.row.goalsFor;
|
|
3106
3479
|
});
|
|
3480
|
+
if (ranked.length === 0) {
|
|
3481
|
+
complete = false;
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
3107
3484
|
out2.push({ group: letter, rows: ranked.map((x) => x.row) });
|
|
3108
3485
|
}
|
|
3109
|
-
|
|
3110
|
-
|
|
3486
|
+
return {
|
|
3487
|
+
items: out2,
|
|
3488
|
+
total: seenGroups.size,
|
|
3489
|
+
shown: out2.length,
|
|
3490
|
+
// We stopped early if the child list or any single group's rows were cut.
|
|
3491
|
+
truncated: !sawAllChildren || rowsTruncated,
|
|
3492
|
+
complete: complete && !rowsTruncated
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
|
|
3496
|
+
var DEFAULT_COMPETITION = "fifa.world";
|
|
3497
|
+
var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
|
|
3498
|
+
var USER_AGENT = `claudinho/${"0.10.0"} (+https://github.com/arturogarrido/claudinho)`;
|
|
3499
|
+
var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
3500
|
+
function competitionBase(slug) {
|
|
3501
|
+
return `${ESPN_SOCCER}/${slug}`;
|
|
3502
|
+
}
|
|
3503
|
+
var DEFAULT_TIMEOUT_MS = 6e3;
|
|
3504
|
+
var STANDINGS_SHARE_MS = 3e4;
|
|
3505
|
+
var ProviderError = class extends Error {
|
|
3506
|
+
kind;
|
|
3507
|
+
status;
|
|
3508
|
+
constructor(message, kind, status) {
|
|
3509
|
+
super(message);
|
|
3510
|
+
this.name = "ProviderError";
|
|
3511
|
+
this.kind = kind;
|
|
3512
|
+
this.status = status;
|
|
3513
|
+
}
|
|
3514
|
+
/** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
|
|
3515
|
+
get throttled() {
|
|
3516
|
+
return this.kind === "http" && (this.status === 429 || this.status === 403);
|
|
3517
|
+
}
|
|
3518
|
+
};
|
|
3519
|
+
function toEspnDate(d) {
|
|
3520
|
+
return d.replace(/\D/g, "").slice(0, 8);
|
|
3521
|
+
}
|
|
3522
|
+
function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length > 0) {
|
|
3523
|
+
if (!hasUsableRecord && (!parsed.complete || parsed.total > 0)) {
|
|
3524
|
+
throw new ProviderError(`ESPN ${kind} payload had no readable records`, "parse");
|
|
3525
|
+
}
|
|
3526
|
+
return [...parsed.items];
|
|
3111
3527
|
}
|
|
3112
3528
|
var EspnAdapter = class {
|
|
3113
3529
|
constructor(opts = {}) {
|
|
3114
3530
|
this.opts = opts;
|
|
3531
|
+
const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
|
|
3532
|
+
this.expectedStandingsGroups = expected ? [...expected] : void 0;
|
|
3533
|
+
this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
|
|
3115
3534
|
}
|
|
3116
3535
|
opts;
|
|
3117
3536
|
name = "espn";
|
|
3118
3537
|
capabilities = { push: false, latencyHintSec: 45 };
|
|
3119
|
-
|
|
3538
|
+
expectedStandingsGroups;
|
|
3539
|
+
standingsFallbackGroups;
|
|
3540
|
+
/** Short-lived team-code -> group-letter map (built lazily from standings). */
|
|
3120
3541
|
groupMap;
|
|
3121
3542
|
/**
|
|
3122
3543
|
* One in-flight/recent standings fetch shared by fetchStandings and
|
|
@@ -3159,38 +3580,48 @@ var EspnAdapter = class {
|
|
|
3159
3580
|
if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
|
|
3160
3581
|
return this.standingsShared.promise;
|
|
3161
3582
|
}
|
|
3162
|
-
const promise = this.get(this.standingsUrl()).then(
|
|
3163
|
-
|
|
3164
|
-
|
|
3583
|
+
const promise = this.get(this.standingsUrl()).then((d) => {
|
|
3584
|
+
const parsed = parseEspnStandings(d);
|
|
3585
|
+
return usableProviderItems(
|
|
3586
|
+
"standings",
|
|
3587
|
+
parsed,
|
|
3588
|
+
parsed.items.some((table) => table.rows.length > 0)
|
|
3589
|
+
);
|
|
3590
|
+
});
|
|
3165
3591
|
this.standingsShared = { at: now, promise };
|
|
3166
|
-
promise.catch(() => {
|
|
3592
|
+
void promise.catch(() => {
|
|
3167
3593
|
if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
|
|
3168
3594
|
});
|
|
3169
3595
|
return promise;
|
|
3170
3596
|
}
|
|
3171
3597
|
/**
|
|
3172
3598
|
* Authoritative, cumulative group tables from the standings endpoint. Throws
|
|
3173
|
-
* on fetch
|
|
3174
|
-
*
|
|
3599
|
+
* on fetch failure. Group-stage only: non-group `children` are filtered out
|
|
3600
|
+
* by {@link parseStandings}; malformed rows are omitted without hiding their
|
|
3601
|
+
* readable siblings.
|
|
3175
3602
|
*/
|
|
3176
3603
|
async fetchStandings() {
|
|
3177
3604
|
return this.sharedStandings();
|
|
3178
3605
|
}
|
|
3179
3606
|
/**
|
|
3180
|
-
* Build (and cache) a team-code -> group-letter map from the standings
|
|
3607
|
+
* Build (and briefly cache) a team-code -> group-letter map from the standings
|
|
3181
3608
|
* endpoint. Best-effort: returns {} if standings are unavailable — but a
|
|
3182
|
-
* transient failure is NOT cached
|
|
3183
|
-
*
|
|
3609
|
+
* transient failure is NOT cached, and a partial successful parse expires at
|
|
3610
|
+
* the standings TTL, so neither can silently drop group letters for the
|
|
3611
|
+
* adapter's lifetime.
|
|
3184
3612
|
* Reuses the same parse/fetch as {@link fetchStandings}, so the two never
|
|
3185
3613
|
* drift and one command never fetches standings twice.
|
|
3186
3614
|
*/
|
|
3187
3615
|
async fetchGroupMap(force = false) {
|
|
3188
|
-
|
|
3616
|
+
const now = Date.now();
|
|
3617
|
+
if (!force && this.groupMap && now - this.groupMap.at < STANDINGS_SHARE_MS) {
|
|
3618
|
+
return this.groupMap.value;
|
|
3619
|
+
}
|
|
3189
3620
|
try {
|
|
3190
3621
|
const tables = await this.sharedStandings();
|
|
3191
3622
|
const map = {};
|
|
3192
3623
|
for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
|
|
3193
|
-
this.groupMap = map;
|
|
3624
|
+
this.groupMap = { at: Date.now(), value: map };
|
|
3194
3625
|
return map;
|
|
3195
3626
|
} catch {
|
|
3196
3627
|
return {};
|
|
@@ -3205,7 +3636,8 @@ var EspnAdapter = class {
|
|
|
3205
3636
|
this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
|
|
3206
3637
|
this.get(url.toString())
|
|
3207
3638
|
]);
|
|
3208
|
-
|
|
3639
|
+
const parsed = parseEspnEvents(data, { groupByTeam });
|
|
3640
|
+
return usableProviderItems("scoreboard", parsed);
|
|
3209
3641
|
}
|
|
3210
3642
|
async get(url) {
|
|
3211
3643
|
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
@@ -3981,12 +4413,12 @@ function resolveCompetition(explicit) {
|
|
|
3981
4413
|
return DEFAULT_COMPETITION;
|
|
3982
4414
|
}
|
|
3983
4415
|
var KNOWN_SOURCES = ["espn"];
|
|
3984
|
-
function makeAdapter(source = "espn") {
|
|
4416
|
+
function makeAdapter(source = "espn", opts = {}) {
|
|
3985
4417
|
switch (source) {
|
|
3986
4418
|
case "espn": {
|
|
3987
4419
|
const competition = resolveCompetition();
|
|
3988
4420
|
const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
|
|
3989
|
-
return new EspnAdapter({ baseUrl });
|
|
4421
|
+
return new EspnAdapter({ baseUrl, enrichGroups: opts.enrichGroups });
|
|
3990
4422
|
}
|
|
3991
4423
|
default:
|
|
3992
4424
|
throw new Error(
|
|
@@ -4015,17 +4447,26 @@ async function getMatchesForDate(adapter, dateISO) {
|
|
|
4015
4447
|
}
|
|
4016
4448
|
async function getStandings(adapter, group) {
|
|
4017
4449
|
const want = group?.toUpperCase();
|
|
4450
|
+
const expected = adapter.expectedStandingsGroups;
|
|
4451
|
+
if (want && expected && !expected.includes(want)) {
|
|
4452
|
+
return { tables: [], degraded: false };
|
|
4453
|
+
}
|
|
4018
4454
|
if (adapter.fetchStandings) {
|
|
4019
4455
|
try {
|
|
4020
4456
|
const all = await adapter.fetchStandings();
|
|
4021
4457
|
const tables2 = (want ? all.filter((t2) => t2.group === want) : all).sort(
|
|
4022
4458
|
(a, b) => a.group.localeCompare(b.group)
|
|
4023
4459
|
);
|
|
4024
|
-
|
|
4460
|
+
const availableGroups = new Set(tables2.map((table) => table.group));
|
|
4461
|
+
const expectedGroupWasOmitted = want ? (expected?.includes(want) ?? false) && tables2.length === 0 : expected?.some((group2) => !availableGroups.has(group2)) ?? false;
|
|
4462
|
+
if (!expectedGroupWasOmitted) {
|
|
4463
|
+
return { tables: tables2, degraded: false, source: adapter.name };
|
|
4464
|
+
}
|
|
4025
4465
|
} catch {
|
|
4026
4466
|
}
|
|
4027
4467
|
}
|
|
4028
|
-
const
|
|
4468
|
+
const fallbackGroups = adapter.standingsFallbackGroups;
|
|
4469
|
+
const letters = fallbackGroups ? want ? fallbackGroups.includes(want) ? [want] : [] : [...new Set(fallbackGroups)].sort((a, b) => a.localeCompare(b)) : [];
|
|
4029
4470
|
const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
|
|
4030
4471
|
return { tables, degraded: true };
|
|
4031
4472
|
}
|
|
@@ -4082,7 +4523,10 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
|
|
|
4082
4523
|
try {
|
|
4083
4524
|
const win = knockoutWindow();
|
|
4084
4525
|
if (adapter.fetchWindow && win) {
|
|
4085
|
-
fixtures = mergeLive(
|
|
4526
|
+
fixtures = mergeLive(
|
|
4527
|
+
fixtures,
|
|
4528
|
+
await adapter.fetchWindow(win.start, win.end)
|
|
4529
|
+
);
|
|
4086
4530
|
}
|
|
4087
4531
|
} catch {
|
|
4088
4532
|
overlayFailed = true;
|
|
@@ -4146,14 +4590,144 @@ async function getMatchById(adapter, id) {
|
|
|
4146
4590
|
async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
|
|
4147
4591
|
try {
|
|
4148
4592
|
const day = now.toISOString().slice(0, 10);
|
|
4149
|
-
const matches = adapter.fetchWindow ?
|
|
4150
|
-
(m) => isLive(m.status)
|
|
4151
|
-
) : await adapter.fetchLive();
|
|
4593
|
+
const matches = (adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchLive()).filter((m) => isLive(m.status));
|
|
4152
4594
|
return { matches, degraded: false, source: adapter.name };
|
|
4153
4595
|
} catch {
|
|
4154
4596
|
return { matches: [], degraded: true };
|
|
4155
4597
|
}
|
|
4156
4598
|
}
|
|
4599
|
+
function pct(p) {
|
|
4600
|
+
return Math.round(p * 100);
|
|
4601
|
+
}
|
|
4602
|
+
var KNOWN_MARKET_SOURCES = ["polymarket", "fake"];
|
|
4603
|
+
function marketSourceLabel(source) {
|
|
4604
|
+
if (source === "polymarket") return "Polymarket";
|
|
4605
|
+
if (source === "fake") return "demo data";
|
|
4606
|
+
return source.charAt(0).toUpperCase() + source.slice(1);
|
|
4607
|
+
}
|
|
4608
|
+
function outcomeLabel(o, match) {
|
|
4609
|
+
if (o.kind === "home") return match.home.name;
|
|
4610
|
+
if (o.kind === "away") return match.away.name;
|
|
4611
|
+
if (o.kind === "draw") return "Draw";
|
|
4612
|
+
return o.label;
|
|
4613
|
+
}
|
|
4614
|
+
function utcHhmm(iso) {
|
|
4615
|
+
const t2 = Date.parse(iso);
|
|
4616
|
+
if (!Number.isFinite(t2)) return "";
|
|
4617
|
+
return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
|
|
4618
|
+
}
|
|
4619
|
+
function marketFavoriteText(signal, match) {
|
|
4620
|
+
const fav = signal.favorite;
|
|
4621
|
+
if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
|
|
4622
|
+
if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
|
|
4623
|
+
const name = fav.kind === "home" ? match.home.name : match.away.name;
|
|
4624
|
+
return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
|
|
4625
|
+
}
|
|
4626
|
+
function marketProbabilityText(signal, match) {
|
|
4627
|
+
const order = ["home", "draw", "away"];
|
|
4628
|
+
const parts = [];
|
|
4629
|
+
for (const kind of order) {
|
|
4630
|
+
const o = signal.outcomes.find((x) => x.kind === kind);
|
|
4631
|
+
if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
4632
|
+
}
|
|
4633
|
+
for (const o of signal.outcomes) {
|
|
4634
|
+
if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
4635
|
+
}
|
|
4636
|
+
return parts.join(" \xB7 ");
|
|
4637
|
+
}
|
|
4638
|
+
function marketAttributionText(signal) {
|
|
4639
|
+
const time = utcHhmm(signal.asOf);
|
|
4640
|
+
const src = `Source: ${marketSourceLabel(signal.source)}`;
|
|
4641
|
+
return time ? `${src} \xB7 updated ${time}` : src;
|
|
4642
|
+
}
|
|
4643
|
+
function marketLine(signal, match) {
|
|
4644
|
+
return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
|
|
4645
|
+
signal.source
|
|
4646
|
+
)} \xB7 informational only`;
|
|
4647
|
+
}
|
|
4648
|
+
function marketBlock(signal, match) {
|
|
4649
|
+
const lines = [];
|
|
4650
|
+
if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
|
|
4651
|
+
lines.push(marketFavoriteText(signal, match));
|
|
4652
|
+
lines.push(marketProbabilityText(signal, match));
|
|
4653
|
+
lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
|
|
4654
|
+
return lines;
|
|
4655
|
+
}
|
|
4656
|
+
var MAX_OUTCOMES = 128;
|
|
4657
|
+
var MATCH_ID = /^[0-9]{1,20}$/;
|
|
4658
|
+
var MARKET_ID = /^(?:[0-9]{1,32}|fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2})$/;
|
|
4659
|
+
var OUTCOME_KINDS = /* @__PURE__ */ new Set(["home", "draw", "away", "other"]);
|
|
4660
|
+
var TEAM_CODE_COLUMNS2 = 8;
|
|
4661
|
+
function sealOutcome(raw) {
|
|
4662
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
4663
|
+
const o = raw;
|
|
4664
|
+
const kind = member(o.kind, OUTCOME_KINDS);
|
|
4665
|
+
const p = probability(o.probability);
|
|
4666
|
+
if (!kind || p === void 0) return void 0;
|
|
4667
|
+
const out2 = { kind };
|
|
4668
|
+
if (o.teamCode !== void 0) {
|
|
4669
|
+
if (typeof o.teamCode !== "string") return void 0;
|
|
4670
|
+
out2.teamCode = humanLabel(o.teamCode, TEAM_CODE_COLUMNS2);
|
|
4671
|
+
}
|
|
4672
|
+
out2.label = humanLabel(o.label);
|
|
4673
|
+
out2.probability = p;
|
|
4674
|
+
if ((out2.kind === "home" || out2.kind === "away") && !out2.teamCode) return void 0;
|
|
4675
|
+
return out2;
|
|
4676
|
+
}
|
|
4677
|
+
function hasDuplicateKind(outcomes) {
|
|
4678
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4679
|
+
for (const o of outcomes) {
|
|
4680
|
+
if (o.kind === "other") continue;
|
|
4681
|
+
if (seen.has(o.kind)) return true;
|
|
4682
|
+
seen.add(o.kind);
|
|
4683
|
+
}
|
|
4684
|
+
return false;
|
|
4685
|
+
}
|
|
4686
|
+
function sealMarketSignal(raw, options = {}) {
|
|
4687
|
+
if (!raw || typeof raw !== "object") return malformed("signal is not an object");
|
|
4688
|
+
const s = raw;
|
|
4689
|
+
const matchId = opaqueId(s.matchId, MATCH_ID);
|
|
4690
|
+
if (!matchId) return malformed("signal names no fixture");
|
|
4691
|
+
if (!Array.isArray(s.outcomes) || s.outcomes.length > MAX_OUTCOMES) {
|
|
4692
|
+
return malformed("signal outcomes are absent or exceed the cap");
|
|
4693
|
+
}
|
|
4694
|
+
const outcomes = [];
|
|
4695
|
+
for (const rawOutcome of takeBounded(s.outcomes, MAX_OUTCOMES)) {
|
|
4696
|
+
const outcome = sealOutcome(rawOutcome);
|
|
4697
|
+
if (!outcome) return malformed("signal carries an unreadable outcome");
|
|
4698
|
+
outcomes.push(outcome);
|
|
4699
|
+
}
|
|
4700
|
+
if (hasDuplicateKind(outcomes)) {
|
|
4701
|
+
return ambiguous("two outcomes claim the same result");
|
|
4702
|
+
}
|
|
4703
|
+
const sourceMarketId = opaqueId(s.sourceMarketId, MARKET_ID);
|
|
4704
|
+
const liquidity = quantity(s.liquidity);
|
|
4705
|
+
const volume24h = quantity(s.volume24h);
|
|
4706
|
+
const out2 = {
|
|
4707
|
+
matchId,
|
|
4708
|
+
// Allow-listed, not merely stripped: this lands in the provider-attribution
|
|
4709
|
+
// slot, where `marketSourceLabel` falls through to the raw string for an
|
|
4710
|
+
// unrecognized provider — attacker prose where the reader expects
|
|
4711
|
+
// "Polymarket".
|
|
4712
|
+
source: member(s.source, new Set(KNOWN_MARKET_SOURCES)) ?? ""
|
|
4713
|
+
};
|
|
4714
|
+
if (sourceMarketId) out2.sourceMarketId = sourceMarketId;
|
|
4715
|
+
out2.asOf = canonicalTimestamp(s.asOf) ?? "";
|
|
4716
|
+
out2.fetchedAt = canonicalTimestamp(s.fetchedAt) ?? "";
|
|
4717
|
+
out2.outcomes = outcomes;
|
|
4718
|
+
const isAmbiguous = s.ambiguous !== false;
|
|
4719
|
+
const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
|
|
4720
|
+
if (favorite) out2.favorite = favorite;
|
|
4721
|
+
if (liquidity !== void 0) out2.liquidity = liquidity;
|
|
4722
|
+
if (volume24h !== void 0) out2.volume24h = volume24h;
|
|
4723
|
+
out2.stale = s.stale !== false;
|
|
4724
|
+
out2.ambiguous = isAmbiguous || out2.source === "";
|
|
4725
|
+
out2.stale = out2.stale || isStaleSignal(out2, { now: options.now, maxAgeMs: options.maxAgeMs });
|
|
4726
|
+
return valid(out2);
|
|
4727
|
+
}
|
|
4728
|
+
function parseCachedMarketSignal(raw, options = {}) {
|
|
4729
|
+
return sealMarketSignal(raw, options);
|
|
4730
|
+
}
|
|
4157
4731
|
var DEFAULT_MAX_AGE_MS = 15 * 6e4;
|
|
4158
4732
|
function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
|
|
4159
4733
|
if (isLive(match.status)) return true;
|
|
@@ -4172,9 +4746,9 @@ function normalizeOutcomes(outcomes) {
|
|
|
4172
4746
|
probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
|
|
4173
4747
|
}));
|
|
4174
4748
|
}
|
|
4175
|
-
function favoriteStrength(
|
|
4176
|
-
if (
|
|
4177
|
-
if (
|
|
4749
|
+
function favoriteStrength(probability2) {
|
|
4750
|
+
if (probability2 >= 0.65) return "clear";
|
|
4751
|
+
if (probability2 >= 0.52) return "slight";
|
|
4178
4752
|
return "close";
|
|
4179
4753
|
}
|
|
4180
4754
|
function deriveFavorite(outcomes) {
|
|
@@ -4193,14 +4767,16 @@ function deriveFavorite(outcomes) {
|
|
|
4193
4767
|
}
|
|
4194
4768
|
function mapsCleanly(match, outcomes) {
|
|
4195
4769
|
if (outcomes.some((o) => o.kind === "other")) return false;
|
|
4770
|
+
const kinds = outcomes.map((o) => o.kind);
|
|
4771
|
+
if (new Set(kinds).size !== kinds.length) return false;
|
|
4196
4772
|
const home = outcomes.find((o) => o.kind === "home");
|
|
4197
4773
|
const away = outcomes.find((o) => o.kind === "away");
|
|
4198
4774
|
const draw = outcomes.find((o) => o.kind === "draw");
|
|
4199
4775
|
if (!home || !away) return false;
|
|
4200
|
-
if (home.teamCode
|
|
4776
|
+
if (!home.teamCode || home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
|
|
4201
4777
|
return false;
|
|
4202
4778
|
}
|
|
4203
|
-
if (away.teamCode
|
|
4779
|
+
if (!away.teamCode || away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
|
|
4204
4780
|
return false;
|
|
4205
4781
|
}
|
|
4206
4782
|
if (match.stage === "GROUP" && !draw) return false;
|
|
@@ -4215,11 +4791,13 @@ function hasSaneDistribution(outcomes) {
|
|
|
4215
4791
|
const sum = priced.reduce((s, o) => s + o.probability, 0);
|
|
4216
4792
|
return sum > 0.97 && sum < 1.03;
|
|
4217
4793
|
}
|
|
4794
|
+
var FUTURE_SKEW_MS = 6e4;
|
|
4218
4795
|
function isStaleSignal(signal, options = {}) {
|
|
4219
4796
|
const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
4220
4797
|
const asOf = Date.parse(signal.asOf);
|
|
4221
4798
|
if (!Number.isFinite(asOf)) return true;
|
|
4222
4799
|
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
4800
|
+
if (asOf - now > FUTURE_SKEW_MS) return true;
|
|
4223
4801
|
return now - asOf > maxAge;
|
|
4224
4802
|
}
|
|
4225
4803
|
function isReliableMarketSignal(signal, options = {}) {
|
|
@@ -4235,8 +4813,8 @@ function isReliableMarketSignal(signal, options = {}) {
|
|
|
4235
4813
|
}
|
|
4236
4814
|
function buildMarketSignal(input) {
|
|
4237
4815
|
const outcomes = normalizeOutcomes(input.outcomes);
|
|
4238
|
-
const
|
|
4239
|
-
const favorite =
|
|
4816
|
+
const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
|
|
4817
|
+
const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
|
|
4240
4818
|
const signal = {
|
|
4241
4819
|
matchId: input.match.id,
|
|
4242
4820
|
source: input.source,
|
|
@@ -4248,66 +4826,33 @@ function buildMarketSignal(input) {
|
|
|
4248
4826
|
liquidity: input.liquidity,
|
|
4249
4827
|
volume24h: input.volume24h,
|
|
4250
4828
|
stale: false,
|
|
4251
|
-
ambiguous
|
|
4829
|
+
ambiguous: ambiguous2
|
|
4252
4830
|
};
|
|
4253
4831
|
signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
return Math.round(p * 100);
|
|
4258
|
-
}
|
|
4259
|
-
function marketSourceLabel(source) {
|
|
4260
|
-
if (source === "polymarket") return "Polymarket";
|
|
4261
|
-
if (source === "fake") return "demo data";
|
|
4262
|
-
return source.charAt(0).toUpperCase() + source.slice(1);
|
|
4263
|
-
}
|
|
4264
|
-
function outcomeLabel(o, match) {
|
|
4265
|
-
if (o.kind === "home") return match.home.name;
|
|
4266
|
-
if (o.kind === "away") return match.away.name;
|
|
4267
|
-
if (o.kind === "draw") return "Draw";
|
|
4268
|
-
return o.label;
|
|
4269
|
-
}
|
|
4270
|
-
function utcHhmm(iso) {
|
|
4271
|
-
const t2 = Date.parse(iso);
|
|
4272
|
-
if (!Number.isFinite(t2)) return "";
|
|
4273
|
-
return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
|
|
4274
|
-
}
|
|
4275
|
-
function marketFavoriteText(signal, match) {
|
|
4276
|
-
const fav = signal.favorite;
|
|
4277
|
-
if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
|
|
4278
|
-
if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
|
|
4279
|
-
const name = fav.kind === "home" ? match.home.name : match.away.name;
|
|
4280
|
-
return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
|
|
4281
|
-
}
|
|
4282
|
-
function marketProbabilityText(signal, match) {
|
|
4283
|
-
const order = ["home", "draw", "away"];
|
|
4284
|
-
const parts = [];
|
|
4285
|
-
for (const kind of order) {
|
|
4286
|
-
const o = signal.outcomes.find((x) => x.kind === kind);
|
|
4287
|
-
if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
4832
|
+
const sealed = sealMarketSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
|
|
4833
|
+
if (sealed.kind !== "valid") {
|
|
4834
|
+
return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
|
|
4288
4835
|
}
|
|
4289
|
-
|
|
4290
|
-
if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
4291
|
-
}
|
|
4292
|
-
return parts.join(" \xB7 ");
|
|
4836
|
+
return { ...sealed.value, ambiguous: sealed.value.ambiguous || ambiguous2 };
|
|
4293
4837
|
}
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4838
|
+
var NONE = { kind: "none" };
|
|
4839
|
+
function selectOne(candidates) {
|
|
4840
|
+
if (candidates.length === 1) return { kind: "one", value: candidates[0] };
|
|
4841
|
+
if (candidates.length === 0) return NONE;
|
|
4842
|
+
return { kind: "ambiguous", count: candidates.length };
|
|
4298
4843
|
}
|
|
4299
|
-
function
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4844
|
+
function resolvedValues(batch) {
|
|
4845
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
4846
|
+
for (const [key, r] of batch.results) if (r.kind === "valid") out2.set(key, r.value);
|
|
4847
|
+
return out2;
|
|
4303
4848
|
}
|
|
4304
|
-
function
|
|
4305
|
-
const
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
return
|
|
4849
|
+
function cacheableKeys(batch) {
|
|
4850
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
4851
|
+
for (const [key, r] of batch.results) if (isCacheable(r)) out2.add(key);
|
|
4852
|
+
return out2;
|
|
4853
|
+
}
|
|
4854
|
+
function emptyBatch() {
|
|
4855
|
+
return { results: /* @__PURE__ */ new Map(), complete: false };
|
|
4311
4856
|
}
|
|
4312
4857
|
var FakeMarketProvider = class {
|
|
4313
4858
|
constructor(opts = {}) {
|
|
@@ -4322,14 +4867,12 @@ var FakeMarketProvider = class {
|
|
|
4322
4867
|
return void 0;
|
|
4323
4868
|
}
|
|
4324
4869
|
async findSignals(matches, options) {
|
|
4325
|
-
const
|
|
4326
|
-
const checked = /* @__PURE__ */ new Set();
|
|
4870
|
+
const results = /* @__PURE__ */ new Map();
|
|
4327
4871
|
for (const m of matches) {
|
|
4328
|
-
checked.add(m.id);
|
|
4329
4872
|
const s = await this.findSignal(m, options);
|
|
4330
|
-
|
|
4873
|
+
results.set(m.id, s ? valid(s) : definitiveNone("fake provider has no signal"));
|
|
4331
4874
|
}
|
|
4332
|
-
return {
|
|
4875
|
+
return { results, complete: true };
|
|
4333
4876
|
}
|
|
4334
4877
|
synthesize(match, options) {
|
|
4335
4878
|
const seed = hash(`${match.home.code}-${match.away.code}`);
|
|
@@ -4346,7 +4889,9 @@ var FakeMarketProvider = class {
|
|
|
4346
4889
|
return buildMarketSignal({
|
|
4347
4890
|
match,
|
|
4348
4891
|
source: "fake",
|
|
4349
|
-
|
|
4892
|
+
// Must satisfy the boundary's opaque-id grammar, like a real one:
|
|
4893
|
+
// a source id that only the live path accepts is the asymmetry itself.
|
|
4894
|
+
sourceMarketId: match.id,
|
|
4350
4895
|
asOf,
|
|
4351
4896
|
fetchedAt: now.toISOString(),
|
|
4352
4897
|
outcomes,
|
|
@@ -4370,6 +4915,8 @@ var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
|
|
|
4370
4915
|
var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
|
|
4371
4916
|
var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
|
|
4372
4917
|
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
4918
|
+
var MAX_EVENT_MARKETS = 256;
|
|
4919
|
+
var DEFAULT_DEADLINE_MS = 15e3;
|
|
4373
4920
|
var WC_SERIES_SLUG = "soccer-fifwc";
|
|
4374
4921
|
var WC_SPORT = "fifwc";
|
|
4375
4922
|
var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
|
|
@@ -4382,49 +4929,81 @@ var PolymarketProvider = class {
|
|
|
4382
4929
|
opts;
|
|
4383
4930
|
name = "polymarket";
|
|
4384
4931
|
async findSignal(match, options) {
|
|
4385
|
-
const deadline =
|
|
4386
|
-
return (await this.resolveOne(match, options, deadline))
|
|
4932
|
+
const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
|
|
4933
|
+
return parsedValue(await this.resolveOne(match, options, deadline));
|
|
4387
4934
|
}
|
|
4388
4935
|
async findSignals(matches, options) {
|
|
4389
|
-
const
|
|
4390
|
-
const
|
|
4391
|
-
|
|
4936
|
+
const results = /* @__PURE__ */ new Map();
|
|
4937
|
+
const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
|
|
4938
|
+
let complete = true;
|
|
4392
4939
|
for (const m of matches) {
|
|
4393
|
-
if (Date.now() >= deadline)
|
|
4940
|
+
if (Date.now() >= deadline) {
|
|
4941
|
+
results.set(m.id, unresolved("enrichment deadline expired"));
|
|
4942
|
+
complete = false;
|
|
4943
|
+
continue;
|
|
4944
|
+
}
|
|
4394
4945
|
const r = await this.resolveOne(m, options, deadline);
|
|
4395
|
-
if (r.
|
|
4396
|
-
|
|
4946
|
+
if (r.kind === "unresolved" || r.kind === "malformed") complete = false;
|
|
4947
|
+
results.set(m.id, r);
|
|
4397
4948
|
}
|
|
4398
|
-
return {
|
|
4949
|
+
return { results, complete };
|
|
4399
4950
|
}
|
|
4400
4951
|
/**
|
|
4401
|
-
* Resolve one match
|
|
4402
|
-
*
|
|
4403
|
-
*
|
|
4404
|
-
*
|
|
4952
|
+
* Resolve one match into a verdict.
|
|
4953
|
+
*
|
|
4954
|
+
* Every exit says which KIND of non-answer it is, because that decides
|
|
4955
|
+
* whether it may be remembered — see `isCacheable`: a conclusion we drew from
|
|
4956
|
+
* a payload we READ is cacheable (including an ambiguity, which is stable),
|
|
4957
|
+
* while a shape we could not read is not. Previously a single
|
|
4958
|
+
* `checked: boolean` collapsed five distinct situations into two, and the
|
|
4959
|
+
* ones that landed on the wrong side of it — an ambiguous payload, a
|
|
4960
|
+
* two-legged market, an incoherent 1X2 — were negative-cached as the fact
|
|
4961
|
+
* that this fixture has no market.
|
|
4405
4962
|
*/
|
|
4406
4963
|
async resolveOne(match, options, deadline = Number.POSITIVE_INFINITY) {
|
|
4407
|
-
const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
|
|
4408
|
-
const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
|
|
4409
|
-
if (slugs.length === 0) return { checked: true };
|
|
4410
4964
|
const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
4411
4965
|
try {
|
|
4966
|
+
const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
|
|
4967
|
+
const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
|
|
4968
|
+
if (slugs.length === 0) return definitiveNone("fixture has no derivable event slug");
|
|
4969
|
+
const RANK = {
|
|
4970
|
+
"definitive-none": 0,
|
|
4971
|
+
ambiguous: 1,
|
|
4972
|
+
unresolved: 2,
|
|
4973
|
+
malformed: 3
|
|
4974
|
+
};
|
|
4975
|
+
let worst;
|
|
4976
|
+
const keepWorst = (r) => {
|
|
4977
|
+
if (r.kind === "definitive-none" || r.kind === "valid") return;
|
|
4978
|
+
if (!worst || (RANK[r.kind] ?? 0) > (RANK[worst.kind] ?? 0)) worst = r;
|
|
4979
|
+
};
|
|
4412
4980
|
for (const slug of slugs) {
|
|
4413
4981
|
const remaining = deadline - Date.now();
|
|
4414
|
-
if (remaining <= 0) return
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4982
|
+
if (remaining <= 0) return unresolved("deadline expired between candidate slugs");
|
|
4983
|
+
let found;
|
|
4984
|
+
try {
|
|
4985
|
+
found = await this.fetchEvent(slug, Math.min(configured, remaining));
|
|
4986
|
+
} catch {
|
|
4987
|
+
keepWorst(malformed("candidate request failed"));
|
|
4988
|
+
continue;
|
|
4989
|
+
}
|
|
4990
|
+
if (found.kind !== "valid") {
|
|
4991
|
+
keepWorst(found);
|
|
4992
|
+
continue;
|
|
4993
|
+
}
|
|
4994
|
+
const r = this.toSignal(match, slug, found.value, options);
|
|
4995
|
+
if (r.kind === "valid") return r;
|
|
4996
|
+
keepWorst(r);
|
|
4418
4997
|
}
|
|
4419
|
-
return
|
|
4998
|
+
return worst ?? definitiveNone("no candidate slug yielded a usable market");
|
|
4420
4999
|
} catch {
|
|
4421
|
-
return
|
|
5000
|
+
return malformed("provider request failed");
|
|
4422
5001
|
}
|
|
4423
5002
|
}
|
|
4424
5003
|
async fetchEvent(slug, timeoutMs) {
|
|
4425
5004
|
const base = this.opts.baseUrl ?? DEFAULT_BASE2;
|
|
4426
5005
|
assertAllowedHost(base);
|
|
4427
|
-
const url = `${base}/events
|
|
5006
|
+
const url = `${base}/events/slug/${encodeURIComponent(slug)}`;
|
|
4428
5007
|
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
4429
5008
|
const res = await doFetch(url, {
|
|
4430
5009
|
signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
|
|
@@ -4433,7 +5012,7 @@ var PolymarketProvider = class {
|
|
|
4433
5012
|
redirect: "error",
|
|
4434
5013
|
headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
|
|
4435
5014
|
});
|
|
4436
|
-
if (res.status === 404) return
|
|
5015
|
+
if (res.status === 404) return definitiveNone("slug returns 404");
|
|
4437
5016
|
if (!res.ok) {
|
|
4438
5017
|
throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
|
|
4439
5018
|
}
|
|
@@ -4442,63 +5021,148 @@ var PolymarketProvider = class {
|
|
|
4442
5021
|
throw new Error(`Polymarket response too large: ${length} bytes`);
|
|
4443
5022
|
}
|
|
4444
5023
|
const data = await res.json();
|
|
5024
|
+
if (Array.isArray(data) && data.length > 1) {
|
|
5025
|
+
return ambiguous("slug returned more than one event");
|
|
5026
|
+
}
|
|
5027
|
+
if (Array.isArray(data) && data.length === 0) return definitiveNone("slug returns no event");
|
|
4445
5028
|
const event = Array.isArray(data) ? data[0] : data;
|
|
4446
|
-
|
|
5029
|
+
if (!event || typeof event !== "object") return malformed("event body is not an object");
|
|
5030
|
+
return valid(event);
|
|
4447
5031
|
}
|
|
4448
5032
|
toSignal(match, eventSlug, event, options) {
|
|
4449
|
-
if (event.active
|
|
4450
|
-
|
|
4451
|
-
|
|
5033
|
+
if (typeof event.active !== "boolean" || typeof event.closed !== "boolean") {
|
|
5034
|
+
return malformed("event active/closed is not a boolean");
|
|
5035
|
+
}
|
|
5036
|
+
if (event.active === false || event.closed === true) {
|
|
5037
|
+
return definitiveNone("event is closed or inactive");
|
|
5038
|
+
}
|
|
5039
|
+
if (event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
|
|
5040
|
+
return definitiveNone("event is not in this competition");
|
|
5041
|
+
}
|
|
5042
|
+
if (typeof event.slug !== "string") {
|
|
5043
|
+
return malformed("event states no slug");
|
|
5044
|
+
}
|
|
5045
|
+
if (event.slug !== eventSlug) return definitiveNone("event is not the one requested");
|
|
5046
|
+
if (typeof event.startTime !== "string" || !canonicalTimestamp(event.startTime)) {
|
|
5047
|
+
return malformed("event startTime missing or unparseable");
|
|
4452
5048
|
}
|
|
4453
|
-
|
|
4454
|
-
const start = event.startTime ? Date.parse(event.startTime) : Number.NaN;
|
|
5049
|
+
const start = Date.parse(event.startTime);
|
|
4455
5050
|
const kick = Date.parse(match.kickoff);
|
|
4456
|
-
if (Number.isFinite(start)
|
|
4457
|
-
return
|
|
5051
|
+
if (!Number.isFinite(start) || !Number.isFinite(kick)) {
|
|
5052
|
+
return malformed("event or fixture kickoff is unreadable");
|
|
5053
|
+
}
|
|
5054
|
+
if (Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
|
|
5055
|
+
return definitiveNone("event kickoff does not match the fixture");
|
|
5056
|
+
}
|
|
5057
|
+
if (!Array.isArray(event.markets)) {
|
|
5058
|
+
return malformed("event markets is not an array");
|
|
5059
|
+
}
|
|
5060
|
+
const marketsTruncated = Array.isArray(event.markets) && event.markets.length > MAX_EVENT_MARKETS;
|
|
5061
|
+
if (marketsTruncated) {
|
|
5062
|
+
return malformed("event market list exceeded the cap");
|
|
4458
5063
|
}
|
|
4459
|
-
const
|
|
4460
|
-
|
|
5064
|
+
const marketList = takeBounded(event.markets, MAX_EVENT_MARKETS);
|
|
5065
|
+
if (marketList.some(
|
|
5066
|
+
(market) => !market || typeof market !== "object" || typeof market.sportsMarketType !== "string"
|
|
5067
|
+
)) {
|
|
5068
|
+
return malformed("event market is missing its market-type discriminator");
|
|
5069
|
+
}
|
|
5070
|
+
const moneyline = marketList.filter(
|
|
5071
|
+
(m) => m?.sportsMarketType === "moneyline"
|
|
4461
5072
|
);
|
|
4462
|
-
const
|
|
4463
|
-
const
|
|
4464
|
-
const
|
|
4465
|
-
|
|
5073
|
+
const homeSel = pickMarket(moneyline, match.home.code, match.home.name);
|
|
5074
|
+
const awaySel = pickMarket(moneyline, match.away.code, match.away.name);
|
|
5075
|
+
const drawSel = pickDraw(moneyline);
|
|
5076
|
+
for (const [side, sel] of [
|
|
5077
|
+
["home", homeSel],
|
|
5078
|
+
["away", awaySel],
|
|
5079
|
+
["draw", drawSel]
|
|
5080
|
+
]) {
|
|
5081
|
+
if (sel.kind === "ambiguous") {
|
|
5082
|
+
return ambiguous(`${sel.count} markets claim the ${side} outcome`);
|
|
5083
|
+
}
|
|
5084
|
+
}
|
|
5085
|
+
if (homeSel.kind !== "one" || awaySel.kind !== "one" || drawSel.kind !== "one") {
|
|
5086
|
+
return definitiveNone("event does not carry all three 1X2 legs");
|
|
5087
|
+
}
|
|
5088
|
+
const homeMarket = homeSel.value;
|
|
5089
|
+
const awayMarket = awaySel.value;
|
|
5090
|
+
const drawMarket = drawSel.value;
|
|
4466
5091
|
const legIds = [homeMarket, awayMarket, drawMarket].filter((m) => m != null).map((m) => m.id ?? m.slug ?? "");
|
|
4467
|
-
if (new Set(legIds).size !== legIds.length)
|
|
5092
|
+
if (new Set(legIds).size !== legIds.length) {
|
|
5093
|
+
return ambiguous("two outcome legs are the same market");
|
|
5094
|
+
}
|
|
4468
5095
|
const legs = [
|
|
4469
5096
|
["home", homeMarket, match.home.code, match.home.name],
|
|
4470
5097
|
["draw", drawMarket, void 0, "Draw"],
|
|
4471
5098
|
["away", awayMarket, match.away.code, match.away.name]
|
|
4472
5099
|
];
|
|
4473
5100
|
const outcomes = [];
|
|
4474
|
-
let asOf = event.updatedAt;
|
|
5101
|
+
let asOf = canonicalTimestamp(event.updatedAt);
|
|
4475
5102
|
let liquidity;
|
|
4476
|
-
for (const [kind, market,
|
|
5103
|
+
for (const [kind, market, teamCode2, label] of legs) {
|
|
4477
5104
|
if (!market) continue;
|
|
4478
|
-
if (market.closed
|
|
4479
|
-
|
|
5105
|
+
if (typeof market.closed !== "boolean" || typeof market.active !== "boolean") {
|
|
5106
|
+
return malformed("market active/closed is not a boolean");
|
|
5107
|
+
}
|
|
5108
|
+
if (market.closed === true || market.active === false) {
|
|
5109
|
+
return definitiveNone("an outcome leg is closed or inactive");
|
|
5110
|
+
}
|
|
5111
|
+
if (market.description && NON_REGULAR_TIME.test(market.description)) {
|
|
5112
|
+
return definitiveNone("an outcome leg is not a regular-time market");
|
|
5113
|
+
}
|
|
4480
5114
|
const yes = yesPrice(market);
|
|
4481
|
-
if (yes == null) return
|
|
4482
|
-
outcomes.push({ kind, teamCode, label, probability: yes });
|
|
4483
|
-
|
|
4484
|
-
|
|
5115
|
+
if (yes == null) return malformed("market is not a readable Yes/No binary");
|
|
5116
|
+
outcomes.push({ kind, teamCode: teamCode2, label, probability: yes });
|
|
5117
|
+
const marketAsOf = canonicalTimestamp(market.updatedAt);
|
|
5118
|
+
if (!marketAsOf) {
|
|
5119
|
+
return malformed("market updatedAt missing or unparseable");
|
|
5120
|
+
}
|
|
5121
|
+
const nowMs = (options?.now ?? this.opts.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
5122
|
+
if (Date.parse(marketAsOf) - nowMs > FUTURE_SKEW_MS) {
|
|
5123
|
+
return malformed("market updatedAt is dated forward");
|
|
5124
|
+
}
|
|
5125
|
+
if (!asOf || Date.parse(marketAsOf) < Date.parse(asOf)) asOf = marketAsOf;
|
|
5126
|
+
const rawLiq = market.liquidityNum ?? market.liquidity;
|
|
5127
|
+
const liq = numberish(rawLiq);
|
|
5128
|
+
if (rawLiq != null && liq == null) {
|
|
5129
|
+
return malformed("market liquidity is unreadable");
|
|
5130
|
+
}
|
|
4485
5131
|
if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
|
|
4486
5132
|
}
|
|
4487
5133
|
const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
|
|
4488
|
-
if (rawSum < 0.9 || rawSum > 1.15)
|
|
5134
|
+
if (rawSum < 0.9 || rawSum > 1.15) {
|
|
5135
|
+
return ambiguous("outcome probabilities do not form a coherent 1X2");
|
|
5136
|
+
}
|
|
5137
|
+
if (!asOf) return malformed("no usable timestamp on the event or its markets");
|
|
4489
5138
|
const signal = buildMarketSignal({
|
|
4490
5139
|
match,
|
|
4491
5140
|
source: "polymarket",
|
|
4492
|
-
|
|
4493
|
-
|
|
5141
|
+
// Echoed into MCP structured content (tools.ts `market.id`), i.e. straight
|
|
5142
|
+
// into an agent's context. Stripping control characters is NOT sufficient
|
|
5143
|
+
// there: printable prose ("IGNORE PREVIOUS INSTRUCTIONS") survives that and
|
|
5144
|
+
// is precisely what matters for a model reading it. Gamma ids are short
|
|
5145
|
+
// opaque tokens, so validate that GRAMMAR and otherwise fall back to the
|
|
5146
|
+
// slug we derived ourselves.
|
|
5147
|
+
// The fallback is grammar-checked too. It is normally a slug we derived
|
|
5148
|
+
// ourselves, but `mapping.2026.json` can override it, so echoing it raw
|
|
5149
|
+
// was the one path around the agent-facing filter this line exists for.
|
|
5150
|
+
sourceMarketId: safeMarketId(event.id) ?? safeDerivedSlug(eventSlug),
|
|
5151
|
+
asOf,
|
|
4494
5152
|
outcomes,
|
|
4495
5153
|
liquidity,
|
|
4496
5154
|
now: options?.now ?? this.opts.now,
|
|
4497
5155
|
maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
|
|
4498
5156
|
});
|
|
4499
|
-
return signal.ambiguous ?
|
|
5157
|
+
return signal.ambiguous ? ambiguous("signal does not map cleanly onto this fixture") : valid(signal);
|
|
4500
5158
|
}
|
|
4501
5159
|
};
|
|
5160
|
+
function safeMarketId(id) {
|
|
5161
|
+
return typeof id === "string" && /^[0-9]{1,32}$/.test(id) ? id : void 0;
|
|
5162
|
+
}
|
|
5163
|
+
function safeDerivedSlug(slug) {
|
|
5164
|
+
return typeof slug === "string" && /^fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2}$/.test(slug) ? slug : void 0;
|
|
5165
|
+
}
|
|
4502
5166
|
var POLYMARKET_TOKEN = {
|
|
4503
5167
|
SUI: "che",
|
|
4504
5168
|
// Switzerland
|
|
@@ -4512,8 +5176,16 @@ var POLYMARKET_TOKEN = {
|
|
|
4512
5176
|
// Croatia
|
|
4513
5177
|
COD: "cdr",
|
|
4514
5178
|
// DR Congo
|
|
4515
|
-
CPV: "cvi"
|
|
5179
|
+
CPV: "cvi",
|
|
4516
5180
|
// Cabo Verde
|
|
5181
|
+
// TWO letters, not three — the one entry that is not ISO alpha-3. Verified
|
|
5182
|
+
// live: `fifwc-kor-cze-2026-06-11` is a 404, `fifwc-kr-cze-2026-06-11`
|
|
5183
|
+
// resolves to "Korea Republic vs. Czechia". Korea's three group fixtures
|
|
5184
|
+
// therefore had no market line at all. The `^[a-z]{3}$` guard in
|
|
5185
|
+
// `deriveEventSlugs` validates the FIFA CODE, not the token, so a two-letter
|
|
5186
|
+
// alias passes through it unharmed.
|
|
5187
|
+
KOR: "kr"
|
|
5188
|
+
// Korea Republic
|
|
4517
5189
|
};
|
|
4518
5190
|
function pmTokens(code) {
|
|
4519
5191
|
const c = code.toLowerCase();
|
|
@@ -4544,18 +5216,21 @@ function slugToken(m) {
|
|
|
4544
5216
|
return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
|
|
4545
5217
|
}
|
|
4546
5218
|
function isDrawMarket(m) {
|
|
4547
|
-
|
|
5219
|
+
const title = (m.groupItemTitle ?? "").trim().toLowerCase();
|
|
5220
|
+
return slugToken(m) === "draw" || title === "draw" || /^draw\s*\(/.test(title);
|
|
4548
5221
|
}
|
|
4549
|
-
function pickMarket(markets,
|
|
4550
|
-
const tokens = pmTokens(
|
|
5222
|
+
function pickMarket(markets, teamCode2, teamName) {
|
|
5223
|
+
const tokens = pmTokens(teamCode2);
|
|
4551
5224
|
const name = teamName.trim().toLowerCase();
|
|
4552
5225
|
const teamMarkets = markets.filter((m) => !isDrawMarket(m));
|
|
4553
|
-
const bySlug = teamMarkets.
|
|
4554
|
-
if (bySlug) return bySlug;
|
|
4555
|
-
|
|
5226
|
+
const bySlug = teamMarkets.filter((m) => tokens.includes(slugToken(m)));
|
|
5227
|
+
if (bySlug.length > 1) return { kind: "ambiguous", count: bySlug.length };
|
|
5228
|
+
const byTitle = name ? teamMarkets.filter((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name) : [];
|
|
5229
|
+
if (byTitle.length > 1) return { kind: "ambiguous", count: byTitle.length };
|
|
5230
|
+
return selectOne([.../* @__PURE__ */ new Set([...bySlug, ...byTitle])]);
|
|
4556
5231
|
}
|
|
4557
5232
|
function pickDraw(markets) {
|
|
4558
|
-
return markets.
|
|
5233
|
+
return selectOne(markets.filter(isDrawMarket));
|
|
4559
5234
|
}
|
|
4560
5235
|
function assertAllowedHost(base) {
|
|
4561
5236
|
let host;
|
|
@@ -4569,20 +5244,29 @@ function assertAllowedHost(base) {
|
|
|
4569
5244
|
}
|
|
4570
5245
|
}
|
|
4571
5246
|
function yesPrice(market) {
|
|
4572
|
-
const labels = parseJsonArray(market.outcomes);
|
|
4573
|
-
const
|
|
4574
|
-
if (
|
|
4575
|
-
const
|
|
4576
|
-
if (
|
|
4577
|
-
const
|
|
4578
|
-
|
|
5247
|
+
const labels = parseJsonArray(market.outcomes).map((l) => l.trim().toLowerCase());
|
|
5248
|
+
const raw = parseJsonArray(market.outcomePrices);
|
|
5249
|
+
if (raw.some((v) => v.trim() === "" || !Number.isFinite(Number(v)))) return void 0;
|
|
5250
|
+
const prices = raw.map((v) => Number(v));
|
|
5251
|
+
if (labels.length !== 2 || prices.length !== 2) return void 0;
|
|
5252
|
+
const i = labels.indexOf("yes");
|
|
5253
|
+
const j = labels.indexOf("no");
|
|
5254
|
+
if (i < 0 || j < 0) return void 0;
|
|
5255
|
+
const yes = prices[i];
|
|
5256
|
+
const no = prices[j];
|
|
5257
|
+
if (![yes, no].every((v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1)) {
|
|
5258
|
+
return void 0;
|
|
5259
|
+
}
|
|
5260
|
+
if (Math.abs(yes + no - 1) > 0.05) return void 0;
|
|
5261
|
+
return yes > 0 ? yes : void 0;
|
|
4579
5262
|
}
|
|
4580
5263
|
function parseJsonArray(v) {
|
|
4581
|
-
|
|
5264
|
+
const asText = (x) => typeof x === "string" || typeof x === "number" ? String(x) : "";
|
|
5265
|
+
if (Array.isArray(v)) return v.map(asText);
|
|
4582
5266
|
if (typeof v === "string") {
|
|
4583
5267
|
try {
|
|
4584
5268
|
const parsed = JSON.parse(v);
|
|
4585
|
-
return Array.isArray(parsed) ? parsed.map(
|
|
5269
|
+
return Array.isArray(parsed) ? parsed.map(asText) : [];
|
|
4586
5270
|
} catch {
|
|
4587
5271
|
return [];
|
|
4588
5272
|
}
|
|
@@ -4590,13 +5274,17 @@ function parseJsonArray(v) {
|
|
|
4590
5274
|
return [];
|
|
4591
5275
|
}
|
|
4592
5276
|
function numberish(v) {
|
|
4593
|
-
if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
|
|
5277
|
+
if (typeof v === "number") return Number.isFinite(v) && v >= 0 ? v : void 0;
|
|
4594
5278
|
if (typeof v === "string") {
|
|
4595
5279
|
const n = Number(v);
|
|
4596
|
-
return Number.isFinite(n) ? n : void 0;
|
|
5280
|
+
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
4597
5281
|
}
|
|
4598
5282
|
return void 0;
|
|
4599
5283
|
}
|
|
5284
|
+
var MARKET_COMPETITIONS = /* @__PURE__ */ new Set([DEFAULT_COMPETITION]);
|
|
5285
|
+
function marketsCoverCompetition(competition = resolveCompetition()) {
|
|
5286
|
+
return MARKET_COMPETITIONS.has(competition);
|
|
5287
|
+
}
|
|
4600
5288
|
function resolveMarketSource(explicit) {
|
|
4601
5289
|
if (explicit) return explicit;
|
|
4602
5290
|
if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
|
|
@@ -4613,6 +5301,7 @@ function makeMarketProvider(source) {
|
|
|
4613
5301
|
return new FakeMarketProvider();
|
|
4614
5302
|
// no synth → yields no signals, no network
|
|
4615
5303
|
default:
|
|
5304
|
+
if (!marketsCoverCompetition()) return new FakeMarketProvider();
|
|
4616
5305
|
return new PolymarketProvider();
|
|
4617
5306
|
}
|
|
4618
5307
|
}
|
|
@@ -4620,7 +5309,7 @@ async function getMarketSignals(provider, matches, options) {
|
|
|
4620
5309
|
try {
|
|
4621
5310
|
return await provider.findSignals(matches, options);
|
|
4622
5311
|
} catch {
|
|
4623
|
-
return
|
|
5312
|
+
return emptyBatch();
|
|
4624
5313
|
}
|
|
4625
5314
|
}
|
|
4626
5315
|
var SHARE_HASHTAG = "#VibingLaVidaLoca";
|
|
@@ -4701,6 +5390,9 @@ function formatShareSnippet(input, options = {}) {
|
|
|
4701
5390
|
if (input.degraded && input.matches.length > 0) {
|
|
4702
5391
|
blocks.push("(Live data unavailable \u2014 showing the bundled schedule, not live scores.)");
|
|
4703
5392
|
}
|
|
5393
|
+
if (includeMarkets && input.marketComplete === false) {
|
|
5394
|
+
blocks.push("(Market data unavailable or incomplete \u2014 not all fixtures were checked.)");
|
|
5395
|
+
}
|
|
4704
5396
|
blocks.push(
|
|
4705
5397
|
shareFooter({
|
|
4706
5398
|
source: input.source,
|
|
@@ -4731,7 +5423,9 @@ function formatShareTable(input, options = {}) {
|
|
|
4731
5423
|
const includeInstall = options.includeInstallLine !== false;
|
|
4732
5424
|
const blocks = [];
|
|
4733
5425
|
if (input.tables.length === 0) {
|
|
4734
|
-
blocks.push(
|
|
5426
|
+
blocks.push(
|
|
5427
|
+
input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
|
|
5428
|
+
);
|
|
4735
5429
|
} else {
|
|
4736
5430
|
for (const { group, rows } of input.tables) {
|
|
4737
5431
|
blocks.push(
|
|
@@ -5015,6 +5709,7 @@ var EN2 = {
|
|
|
5015
5709
|
"table.title": "Group {group}",
|
|
5016
5710
|
"table.none": "No group found for {group}.",
|
|
5017
5711
|
"table.degraded": "Live standings unavailable \u2014 showing the group roster.",
|
|
5712
|
+
"table.unavailable": "Live standings unavailable.",
|
|
5018
5713
|
"table.empty": "No standings available.",
|
|
5019
5714
|
"match.none": "No match found with id {id}.",
|
|
5020
5715
|
"status.live": "LIVE",
|
|
@@ -5055,6 +5750,7 @@ var ES2 = {
|
|
|
5055
5750
|
"table.title": "Grupo {group}",
|
|
5056
5751
|
"table.none": "No se encontr\xF3 el grupo {group}.",
|
|
5057
5752
|
"table.degraded": "Tabla en vivo no disponible \u2014 mostrando la lista del grupo.",
|
|
5753
|
+
"table.unavailable": "Tabla en vivo no disponible.",
|
|
5058
5754
|
"table.empty": "No hay clasificaci\xF3n disponible.",
|
|
5059
5755
|
"match.none": "No se encontr\xF3 partido con id {id}.",
|
|
5060
5756
|
"status.live": "EN VIVO",
|
|
@@ -5095,6 +5791,7 @@ var PT2 = {
|
|
|
5095
5791
|
"table.title": "Grupo {group}",
|
|
5096
5792
|
"table.none": "Grupo {group} n\xE3o encontrado.",
|
|
5097
5793
|
"table.degraded": "Classifica\xE7\xE3o ao vivo indispon\xEDvel \u2014 mostrando os times do grupo.",
|
|
5794
|
+
"table.unavailable": "Classifica\xE7\xE3o ao vivo indispon\xEDvel.",
|
|
5098
5795
|
"table.empty": "Classifica\xE7\xE3o indispon\xEDvel.",
|
|
5099
5796
|
"match.none": "Nenhum jogo encontrado com id {id}.",
|
|
5100
5797
|
"status.live": "AO VIVO",
|
|
@@ -5135,6 +5832,7 @@ var FR2 = {
|
|
|
5135
5832
|
"table.title": "Groupe {group}",
|
|
5136
5833
|
"table.none": "Groupe {group} introuvable.",
|
|
5137
5834
|
"table.degraded": "Classement en direct indisponible \u2014 affichage de la composition du groupe.",
|
|
5835
|
+
"table.unavailable": "Classement en direct indisponible.",
|
|
5138
5836
|
"table.empty": "Aucun classement disponible.",
|
|
5139
5837
|
"match.none": "Aucun match trouv\xE9 avec id {id}.",
|
|
5140
5838
|
"status.live": "DIRECT",
|
|
@@ -5245,7 +5943,19 @@ function dataSource(source, lang, c) {
|
|
|
5245
5943
|
}
|
|
5246
5944
|
|
|
5247
5945
|
// src/marketCache.ts
|
|
5248
|
-
import { readFileSync as
|
|
5946
|
+
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
5947
|
+
import { join as join3 } from "path";
|
|
5948
|
+
|
|
5949
|
+
// src/cache.ts
|
|
5950
|
+
import {
|
|
5951
|
+
closeSync as closeSync2,
|
|
5952
|
+
mkdirSync as mkdirSync2,
|
|
5953
|
+
openSync as openSync2,
|
|
5954
|
+
readFileSync as readFileSync2,
|
|
5955
|
+
rmSync,
|
|
5956
|
+
statSync,
|
|
5957
|
+
writeSync as writeSync2
|
|
5958
|
+
} from "fs";
|
|
5249
5959
|
import { join as join2 } from "path";
|
|
5250
5960
|
|
|
5251
5961
|
// src/paths.ts
|
|
@@ -5268,133 +5978,63 @@ function writeFileAtomic(path, data) {
|
|
|
5268
5978
|
renameSync(tmp, path);
|
|
5269
5979
|
}
|
|
5270
5980
|
|
|
5271
|
-
// src/marketCache.ts
|
|
5272
|
-
var POSITIVE_TTL_MS = 10 * 6e4;
|
|
5273
|
-
var NEGATIVE_TTL_MS = 3 * 6e4;
|
|
5274
|
-
function cachePath() {
|
|
5275
|
-
return join2(cacheDir(), "market-signals.json");
|
|
5276
|
-
}
|
|
5277
|
-
function readFile() {
|
|
5278
|
-
try {
|
|
5279
|
-
return JSON.parse(readFileSync2(cachePath(), "utf8"));
|
|
5280
|
-
} catch {
|
|
5281
|
-
return void 0;
|
|
5282
|
-
}
|
|
5283
|
-
}
|
|
5284
|
-
function readMarketCache(source, competition, now = Date.now()) {
|
|
5285
|
-
const signals = /* @__PURE__ */ new Map();
|
|
5286
|
-
const checked = /* @__PURE__ */ new Set();
|
|
5287
|
-
const file = readFile();
|
|
5288
|
-
if (!file || file.source !== source || file.competition !== competition) {
|
|
5289
|
-
return { signals, checked };
|
|
5290
|
-
}
|
|
5291
|
-
for (const [id, entry] of Object.entries(file.entries ?? {})) {
|
|
5292
|
-
const t2 = Date.parse(entry.fetchedAt);
|
|
5293
|
-
if (!Number.isFinite(t2)) continue;
|
|
5294
|
-
const ttl = entry.signal ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS;
|
|
5295
|
-
if (now - t2 > ttl) continue;
|
|
5296
|
-
checked.add(id);
|
|
5297
|
-
if (entry.signal) signals.set(id, entry.signal);
|
|
5298
|
-
}
|
|
5299
|
-
return { signals, checked };
|
|
5300
|
-
}
|
|
5301
|
-
function writeMarketCache(source, competition, attempted, fetched, now = Date.now()) {
|
|
5302
|
-
if (attempted.length === 0) return;
|
|
5303
|
-
try {
|
|
5304
|
-
const existing = readFile();
|
|
5305
|
-
const base = existing && existing.source === source && existing.competition === competition ? existing : { source, competition, entries: {} };
|
|
5306
|
-
const fetchedAt = new Date(now).toISOString();
|
|
5307
|
-
for (const id of attempted) {
|
|
5308
|
-
base.entries[id] = { fetchedAt, signal: fetched.get(id) ?? null };
|
|
5309
|
-
}
|
|
5310
|
-
writeFileAtomic(cachePath(), JSON.stringify(base));
|
|
5311
|
-
} catch {
|
|
5312
|
-
}
|
|
5313
|
-
}
|
|
5314
|
-
|
|
5315
|
-
// src/starNudge.ts
|
|
5316
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
5317
|
-
import { join as join3 } from "path";
|
|
5318
|
-
var REPO_URL = "https://github.com/arturogarrido/claudinho";
|
|
5319
|
-
var NUDGE_EVERY = 5;
|
|
5320
|
-
function counterPath() {
|
|
5321
|
-
return join3(cacheDir(), "runs.json");
|
|
5322
|
-
}
|
|
5323
|
-
function shouldNudge(runCount, every = NUDGE_EVERY) {
|
|
5324
|
-
return runCount > 0 && runCount % every === 0;
|
|
5325
|
-
}
|
|
5326
|
-
function bumpRunCount(path = counterPath()) {
|
|
5327
|
-
try {
|
|
5328
|
-
let count = 0;
|
|
5329
|
-
try {
|
|
5330
|
-
const raw = JSON.parse(readFileSync3(path, "utf8"));
|
|
5331
|
-
if (typeof raw.count === "number" && Number.isFinite(raw.count)) count = raw.count;
|
|
5332
|
-
} catch {
|
|
5333
|
-
count = 0;
|
|
5334
|
-
}
|
|
5335
|
-
count += 1;
|
|
5336
|
-
writeFileAtomic(path, JSON.stringify({ count }));
|
|
5337
|
-
return count;
|
|
5338
|
-
} catch {
|
|
5339
|
-
return void 0;
|
|
5340
|
-
}
|
|
5341
|
-
}
|
|
5342
|
-
|
|
5343
|
-
// src/clipboard.ts
|
|
5344
|
-
import { spawnSync } from "child_process";
|
|
5345
|
-
function clipboardTools(platform) {
|
|
5346
|
-
if (platform === "darwin") return [{ cmd: "pbcopy", args: [] }];
|
|
5347
|
-
if (platform === "win32") return [{ cmd: "clip", args: [] }];
|
|
5348
|
-
return [
|
|
5349
|
-
{ cmd: "wl-copy", args: [] },
|
|
5350
|
-
{ cmd: "xclip", args: ["-selection", "clipboard"] },
|
|
5351
|
-
{ cmd: "xsel", args: ["--clipboard", "--input"] }
|
|
5352
|
-
];
|
|
5353
|
-
}
|
|
5354
|
-
function copyToClipboard(text, platform = process.platform) {
|
|
5355
|
-
for (const { cmd, args } of clipboardTools(platform)) {
|
|
5356
|
-
try {
|
|
5357
|
-
const res = spawnSync(cmd, args, {
|
|
5358
|
-
input: text,
|
|
5359
|
-
stdio: ["pipe", "ignore", "ignore"],
|
|
5360
|
-
timeout: 1e3
|
|
5361
|
-
});
|
|
5362
|
-
if (!res.error && res.status === 0) return true;
|
|
5363
|
-
} catch {
|
|
5364
|
-
}
|
|
5365
|
-
}
|
|
5366
|
-
return false;
|
|
5367
|
-
}
|
|
5368
|
-
|
|
5369
5981
|
// src/cache.ts
|
|
5370
|
-
import {
|
|
5371
|
-
closeSync as closeSync2,
|
|
5372
|
-
mkdirSync as mkdirSync2,
|
|
5373
|
-
openSync as openSync2,
|
|
5374
|
-
readFileSync as readFileSync4,
|
|
5375
|
-
rmSync,
|
|
5376
|
-
statSync,
|
|
5377
|
-
writeSync as writeSync2
|
|
5378
|
-
} from "fs";
|
|
5379
|
-
import { join as join4 } from "path";
|
|
5380
5982
|
var CACHE_VERSION = 2;
|
|
5983
|
+
var MAX_STATE_BYTES = 1024 * 1024;
|
|
5984
|
+
var MAX_STATE_RECORDS = 1024;
|
|
5381
5985
|
var LOCK_STALE_MS = 6e4;
|
|
5382
|
-
function
|
|
5986
|
+
function cachePath(source = "espn", competition = DEFAULT_COMPETITION) {
|
|
5383
5987
|
if (source === "espn" && competition === DEFAULT_COMPETITION) {
|
|
5384
|
-
return
|
|
5988
|
+
return join2(cacheDir(), "state.json");
|
|
5385
5989
|
}
|
|
5386
5990
|
const slug = `${source}.${competition}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
5387
|
-
return
|
|
5991
|
+
return join2(cacheDir(), `state.${slug}.json`);
|
|
5388
5992
|
}
|
|
5389
5993
|
function lockPath() {
|
|
5390
|
-
return
|
|
5994
|
+
return join2(cacheDir(), "refresh.lock");
|
|
5995
|
+
}
|
|
5996
|
+
function validStamp(value) {
|
|
5997
|
+
if (typeof value !== "string") return false;
|
|
5998
|
+
const match = value.match(
|
|
5999
|
+
/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,3}))?Z$/
|
|
6000
|
+
);
|
|
6001
|
+
if (!match) return false;
|
|
6002
|
+
const parsed = Date.parse(value);
|
|
6003
|
+
if (!Number.isFinite(parsed)) return false;
|
|
6004
|
+
const canonical = `${match[1]}.${(match[2] ?? "").padEnd(3, "0")}Z`;
|
|
6005
|
+
return new Date(parsed).toISOString() === canonical;
|
|
6006
|
+
}
|
|
6007
|
+
function validScope(value) {
|
|
6008
|
+
return typeof value === "string" && value.length > 0 && value.length <= 128 && /^[a-zA-Z0-9._-]+$/.test(value);
|
|
6009
|
+
}
|
|
6010
|
+
function isCacheState(value) {
|
|
6011
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
6012
|
+
const s = value;
|
|
6013
|
+
if (s.version !== CACHE_VERSION) return false;
|
|
6014
|
+
if (!validStamp(s.updatedAt) || typeof s.degraded !== "boolean") return false;
|
|
6015
|
+
if (!validScope(s.source) || !validScope(s.competition)) return false;
|
|
6016
|
+
if (!Array.isArray(s.live) || s.live.length > MAX_STATE_RECORDS) return false;
|
|
6017
|
+
if (s.fixtures !== void 0 && (!Array.isArray(s.fixtures) || s.fixtures.length > MAX_STATE_RECORDS)) {
|
|
6018
|
+
return false;
|
|
6019
|
+
}
|
|
6020
|
+
for (const key of [
|
|
6021
|
+
"fixturesUpdatedAt",
|
|
6022
|
+
"fixturesAttemptedAt",
|
|
6023
|
+
"backoffUntil"
|
|
6024
|
+
]) {
|
|
6025
|
+
if (s[key] !== void 0 && !validStamp(s[key])) return false;
|
|
6026
|
+
}
|
|
6027
|
+
return true;
|
|
5391
6028
|
}
|
|
5392
6029
|
function readState(source = "espn", competition = DEFAULT_COMPETITION) {
|
|
5393
6030
|
try {
|
|
5394
|
-
const
|
|
5395
|
-
|
|
5396
|
-
);
|
|
5397
|
-
|
|
6031
|
+
const path = cachePath(source, competition);
|
|
6032
|
+
const info = statSync(path);
|
|
6033
|
+
if (!info.isFile() || info.size > MAX_STATE_BYTES) return void 0;
|
|
6034
|
+
const bytes = readFileSync2(path);
|
|
6035
|
+
if (bytes.byteLength > MAX_STATE_BYTES) return void 0;
|
|
6036
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
6037
|
+
return isCacheState(parsed) ? parsed : void 0;
|
|
5398
6038
|
} catch {
|
|
5399
6039
|
return void 0;
|
|
5400
6040
|
}
|
|
@@ -5405,41 +6045,45 @@ function readCurrentState(source, competition) {
|
|
|
5405
6045
|
}
|
|
5406
6046
|
function writeState(state) {
|
|
5407
6047
|
writeFileAtomic(
|
|
5408
|
-
|
|
6048
|
+
cachePath(state.source, state.competition),
|
|
5409
6049
|
JSON.stringify({ ...state, version: CACHE_VERSION })
|
|
5410
6050
|
);
|
|
5411
6051
|
}
|
|
6052
|
+
var MAX_BACKOFF_MS = 30 * 6e4;
|
|
5412
6053
|
function backoffActive(state, now = Date.now()) {
|
|
5413
6054
|
if (!state?.backoffUntil) return false;
|
|
5414
6055
|
const t2 = Date.parse(state.backoffUntil);
|
|
5415
|
-
|
|
6056
|
+
if (!Number.isFinite(t2)) return false;
|
|
6057
|
+
return now < t2 && t2 - now <= MAX_BACKOFF_MS;
|
|
5416
6058
|
}
|
|
5417
6059
|
function fixturesAttemptAgeMs(state, now = Date.now()) {
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
6060
|
+
return stampAgeMs(state?.fixturesAttemptedAt, now);
|
|
6061
|
+
}
|
|
6062
|
+
var FUTURE_SKEW_MS2 = 6e4;
|
|
6063
|
+
function stampAgeMs(value, now) {
|
|
6064
|
+
if (!value) return Infinity;
|
|
6065
|
+
const t2 = Date.parse(value);
|
|
6066
|
+
if (!Number.isFinite(t2)) return Infinity;
|
|
6067
|
+
const age = now - t2;
|
|
6068
|
+
return age < -FUTURE_SKEW_MS2 ? Infinity : age;
|
|
5421
6069
|
}
|
|
5422
6070
|
function ageMs(state, now = Date.now()) {
|
|
5423
|
-
|
|
5424
|
-
const t2 = Date.parse(state.updatedAt);
|
|
5425
|
-
return Number.isFinite(t2) ? now - t2 : Infinity;
|
|
6071
|
+
return stampAgeMs(state?.updatedAt, now);
|
|
5426
6072
|
}
|
|
5427
6073
|
function fixturesAgeMs(state, now = Date.now()) {
|
|
5428
|
-
|
|
5429
|
-
const t2 = Date.parse(state.fixturesUpdatedAt);
|
|
5430
|
-
return Number.isFinite(t2) ? now - t2 : Infinity;
|
|
6074
|
+
return stampAgeMs(state?.fixturesUpdatedAt, now);
|
|
5431
6075
|
}
|
|
5432
6076
|
function lockAgeMs(now = Date.now()) {
|
|
5433
6077
|
const lp = lockPath();
|
|
5434
6078
|
try {
|
|
5435
|
-
const contents =
|
|
6079
|
+
const contents = readFileSync2(lp, "utf8");
|
|
5436
6080
|
const written = Number.parseInt(contents.split(/\s+/)[1] ?? "", 10);
|
|
5437
|
-
if (Number.isFinite(written)) return
|
|
6081
|
+
if (Number.isFinite(written)) return stampAgeMs(new Date(written).toISOString(), now);
|
|
5438
6082
|
} catch {
|
|
5439
6083
|
return Infinity;
|
|
5440
6084
|
}
|
|
5441
6085
|
try {
|
|
5442
|
-
return
|
|
6086
|
+
return stampAgeMs(new Date(statSync(lp).mtimeMs).toISOString(), now);
|
|
5443
6087
|
} catch {
|
|
5444
6088
|
return Infinity;
|
|
5445
6089
|
}
|
|
@@ -5487,6 +6131,169 @@ function releaseLock() {
|
|
|
5487
6131
|
}
|
|
5488
6132
|
}
|
|
5489
6133
|
|
|
6134
|
+
// src/marketCache.ts
|
|
6135
|
+
var POSITIVE_TTL_MS = 10 * 6e4;
|
|
6136
|
+
var NEGATIVE_TTL_MS = 3 * 6e4;
|
|
6137
|
+
var MAX_MARKET_CACHE_BYTES = 1024 * 1024;
|
|
6138
|
+
var FUTURE_SKEW_MS3 = 6e4;
|
|
6139
|
+
function cachePath2() {
|
|
6140
|
+
return join3(cacheDir(), "market-signals.json");
|
|
6141
|
+
}
|
|
6142
|
+
function readFile() {
|
|
6143
|
+
try {
|
|
6144
|
+
const path = cachePath2();
|
|
6145
|
+
const info = statSync2(path);
|
|
6146
|
+
if (!info.isFile() || info.size > MAX_MARKET_CACHE_BYTES) return void 0;
|
|
6147
|
+
const bytes = readFileSync3(path);
|
|
6148
|
+
if (bytes.byteLength > MAX_MARKET_CACHE_BYTES) return void 0;
|
|
6149
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
6150
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
6151
|
+
return parsed;
|
|
6152
|
+
} catch {
|
|
6153
|
+
return void 0;
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
function isEntryShaped(e) {
|
|
6157
|
+
if (!e || typeof e !== "object") return false;
|
|
6158
|
+
const entry = e;
|
|
6159
|
+
if (typeof entry.fetchedAt !== "string" || !Number.isFinite(Date.parse(entry.fetchedAt))) {
|
|
6160
|
+
return false;
|
|
6161
|
+
}
|
|
6162
|
+
if (entry.signal === null) return true;
|
|
6163
|
+
return !!entry.signal && typeof entry.signal === "object";
|
|
6164
|
+
}
|
|
6165
|
+
function isUsableSignal(s) {
|
|
6166
|
+
if (!s) return false;
|
|
6167
|
+
if (s.source === "" || s.asOf === "" || s.outcomes.length === 0) return false;
|
|
6168
|
+
if (s.outcomes.some((o) => o.kind === "other")) return false;
|
|
6169
|
+
if (s.ambiguous) return false;
|
|
6170
|
+
if (!s.favorite) return false;
|
|
6171
|
+
return hasSaneDistribution(s.outcomes);
|
|
6172
|
+
}
|
|
6173
|
+
function readMarketCache(source, competition, now = Date.now()) {
|
|
6174
|
+
const signals = /* @__PURE__ */ new Map();
|
|
6175
|
+
const checked = /* @__PURE__ */ new Set();
|
|
6176
|
+
const file = readFile();
|
|
6177
|
+
if (!file || file.source !== source || file.competition !== competition) {
|
|
6178
|
+
return { signals, checked };
|
|
6179
|
+
}
|
|
6180
|
+
const entries = file.entries;
|
|
6181
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
|
6182
|
+
return { signals, checked };
|
|
6183
|
+
}
|
|
6184
|
+
let examined = 0;
|
|
6185
|
+
for (const id in entries) {
|
|
6186
|
+
if (!Object.hasOwn(entries, id)) continue;
|
|
6187
|
+
if (examined >= 256) break;
|
|
6188
|
+
examined += 1;
|
|
6189
|
+
const raw = entries[id];
|
|
6190
|
+
if (!isEntryShaped(raw)) continue;
|
|
6191
|
+
const entry = raw;
|
|
6192
|
+
const t2 = Date.parse(entry.fetchedAt);
|
|
6193
|
+
const ttl = entry.signal ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS;
|
|
6194
|
+
const age = now - t2;
|
|
6195
|
+
if (age > ttl || age < -FUTURE_SKEW_MS3) continue;
|
|
6196
|
+
if (entry.signal === null) {
|
|
6197
|
+
checked.add(id);
|
|
6198
|
+
continue;
|
|
6199
|
+
}
|
|
6200
|
+
const sealed = parseCachedMarketSignal(entry.signal, { now: new Date(now) });
|
|
6201
|
+
if (sealed.kind !== "valid") continue;
|
|
6202
|
+
const clean = sealed.value;
|
|
6203
|
+
if (clean.matchId !== id || clean.source !== source) continue;
|
|
6204
|
+
if (!isUsableSignal(clean)) continue;
|
|
6205
|
+
checked.add(id);
|
|
6206
|
+
signals.set(id, clean);
|
|
6207
|
+
}
|
|
6208
|
+
return { signals, checked };
|
|
6209
|
+
}
|
|
6210
|
+
function writeMarketCache(source, competition, attempted, fetched, now = Date.now()) {
|
|
6211
|
+
if (attempted.length === 0) return;
|
|
6212
|
+
try {
|
|
6213
|
+
const existing = readFile();
|
|
6214
|
+
const reuse = existing && existing.source === source && existing.competition === competition;
|
|
6215
|
+
const carried = {};
|
|
6216
|
+
if (reuse && existing.entries && typeof existing.entries === "object" && !Array.isArray(existing.entries)) {
|
|
6217
|
+
let examined = 0;
|
|
6218
|
+
for (const id in existing.entries) {
|
|
6219
|
+
if (!Object.hasOwn(existing.entries, id)) continue;
|
|
6220
|
+
if (examined >= 256) break;
|
|
6221
|
+
examined += 1;
|
|
6222
|
+
const raw = existing.entries[id];
|
|
6223
|
+
if (!isEntryShaped(raw)) continue;
|
|
6224
|
+
const age = stampAgeMs(raw.fetchedAt, now);
|
|
6225
|
+
if (age > (raw.signal ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS)) continue;
|
|
6226
|
+
if (age < -FUTURE_SKEW_MS3) continue;
|
|
6227
|
+
if (raw.signal !== null && !isUsableSignal(parsedValue(parseCachedMarketSignal(raw.signal, { now: new Date(now) })))) {
|
|
6228
|
+
continue;
|
|
6229
|
+
}
|
|
6230
|
+
carried[id] = raw;
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
const base = { source, competition, entries: carried };
|
|
6234
|
+
const fetchedAt = new Date(now).toISOString();
|
|
6235
|
+
for (const id of attempted.slice(0, 256)) {
|
|
6236
|
+
base.entries[id] = { fetchedAt, signal: fetched.get(id) ?? null };
|
|
6237
|
+
}
|
|
6238
|
+
writeFileAtomic(cachePath2(), JSON.stringify(base));
|
|
6239
|
+
} catch {
|
|
6240
|
+
}
|
|
6241
|
+
}
|
|
6242
|
+
|
|
6243
|
+
// src/starNudge.ts
|
|
6244
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
6245
|
+
import { join as join4 } from "path";
|
|
6246
|
+
var REPO_URL = "https://github.com/arturogarrido/claudinho";
|
|
6247
|
+
var NUDGE_EVERY = 5;
|
|
6248
|
+
function counterPath() {
|
|
6249
|
+
return join4(cacheDir(), "runs.json");
|
|
6250
|
+
}
|
|
6251
|
+
function shouldNudge(runCount, every = NUDGE_EVERY) {
|
|
6252
|
+
return runCount > 0 && runCount % every === 0;
|
|
6253
|
+
}
|
|
6254
|
+
function bumpRunCount(path = counterPath()) {
|
|
6255
|
+
try {
|
|
6256
|
+
let count2 = 0;
|
|
6257
|
+
try {
|
|
6258
|
+
const raw = JSON.parse(readFileSync4(path, "utf8"));
|
|
6259
|
+
if (typeof raw.count === "number" && Number.isFinite(raw.count)) count2 = raw.count;
|
|
6260
|
+
} catch {
|
|
6261
|
+
count2 = 0;
|
|
6262
|
+
}
|
|
6263
|
+
count2 += 1;
|
|
6264
|
+
writeFileAtomic(path, JSON.stringify({ count: count2 }));
|
|
6265
|
+
return count2;
|
|
6266
|
+
} catch {
|
|
6267
|
+
return void 0;
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
|
|
6271
|
+
// src/clipboard.ts
|
|
6272
|
+
import { spawnSync } from "child_process";
|
|
6273
|
+
function clipboardTools(platform) {
|
|
6274
|
+
if (platform === "darwin") return [{ cmd: "pbcopy", args: [] }];
|
|
6275
|
+
if (platform === "win32") return [{ cmd: "clip", args: [] }];
|
|
6276
|
+
return [
|
|
6277
|
+
{ cmd: "wl-copy", args: [] },
|
|
6278
|
+
{ cmd: "xclip", args: ["-selection", "clipboard"] },
|
|
6279
|
+
{ cmd: "xsel", args: ["--clipboard", "--input"] }
|
|
6280
|
+
];
|
|
6281
|
+
}
|
|
6282
|
+
function copyToClipboard(text, platform = process.platform) {
|
|
6283
|
+
for (const { cmd, args } of clipboardTools(platform)) {
|
|
6284
|
+
try {
|
|
6285
|
+
const res = spawnSync(cmd, args, {
|
|
6286
|
+
input: text,
|
|
6287
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
6288
|
+
timeout: 1e3
|
|
6289
|
+
});
|
|
6290
|
+
if (!res.error && res.status === 0) return true;
|
|
6291
|
+
} catch {
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6294
|
+
return false;
|
|
6295
|
+
}
|
|
6296
|
+
|
|
5490
6297
|
// src/statusline.ts
|
|
5491
6298
|
var DISPLAY_STALE_MS = 5 * 6e4;
|
|
5492
6299
|
var TOURNAMENT_COMPLETE_LINE = "\u26BD World Cup 2026 is complete \xB7 Thanks for vibing with Claudinho";
|
|
@@ -5523,36 +6330,110 @@ function matchSegment(m, compact, flags) {
|
|
|
5523
6330
|
const away = compact ? m.away.flag : `${m.away.code} ${m.away.flag}`;
|
|
5524
6331
|
return `${home} ${scoreline(m)} ${away} ${minute}`;
|
|
5525
6332
|
}
|
|
6333
|
+
var MAX_LIVE_CONSIDERED = 64;
|
|
6334
|
+
var MAX_LIVE_EXAMINED = 512;
|
|
6335
|
+
function sealFixtures(raw) {
|
|
6336
|
+
if (raw === void 0) {
|
|
6337
|
+
return { items: [], total: 0, shown: 0, truncated: false, complete: true };
|
|
6338
|
+
}
|
|
6339
|
+
if (!Array.isArray(raw)) {
|
|
6340
|
+
return { items: [], total: 0, shown: 0, truncated: false, complete: false };
|
|
6341
|
+
}
|
|
6342
|
+
const out2 = [];
|
|
6343
|
+
let inspected = 0;
|
|
6344
|
+
let readable = true;
|
|
6345
|
+
for (const rec of raw) {
|
|
6346
|
+
if (out2.length >= MAX_LIVE_CONSIDERED || inspected >= MAX_LIVE_EXAMINED) break;
|
|
6347
|
+
inspected++;
|
|
6348
|
+
if (!isMatchShaped(rec)) {
|
|
6349
|
+
readable = false;
|
|
6350
|
+
continue;
|
|
6351
|
+
}
|
|
6352
|
+
const sealed = parsedValue(parseCachedMatch(rec, { events: false }));
|
|
6353
|
+
if (sealed) out2.push(sealed);
|
|
6354
|
+
else readable = false;
|
|
6355
|
+
}
|
|
6356
|
+
const exhausted = inspected === raw.length;
|
|
6357
|
+
const complete = exhausted && readable;
|
|
6358
|
+
return {
|
|
6359
|
+
items: out2,
|
|
6360
|
+
// Exact only when complete; otherwise this is the number actually sealed.
|
|
6361
|
+
// Callers use a nonnumeric marker for an incomplete scan.
|
|
6362
|
+
total: out2.length,
|
|
6363
|
+
shown: out2.length,
|
|
6364
|
+
truncated: !exhausted,
|
|
6365
|
+
complete
|
|
6366
|
+
};
|
|
6367
|
+
}
|
|
5526
6368
|
function liveMatchesFromCache(state, nowMs = Date.now()) {
|
|
5527
6369
|
const fresh = state && ageMs(state, nowMs) < DISPLAY_STALE_MS;
|
|
5528
|
-
const
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
6370
|
+
const rawLive = fresh ? state?.live : [];
|
|
6371
|
+
if (!Array.isArray(rawLive)) {
|
|
6372
|
+
return { items: [], total: 0, shown: 0, truncated: false, complete: false };
|
|
6373
|
+
}
|
|
6374
|
+
const liveArr = rawLive;
|
|
6375
|
+
const out2 = [];
|
|
6376
|
+
let inspected = 0;
|
|
6377
|
+
let readable = true;
|
|
6378
|
+
for (let i = 0; i < liveArr.length; i++) {
|
|
6379
|
+
if (out2.length >= MAX_LIVE_CONSIDERED || inspected >= MAX_LIVE_EXAMINED) break;
|
|
6380
|
+
inspected++;
|
|
6381
|
+
const raw = liveArr[i];
|
|
6382
|
+
if (!raw || typeof raw !== "object") {
|
|
6383
|
+
readable = false;
|
|
6384
|
+
continue;
|
|
6385
|
+
}
|
|
6386
|
+
const m = raw;
|
|
6387
|
+
if (!isLive(m.status) || !m.home?.code || !m.away?.code) {
|
|
6388
|
+
readable = false;
|
|
6389
|
+
continue;
|
|
6390
|
+
}
|
|
6391
|
+
const sealed = parsedValue(parseCachedMatch(m, { events: false }));
|
|
6392
|
+
if (sealed) out2.push(sealed);
|
|
6393
|
+
else readable = false;
|
|
6394
|
+
}
|
|
6395
|
+
const exhausted = inspected === liveArr.length;
|
|
6396
|
+
const complete = exhausted && readable;
|
|
6397
|
+
return {
|
|
6398
|
+
items: out2,
|
|
6399
|
+
total: out2.length,
|
|
6400
|
+
shown: out2.length,
|
|
6401
|
+
truncated: !exhausted,
|
|
6402
|
+
// False when we stopped early or a record was unreadable. In either case an
|
|
6403
|
+
// empty list must not render as the authoritative "nothing is on".
|
|
6404
|
+
complete
|
|
6405
|
+
};
|
|
5532
6406
|
}
|
|
6407
|
+
var DEFAULT_MAX_SEGMENTS = 8;
|
|
6408
|
+
var MAX_LINE_COLUMNS = 200;
|
|
5533
6409
|
function renderPrompt(state, opts = {}) {
|
|
6410
|
+
return truncateVisible(renderPromptLine(state, opts), MAX_LINE_COLUMNS);
|
|
6411
|
+
}
|
|
6412
|
+
function renderPromptLine(state, opts = {}) {
|
|
5534
6413
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
5535
6414
|
const nowMs = now.getTime();
|
|
5536
6415
|
const defaultCompetition = opts.defaultCompetition ?? true;
|
|
5537
6416
|
const compact = opts.compact ?? true;
|
|
5538
6417
|
const flags = opts.flags ?? true;
|
|
5539
6418
|
const team = opts.team?.toUpperCase();
|
|
5540
|
-
const
|
|
5541
|
-
const
|
|
6419
|
+
const liveList = liveMatchesFromCache(state, nowMs);
|
|
6420
|
+
const live = liveList.items;
|
|
6421
|
+
const cachedFixtureList = sealFixtures(state?.fixtures);
|
|
6422
|
+
const cachedFixtures = [...cachedFixtureList.items];
|
|
5542
6423
|
const schedule = cachedFixtures.length ? mergeLive(allFixtures(), cachedFixtures) : void 0;
|
|
5543
6424
|
if (team) {
|
|
5544
6425
|
const mine = live.find((m) => m.home?.code === team || m.away?.code === team);
|
|
5545
6426
|
if (mine) return `\u26BD ${matchSegment(mine, compact, flags)}`;
|
|
5546
6427
|
} else if (live.length > 0) {
|
|
5547
|
-
const max = opts.max && opts.max > 0 ? opts.max :
|
|
6428
|
+
const max = opts.max && opts.max > 0 ? Math.min(opts.max, DEFAULT_MAX_SEGMENTS) : DEFAULT_MAX_SEGMENTS;
|
|
5548
6429
|
const shown = live.slice(0, max);
|
|
5549
|
-
let line2 = "\u26BD " + shown.map((m) => matchSegment(m, compact, flags)).join(" \xB7 ");
|
|
5550
6430
|
const overflow = live.length - shown.length;
|
|
5551
|
-
|
|
5552
|
-
|
|
6431
|
+
const marker = !liveList.complete ? " +more" : overflow > 0 ? ` +${overflow}` : "";
|
|
6432
|
+
const body = "\u26BD " + shown.map((m) => matchSegment(m, compact, flags)).join(" \xB7 ");
|
|
6433
|
+
return truncateVisible(body, MAX_LINE_COLUMNS - displayWidth(marker)) + marker;
|
|
5553
6434
|
}
|
|
5554
6435
|
const cacheFresh = !!state && state.degraded !== true && ageMs(state, nowMs) < DISPLAY_STALE_MS;
|
|
5555
|
-
if (!cacheFresh) {
|
|
6436
|
+
if (!cacheFresh || !liveList.complete) {
|
|
5556
6437
|
const win = fixturesInLiveWindow(nowMs, schedule).filter(
|
|
5557
6438
|
(m) => !team || m.home.code === team || m.away.code === team
|
|
5558
6439
|
);
|
|
@@ -5574,14 +6455,24 @@ function renderPrompt(state, opts = {}) {
|
|
|
5574
6455
|
}
|
|
5575
6456
|
|
|
5576
6457
|
// src/hook.ts
|
|
5577
|
-
|
|
6458
|
+
var MAX_HOOK_MATCHES = 12;
|
|
6459
|
+
var MAX_HOOK_CODE_POINTS = 4096;
|
|
6460
|
+
function boundContext(text, marker = "") {
|
|
6461
|
+
const points = [...text];
|
|
6462
|
+
if (points.length + [...marker].length <= MAX_HOOK_CODE_POINTS) return text + marker;
|
|
6463
|
+
const room = Math.max(0, MAX_HOOK_CODE_POINTS - [...marker].length);
|
|
6464
|
+
return `${points.slice(0, room).join("")}
|
|
6465
|
+
(context truncated)${marker}`;
|
|
6466
|
+
}
|
|
6467
|
+
function rosterPinned(t2, pin) {
|
|
6468
|
+
if (!pin) return t2;
|
|
5578
6469
|
const { team } = lookupTeam(t2.code);
|
|
5579
6470
|
return team ? { ...t2, name: team.name, flag: team.flag } : t2;
|
|
5580
6471
|
}
|
|
5581
|
-
function line(m, flags) {
|
|
6472
|
+
function line(m, flags, pin) {
|
|
5582
6473
|
const minute = m.status === "HT" ? "half-time" : m.minute ? `${m.minute}'` : "live";
|
|
5583
|
-
const h = rosterPinned(m.home);
|
|
5584
|
-
const a = rosterPinned(m.away);
|
|
6474
|
+
const h = rosterPinned(m.home, pin);
|
|
6475
|
+
const a = rosterPinned(m.away, pin);
|
|
5585
6476
|
const home = flags ? `${h.flag} ${h.name}` : h.name;
|
|
5586
6477
|
const away = flags ? `${a.name} ${a.flag}` : a.name;
|
|
5587
6478
|
return `${home} ${scoreline(m)} ${away} (${minute})`;
|
|
@@ -5590,7 +6481,9 @@ function renderHook(state, opts = {}) {
|
|
|
5590
6481
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
5591
6482
|
const team = opts.team?.toUpperCase();
|
|
5592
6483
|
const flags = opts.flags ?? true;
|
|
5593
|
-
|
|
6484
|
+
const pin = opts.defaultCompetition ?? true;
|
|
6485
|
+
const liveList = liveMatchesFromCache(state, now.getTime());
|
|
6486
|
+
let live = [...liveList.items];
|
|
5594
6487
|
if (live.length === 0) return "";
|
|
5595
6488
|
if (team) {
|
|
5596
6489
|
live = [...live].sort((a, b) => {
|
|
@@ -5599,9 +6492,13 @@ function renderHook(state, opts = {}) {
|
|
|
5599
6492
|
return aHas - bHas;
|
|
5600
6493
|
});
|
|
5601
6494
|
}
|
|
5602
|
-
const
|
|
5603
|
-
|
|
5604
|
-
|
|
6495
|
+
const shown = live.slice(0, MAX_HOOK_MATCHES);
|
|
6496
|
+
const overflow = live.length - shown.length;
|
|
6497
|
+
const lines = shown.map((mm) => line(mm, flags, pin)).join("\n");
|
|
6498
|
+
const more = !liveList.complete ? "\n(more live matches may not be shown)" : overflow > 0 ? `
|
|
6499
|
+
(+${overflow} more not shown)` : "";
|
|
6500
|
+
return boundContext(`[Claudinho \u2014 live football scores right now]
|
|
6501
|
+
${lines}`, more);
|
|
5605
6502
|
}
|
|
5606
6503
|
|
|
5607
6504
|
// src/refresh.ts
|
|
@@ -5629,11 +6526,7 @@ function liveWindowActive(nowMs) {
|
|
|
5629
6526
|
return inLiveWindow(nowMs);
|
|
5630
6527
|
}
|
|
5631
6528
|
function liveAdapter(source) {
|
|
5632
|
-
|
|
5633
|
-
if (source === "espn" && competition === DEFAULT_COMPETITION) {
|
|
5634
|
-
return new EspnAdapter({ enrichGroups: false });
|
|
5635
|
-
}
|
|
5636
|
-
return makeAdapter(source);
|
|
6529
|
+
return makeAdapter(source, { enrichGroups: false });
|
|
5637
6530
|
}
|
|
5638
6531
|
async function runRefresh(opts = {}) {
|
|
5639
6532
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
@@ -5890,11 +6783,15 @@ function adapterFor({ cfg, adapter }) {
|
|
|
5890
6783
|
}
|
|
5891
6784
|
var DEFAULT_ON_MARKET_OPTS = { deadlineMs: 2e3, timeoutMs: 2500 };
|
|
5892
6785
|
var MARKETS_CMD_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
|
|
5893
|
-
async function marketSignalsFor(ctx, matches, opts = {}) {
|
|
5894
|
-
if (ctx.marketProvider)
|
|
6786
|
+
async function marketSignalsFor(ctx, matches, opts = {}, providerFactory = makeMarketProvider) {
|
|
6787
|
+
if (ctx.marketProvider) {
|
|
6788
|
+
const b = await getMarketSignals(ctx.marketProvider, matches, opts);
|
|
6789
|
+
return { signals: resolvedValues(b), complete: b.complete };
|
|
6790
|
+
}
|
|
5895
6791
|
const source = resolveMarketSource();
|
|
5896
6792
|
if (source !== "polymarket") {
|
|
5897
|
-
|
|
6793
|
+
const b = await getMarketSignals(providerFactory(source), matches, opts);
|
|
6794
|
+
return { signals: resolvedValues(b), complete: b.complete };
|
|
5898
6795
|
}
|
|
5899
6796
|
const competition = resolveCompetition();
|
|
5900
6797
|
const { signals: cached, checked: cachedIds } = readMarketCache("polymarket", competition);
|
|
@@ -5902,35 +6799,39 @@ async function marketSignalsFor(ctx, matches, opts = {}) {
|
|
|
5902
6799
|
const miss = [];
|
|
5903
6800
|
for (const m of matches) {
|
|
5904
6801
|
const hit = cached.get(m.id);
|
|
5905
|
-
if (hit
|
|
5906
|
-
|
|
6802
|
+
if (hit && marketSignalRendersFor(m, hit)) {
|
|
6803
|
+
result.set(m.id, hit);
|
|
6804
|
+
continue;
|
|
6805
|
+
}
|
|
6806
|
+
if (!hit && cachedIds.has(m.id)) continue;
|
|
6807
|
+
miss.push(m);
|
|
5907
6808
|
}
|
|
6809
|
+
let complete = true;
|
|
5908
6810
|
if (miss.length > 0) {
|
|
5909
|
-
const
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
);
|
|
5914
|
-
writeMarketCache("polymarket", competition, [...checked], fetched);
|
|
6811
|
+
const batch = await getMarketSignals(providerFactory("polymarket"), miss, opts);
|
|
6812
|
+
const fetched = resolvedValues(batch);
|
|
6813
|
+
complete = batch.complete;
|
|
6814
|
+
writeMarketCache("polymarket", competition, [...cacheableKeys(batch)], fetched);
|
|
5915
6815
|
for (const [id, s] of fetched) result.set(id, s);
|
|
5916
6816
|
}
|
|
5917
|
-
return result;
|
|
6817
|
+
return { signals: result, complete };
|
|
5918
6818
|
}
|
|
5919
6819
|
async function reliableMarketSignals(ctx, matches) {
|
|
5920
|
-
if (ctx.cfg.markets === false) return /* @__PURE__ */ new Map();
|
|
6820
|
+
if (ctx.cfg.markets === false) return { signals: /* @__PURE__ */ new Map(), complete: true };
|
|
5921
6821
|
const now = ctx.now ?? /* @__PURE__ */ new Date();
|
|
5922
6822
|
const relevant = matches.filter((m) => marketRelevant(m, now));
|
|
5923
|
-
if (relevant.length === 0) return /* @__PURE__ */ new Map();
|
|
6823
|
+
if (relevant.length === 0) return { signals: /* @__PURE__ */ new Map(), complete: true };
|
|
5924
6824
|
const raw = await marketSignalsFor(ctx, relevant, DEFAULT_ON_MARKET_OPTS);
|
|
5925
6825
|
const out2 = /* @__PURE__ */ new Map();
|
|
5926
|
-
for (const [id, s] of raw) {
|
|
6826
|
+
for (const [id, s] of raw.signals) {
|
|
5927
6827
|
const m = relevant.find((x) => x.id === id);
|
|
5928
6828
|
if (m && isReliableMarketSignal(s, { now }) && marketSignalRendersFor(m, s)) out2.set(id, s);
|
|
5929
6829
|
}
|
|
5930
|
-
return out2;
|
|
6830
|
+
return { signals: out2, complete: raw.complete };
|
|
5931
6831
|
}
|
|
5932
6832
|
async function reliableMarketSignalFor(ctx, match) {
|
|
5933
|
-
|
|
6833
|
+
const result = await reliableMarketSignals(ctx, [match]);
|
|
6834
|
+
return { signal: result.signals.get(match.id), complete: result.complete };
|
|
5934
6835
|
}
|
|
5935
6836
|
function out(line2 = "") {
|
|
5936
6837
|
process.stdout.write(line2 + "\n");
|
|
@@ -5989,14 +6890,15 @@ async function cmdToday(date, ctx) {
|
|
|
5989
6890
|
const targetDate = date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz);
|
|
5990
6891
|
const { matches, degraded, source } = await getMatchesForDate(adapter, targetDate);
|
|
5991
6892
|
const todays = fixturesByDate(targetDate, matches, cfg.tz);
|
|
5992
|
-
const
|
|
6893
|
+
const market = await reliableMarketSignals(ctx, todays);
|
|
5993
6894
|
if (cfg.json) {
|
|
5994
6895
|
emitJson({
|
|
5995
6896
|
date: targetDate,
|
|
5996
6897
|
degraded,
|
|
5997
6898
|
source: source ?? null,
|
|
5998
6899
|
matches: todays,
|
|
5999
|
-
|
|
6900
|
+
marketComplete: market.complete,
|
|
6901
|
+
marketSignals: Object.fromEntries(market.signals)
|
|
6000
6902
|
});
|
|
6001
6903
|
return;
|
|
6002
6904
|
}
|
|
@@ -6011,10 +6913,13 @@ async function cmdToday(date, ctx) {
|
|
|
6011
6913
|
} else {
|
|
6012
6914
|
for (const m of todays) {
|
|
6013
6915
|
out(matchLine(m, cfg, t2, c, flags));
|
|
6014
|
-
const s = signals.get(m.id);
|
|
6916
|
+
const s = market.signals.get(m.id);
|
|
6015
6917
|
if (s) out(" " + c.dim(marketLine(s, m)));
|
|
6016
6918
|
}
|
|
6017
6919
|
}
|
|
6920
|
+
if (!market.complete) {
|
|
6921
|
+
out(c.dim(" Market data unavailable or incomplete \u2014 not all fixtures were checked."));
|
|
6922
|
+
}
|
|
6018
6923
|
out();
|
|
6019
6924
|
if (degraded) out(c.dim(" " + t2("feed.degraded")));
|
|
6020
6925
|
const src = dataSource(source, cfg.lang, c);
|
|
@@ -6099,9 +7004,9 @@ function cmdTeam(query, ctx) {
|
|
|
6099
7004
|
const c = painterFor(cfg);
|
|
6100
7005
|
const flags = flagsEnabled();
|
|
6101
7006
|
const label = (tm) => {
|
|
6102
|
-
const
|
|
7007
|
+
const flag2 = flags ? `${tm.flag} ` : "";
|
|
6103
7008
|
const grp = tm.group ? ` \xB7 ${t2("team.group", { group: tm.group })}` : "";
|
|
6104
|
-
return ` ${
|
|
7009
|
+
return ` ${flag2}${c.bold(tm.name)} ${c.dim(tm.code + grp)}`;
|
|
6105
7010
|
};
|
|
6106
7011
|
out();
|
|
6107
7012
|
if (!q) {
|
|
@@ -6137,11 +7042,12 @@ async function cmdTable(group, ctx) {
|
|
|
6137
7042
|
out();
|
|
6138
7043
|
out(
|
|
6139
7044
|
c.dim(
|
|
6140
|
-
" " + (group ? t2("table.none", { group: group.toUpperCase() }) : t2("table.empty"))
|
|
7045
|
+
" " + (degraded ? t2("table.unavailable") : group ? t2("table.none", { group: group.toUpperCase() }) : t2("table.empty"))
|
|
6141
7046
|
)
|
|
6142
7047
|
);
|
|
6143
7048
|
out();
|
|
6144
|
-
|
|
7049
|
+
const src2 = dataSource(source, cfg.lang, c);
|
|
7050
|
+
if (src2) out(src2);
|
|
6145
7051
|
out(disclaimer(t2, c));
|
|
6146
7052
|
return;
|
|
6147
7053
|
}
|
|
@@ -6264,7 +7170,13 @@ function cmdHook({ cfg }) {
|
|
|
6264
7170
|
try {
|
|
6265
7171
|
const team = resolveEnvTeam(process.env.CLAUDINHO_TEAM);
|
|
6266
7172
|
const state = readCurrentState(cfg.source, resolveCompetition());
|
|
6267
|
-
const ctx = renderHook(state, {
|
|
7173
|
+
const ctx = renderHook(state, {
|
|
7174
|
+
team,
|
|
7175
|
+
flags: flagsEnabled(),
|
|
7176
|
+
// The bundled roster names World Cup nations only; on another competition
|
|
7177
|
+
// a club sharing a nation's code must not be renamed to that nation.
|
|
7178
|
+
defaultCompetition: resolveCompetition() === DEFAULT_COMPETITION
|
|
7179
|
+
});
|
|
6268
7180
|
if (ctx) out(ctx);
|
|
6269
7181
|
if (!state && !isLockFresh() || shouldRefresh(Date.now(), state) || shouldRefreshFixtures(Date.now(), state)) {
|
|
6270
7182
|
spawnRefresh(cfg.source);
|
|
@@ -6352,13 +7264,14 @@ async function cmdMatch(id, ctx) {
|
|
|
6352
7264
|
const { cfg, t: t2 } = ctx;
|
|
6353
7265
|
precheck(cfg, t2);
|
|
6354
7266
|
const { match, degraded, source: liveSource } = await getMatchById(adapterFor(ctx), id);
|
|
6355
|
-
const
|
|
7267
|
+
const market = match ? await reliableMarketSignalFor(ctx, match) : { signal: void 0, complete: true };
|
|
6356
7268
|
if (cfg.json) {
|
|
6357
7269
|
emitJson({
|
|
6358
7270
|
degraded,
|
|
6359
7271
|
match: match ?? null,
|
|
6360
7272
|
source: liveSource ?? null,
|
|
6361
|
-
|
|
7273
|
+
marketComplete: market.complete,
|
|
7274
|
+
marketSignal: market.signal ?? null
|
|
6362
7275
|
});
|
|
6363
7276
|
return;
|
|
6364
7277
|
}
|
|
@@ -6386,9 +7299,13 @@ async function cmdMatch(id, ctx) {
|
|
|
6386
7299
|
out(` ${e.minute}' ${e.type} ${e.teamCode}${e.player ? ` \u2014 ${e.player}` : ""}`);
|
|
6387
7300
|
}
|
|
6388
7301
|
}
|
|
6389
|
-
if (
|
|
7302
|
+
if (market.signal) {
|
|
6390
7303
|
out();
|
|
6391
|
-
for (const mline of marketBlock(
|
|
7304
|
+
for (const mline of marketBlock(market.signal, match)) out(" " + c.dim(mline));
|
|
7305
|
+
}
|
|
7306
|
+
if (!market.complete) {
|
|
7307
|
+
out();
|
|
7308
|
+
out(c.dim(" Market data unavailable or incomplete \u2014 this match could not be checked."));
|
|
6392
7309
|
}
|
|
6393
7310
|
out();
|
|
6394
7311
|
if (degraded) out(c.dim(" " + t2("feed.degraded")));
|
|
@@ -6398,6 +7315,7 @@ async function cmdMatch(id, ctx) {
|
|
|
6398
7315
|
maybeStarNudge(ctx);
|
|
6399
7316
|
}
|
|
6400
7317
|
var MARKET_INFO = "Prediction-market data is informational only.";
|
|
7318
|
+
var MARKETS_SCOPE_NOTE = "Market signals cover the World Cup only; none are read for this competition.";
|
|
6401
7319
|
function marketDisplayable(match, sig) {
|
|
6402
7320
|
return marketSignalRendersFor(match, sig) && !sig.ambiguous && sig.favorite != null && hasSaneDistribution(sig.outcomes);
|
|
6403
7321
|
}
|
|
@@ -6406,6 +7324,7 @@ function marketHeaderLine(m, cfg) {
|
|
|
6406
7324
|
return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag} \xB7 ${when}`;
|
|
6407
7325
|
}
|
|
6408
7326
|
function noSignalLine(m, now) {
|
|
7327
|
+
if (!marketsCoverCompetition()) return MARKETS_SCOPE_NOTE;
|
|
6409
7328
|
if (marketRelevant(m, now)) return "No market signal for this match.";
|
|
6410
7329
|
return isFinished(m.status) ? "Match has finished \u2014 market signals are pre-match and in-play reads." : "Match appears to have finished \u2014 market signals are pre-match and in-play reads.";
|
|
6411
7330
|
}
|
|
@@ -6419,14 +7338,16 @@ async function cmdMarkets(target, team, ctx) {
|
|
|
6419
7338
|
const code = resolveTeamArg(team, "Usage: claudinho markets next <team> (or set CLAUDINHO_TEAM)", t2);
|
|
6420
7339
|
const now2 = ctx.now ?? /* @__PURE__ */ new Date();
|
|
6421
7340
|
const { match: fixture, degraded } = await marketFixtureForTeam(adapterFor(ctx), code, now2);
|
|
6422
|
-
const
|
|
6423
|
-
const
|
|
7341
|
+
const market = fixture && marketRelevant(fixture, now2) ? await marketSignalsFor(ctx, [fixture], MARKETS_CMD_OPTS) : { signals: /* @__PURE__ */ new Map(), complete: true };
|
|
7342
|
+
const sig = fixture ? market.signals.get(fixture.id) : void 0;
|
|
7343
|
+
const shown = market.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
|
|
6424
7344
|
if (cfg.json) {
|
|
6425
7345
|
emitJson({
|
|
6426
7346
|
team: code,
|
|
6427
7347
|
matchId: fixture?.id ?? null,
|
|
6428
7348
|
degraded,
|
|
6429
7349
|
informationalOnly: true,
|
|
7350
|
+
complete: market.complete,
|
|
6430
7351
|
signal: shown ?? null
|
|
6431
7352
|
});
|
|
6432
7353
|
return;
|
|
@@ -6439,7 +7360,9 @@ async function cmdMarkets(target, team, ctx) {
|
|
|
6439
7360
|
out(header(marketHeaderLine(fixture, cfg), c2));
|
|
6440
7361
|
out();
|
|
6441
7362
|
if (shown) printMarketBlock(fixture, shown, c2);
|
|
6442
|
-
else
|
|
7363
|
+
else if (!market.complete) {
|
|
7364
|
+
out(c2.dim(" Market data unavailable or incomplete \u2014 this match could not be checked."));
|
|
7365
|
+
} else out(c2.dim(" " + noSignalLine(fixture, now2)));
|
|
6443
7366
|
}
|
|
6444
7367
|
out();
|
|
6445
7368
|
out(disclaimer(t2, c2));
|
|
@@ -6450,10 +7373,16 @@ async function cmdMarkets(target, team, ctx) {
|
|
|
6450
7373
|
precheck(cfg, t2);
|
|
6451
7374
|
const now2 = ctx.now ?? /* @__PURE__ */ new Date();
|
|
6452
7375
|
const { match } = await getMatchById(adapterFor(ctx), target);
|
|
6453
|
-
const
|
|
6454
|
-
const
|
|
7376
|
+
const market = match && marketRelevant(match, now2) ? await marketSignalsFor(ctx, [match], MARKETS_CMD_OPTS) : { signals: /* @__PURE__ */ new Map(), complete: true };
|
|
7377
|
+
const sig = match ? market.signals.get(match.id) : void 0;
|
|
7378
|
+
const shown = market.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
|
|
6455
7379
|
if (cfg.json) {
|
|
6456
|
-
emitJson({
|
|
7380
|
+
emitJson({
|
|
7381
|
+
matchId: target,
|
|
7382
|
+
informationalOnly: true,
|
|
7383
|
+
complete: market.complete,
|
|
7384
|
+
signal: shown ?? null
|
|
7385
|
+
});
|
|
6457
7386
|
return;
|
|
6458
7387
|
}
|
|
6459
7388
|
const c2 = painterFor(cfg);
|
|
@@ -6464,7 +7393,9 @@ async function cmdMarkets(target, team, ctx) {
|
|
|
6464
7393
|
out(header(marketHeaderLine(match, cfg), c2));
|
|
6465
7394
|
out();
|
|
6466
7395
|
if (shown) printMarketBlock(match, shown, c2);
|
|
6467
|
-
else
|
|
7396
|
+
else if (!market.complete) {
|
|
7397
|
+
out(c2.dim(" Market data unavailable or incomplete \u2014 this match could not be checked."));
|
|
7398
|
+
} else out(c2.dim(" " + noSignalLine(match, now2)));
|
|
6468
7399
|
}
|
|
6469
7400
|
out();
|
|
6470
7401
|
out(disclaimer(t2, c2));
|
|
@@ -6478,14 +7409,14 @@ async function cmdMarkets(target, team, ctx) {
|
|
|
6478
7409
|
const { matches } = await getMatchesForDate(adapterFor(ctx), date);
|
|
6479
7410
|
const todays = fixturesByDate(date, matches, cfg.tz);
|
|
6480
7411
|
const relevant = todays.filter((m) => marketRelevant(m, now));
|
|
6481
|
-
const signals = await marketSignalsFor(ctx, relevant, MARKETS_CMD_OPTS);
|
|
7412
|
+
const { signals, complete } = await marketSignalsFor(ctx, relevant, MARKETS_CMD_OPTS);
|
|
6482
7413
|
const rows = relevant.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
|
|
6483
7414
|
(r) => !!r.signal && marketDisplayable(r.match, r.signal)
|
|
6484
7415
|
);
|
|
6485
7416
|
if (cfg.json) {
|
|
6486
7417
|
const marketSignals = {};
|
|
6487
7418
|
for (const r of rows) marketSignals[r.match.id] = r.signal;
|
|
6488
|
-
emitJson({ date, informationalOnly: true, marketSignals });
|
|
7419
|
+
emitJson({ date, informationalOnly: true, complete, marketSignals });
|
|
6489
7420
|
return;
|
|
6490
7421
|
}
|
|
6491
7422
|
const c = painterFor(cfg);
|
|
@@ -6493,13 +7424,21 @@ async function cmdMarkets(target, team, ctx) {
|
|
|
6493
7424
|
out(header(`Market signals \xB7 ${date}`, c));
|
|
6494
7425
|
out();
|
|
6495
7426
|
if (rows.length === 0) {
|
|
6496
|
-
out(
|
|
7427
|
+
out(
|
|
7428
|
+
c.dim(
|
|
7429
|
+
!marketsCoverCompetition() ? ` ${MARKETS_SCOPE_NOTE}` : complete ? ` No market signals available for ${date}.` : ` Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`
|
|
7430
|
+
)
|
|
7431
|
+
);
|
|
6497
7432
|
} else {
|
|
6498
7433
|
for (const { match, signal } of rows) {
|
|
6499
7434
|
out(" " + c.bold(marketHeaderLine(match, cfg)));
|
|
6500
7435
|
printMarketBlock(match, signal, c);
|
|
6501
7436
|
out();
|
|
6502
7437
|
}
|
|
7438
|
+
if (!complete) {
|
|
7439
|
+
out(c.dim(` Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`));
|
|
7440
|
+
out();
|
|
7441
|
+
}
|
|
6503
7442
|
}
|
|
6504
7443
|
out(disclaimer(t2, c));
|
|
6505
7444
|
out(c.dim(MARKET_INFO));
|
|
@@ -6510,11 +7449,11 @@ function pickShareStyle(v) {
|
|
|
6510
7449
|
async function reliableShareSignals(ctx, matches) {
|
|
6511
7450
|
const raw = await reliableMarketSignals(ctx, matches);
|
|
6512
7451
|
const out2 = /* @__PURE__ */ new Map();
|
|
6513
|
-
for (const [id, s] of raw) {
|
|
7452
|
+
for (const [id, s] of raw.signals) {
|
|
6514
7453
|
const m = matches.find((x) => x.id === id);
|
|
6515
7454
|
if (m && marketDisplayable(m, s)) out2.set(id, s);
|
|
6516
7455
|
}
|
|
6517
|
-
return out2;
|
|
7456
|
+
return { signals: out2, complete: raw.complete };
|
|
6518
7457
|
}
|
|
6519
7458
|
function emitShare(ctx, e, copy) {
|
|
6520
7459
|
const snippet = formatShareSnippet(e.input, e.options);
|
|
@@ -6529,6 +7468,7 @@ function emitShare(ctx, e, copy) {
|
|
|
6529
7468
|
style: e.options.style ?? "social",
|
|
6530
7469
|
snippet,
|
|
6531
7470
|
matches: e.input.matches,
|
|
7471
|
+
marketComplete: e.input.marketComplete ?? true,
|
|
6532
7472
|
marketSignals: Object.fromEntries(e.input.marketSignals ?? /* @__PURE__ */ new Map())
|
|
6533
7473
|
});
|
|
6534
7474
|
} else {
|
|
@@ -6648,11 +7588,12 @@ async function cmdShare(target, team, opts, ctx) {
|
|
|
6648
7588
|
{
|
|
6649
7589
|
group,
|
|
6650
7590
|
tables,
|
|
6651
|
-
// Degraded ⇒
|
|
7591
|
+
// Degraded ⇒ no live provider: no attribution. Open-scope outages
|
|
7592
|
+
// have no compatible bundled roster, so name the outage in the empty card.
|
|
6652
7593
|
source: degraded2 ? void 0 : source2,
|
|
6653
7594
|
degraded: degraded2,
|
|
6654
7595
|
installLine: group ? `npx @claudinho/cli table ${group}` : "npx @claudinho/cli table",
|
|
6655
|
-
emptyNote: group ? `No group ${group}.` : "No standings available.",
|
|
7596
|
+
emptyNote: degraded2 ? "Live standings unavailable." : group ? `No group ${group}.` : "No standings available.",
|
|
6656
7597
|
options: baseOptions
|
|
6657
7598
|
},
|
|
6658
7599
|
copy
|
|
@@ -6699,7 +7640,7 @@ async function cmdShare(target, team, opts, ctx) {
|
|
|
6699
7640
|
ctx.now ?? /* @__PURE__ */ new Date()
|
|
6700
7641
|
);
|
|
6701
7642
|
const matches = fixture ? [fixture] : [];
|
|
6702
|
-
const
|
|
7643
|
+
const market2 = await reliableShareSignals(ctx, matches);
|
|
6703
7644
|
const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
|
|
6704
7645
|
emitShare(
|
|
6705
7646
|
ctx,
|
|
@@ -6710,7 +7651,8 @@ async function cmdShare(target, team, opts, ctx) {
|
|
|
6710
7651
|
input: {
|
|
6711
7652
|
title: `Next up for ${teamName}`,
|
|
6712
7653
|
matches,
|
|
6713
|
-
marketSignals:
|
|
7654
|
+
marketSignals: market2.signals,
|
|
7655
|
+
marketComplete: market2.complete,
|
|
6714
7656
|
// Attribute the provider when the overlay resolved the tie (knockout);
|
|
6715
7657
|
// undefined for a static group fixture — parity with CLI `next`.
|
|
6716
7658
|
source: source2,
|
|
@@ -6731,7 +7673,7 @@ async function cmdShare(target, team, opts, ctx) {
|
|
|
6731
7673
|
precheck(cfg, t2);
|
|
6732
7674
|
const { match, degraded: degraded2, source: source2 } = await getMatchById(adapterFor(ctx), target);
|
|
6733
7675
|
const matches = match ? [match] : [];
|
|
6734
|
-
const
|
|
7676
|
+
const market2 = await reliableShareSignals(ctx, matches);
|
|
6735
7677
|
emitShare(
|
|
6736
7678
|
ctx,
|
|
6737
7679
|
{
|
|
@@ -6740,7 +7682,8 @@ async function cmdShare(target, team, opts, ctx) {
|
|
|
6740
7682
|
input: {
|
|
6741
7683
|
title: "Match pulse",
|
|
6742
7684
|
matches,
|
|
6743
|
-
marketSignals:
|
|
7685
|
+
marketSignals: market2.signals,
|
|
7686
|
+
marketComplete: market2.complete,
|
|
6744
7687
|
source: source2,
|
|
6745
7688
|
degraded: degraded2,
|
|
6746
7689
|
emptyNote: `No match found with id ${target}.`,
|
|
@@ -6759,7 +7702,7 @@ async function cmdShare(target, team, opts, ctx) {
|
|
|
6759
7702
|
const date = explicitDate ?? localDate((/* @__PURE__ */ new Date()).toISOString(), cfg.tz);
|
|
6760
7703
|
const { matches: all, degraded, source } = await getMatchesForDate(adapterFor(ctx), date);
|
|
6761
7704
|
const todays = fixturesByDate(date, all, cfg.tz);
|
|
6762
|
-
const
|
|
7705
|
+
const market = await reliableShareSignals(ctx, todays);
|
|
6763
7706
|
const human = formatDate(`${date}T12:00:00.000Z`, { tz: cfg.tz, locale: cfg.lang });
|
|
6764
7707
|
const title = explicitDate ? `Matches \xB7 ${human}` : `Today's matches \xB7 ${human}`;
|
|
6765
7708
|
emitShare(
|
|
@@ -6770,7 +7713,8 @@ async function cmdShare(target, team, opts, ctx) {
|
|
|
6770
7713
|
input: {
|
|
6771
7714
|
title,
|
|
6772
7715
|
matches: todays,
|
|
6773
|
-
marketSignals: signals,
|
|
7716
|
+
marketSignals: market.signals,
|
|
7717
|
+
marketComplete: market.complete,
|
|
6774
7718
|
source,
|
|
6775
7719
|
degraded,
|
|
6776
7720
|
emptyNote: `No matches scheduled for ${human}.`,
|
|
@@ -6873,7 +7817,7 @@ function cmdVibe(ctx) {
|
|
|
6873
7817
|
try {
|
|
6874
7818
|
const state = readCurrentState(cfg.source, resolveCompetition());
|
|
6875
7819
|
liveSeg = vibeLiveSegment(
|
|
6876
|
-
liveMatchesFromCache(state, (ctx.now ?? /* @__PURE__ */ new Date()).getTime()),
|
|
7820
|
+
liveMatchesFromCache(state, (ctx.now ?? /* @__PURE__ */ new Date()).getTime()).items,
|
|
6877
7821
|
// Name-or-code, matching the statusline/hook (offline lookup).
|
|
6878
7822
|
resolveEnvTeam(process.env.CLAUDINHO_TEAM)
|
|
6879
7823
|
);
|
|
@@ -6899,7 +7843,7 @@ function handlePipeError(stream) {
|
|
|
6899
7843
|
}
|
|
6900
7844
|
handlePipeError(process.stdout);
|
|
6901
7845
|
handlePipeError(process.stderr);
|
|
6902
|
-
var VERSION = "0.
|
|
7846
|
+
var VERSION = "0.10.0";
|
|
6903
7847
|
var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
|
|
6904
7848
|
function ctxFrom(cmd) {
|
|
6905
7849
|
let root = cmd;
|