@claudinho/core 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 +1 -1
- package/dist/index.d.ts +424 -175
- package/dist/index.js +1093 -335
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -149,10 +149,11 @@ var ALIASES = [
|
|
|
149
149
|
["Burma", "MM"],
|
|
150
150
|
["Cape Verde", "CV"]
|
|
151
151
|
];
|
|
152
|
-
var BY_NATION = Object.
|
|
153
|
-
|
|
152
|
+
var BY_NATION = Object.assign(
|
|
153
|
+
/* @__PURE__ */ Object.create(null),
|
|
154
|
+
Object.fromEntries([...NATIONS, ...ALIASES].map(([name, code]) => [norm(name), code]))
|
|
154
155
|
);
|
|
155
|
-
var BY_CODE = {
|
|
156
|
+
var BY_CODE = Object.assign(/* @__PURE__ */ Object.create(null), {
|
|
156
157
|
MEX: "MX",
|
|
157
158
|
RSA: "ZA",
|
|
158
159
|
KOR: "KR",
|
|
@@ -213,7 +214,7 @@ var BY_CODE = {
|
|
|
213
214
|
HON: "HN",
|
|
214
215
|
COD: "CD",
|
|
215
216
|
MLI: "ML"
|
|
216
|
-
};
|
|
217
|
+
});
|
|
217
218
|
function nationToFlag(nameOrCode) {
|
|
218
219
|
const region = nationToRegion(nameOrCode);
|
|
219
220
|
return region ? flagEmoji(region) : NEUTRAL;
|
|
@@ -221,7 +222,7 @@ function nationToFlag(nameOrCode) {
|
|
|
221
222
|
var INTL_BY_NAME;
|
|
222
223
|
function intlNameMap() {
|
|
223
224
|
if (INTL_BY_NAME) return INTL_BY_NAME;
|
|
224
|
-
const map =
|
|
225
|
+
const map = /* @__PURE__ */ Object.create(null);
|
|
225
226
|
try {
|
|
226
227
|
const dn = new Intl.DisplayNames(["en"], { type: "region" });
|
|
227
228
|
for (let a = 65; a <= 90; a++) {
|
|
@@ -246,11 +247,8 @@ function intlNameMap() {
|
|
|
246
247
|
}
|
|
247
248
|
function nationToRegion(nameOrCode) {
|
|
248
249
|
if (!nameOrCode) return void 0;
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
const byCode = BY_CODE[nameOrCode.trim().toUpperCase()];
|
|
252
|
-
if (byCode) return byCode;
|
|
253
|
-
return intlNameMap()[norm(nameOrCode)];
|
|
250
|
+
const str = (v) => typeof v === "string" && v ? v : void 0;
|
|
251
|
+
return str(BY_NATION[norm(nameOrCode)]) ?? str(BY_CODE[nameOrCode.trim().toUpperCase()]) ?? str(intlNameMap()[norm(nameOrCode)]);
|
|
254
252
|
}
|
|
255
253
|
|
|
256
254
|
// src/i18n.ts
|
|
@@ -272,6 +270,7 @@ var EN = {
|
|
|
272
270
|
"bracket.slot.loser": "{stage} {n} loser",
|
|
273
271
|
"bracket.slot.tbd": "TBD",
|
|
274
272
|
"live.data": "Live data: {source}",
|
|
273
|
+
"standings.unavailable": "Live standings unavailable.",
|
|
275
274
|
"share.tryIt": "Try it: {line}",
|
|
276
275
|
"stage.group": "Group {group}",
|
|
277
276
|
"stage.groupStage": "Group stage",
|
|
@@ -301,6 +300,7 @@ var ES = {
|
|
|
301
300
|
"bracket.slot.loser": "Perdedor {stage} {n}",
|
|
302
301
|
"bracket.slot.tbd": "Por definir",
|
|
303
302
|
"live.data": "Datos en vivo: {source}",
|
|
303
|
+
"standings.unavailable": "Tabla en vivo no disponible.",
|
|
304
304
|
"share.tryIt": "Pru\xE9balo: {line}",
|
|
305
305
|
"stage.group": "Grupo {group}",
|
|
306
306
|
"stage.groupStage": "Fase de grupos",
|
|
@@ -330,6 +330,7 @@ var PT = {
|
|
|
330
330
|
"bracket.slot.loser": "Perdedor {stage} {n}",
|
|
331
331
|
"bracket.slot.tbd": "A definir",
|
|
332
332
|
"live.data": "Dados ao vivo: {source}",
|
|
333
|
+
"standings.unavailable": "Classifica\xE7\xE3o ao vivo indispon\xEDvel.",
|
|
333
334
|
"share.tryIt": "Experimente: {line}",
|
|
334
335
|
"stage.group": "Grupo {group}",
|
|
335
336
|
"stage.groupStage": "Fase de grupos",
|
|
@@ -359,6 +360,7 @@ var FR = {
|
|
|
359
360
|
"bracket.slot.loser": "Perdant {stage} {n}",
|
|
360
361
|
"bracket.slot.tbd": "\xC0 d\xE9finir",
|
|
361
362
|
"live.data": "Donn\xE9es en direct : {source}",
|
|
363
|
+
"standings.unavailable": "Classement en direct indisponible.",
|
|
362
364
|
"share.tryIt": "Essayez : {line}",
|
|
363
365
|
"stage.group": "Groupe {group}",
|
|
364
366
|
"stage.groupStage": "Phase de groupes",
|
|
@@ -447,7 +449,14 @@ function safeLocale(locale) {
|
|
|
447
449
|
return "en";
|
|
448
450
|
}
|
|
449
451
|
}
|
|
452
|
+
function parsedDate(iso) {
|
|
453
|
+
const t2 = Date.parse(iso);
|
|
454
|
+
return Number.isFinite(t2) ? new Date(t2) : void 0;
|
|
455
|
+
}
|
|
456
|
+
var UNKNOWN_TIME = "\u2014";
|
|
450
457
|
function formatKickoff(iso, opts = {}) {
|
|
458
|
+
const when = parsedDate(iso);
|
|
459
|
+
if (!when) return UNKNOWN_TIME;
|
|
451
460
|
const tz = resolveTz(opts.tz);
|
|
452
461
|
const locale = safeLocale(opts.locale);
|
|
453
462
|
return new Intl.DateTimeFormat(locale, {
|
|
@@ -457,18 +466,22 @@ function formatKickoff(iso, opts = {}) {
|
|
|
457
466
|
minute: "2-digit",
|
|
458
467
|
hour12: false,
|
|
459
468
|
timeZone: tz
|
|
460
|
-
}).format(
|
|
469
|
+
}).format(when);
|
|
461
470
|
}
|
|
462
471
|
function formatDate(iso, opts = {}) {
|
|
472
|
+
const when = parsedDate(iso);
|
|
473
|
+
if (!when) return UNKNOWN_TIME;
|
|
463
474
|
const tz = resolveTz(opts.tz);
|
|
464
475
|
const locale = safeLocale(opts.locale);
|
|
465
476
|
return new Intl.DateTimeFormat(locale, {
|
|
466
477
|
month: "short",
|
|
467
478
|
day: "numeric",
|
|
468
479
|
timeZone: tz
|
|
469
|
-
}).format(
|
|
480
|
+
}).format(when);
|
|
470
481
|
}
|
|
471
482
|
function formatTime(iso, opts = {}) {
|
|
483
|
+
const when = parsedDate(iso);
|
|
484
|
+
if (!when) return UNKNOWN_TIME;
|
|
472
485
|
const tz = resolveTz(opts.tz);
|
|
473
486
|
const locale = safeLocale(opts.locale);
|
|
474
487
|
return new Intl.DateTimeFormat(locale, {
|
|
@@ -476,10 +489,12 @@ function formatTime(iso, opts = {}) {
|
|
|
476
489
|
minute: "2-digit",
|
|
477
490
|
hour12: false,
|
|
478
491
|
timeZone: tz
|
|
479
|
-
}).format(
|
|
492
|
+
}).format(when);
|
|
480
493
|
}
|
|
481
494
|
function countdown(iso, from = /* @__PURE__ */ new Date()) {
|
|
482
|
-
const
|
|
495
|
+
const when = parsedDate(iso);
|
|
496
|
+
if (!when) return UNKNOWN_TIME;
|
|
497
|
+
const ms = when.getTime() - from.getTime();
|
|
483
498
|
if (ms <= 0) return "now";
|
|
484
499
|
const totalMin = Math.floor(ms / 6e4);
|
|
485
500
|
const days = Math.floor(totalMin / 1440);
|
|
@@ -490,75 +505,58 @@ function countdown(iso, from = /* @__PURE__ */ new Date()) {
|
|
|
490
505
|
return `${mins}m`;
|
|
491
506
|
}
|
|
492
507
|
function localDate(iso, tz) {
|
|
508
|
+
const when = parsedDate(iso);
|
|
509
|
+
if (!when) return "";
|
|
493
510
|
const zone = resolveTz(tz);
|
|
494
511
|
return new Intl.DateTimeFormat("en-CA", {
|
|
495
512
|
year: "numeric",
|
|
496
513
|
month: "2-digit",
|
|
497
514
|
day: "2-digit",
|
|
498
515
|
timeZone: zone
|
|
499
|
-
}).format(
|
|
516
|
+
}).format(when);
|
|
500
517
|
}
|
|
501
518
|
function shiftUtcDate(dateISO, days) {
|
|
502
519
|
const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
|
|
503
520
|
return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
|
|
504
521
|
}
|
|
505
522
|
|
|
506
|
-
// src/sanitize.ts
|
|
507
|
-
var FEED_TEXT_MAX = 100;
|
|
508
|
-
function sanitizeFeedText(value, max = FEED_TEXT_MAX) {
|
|
509
|
-
let out = "";
|
|
510
|
-
let count = 0;
|
|
511
|
-
for (const ch of String(value)) {
|
|
512
|
-
const cp = ch.codePointAt(0) ?? 0;
|
|
513
|
-
const isWhitespaceControl = cp === 9 || cp === 10 || cp === 13;
|
|
514
|
-
if ((cp <= 31 || cp >= 127 && cp <= 159) && !isWhitespaceControl) continue;
|
|
515
|
-
if (count >= max) break;
|
|
516
|
-
out += isWhitespaceControl ? " " : ch;
|
|
517
|
-
count++;
|
|
518
|
-
}
|
|
519
|
-
return out;
|
|
520
|
-
}
|
|
521
|
-
function sanitizeTeam(t2) {
|
|
522
|
-
return {
|
|
523
|
-
...t2 ?? {},
|
|
524
|
-
code: sanitizeFeedText(t2?.code ?? ""),
|
|
525
|
-
name: sanitizeFeedText(t2?.name ?? ""),
|
|
526
|
-
flag: sanitizeFeedText(t2?.flag ?? "")
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
function finiteOrUndefined(v) {
|
|
530
|
-
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
531
|
-
}
|
|
532
|
-
function sanitizeScorePair(v) {
|
|
533
|
-
const home = finiteOrUndefined(v?.home);
|
|
534
|
-
const away = finiteOrUndefined(v?.away);
|
|
535
|
-
return home !== void 0 && away !== void 0 ? { home, away } : void 0;
|
|
536
|
-
}
|
|
537
|
-
function sanitizeMatchStrings(m) {
|
|
538
|
-
const score = sanitizeScorePair(m.score);
|
|
539
|
-
return {
|
|
540
|
-
...m,
|
|
541
|
-
venue: sanitizeFeedText(m.venue ?? ""),
|
|
542
|
-
city: m.city == null ? m.city : sanitizeFeedText(m.city),
|
|
543
|
-
country: m.country == null ? m.country : sanitizeFeedText(m.country),
|
|
544
|
-
home: sanitizeTeam(m.home),
|
|
545
|
-
away: sanitizeTeam(m.away),
|
|
546
|
-
score,
|
|
547
|
-
shootout: score ? sanitizeScorePair(m.shootout) : void 0,
|
|
548
|
-
minute: finiteOrUndefined(m.minute)
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
|
|
552
523
|
// src/text.ts
|
|
553
524
|
var segmenter = new Intl.Segmenter();
|
|
554
525
|
var WIDE_CLUSTER = new RegExp("^(?:\\p{Regional_Indicator}|\\p{Extended_Pictographic})", "u");
|
|
526
|
+
var WIDE_BASE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-ꥠ-가-힣豈-︐-︙︰--⦆¢-₩\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}]/u;
|
|
527
|
+
var KEYCAP = /\u{20E3}/u;
|
|
528
|
+
var ZERO_WIDTH_BASE = /[\p{Mn}\p{Me}\p{Cf}\p{Cc}]/u;
|
|
529
|
+
function clusterWidth(segment) {
|
|
530
|
+
if (WIDE_CLUSTER.test(segment) || KEYCAP.test(segment)) return 2;
|
|
531
|
+
const first = segment.codePointAt(0);
|
|
532
|
+
if (first === void 0) return 0;
|
|
533
|
+
const base = String.fromCodePoint(first);
|
|
534
|
+
if (ZERO_WIDTH_BASE.test(base)) return 0;
|
|
535
|
+
return WIDE_BASE.test(base) ? 2 : 1;
|
|
536
|
+
}
|
|
555
537
|
function displayWidth(s) {
|
|
556
538
|
let w = 0;
|
|
557
539
|
for (const { segment } of segmenter.segment(s)) {
|
|
558
|
-
w +=
|
|
540
|
+
w += clusterWidth(segment);
|
|
559
541
|
}
|
|
560
542
|
return w;
|
|
561
543
|
}
|
|
544
|
+
function truncateVisible(s, maxColumns, marker = "\u2026") {
|
|
545
|
+
if (displayWidth(s) <= maxColumns) return s;
|
|
546
|
+
const budget = Math.max(0, maxColumns - displayWidth(marker));
|
|
547
|
+
let out = "";
|
|
548
|
+
let w = 0;
|
|
549
|
+
for (const { segment } of segmenter.segment(s)) {
|
|
550
|
+
const cw = clusterWidth(segment);
|
|
551
|
+
if (w + cw > budget) break;
|
|
552
|
+
out += segment;
|
|
553
|
+
w += cw;
|
|
554
|
+
}
|
|
555
|
+
return out + marker;
|
|
556
|
+
}
|
|
557
|
+
function* graphemes(s) {
|
|
558
|
+
for (const { segment } of segmenter.segment(s)) yield segment;
|
|
559
|
+
}
|
|
562
560
|
function padVisible(s, width) {
|
|
563
561
|
const w = displayWidth(s);
|
|
564
562
|
return w >= width ? s : s + " ".repeat(width - w);
|
|
@@ -3094,48 +3092,318 @@ function rosterAtZero(matches) {
|
|
|
3094
3092
|
return [...teams.values()].sort((a, b) => a.name.localeCompare(b.name)).map(blankRow);
|
|
3095
3093
|
}
|
|
3096
3094
|
|
|
3097
|
-
// src/
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3095
|
+
// src/trust/bounded.ts
|
|
3096
|
+
function bounded(items, max, complete = true) {
|
|
3097
|
+
const kept = items.length > max ? items.slice(0, max) : items;
|
|
3098
|
+
return {
|
|
3099
|
+
items: kept,
|
|
3100
|
+
total: items.length,
|
|
3101
|
+
shown: kept.length,
|
|
3102
|
+
truncated: items.length > kept.length,
|
|
3103
|
+
complete
|
|
3104
|
+
};
|
|
3105
3105
|
}
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3106
|
+
function takeBounded(value, max) {
|
|
3107
|
+
if (!Array.isArray(value)) return [];
|
|
3108
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
// src/trust/result.ts
|
|
3112
|
+
var valid = (value) => ({ kind: "valid", value });
|
|
3113
|
+
var definitiveNone = (reason) => ({
|
|
3114
|
+
kind: "definitive-none",
|
|
3115
|
+
reason
|
|
3116
|
+
});
|
|
3117
|
+
var malformed = (reason) => ({ kind: "malformed", reason });
|
|
3118
|
+
var ambiguous = (reason) => ({ kind: "ambiguous", reason });
|
|
3119
|
+
var unresolved = (reason) => ({ kind: "unresolved", reason });
|
|
3120
|
+
function parsedValue(r) {
|
|
3121
|
+
return r.kind === "valid" ? r.value : void 0;
|
|
3122
|
+
}
|
|
3123
|
+
function isCacheable(r) {
|
|
3124
|
+
return r.kind === "valid" || r.kind === "definitive-none" || r.kind === "ambiguous";
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
// src/trust/roles.ts
|
|
3128
|
+
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");
|
|
3129
|
+
var EMOJI_IN_LABEL = new RegExp("\\p{Extended_Pictographic}|\\p{Regional_Indicator}|\\p{Emoji_Modifier}|\\u{20E3}", "u");
|
|
3130
|
+
var MAX_LABEL_INPUT_UNITS = 4096;
|
|
3131
|
+
var MAX_LABEL_COLUMNS = 100;
|
|
3132
|
+
function humanLabel(value, maxColumns = MAX_LABEL_COLUMNS) {
|
|
3133
|
+
if (typeof value !== "string" || value === "") return "";
|
|
3134
|
+
const capped = value.length > MAX_LABEL_INPUT_UNITS ? value.slice(0, MAX_LABEL_INPUT_UNITS) : value;
|
|
3135
|
+
return visible(runToFixedPoint(capped, maxColumns));
|
|
3136
|
+
}
|
|
3137
|
+
function visible(label) {
|
|
3138
|
+
return label !== "" && displayWidth(label) === 0 ? "" : label;
|
|
3139
|
+
}
|
|
3140
|
+
function runToFixedPoint(capped, maxColumns) {
|
|
3141
|
+
const first = sealLabelOnce(capped, maxColumns);
|
|
3142
|
+
if (!lastPassDropped) return first;
|
|
3143
|
+
let out = first;
|
|
3144
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
3145
|
+
const again = sealLabelOnce(out, maxColumns);
|
|
3146
|
+
if (again === out) return out;
|
|
3147
|
+
out = again;
|
|
3116
3148
|
}
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3149
|
+
return sealLabelOnce(out, maxColumns) === out ? out : "";
|
|
3150
|
+
}
|
|
3151
|
+
var lastPassDropped = false;
|
|
3152
|
+
function sealLabelOnce(value, maxColumns) {
|
|
3153
|
+
lastPassDropped = false;
|
|
3154
|
+
let normalized;
|
|
3155
|
+
try {
|
|
3156
|
+
normalized = value.normalize("NFC");
|
|
3157
|
+
} catch {
|
|
3158
|
+
return "";
|
|
3120
3159
|
}
|
|
3121
|
-
|
|
3160
|
+
const maxCodePoints = Math.max(16, maxColumns * 4);
|
|
3161
|
+
let out = "";
|
|
3162
|
+
let width = 0;
|
|
3163
|
+
let points = 0;
|
|
3164
|
+
for (const cluster of graphemes(normalized)) {
|
|
3165
|
+
if ([...cluster].length > 8) {
|
|
3166
|
+
lastPassDropped = true;
|
|
3167
|
+
continue;
|
|
3168
|
+
}
|
|
3169
|
+
if (EMOJI_IN_LABEL.test(cluster)) {
|
|
3170
|
+
lastPassDropped = true;
|
|
3171
|
+
continue;
|
|
3172
|
+
}
|
|
3173
|
+
let piece = "";
|
|
3174
|
+
for (const ch of cluster) {
|
|
3175
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
3176
|
+
if (cp === 9 || cp === 10 || cp === 13) {
|
|
3177
|
+
piece += " ";
|
|
3178
|
+
continue;
|
|
3179
|
+
}
|
|
3180
|
+
if (FORBIDDEN_IN_LABEL.test(ch)) {
|
|
3181
|
+
lastPassDropped = true;
|
|
3182
|
+
continue;
|
|
3183
|
+
}
|
|
3184
|
+
piece += ch;
|
|
3185
|
+
}
|
|
3186
|
+
if (!piece) {
|
|
3187
|
+
lastPassDropped = true;
|
|
3188
|
+
continue;
|
|
3189
|
+
}
|
|
3190
|
+
const w = displayWidth(piece);
|
|
3191
|
+
const cps = [...piece].length;
|
|
3192
|
+
if (width + w > maxColumns || points + cps > maxCodePoints) break;
|
|
3193
|
+
out += piece;
|
|
3194
|
+
width += w;
|
|
3195
|
+
points += cps;
|
|
3196
|
+
}
|
|
3197
|
+
try {
|
|
3198
|
+
return out.normalize("NFC").trim();
|
|
3199
|
+
} catch {
|
|
3200
|
+
return out.trim();
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
function opaqueId(value, grammar) {
|
|
3204
|
+
return typeof value === "string" && grammar.test(value) ? value : void 0;
|
|
3205
|
+
}
|
|
3206
|
+
var ESPN_ID = /^[0-9]{1,20}$/;
|
|
3207
|
+
var ISO_INSTANT = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:[Zz]|[+-]\d{2}(?::?\d{2})?)$/;
|
|
3208
|
+
var ISO_CANONICAL = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
3209
|
+
function calendarValid(iso) {
|
|
3210
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
|
|
3211
|
+
if (!m) return false;
|
|
3212
|
+
const year = Number(m[1]);
|
|
3213
|
+
const month = Number(m[2]);
|
|
3214
|
+
const day = Number(m[3]);
|
|
3215
|
+
if (month < 1 || month > 12 || day < 1) return false;
|
|
3216
|
+
return day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
3217
|
+
}
|
|
3218
|
+
function canonicalTimestamp(value) {
|
|
3219
|
+
if (typeof value !== "string" || !ISO_INSTANT.test(value)) return void 0;
|
|
3220
|
+
if (!calendarValid(value)) return void 0;
|
|
3221
|
+
const t2 = Date.parse(value);
|
|
3222
|
+
if (!Number.isFinite(t2)) return void 0;
|
|
3223
|
+
const out = new Date(t2).toISOString();
|
|
3224
|
+
return ISO_CANONICAL.test(out) ? out : void 0;
|
|
3225
|
+
}
|
|
3226
|
+
function productFlag(nameOrCode) {
|
|
3227
|
+
return nationToFlag(nameOrCode);
|
|
3228
|
+
}
|
|
3229
|
+
function count(value, max) {
|
|
3230
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= max ? value : void 0;
|
|
3231
|
+
}
|
|
3232
|
+
function quantity(value) {
|
|
3233
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
3234
|
+
}
|
|
3235
|
+
function probability(value) {
|
|
3236
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
|
|
3237
|
+
}
|
|
3238
|
+
function member(value, allowed) {
|
|
3239
|
+
return typeof value === "string" && allowed.has(value) ? value : void 0;
|
|
3240
|
+
}
|
|
3241
|
+
function flag(value) {
|
|
3242
|
+
return typeof value === "boolean" ? value : void 0;
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// src/trust/match.ts
|
|
3246
|
+
var MAX_GOALS = 99;
|
|
3247
|
+
var MAX_MINUTE = 200;
|
|
3248
|
+
var MAX_MATCH_EVENTS = 128;
|
|
3249
|
+
var TEAM_CODE_COLUMNS = 8;
|
|
3250
|
+
var STAGES = /* @__PURE__ */ new Set(["GROUP", "R32", "R16", "QF", "SF", "3P", "F", "FRIENDLY"]);
|
|
3251
|
+
var STATUSES = /* @__PURE__ */ new Set(["SCHEDULED", "LIVE", "HT", "FT", "POSTPONED", "CANCELLED"]);
|
|
3252
|
+
var EVENT_TYPES = /* @__PURE__ */ new Set(["GOAL", "OWN_GOAL", "PEN", "YELLOW", "RED", "SUB"]);
|
|
3253
|
+
function teamCode(raw, fallbackName) {
|
|
3254
|
+
const upper = typeof raw === "string" ? raw.toUpperCase() : raw;
|
|
3255
|
+
const stated = humanLabel(upper, TEAM_CODE_COLUMNS);
|
|
3256
|
+
if (stated) return stated;
|
|
3257
|
+
return humanLabel([...fallbackName].slice(0, 3).join("").toUpperCase(), TEAM_CODE_COLUMNS);
|
|
3258
|
+
}
|
|
3259
|
+
function sealTeam(raw) {
|
|
3260
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
3261
|
+
const t2 = raw;
|
|
3262
|
+
const name = humanLabel(t2.name);
|
|
3263
|
+
if (!name) return void 0;
|
|
3264
|
+
const code = teamCode(t2.code, name);
|
|
3265
|
+
return { code, name, flag: productFlag(name) };
|
|
3266
|
+
}
|
|
3267
|
+
function sealScorePair(raw) {
|
|
3268
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
3269
|
+
const v = raw;
|
|
3270
|
+
const home = count(v.home, MAX_GOALS);
|
|
3271
|
+
const away = count(v.away, MAX_GOALS);
|
|
3272
|
+
return home !== void 0 && away !== void 0 ? { home, away } : void 0;
|
|
3273
|
+
}
|
|
3274
|
+
function sealEvent(raw) {
|
|
3275
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
3276
|
+
const e = raw;
|
|
3277
|
+
const type = member(e.type, EVENT_TYPES);
|
|
3278
|
+
if (!type) return void 0;
|
|
3279
|
+
const minute = count(e.minute, MAX_MINUTE);
|
|
3280
|
+
if (minute === void 0) return void 0;
|
|
3281
|
+
const out = { type, minute, teamCode: humanLabel(e.teamCode, TEAM_CODE_COLUMNS) };
|
|
3282
|
+
const player = humanLabel(e.player);
|
|
3283
|
+
if (player) out.player = player;
|
|
3284
|
+
return out;
|
|
3285
|
+
}
|
|
3286
|
+
function sealMatch(parts, opts = {}) {
|
|
3287
|
+
if (!parts || typeof parts !== "object") return malformed("match is not an object");
|
|
3288
|
+
const id = opaqueId(parts.id, ESPN_ID);
|
|
3289
|
+
if (!id) return malformed("match id is absent or not an identifier");
|
|
3290
|
+
const kickoff = canonicalTimestamp(parts.kickoff);
|
|
3291
|
+
if (!kickoff) return malformed("match kickoff is absent or not one instant");
|
|
3292
|
+
const stage = member(parts.stage, STAGES);
|
|
3293
|
+
if (!stage) return malformed("match stage is not a known stage");
|
|
3294
|
+
const status = member(parts.status, STATUSES);
|
|
3295
|
+
if (!status) return malformed("match status is not a known status");
|
|
3296
|
+
const home = sealTeam(parts.home);
|
|
3297
|
+
const away = sealTeam(parts.away);
|
|
3298
|
+
if (!home || !away) return malformed("match does not name both teams");
|
|
3299
|
+
if (home.code === away.code && home.name === away.name) {
|
|
3300
|
+
return definitiveNone("both competitors are the same team");
|
|
3301
|
+
}
|
|
3302
|
+
const group = humanLabel(parts.group) || void 0;
|
|
3303
|
+
const city = humanLabel(parts.city) || void 0;
|
|
3304
|
+
const country = humanLabel(parts.country) || void 0;
|
|
3305
|
+
const score = status === "SCHEDULED" ? void 0 : sealScorePair(parts.score);
|
|
3306
|
+
if ((status === "LIVE" || status === "HT" || status === "FT") && !score) {
|
|
3307
|
+
return malformed("match claims an unreadable score");
|
|
3308
|
+
}
|
|
3309
|
+
const finished = status === "FT";
|
|
3310
|
+
const canGoToPenalties = stage !== "GROUP";
|
|
3311
|
+
const shootoutPresent = parts.shootout !== void 0 && parts.shootout !== null;
|
|
3312
|
+
const parsedShootout = shootoutPresent ? sealScorePair(parts.shootout) : void 0;
|
|
3313
|
+
const shootoutStatus = status === "LIVE" || status === "FT";
|
|
3314
|
+
const shootout = parsedShootout && score && canGoToPenalties && shootoutStatus && !(finished && parsedShootout.home === parsedShootout.away) ? parsedShootout : void 0;
|
|
3315
|
+
const claimedWinner = teamCode(parts.winnerCode, "");
|
|
3316
|
+
const claimedSide = claimedWinner === home.code ? "home" : claimedWinner === away.code ? "away" : void 0;
|
|
3317
|
+
let winnerCode;
|
|
3318
|
+
const unusableShootoutCouldDecide = canGoToPenalties && shootoutPresent && !shootout;
|
|
3319
|
+
if (claimedSide && score && finished && !unusableShootoutCouldDecide) {
|
|
3320
|
+
const level = score.home === score.away;
|
|
3321
|
+
const decider = shootout ?? score;
|
|
3322
|
+
if (decider && decider.home !== decider.away) {
|
|
3323
|
+
if ((decider.home > decider.away ? "home" : "away") === claimedSide) winnerCode = claimedWinner;
|
|
3324
|
+
} else if (level && !shootoutPresent && canGoToPenalties) {
|
|
3325
|
+
winnerCode = claimedWinner;
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
const events = opts.events === false ? [] : takeBounded(parts.events, MAX_MATCH_EVENTS).map(sealEvent).filter((e) => !!e);
|
|
3329
|
+
const out = { id, stage };
|
|
3330
|
+
if (group) out.group = group;
|
|
3331
|
+
out.kickoff = kickoff;
|
|
3332
|
+
out.venue = humanLabel(parts.venue);
|
|
3333
|
+
if (city) out.city = city;
|
|
3334
|
+
if (country) out.country = country;
|
|
3335
|
+
out.home = home;
|
|
3336
|
+
out.away = away;
|
|
3337
|
+
if (score) out.score = score;
|
|
3338
|
+
if (shootout) out.shootout = shootout;
|
|
3339
|
+
const minute = count(parts.minute, MAX_MINUTE);
|
|
3340
|
+
if (minute !== void 0) out.minute = minute;
|
|
3341
|
+
out.status = status;
|
|
3342
|
+
if (events.length) out.events = events;
|
|
3343
|
+
if (winnerCode) out.winnerCode = winnerCode;
|
|
3344
|
+
out.updatedAt = canonicalTimestamp(parts.updatedAt) ?? "";
|
|
3345
|
+
return valid(out);
|
|
3346
|
+
}
|
|
3347
|
+
function parseCachedMatch(raw, opts = {}) {
|
|
3348
|
+
if (!raw || typeof raw !== "object") return definitiveNone("cache entry is not an object");
|
|
3349
|
+
return sealMatch(raw, opts);
|
|
3350
|
+
}
|
|
3351
|
+
function parseCachedMatches(raw, max, opts = {}) {
|
|
3352
|
+
const total = Array.isArray(raw) ? raw.length : 0;
|
|
3353
|
+
const considered = takeBounded(raw, max);
|
|
3354
|
+
const items = considered.map((m) => parseCachedMatch(m, opts)).flatMap((r) => r.kind === "valid" ? [r.value] : []);
|
|
3355
|
+
return {
|
|
3356
|
+
items,
|
|
3357
|
+
total,
|
|
3358
|
+
shown: items.length,
|
|
3359
|
+
truncated: total > considered.length,
|
|
3360
|
+
// Some record in the window was unreadable, or the window did not cover the
|
|
3361
|
+
// file — either way we are not reporting on all of it.
|
|
3362
|
+
complete: items.length === total
|
|
3363
|
+
};
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
// src/trust/espn.ts
|
|
3367
|
+
var MAX_EVENTS = 300;
|
|
3368
|
+
var MAX_GROUPS = 16;
|
|
3369
|
+
var MAX_GROUP_ROWS = 32;
|
|
3370
|
+
var STAGES2 = /* @__PURE__ */ new Set(["GROUP", "R32", "R16", "QF", "SF", "3P", "F", "FRIENDLY"]);
|
|
3371
|
+
function teamNames(t2) {
|
|
3372
|
+
return [t2?.displayName, t2?.name, t2?.location, t2?.shortDisplayName, t2?.abbreviation].map((v) => humanLabel(v)).filter((v) => v !== "");
|
|
3373
|
+
}
|
|
3374
|
+
function toParticipant(raw) {
|
|
3375
|
+
if (!raw || typeof raw !== "object") return malformed("competitor is not an object");
|
|
3376
|
+
const names = teamNames(raw.team);
|
|
3377
|
+
if (names.length === 0) return malformed("competitor names no team");
|
|
3378
|
+
const name = names[0];
|
|
3379
|
+
const code = teamCode(raw.team?.abbreviation, name);
|
|
3380
|
+
const team = { code, name, flag: productFlag(name) };
|
|
3381
|
+
const providerId = opaqueId(raw.team?.id, ESPN_ID);
|
|
3382
|
+
const known = productFlag(name) !== nationToFlag("");
|
|
3383
|
+
return valid(
|
|
3384
|
+
providerId && known ? { kind: "team", providerId, team } : { kind: "slot", team }
|
|
3385
|
+
);
|
|
3386
|
+
}
|
|
3122
3387
|
function mapStatus(st) {
|
|
3123
|
-
const
|
|
3124
|
-
const
|
|
3388
|
+
const type = st?.type;
|
|
3389
|
+
const name = typeof type?.name === "string" ? type.name.toUpperCase() : "";
|
|
3390
|
+
const state = typeof type?.state === "string" ? type.state : "";
|
|
3125
3391
|
if (name.includes("HALFTIME")) return "HT";
|
|
3126
3392
|
if (name.includes("POSTPONED")) return "POSTPONED";
|
|
3127
3393
|
if (name.includes("CANCEL")) return "CANCELLED";
|
|
3128
3394
|
if (state === "pre") return "SCHEDULED";
|
|
3129
3395
|
if (state === "post") return "FT";
|
|
3130
3396
|
if (state === "in") return "LIVE";
|
|
3131
|
-
return
|
|
3397
|
+
return void 0;
|
|
3132
3398
|
}
|
|
3133
3399
|
function parseMinute(st) {
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
if (
|
|
3138
|
-
|
|
3400
|
+
const s = st;
|
|
3401
|
+
if (s?.type?.state !== "in") return void 0;
|
|
3402
|
+
const dc = typeof s.displayClock === "string" ? s.displayClock.match(/(\d+)/) : null;
|
|
3403
|
+
if (dc) return count(Number.parseInt(dc[1], 10), MAX_MINUTE);
|
|
3404
|
+
if (typeof s.clock === "number" && s.clock > 0) {
|
|
3405
|
+
const n = Math.floor(s.clock / 60);
|
|
3406
|
+
return n > 0 ? count(n, MAX_MINUTE) : void 0;
|
|
3139
3407
|
}
|
|
3140
3408
|
return void 0;
|
|
3141
3409
|
}
|
|
@@ -3149,58 +3417,74 @@ var SLUG_TO_STAGE = {
|
|
|
3149
3417
|
final: "F"
|
|
3150
3418
|
};
|
|
3151
3419
|
function stageFromSlug(slug) {
|
|
3152
|
-
if (slug
|
|
3153
|
-
if (
|
|
3420
|
+
if (slug == null || slug === "") return "GROUP";
|
|
3421
|
+
if (typeof slug === "string" && Object.hasOwn(SLUG_TO_STAGE, slug)) {
|
|
3422
|
+
const mapped = SLUG_TO_STAGE[slug];
|
|
3423
|
+
if (mapped) return mapped;
|
|
3424
|
+
}
|
|
3154
3425
|
return "FRIENDLY";
|
|
3155
3426
|
}
|
|
3156
|
-
function
|
|
3157
|
-
if (
|
|
3158
|
-
|
|
3159
|
-
return
|
|
3160
|
-
}
|
|
3161
|
-
function
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
);
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
}
|
|
3172
|
-
|
|
3173
|
-
const
|
|
3174
|
-
const
|
|
3175
|
-
|
|
3176
|
-
const
|
|
3427
|
+
function toGoals(v) {
|
|
3428
|
+
if (typeof v === "number") return count(v, MAX_GOALS);
|
|
3429
|
+
if (typeof v !== "string" || v.trim() === "") return void 0;
|
|
3430
|
+
return /^-?\d{1,9}$/.test(v.trim()) ? count(Number(v.trim()), MAX_GOALS) : void 0;
|
|
3431
|
+
}
|
|
3432
|
+
function parseEspnEvent(raw, ctx = {}) {
|
|
3433
|
+
if (!raw || typeof raw !== "object") return malformed("event is not an object");
|
|
3434
|
+
const ev = raw;
|
|
3435
|
+
const id = opaqueId(ev.id, ESPN_ID);
|
|
3436
|
+
if (!id) return malformed("event id is absent or not an identifier");
|
|
3437
|
+
const kickoff = canonicalTimestamp(ev.date);
|
|
3438
|
+
if (!kickoff) return malformed("event date is absent or not one instant");
|
|
3439
|
+
const comp = Array.isArray(ev.competitions) ? ev.competitions[0] : void 0;
|
|
3440
|
+
const rawCompetitors = takeBounded(comp?.competitors, 4);
|
|
3441
|
+
if (rawCompetitors.length !== 2) {
|
|
3442
|
+
return definitiveNone(`expected 2 competitors, found ${rawCompetitors.length}`);
|
|
3443
|
+
}
|
|
3444
|
+
const homeRaw = rawCompetitors.find((c) => c?.homeAway === "home");
|
|
3445
|
+
const awayRaw = rawCompetitors.find((c) => c?.homeAway === "away");
|
|
3446
|
+
if (!homeRaw || !awayRaw) return definitiveNone("competitors do not state home and away");
|
|
3447
|
+
const homeP = toParticipant(homeRaw);
|
|
3448
|
+
const awayP = toParticipant(awayRaw);
|
|
3449
|
+
if (homeP.kind !== "valid") return homeP;
|
|
3450
|
+
if (awayP.kind !== "valid") return awayP;
|
|
3451
|
+
const h = homeP.value;
|
|
3452
|
+
const a = awayP.value;
|
|
3453
|
+
if (h.kind === "team" && a.kind === "team" && h.providerId === a.providerId) {
|
|
3454
|
+
return definitiveNone("both competitors are the same team");
|
|
3455
|
+
}
|
|
3456
|
+
const home = homeP.value.team;
|
|
3457
|
+
const away = awayP.value.team;
|
|
3177
3458
|
const status = mapStatus(ev.status ?? comp?.status);
|
|
3178
|
-
|
|
3179
|
-
const
|
|
3180
|
-
const away = toTeam(awayC?.team);
|
|
3459
|
+
if (!status) return malformed("event status is not a status we recognize");
|
|
3460
|
+
const stage = member(stageFromSlug(ev.season?.slug), STAGES2) ?? "FRIENDLY";
|
|
3181
3461
|
let group;
|
|
3182
3462
|
if (stage === "GROUP" && ctx.groupByTeam) {
|
|
3183
3463
|
group = ctx.groupByTeam[home.code] ?? ctx.groupByTeam[away.code];
|
|
3184
3464
|
}
|
|
3185
|
-
const hs =
|
|
3186
|
-
const as =
|
|
3187
|
-
const
|
|
3465
|
+
const hs = toGoals(homeRaw.score);
|
|
3466
|
+
const as = toGoals(awayRaw.score);
|
|
3467
|
+
const scoreExpected = status === "LIVE" || status === "HT" || status === "FT";
|
|
3468
|
+
const hasScore = scoreExpected && hs !== void 0 && as !== void 0;
|
|
3469
|
+
if (scoreExpected && !hasScore) return malformed("event score is absent or unreadable");
|
|
3470
|
+
const hShoot = toGoals(homeRaw.shootoutScore);
|
|
3471
|
+
const aShoot = toGoals(awayRaw.shootoutScore);
|
|
3472
|
+
const shootoutPresent = homeRaw.shootoutScore !== void 0 || awayRaw.shootoutScore !== void 0;
|
|
3473
|
+
const shootout = shootoutPresent ? { home: hShoot, away: aShoot } : void 0;
|
|
3188
3474
|
let winnerCode;
|
|
3189
3475
|
if (isFinished(status)) {
|
|
3190
|
-
|
|
3191
|
-
|
|
3476
|
+
const winners = [homeRaw, awayRaw].filter((c) => flag(c.winner) === true);
|
|
3477
|
+
if (winners.length === 1) winnerCode = winners[0] === homeRaw ? home.code : away.code;
|
|
3192
3478
|
}
|
|
3193
|
-
const
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
return {
|
|
3197
|
-
id: ev.id,
|
|
3479
|
+
const venue = comp?.venue;
|
|
3480
|
+
return sealMatch({
|
|
3481
|
+
id,
|
|
3198
3482
|
stage,
|
|
3199
3483
|
group,
|
|
3200
|
-
kickoff
|
|
3201
|
-
venue:
|
|
3202
|
-
city:
|
|
3203
|
-
country:
|
|
3484
|
+
kickoff,
|
|
3485
|
+
venue: venue?.fullName,
|
|
3486
|
+
city: venue?.address?.city,
|
|
3487
|
+
country: venue?.address?.country,
|
|
3204
3488
|
home,
|
|
3205
3489
|
away,
|
|
3206
3490
|
score: hasScore ? { home: hs, away: as } : void 0,
|
|
@@ -3208,57 +3492,232 @@ function mapEspnEvent(ev, ctx = {}) {
|
|
|
3208
3492
|
minute: parseMinute(ev.status ?? comp?.status),
|
|
3209
3493
|
status,
|
|
3210
3494
|
winnerCode,
|
|
3211
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3212
|
-
|
|
3495
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3496
|
+
events: ev.events
|
|
3497
|
+
});
|
|
3213
3498
|
}
|
|
3214
|
-
function
|
|
3215
|
-
|
|
3499
|
+
function parseEspnEvents(raw, ctx = {}) {
|
|
3500
|
+
const all = raw?.events;
|
|
3501
|
+
const readable = Array.isArray(all);
|
|
3502
|
+
const total = readable ? all.length : 0;
|
|
3503
|
+
const considered = takeBounded(all, MAX_EVENTS);
|
|
3504
|
+
const items = [];
|
|
3505
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
3506
|
+
let complete = readable && considered.length === total;
|
|
3507
|
+
for (const event of considered) {
|
|
3508
|
+
const parsed = parseEspnEvent(event, ctx);
|
|
3509
|
+
if (parsed.kind !== "valid") {
|
|
3510
|
+
if (parsed.kind !== "definitive-none") complete = false;
|
|
3511
|
+
continue;
|
|
3512
|
+
}
|
|
3513
|
+
if (seenIds.has(parsed.value.id)) {
|
|
3514
|
+
complete = false;
|
|
3515
|
+
continue;
|
|
3516
|
+
}
|
|
3517
|
+
seenIds.add(parsed.value.id);
|
|
3518
|
+
items.push(parsed.value);
|
|
3519
|
+
}
|
|
3520
|
+
return {
|
|
3521
|
+
items,
|
|
3522
|
+
total,
|
|
3523
|
+
shown: items.length,
|
|
3524
|
+
truncated: total > considered.length,
|
|
3525
|
+
// Some record was unreadable, or the window did not cover the payload, or
|
|
3526
|
+
// the envelope itself was not a list — none of those is a complete account
|
|
3527
|
+
// of what the provider sent.
|
|
3528
|
+
complete
|
|
3529
|
+
};
|
|
3216
3530
|
}
|
|
3217
|
-
function statVal(stats, name) {
|
|
3218
|
-
|
|
3219
|
-
|
|
3531
|
+
function statVal(stats, name, signed = false) {
|
|
3532
|
+
if (!Array.isArray(stats) || stats.length > 64) return void 0;
|
|
3533
|
+
const matches = takeBounded(stats, 64).filter(
|
|
3534
|
+
(s) => s?.name === name
|
|
3535
|
+
);
|
|
3536
|
+
if (matches.length !== 1) return void 0;
|
|
3537
|
+
const v = matches[0]?.value;
|
|
3538
|
+
if (typeof v !== "number" || !Number.isFinite(v) || !Number.isInteger(v)) return void 0;
|
|
3539
|
+
const limit = 1e3;
|
|
3540
|
+
return v > limit || v < (signed ? -limit : 0) ? void 0 : v;
|
|
3541
|
+
}
|
|
3542
|
+
function optionalStatVal(stats, name) {
|
|
3543
|
+
if (!Array.isArray(stats) || stats.length > 64) return void 0;
|
|
3544
|
+
const matches = takeBounded(stats, 64).filter(
|
|
3545
|
+
(s) => s?.name === name
|
|
3546
|
+
);
|
|
3547
|
+
if (matches.length === 0) return 0;
|
|
3548
|
+
if (matches.length !== 1) return void 0;
|
|
3549
|
+
const v = matches[0]?.value;
|
|
3550
|
+
return typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e3 ? v : void 0;
|
|
3220
3551
|
}
|
|
3221
3552
|
function entryToRow(e) {
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3553
|
+
const names = teamNames(e?.team);
|
|
3554
|
+
if (names.length === 0) return definitiveNone("standings entry names no team");
|
|
3555
|
+
const name = names[0];
|
|
3556
|
+
const code = teamCode(e?.team?.abbreviation, name);
|
|
3557
|
+
const played = statVal(e.stats, "gamesPlayed");
|
|
3558
|
+
const won = statVal(e.stats, "wins");
|
|
3559
|
+
const drawn = statVal(e.stats, "ties");
|
|
3560
|
+
const lost = statVal(e.stats, "losses");
|
|
3561
|
+
const goalsFor = statVal(e.stats, "pointsFor");
|
|
3562
|
+
const goalsAgainst = statVal(e.stats, "pointsAgainst");
|
|
3563
|
+
const goalDiff = statVal(e.stats, "pointDifferential", true);
|
|
3564
|
+
const points = statVal(e.stats, "points", true);
|
|
3565
|
+
const deductions = optionalStatVal(e.stats, "deductions");
|
|
3566
|
+
const providerRank = statVal(e.stats, "rank");
|
|
3567
|
+
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) {
|
|
3568
|
+
return malformed("standings entry has missing or invalid statistics");
|
|
3569
|
+
}
|
|
3570
|
+
if (played !== won + drawn + lost) {
|
|
3571
|
+
return malformed("standings entry games do not add up");
|
|
3572
|
+
}
|
|
3573
|
+
if (goalDiff !== goalsFor - goalsAgainst) {
|
|
3574
|
+
return malformed("standings entry goal difference does not add up");
|
|
3575
|
+
}
|
|
3576
|
+
if (points !== won * 3 + drawn - deductions) {
|
|
3577
|
+
return malformed("standings entry points do not add up");
|
|
3578
|
+
}
|
|
3579
|
+
return valid({
|
|
3580
|
+
team: { code, name, flag: productFlag(name) },
|
|
3581
|
+
played,
|
|
3582
|
+
won,
|
|
3583
|
+
drawn,
|
|
3584
|
+
lost,
|
|
3585
|
+
goalsFor,
|
|
3586
|
+
goalsAgainst,
|
|
3587
|
+
goalDiff,
|
|
3588
|
+
points,
|
|
3589
|
+
providerId: opaqueId(e?.team?.id, ESPN_ID),
|
|
3590
|
+
providerRank
|
|
3591
|
+
});
|
|
3233
3592
|
}
|
|
3234
|
-
function
|
|
3593
|
+
function parseEspnStandings(raw) {
|
|
3594
|
+
const rawChildren = raw?.children;
|
|
3595
|
+
const readable = Array.isArray(rawChildren);
|
|
3596
|
+
const rawCount = readable ? rawChildren.length : 0;
|
|
3597
|
+
const children = takeBounded(rawChildren, MAX_GROUPS * 4);
|
|
3598
|
+
const sawAllChildren = rawCount === children.length;
|
|
3599
|
+
let rowsTruncated = false;
|
|
3600
|
+
let complete = readable && sawAllChildren;
|
|
3235
3601
|
const out = [];
|
|
3236
|
-
|
|
3237
|
-
|
|
3602
|
+
const seenGroups = /* @__PURE__ */ new Set();
|
|
3603
|
+
const seenProviderIds = /* @__PURE__ */ new Set();
|
|
3604
|
+
for (const child of children) {
|
|
3605
|
+
const label = humanLabel(child?.name ?? child?.abbreviation);
|
|
3606
|
+
const letter = label.match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
|
|
3238
3607
|
if (!letter) continue;
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
}
|
|
3608
|
+
if (seenGroups.has(letter)) {
|
|
3609
|
+
complete = false;
|
|
3610
|
+
continue;
|
|
3611
|
+
}
|
|
3612
|
+
seenGroups.add(letter);
|
|
3613
|
+
const rawEntries = child?.standings?.entries;
|
|
3614
|
+
if (!Array.isArray(rawEntries) || rawEntries.length === 0) {
|
|
3615
|
+
complete = false;
|
|
3616
|
+
continue;
|
|
3617
|
+
}
|
|
3618
|
+
if (rawEntries.length > MAX_GROUP_ROWS) {
|
|
3619
|
+
rowsTruncated = true;
|
|
3620
|
+
complete = false;
|
|
3621
|
+
}
|
|
3622
|
+
const entries = takeBounded(rawEntries, MAX_GROUP_ROWS);
|
|
3623
|
+
const seenCodes = /* @__PURE__ */ new Map();
|
|
3624
|
+
const seenRanks = /* @__PURE__ */ new Set();
|
|
3625
|
+
const ranked = [];
|
|
3626
|
+
for (const e of entries) {
|
|
3627
|
+
const r = entryToRow(e);
|
|
3628
|
+
if (r.kind !== "valid") {
|
|
3629
|
+
if (r.kind !== "definitive-none") complete = false;
|
|
3630
|
+
continue;
|
|
3631
|
+
}
|
|
3632
|
+
const { providerId, providerRank } = r.value;
|
|
3633
|
+
const code = r.value.team.code;
|
|
3634
|
+
const priorHadId = seenCodes.get(code);
|
|
3635
|
+
const codeCollision = priorHadId !== void 0 && (providerId === void 0 || priorHadId === false);
|
|
3636
|
+
if (codeCollision || seenRanks.has(providerRank) || providerId !== void 0 && seenProviderIds.has(providerId)) {
|
|
3637
|
+
complete = false;
|
|
3638
|
+
continue;
|
|
3639
|
+
}
|
|
3640
|
+
seenCodes.set(code, providerId !== void 0);
|
|
3641
|
+
seenRanks.add(providerRank);
|
|
3642
|
+
if (providerId !== void 0) seenProviderIds.add(providerId);
|
|
3643
|
+
const { providerId: _dropId, providerRank: rank, ...row } = r.value;
|
|
3644
|
+
ranked.push({ row, rank });
|
|
3645
|
+
}
|
|
3243
3646
|
ranked.sort((a, b) => {
|
|
3244
3647
|
if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
return
|
|
3648
|
+
if (b.row.points !== a.row.points) return b.row.points - a.row.points;
|
|
3649
|
+
if (b.row.goalDiff !== a.row.goalDiff) return b.row.goalDiff - a.row.goalDiff;
|
|
3650
|
+
return b.row.goalsFor - a.row.goalsFor;
|
|
3248
3651
|
});
|
|
3652
|
+
if (ranked.length === 0) {
|
|
3653
|
+
complete = false;
|
|
3654
|
+
continue;
|
|
3655
|
+
}
|
|
3249
3656
|
out.push({ group: letter, rows: ranked.map((x) => x.row) });
|
|
3250
3657
|
}
|
|
3251
|
-
|
|
3252
|
-
|
|
3658
|
+
return {
|
|
3659
|
+
items: out,
|
|
3660
|
+
total: seenGroups.size,
|
|
3661
|
+
shown: out.length,
|
|
3662
|
+
// We stopped early if the child list or any single group's rows were cut.
|
|
3663
|
+
truncated: !sawAllChildren || rowsTruncated,
|
|
3664
|
+
complete: complete && !rowsTruncated
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
// src/adapters/espn.ts
|
|
3669
|
+
var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
|
|
3670
|
+
var DEFAULT_COMPETITION = "fifa.world";
|
|
3671
|
+
var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
|
|
3672
|
+
var USER_AGENT = `claudinho/${"0.10.0"} (+https://github.com/arturogarrido/claudinho)`;
|
|
3673
|
+
var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
3674
|
+
function competitionBase(slug) {
|
|
3675
|
+
return `${ESPN_SOCCER}/${slug}`;
|
|
3676
|
+
}
|
|
3677
|
+
var DEFAULT_TIMEOUT_MS = 6e3;
|
|
3678
|
+
var STANDINGS_SHARE_MS = 3e4;
|
|
3679
|
+
var ProviderError = class extends Error {
|
|
3680
|
+
kind;
|
|
3681
|
+
status;
|
|
3682
|
+
constructor(message, kind, status) {
|
|
3683
|
+
super(message);
|
|
3684
|
+
this.name = "ProviderError";
|
|
3685
|
+
this.kind = kind;
|
|
3686
|
+
this.status = status;
|
|
3687
|
+
}
|
|
3688
|
+
/** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
|
|
3689
|
+
get throttled() {
|
|
3690
|
+
return this.kind === "http" && (this.status === 429 || this.status === 403);
|
|
3691
|
+
}
|
|
3692
|
+
};
|
|
3693
|
+
function toEspnDate(d) {
|
|
3694
|
+
return d.replace(/\D/g, "").slice(0, 8);
|
|
3695
|
+
}
|
|
3696
|
+
function mapEspnEvent(ev, ctx = {}) {
|
|
3697
|
+
return parsedValue(parseEspnEvent(ev, ctx));
|
|
3698
|
+
}
|
|
3699
|
+
function parseStandings(data) {
|
|
3700
|
+
return [...parseEspnStandings(data).items];
|
|
3701
|
+
}
|
|
3702
|
+
function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length > 0) {
|
|
3703
|
+
if (!hasUsableRecord && (!parsed.complete || parsed.total > 0)) {
|
|
3704
|
+
throw new ProviderError(`ESPN ${kind} payload had no readable records`, "parse");
|
|
3705
|
+
}
|
|
3706
|
+
return [...parsed.items];
|
|
3253
3707
|
}
|
|
3254
3708
|
var EspnAdapter = class {
|
|
3255
3709
|
constructor(opts = {}) {
|
|
3256
3710
|
this.opts = opts;
|
|
3711
|
+
const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
|
|
3712
|
+
this.expectedStandingsGroups = expected ? [...expected] : void 0;
|
|
3713
|
+
this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
|
|
3257
3714
|
}
|
|
3258
3715
|
opts;
|
|
3259
3716
|
name = "espn";
|
|
3260
3717
|
capabilities = { push: false, latencyHintSec: 45 };
|
|
3261
|
-
|
|
3718
|
+
expectedStandingsGroups;
|
|
3719
|
+
standingsFallbackGroups;
|
|
3720
|
+
/** Short-lived team-code -> group-letter map (built lazily from standings). */
|
|
3262
3721
|
groupMap;
|
|
3263
3722
|
/**
|
|
3264
3723
|
* One in-flight/recent standings fetch shared by fetchStandings and
|
|
@@ -3301,38 +3760,48 @@ var EspnAdapter = class {
|
|
|
3301
3760
|
if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
|
|
3302
3761
|
return this.standingsShared.promise;
|
|
3303
3762
|
}
|
|
3304
|
-
const promise = this.get(this.standingsUrl()).then(
|
|
3305
|
-
|
|
3306
|
-
|
|
3763
|
+
const promise = this.get(this.standingsUrl()).then((d) => {
|
|
3764
|
+
const parsed = parseEspnStandings(d);
|
|
3765
|
+
return usableProviderItems(
|
|
3766
|
+
"standings",
|
|
3767
|
+
parsed,
|
|
3768
|
+
parsed.items.some((table) => table.rows.length > 0)
|
|
3769
|
+
);
|
|
3770
|
+
});
|
|
3307
3771
|
this.standingsShared = { at: now, promise };
|
|
3308
|
-
promise.catch(() => {
|
|
3772
|
+
void promise.catch(() => {
|
|
3309
3773
|
if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
|
|
3310
3774
|
});
|
|
3311
3775
|
return promise;
|
|
3312
3776
|
}
|
|
3313
3777
|
/**
|
|
3314
3778
|
* Authoritative, cumulative group tables from the standings endpoint. Throws
|
|
3315
|
-
* on fetch
|
|
3316
|
-
*
|
|
3779
|
+
* on fetch failure. Group-stage only: non-group `children` are filtered out
|
|
3780
|
+
* by {@link parseStandings}; malformed rows are omitted without hiding their
|
|
3781
|
+
* readable siblings.
|
|
3317
3782
|
*/
|
|
3318
3783
|
async fetchStandings() {
|
|
3319
3784
|
return this.sharedStandings();
|
|
3320
3785
|
}
|
|
3321
3786
|
/**
|
|
3322
|
-
* Build (and cache) a team-code -> group-letter map from the standings
|
|
3787
|
+
* Build (and briefly cache) a team-code -> group-letter map from the standings
|
|
3323
3788
|
* endpoint. Best-effort: returns {} if standings are unavailable — but a
|
|
3324
|
-
* transient failure is NOT cached
|
|
3325
|
-
*
|
|
3789
|
+
* transient failure is NOT cached, and a partial successful parse expires at
|
|
3790
|
+
* the standings TTL, so neither can silently drop group letters for the
|
|
3791
|
+
* adapter's lifetime.
|
|
3326
3792
|
* Reuses the same parse/fetch as {@link fetchStandings}, so the two never
|
|
3327
3793
|
* drift and one command never fetches standings twice.
|
|
3328
3794
|
*/
|
|
3329
3795
|
async fetchGroupMap(force = false) {
|
|
3330
|
-
|
|
3796
|
+
const now = Date.now();
|
|
3797
|
+
if (!force && this.groupMap && now - this.groupMap.at < STANDINGS_SHARE_MS) {
|
|
3798
|
+
return this.groupMap.value;
|
|
3799
|
+
}
|
|
3331
3800
|
try {
|
|
3332
3801
|
const tables = await this.sharedStandings();
|
|
3333
3802
|
const map = {};
|
|
3334
3803
|
for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
|
|
3335
|
-
this.groupMap = map;
|
|
3804
|
+
this.groupMap = { at: Date.now(), value: map };
|
|
3336
3805
|
return map;
|
|
3337
3806
|
} catch {
|
|
3338
3807
|
return {};
|
|
@@ -3347,7 +3816,8 @@ var EspnAdapter = class {
|
|
|
3347
3816
|
this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
|
|
3348
3817
|
this.get(url.toString())
|
|
3349
3818
|
]);
|
|
3350
|
-
|
|
3819
|
+
const parsed = parseEspnEvents(data, { groupByTeam });
|
|
3820
|
+
return usableProviderItems("scoreboard", parsed);
|
|
3351
3821
|
}
|
|
3352
3822
|
async get(url) {
|
|
3353
3823
|
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
@@ -3539,17 +4009,17 @@ function validateWinnerChain(nodes, problems) {
|
|
|
3539
4009
|
{ from: "QF", to: "SF", count: 4 },
|
|
3540
4010
|
{ from: "SF", to: "F", count: 2 }
|
|
3541
4011
|
];
|
|
3542
|
-
for (const { from, to, count } of chains) {
|
|
4012
|
+
for (const { from, to, count: count2 } of chains) {
|
|
3543
4013
|
const refs = nodes.filter((n) => n.stage === to).flatMap((n) => [n.home, n.away]).filter((r) => r.kind === "winner" && r.stage === from);
|
|
3544
4014
|
const indices = refs.map((r) => r.index);
|
|
3545
|
-
if (indices.length !==
|
|
3546
|
-
problems.push(`${to}: expected ${
|
|
4015
|
+
if (indices.length !== count2) {
|
|
4016
|
+
problems.push(`${to}: expected ${count2} ${from} winner refs, got ${indices.length}`);
|
|
3547
4017
|
}
|
|
3548
4018
|
const unique = new Set(indices);
|
|
3549
|
-
if (unique.size !==
|
|
3550
|
-
problems.push(`${to}: ${from} winner indices must be unique 1..${
|
|
4019
|
+
if (unique.size !== count2) {
|
|
4020
|
+
problems.push(`${to}: ${from} winner indices must be unique 1..${count2}`);
|
|
3551
4021
|
}
|
|
3552
|
-
for (let i = 1; i <=
|
|
4022
|
+
for (let i = 1; i <= count2; i++) {
|
|
3553
4023
|
if (!unique.has(i)) problems.push(`${to}: missing ${from} winner index ${i}`);
|
|
3554
4024
|
if (!indexMap.has(matchKey(from, i))) problems.push(`missing ${from} match index ${i}`);
|
|
3555
4025
|
}
|
|
@@ -4284,7 +4754,7 @@ function loadBracketTopology() {
|
|
|
4284
4754
|
return TOPOLOGY;
|
|
4285
4755
|
}
|
|
4286
4756
|
|
|
4287
|
-
// src/
|
|
4757
|
+
// src/competition.ts
|
|
4288
4758
|
function resolveCompetition(explicit) {
|
|
4289
4759
|
if (explicit) return explicit;
|
|
4290
4760
|
if (typeof process !== "undefined" && process.env?.CLAUDINHO_COMPETITION) {
|
|
@@ -4292,13 +4762,15 @@ function resolveCompetition(explicit) {
|
|
|
4292
4762
|
}
|
|
4293
4763
|
return DEFAULT_COMPETITION;
|
|
4294
4764
|
}
|
|
4765
|
+
|
|
4766
|
+
// src/live.ts
|
|
4295
4767
|
var KNOWN_SOURCES = ["espn"];
|
|
4296
|
-
function makeAdapter(source = "espn") {
|
|
4768
|
+
function makeAdapter(source = "espn", opts = {}) {
|
|
4297
4769
|
switch (source) {
|
|
4298
4770
|
case "espn": {
|
|
4299
4771
|
const competition = resolveCompetition();
|
|
4300
4772
|
const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
|
|
4301
|
-
return new EspnAdapter({ baseUrl });
|
|
4773
|
+
return new EspnAdapter({ baseUrl, enrichGroups: opts.enrichGroups });
|
|
4302
4774
|
}
|
|
4303
4775
|
default:
|
|
4304
4776
|
throw new Error(
|
|
@@ -4327,17 +4799,26 @@ async function getMatchesForDate(adapter, dateISO) {
|
|
|
4327
4799
|
}
|
|
4328
4800
|
async function getStandings(adapter, group) {
|
|
4329
4801
|
const want = group?.toUpperCase();
|
|
4802
|
+
const expected = adapter.expectedStandingsGroups;
|
|
4803
|
+
if (want && expected && !expected.includes(want)) {
|
|
4804
|
+
return { tables: [], degraded: false };
|
|
4805
|
+
}
|
|
4330
4806
|
if (adapter.fetchStandings) {
|
|
4331
4807
|
try {
|
|
4332
4808
|
const all = await adapter.fetchStandings();
|
|
4333
4809
|
const tables2 = (want ? all.filter((t2) => t2.group === want) : all).sort(
|
|
4334
4810
|
(a, b) => a.group.localeCompare(b.group)
|
|
4335
4811
|
);
|
|
4336
|
-
|
|
4812
|
+
const availableGroups = new Set(tables2.map((table) => table.group));
|
|
4813
|
+
const expectedGroupWasOmitted = want ? (expected?.includes(want) ?? false) && tables2.length === 0 : expected?.some((group2) => !availableGroups.has(group2)) ?? false;
|
|
4814
|
+
if (!expectedGroupWasOmitted) {
|
|
4815
|
+
return { tables: tables2, degraded: false, source: adapter.name };
|
|
4816
|
+
}
|
|
4337
4817
|
} catch {
|
|
4338
4818
|
}
|
|
4339
4819
|
}
|
|
4340
|
-
const
|
|
4820
|
+
const fallbackGroups = adapter.standingsFallbackGroups;
|
|
4821
|
+
const letters = fallbackGroups ? want ? fallbackGroups.includes(want) ? [want] : [] : [...new Set(fallbackGroups)].sort((a, b) => a.localeCompare(b)) : [];
|
|
4341
4822
|
const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
|
|
4342
4823
|
return { tables, degraded: true };
|
|
4343
4824
|
}
|
|
@@ -4394,7 +4875,10 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
|
|
|
4394
4875
|
try {
|
|
4395
4876
|
const win = knockoutWindow();
|
|
4396
4877
|
if (adapter.fetchWindow && win) {
|
|
4397
|
-
fixtures = mergeLive(
|
|
4878
|
+
fixtures = mergeLive(
|
|
4879
|
+
fixtures,
|
|
4880
|
+
await adapter.fetchWindow(win.start, win.end)
|
|
4881
|
+
);
|
|
4398
4882
|
}
|
|
4399
4883
|
} catch {
|
|
4400
4884
|
overlayFailed = true;
|
|
@@ -4458,15 +4942,149 @@ async function getMatchById(adapter, id) {
|
|
|
4458
4942
|
async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
|
|
4459
4943
|
try {
|
|
4460
4944
|
const day = now.toISOString().slice(0, 10);
|
|
4461
|
-
const matches = adapter.fetchWindow ?
|
|
4462
|
-
(m) => isLive(m.status)
|
|
4463
|
-
) : await adapter.fetchLive();
|
|
4945
|
+
const matches = (adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchLive()).filter((m) => isLive(m.status));
|
|
4464
4946
|
return { matches, degraded: false, source: adapter.name };
|
|
4465
4947
|
} catch {
|
|
4466
4948
|
return { matches: [], degraded: true };
|
|
4467
4949
|
}
|
|
4468
4950
|
}
|
|
4469
4951
|
|
|
4952
|
+
// src/markets/format.ts
|
|
4953
|
+
function pct(p) {
|
|
4954
|
+
return Math.round(p * 100);
|
|
4955
|
+
}
|
|
4956
|
+
var KNOWN_MARKET_SOURCES = ["polymarket", "fake"];
|
|
4957
|
+
function marketSourceLabel(source) {
|
|
4958
|
+
if (source === "polymarket") return "Polymarket";
|
|
4959
|
+
if (source === "fake") return "demo data";
|
|
4960
|
+
return source.charAt(0).toUpperCase() + source.slice(1);
|
|
4961
|
+
}
|
|
4962
|
+
function outcomeLabel(o, match) {
|
|
4963
|
+
if (o.kind === "home") return match.home.name;
|
|
4964
|
+
if (o.kind === "away") return match.away.name;
|
|
4965
|
+
if (o.kind === "draw") return "Draw";
|
|
4966
|
+
return o.label;
|
|
4967
|
+
}
|
|
4968
|
+
function utcHhmm(iso) {
|
|
4969
|
+
const t2 = Date.parse(iso);
|
|
4970
|
+
if (!Number.isFinite(t2)) return "";
|
|
4971
|
+
return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
|
|
4972
|
+
}
|
|
4973
|
+
function marketFavoriteText(signal, match) {
|
|
4974
|
+
const fav = signal.favorite;
|
|
4975
|
+
if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
|
|
4976
|
+
if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
|
|
4977
|
+
const name = fav.kind === "home" ? match.home.name : match.away.name;
|
|
4978
|
+
return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
|
|
4979
|
+
}
|
|
4980
|
+
function marketProbabilityText(signal, match) {
|
|
4981
|
+
const order = ["home", "draw", "away"];
|
|
4982
|
+
const parts = [];
|
|
4983
|
+
for (const kind of order) {
|
|
4984
|
+
const o = signal.outcomes.find((x) => x.kind === kind);
|
|
4985
|
+
if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
4986
|
+
}
|
|
4987
|
+
for (const o of signal.outcomes) {
|
|
4988
|
+
if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
4989
|
+
}
|
|
4990
|
+
return parts.join(" \xB7 ");
|
|
4991
|
+
}
|
|
4992
|
+
function marketAttributionText(signal) {
|
|
4993
|
+
const time = utcHhmm(signal.asOf);
|
|
4994
|
+
const src = `Source: ${marketSourceLabel(signal.source)}`;
|
|
4995
|
+
return time ? `${src} \xB7 updated ${time}` : src;
|
|
4996
|
+
}
|
|
4997
|
+
function marketLine(signal, match) {
|
|
4998
|
+
return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
|
|
4999
|
+
signal.source
|
|
5000
|
+
)} \xB7 informational only`;
|
|
5001
|
+
}
|
|
5002
|
+
function marketBlock(signal, match) {
|
|
5003
|
+
const lines = [];
|
|
5004
|
+
if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
|
|
5005
|
+
lines.push(marketFavoriteText(signal, match));
|
|
5006
|
+
lines.push(marketProbabilityText(signal, match));
|
|
5007
|
+
lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
|
|
5008
|
+
return lines;
|
|
5009
|
+
}
|
|
5010
|
+
|
|
5011
|
+
// src/trust/market.ts
|
|
5012
|
+
var MAX_OUTCOMES = 128;
|
|
5013
|
+
var MATCH_ID = /^[0-9]{1,20}$/;
|
|
5014
|
+
var MARKET_ID = /^(?:[0-9]{1,32}|fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2})$/;
|
|
5015
|
+
var OUTCOME_KINDS = /* @__PURE__ */ new Set(["home", "draw", "away", "other"]);
|
|
5016
|
+
var TEAM_CODE_COLUMNS2 = 8;
|
|
5017
|
+
function sealOutcome(raw) {
|
|
5018
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
5019
|
+
const o = raw;
|
|
5020
|
+
const kind = member(o.kind, OUTCOME_KINDS);
|
|
5021
|
+
const p = probability(o.probability);
|
|
5022
|
+
if (!kind || p === void 0) return void 0;
|
|
5023
|
+
const out = { kind };
|
|
5024
|
+
if (o.teamCode !== void 0) {
|
|
5025
|
+
if (typeof o.teamCode !== "string") return void 0;
|
|
5026
|
+
out.teamCode = humanLabel(o.teamCode, TEAM_CODE_COLUMNS2);
|
|
5027
|
+
}
|
|
5028
|
+
out.label = humanLabel(o.label);
|
|
5029
|
+
out.probability = p;
|
|
5030
|
+
if ((out.kind === "home" || out.kind === "away") && !out.teamCode) return void 0;
|
|
5031
|
+
return out;
|
|
5032
|
+
}
|
|
5033
|
+
function hasDuplicateKind(outcomes) {
|
|
5034
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5035
|
+
for (const o of outcomes) {
|
|
5036
|
+
if (o.kind === "other") continue;
|
|
5037
|
+
if (seen.has(o.kind)) return true;
|
|
5038
|
+
seen.add(o.kind);
|
|
5039
|
+
}
|
|
5040
|
+
return false;
|
|
5041
|
+
}
|
|
5042
|
+
function sealMarketSignal(raw, options = {}) {
|
|
5043
|
+
if (!raw || typeof raw !== "object") return malformed("signal is not an object");
|
|
5044
|
+
const s = raw;
|
|
5045
|
+
const matchId = opaqueId(s.matchId, MATCH_ID);
|
|
5046
|
+
if (!matchId) return malformed("signal names no fixture");
|
|
5047
|
+
if (!Array.isArray(s.outcomes) || s.outcomes.length > MAX_OUTCOMES) {
|
|
5048
|
+
return malformed("signal outcomes are absent or exceed the cap");
|
|
5049
|
+
}
|
|
5050
|
+
const outcomes = [];
|
|
5051
|
+
for (const rawOutcome of takeBounded(s.outcomes, MAX_OUTCOMES)) {
|
|
5052
|
+
const outcome = sealOutcome(rawOutcome);
|
|
5053
|
+
if (!outcome) return malformed("signal carries an unreadable outcome");
|
|
5054
|
+
outcomes.push(outcome);
|
|
5055
|
+
}
|
|
5056
|
+
if (hasDuplicateKind(outcomes)) {
|
|
5057
|
+
return ambiguous("two outcomes claim the same result");
|
|
5058
|
+
}
|
|
5059
|
+
const sourceMarketId = opaqueId(s.sourceMarketId, MARKET_ID);
|
|
5060
|
+
const liquidity = quantity(s.liquidity);
|
|
5061
|
+
const volume24h = quantity(s.volume24h);
|
|
5062
|
+
const out = {
|
|
5063
|
+
matchId,
|
|
5064
|
+
// Allow-listed, not merely stripped: this lands in the provider-attribution
|
|
5065
|
+
// slot, where `marketSourceLabel` falls through to the raw string for an
|
|
5066
|
+
// unrecognized provider — attacker prose where the reader expects
|
|
5067
|
+
// "Polymarket".
|
|
5068
|
+
source: member(s.source, new Set(KNOWN_MARKET_SOURCES)) ?? ""
|
|
5069
|
+
};
|
|
5070
|
+
if (sourceMarketId) out.sourceMarketId = sourceMarketId;
|
|
5071
|
+
out.asOf = canonicalTimestamp(s.asOf) ?? "";
|
|
5072
|
+
out.fetchedAt = canonicalTimestamp(s.fetchedAt) ?? "";
|
|
5073
|
+
out.outcomes = outcomes;
|
|
5074
|
+
const isAmbiguous = s.ambiguous !== false;
|
|
5075
|
+
const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
|
|
5076
|
+
if (favorite) out.favorite = favorite;
|
|
5077
|
+
if (liquidity !== void 0) out.liquidity = liquidity;
|
|
5078
|
+
if (volume24h !== void 0) out.volume24h = volume24h;
|
|
5079
|
+
out.stale = s.stale !== false;
|
|
5080
|
+
out.ambiguous = isAmbiguous || out.source === "";
|
|
5081
|
+
out.stale = out.stale || isStaleSignal(out, { now: options.now, maxAgeMs: options.maxAgeMs });
|
|
5082
|
+
return valid(out);
|
|
5083
|
+
}
|
|
5084
|
+
function parseCachedMarketSignal(raw, options = {}) {
|
|
5085
|
+
return sealMarketSignal(raw, options);
|
|
5086
|
+
}
|
|
5087
|
+
|
|
4470
5088
|
// src/markets/normalize.ts
|
|
4471
5089
|
var DEFAULT_MAX_AGE_MS = 15 * 6e4;
|
|
4472
5090
|
function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
|
|
@@ -4486,9 +5104,9 @@ function normalizeOutcomes(outcomes) {
|
|
|
4486
5104
|
probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
|
|
4487
5105
|
}));
|
|
4488
5106
|
}
|
|
4489
|
-
function favoriteStrength(
|
|
4490
|
-
if (
|
|
4491
|
-
if (
|
|
5107
|
+
function favoriteStrength(probability2) {
|
|
5108
|
+
if (probability2 >= 0.65) return "clear";
|
|
5109
|
+
if (probability2 >= 0.52) return "slight";
|
|
4492
5110
|
return "close";
|
|
4493
5111
|
}
|
|
4494
5112
|
function deriveFavorite(outcomes) {
|
|
@@ -4507,14 +5125,16 @@ function deriveFavorite(outcomes) {
|
|
|
4507
5125
|
}
|
|
4508
5126
|
function mapsCleanly(match, outcomes) {
|
|
4509
5127
|
if (outcomes.some((o) => o.kind === "other")) return false;
|
|
5128
|
+
const kinds = outcomes.map((o) => o.kind);
|
|
5129
|
+
if (new Set(kinds).size !== kinds.length) return false;
|
|
4510
5130
|
const home = outcomes.find((o) => o.kind === "home");
|
|
4511
5131
|
const away = outcomes.find((o) => o.kind === "away");
|
|
4512
5132
|
const draw = outcomes.find((o) => o.kind === "draw");
|
|
4513
5133
|
if (!home || !away) return false;
|
|
4514
|
-
if (home.teamCode
|
|
5134
|
+
if (!home.teamCode || home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
|
|
4515
5135
|
return false;
|
|
4516
5136
|
}
|
|
4517
|
-
if (away.teamCode
|
|
5137
|
+
if (!away.teamCode || away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
|
|
4518
5138
|
return false;
|
|
4519
5139
|
}
|
|
4520
5140
|
if (match.stage === "GROUP" && !draw) return false;
|
|
@@ -4529,11 +5149,13 @@ function hasSaneDistribution(outcomes) {
|
|
|
4529
5149
|
const sum = priced.reduce((s, o) => s + o.probability, 0);
|
|
4530
5150
|
return sum > 0.97 && sum < 1.03;
|
|
4531
5151
|
}
|
|
5152
|
+
var FUTURE_SKEW_MS = 6e4;
|
|
4532
5153
|
function isStaleSignal(signal, options = {}) {
|
|
4533
5154
|
const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
4534
5155
|
const asOf = Date.parse(signal.asOf);
|
|
4535
5156
|
if (!Number.isFinite(asOf)) return true;
|
|
4536
5157
|
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
5158
|
+
if (asOf - now > FUTURE_SKEW_MS) return true;
|
|
4537
5159
|
return now - asOf > maxAge;
|
|
4538
5160
|
}
|
|
4539
5161
|
function isReliableMarketSignal(signal, options = {}) {
|
|
@@ -4549,8 +5171,8 @@ function isReliableMarketSignal(signal, options = {}) {
|
|
|
4549
5171
|
}
|
|
4550
5172
|
function buildMarketSignal(input) {
|
|
4551
5173
|
const outcomes = normalizeOutcomes(input.outcomes);
|
|
4552
|
-
const
|
|
4553
|
-
const favorite =
|
|
5174
|
+
const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
|
|
5175
|
+
const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
|
|
4554
5176
|
const signal = {
|
|
4555
5177
|
matchId: input.match.id,
|
|
4556
5178
|
source: input.source,
|
|
@@ -4562,68 +5184,35 @@ function buildMarketSignal(input) {
|
|
|
4562
5184
|
liquidity: input.liquidity,
|
|
4563
5185
|
volume24h: input.volume24h,
|
|
4564
5186
|
stale: false,
|
|
4565
|
-
ambiguous
|
|
5187
|
+
ambiguous: ambiguous2
|
|
4566
5188
|
};
|
|
4567
5189
|
signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
// src/markets/format.ts
|
|
4572
|
-
function pct(p) {
|
|
4573
|
-
return Math.round(p * 100);
|
|
4574
|
-
}
|
|
4575
|
-
function marketSourceLabel(source) {
|
|
4576
|
-
if (source === "polymarket") return "Polymarket";
|
|
4577
|
-
if (source === "fake") return "demo data";
|
|
4578
|
-
return source.charAt(0).toUpperCase() + source.slice(1);
|
|
4579
|
-
}
|
|
4580
|
-
function outcomeLabel(o, match) {
|
|
4581
|
-
if (o.kind === "home") return match.home.name;
|
|
4582
|
-
if (o.kind === "away") return match.away.name;
|
|
4583
|
-
if (o.kind === "draw") return "Draw";
|
|
4584
|
-
return o.label;
|
|
4585
|
-
}
|
|
4586
|
-
function utcHhmm(iso) {
|
|
4587
|
-
const t2 = Date.parse(iso);
|
|
4588
|
-
if (!Number.isFinite(t2)) return "";
|
|
4589
|
-
return `${new Date(t2).toISOString().slice(11, 16)} UTC`;
|
|
4590
|
-
}
|
|
4591
|
-
function marketFavoriteText(signal, match) {
|
|
4592
|
-
const fav = signal.favorite;
|
|
4593
|
-
if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
|
|
4594
|
-
if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
|
|
4595
|
-
const name = fav.kind === "home" ? match.home.name : match.away.name;
|
|
4596
|
-
return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
|
|
4597
|
-
}
|
|
4598
|
-
function marketProbabilityText(signal, match) {
|
|
4599
|
-
const order = ["home", "draw", "away"];
|
|
4600
|
-
const parts = [];
|
|
4601
|
-
for (const kind of order) {
|
|
4602
|
-
const o = signal.outcomes.find((x) => x.kind === kind);
|
|
4603
|
-
if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
5190
|
+
const sealed = sealMarketSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
|
|
5191
|
+
if (sealed.kind !== "valid") {
|
|
5192
|
+
return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
|
|
4604
5193
|
}
|
|
4605
|
-
|
|
4606
|
-
if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
4607
|
-
}
|
|
4608
|
-
return parts.join(" \xB7 ");
|
|
5194
|
+
return { ...sealed.value, ambiguous: sealed.value.ambiguous || ambiguous2 };
|
|
4609
5195
|
}
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
5196
|
+
|
|
5197
|
+
// src/trust/batch.ts
|
|
5198
|
+
var NONE = { kind: "none" };
|
|
5199
|
+
function selectOne(candidates) {
|
|
5200
|
+
if (candidates.length === 1) return { kind: "one", value: candidates[0] };
|
|
5201
|
+
if (candidates.length === 0) return NONE;
|
|
5202
|
+
return { kind: "ambiguous", count: candidates.length };
|
|
5203
|
+
}
|
|
5204
|
+
function resolvedValues(batch) {
|
|
5205
|
+
const out = /* @__PURE__ */ new Map();
|
|
5206
|
+
for (const [key, r] of batch.results) if (r.kind === "valid") out.set(key, r.value);
|
|
5207
|
+
return out;
|
|
4614
5208
|
}
|
|
4615
|
-
function
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
5209
|
+
function cacheableKeys(batch) {
|
|
5210
|
+
const out = /* @__PURE__ */ new Set();
|
|
5211
|
+
for (const [key, r] of batch.results) if (isCacheable(r)) out.add(key);
|
|
5212
|
+
return out;
|
|
4619
5213
|
}
|
|
4620
|
-
function
|
|
4621
|
-
|
|
4622
|
-
if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
|
|
4623
|
-
lines.push(marketFavoriteText(signal, match));
|
|
4624
|
-
lines.push(marketProbabilityText(signal, match));
|
|
4625
|
-
lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
|
|
4626
|
-
return lines;
|
|
5214
|
+
function emptyBatch() {
|
|
5215
|
+
return { results: /* @__PURE__ */ new Map(), complete: false };
|
|
4627
5216
|
}
|
|
4628
5217
|
|
|
4629
5218
|
// src/markets/fake.ts
|
|
@@ -4640,14 +5229,12 @@ var FakeMarketProvider = class {
|
|
|
4640
5229
|
return void 0;
|
|
4641
5230
|
}
|
|
4642
5231
|
async findSignals(matches, options) {
|
|
4643
|
-
const
|
|
4644
|
-
const checked = /* @__PURE__ */ new Set();
|
|
5232
|
+
const results = /* @__PURE__ */ new Map();
|
|
4645
5233
|
for (const m of matches) {
|
|
4646
|
-
checked.add(m.id);
|
|
4647
5234
|
const s = await this.findSignal(m, options);
|
|
4648
|
-
|
|
5235
|
+
results.set(m.id, s ? valid(s) : definitiveNone("fake provider has no signal"));
|
|
4649
5236
|
}
|
|
4650
|
-
return {
|
|
5237
|
+
return { results, complete: true };
|
|
4651
5238
|
}
|
|
4652
5239
|
synthesize(match, options) {
|
|
4653
5240
|
const seed = hash(`${match.home.code}-${match.away.code}`);
|
|
@@ -4664,7 +5251,9 @@ var FakeMarketProvider = class {
|
|
|
4664
5251
|
return buildMarketSignal({
|
|
4665
5252
|
match,
|
|
4666
5253
|
source: "fake",
|
|
4667
|
-
|
|
5254
|
+
// Must satisfy the boundary's opaque-id grammar, like a real one:
|
|
5255
|
+
// a source id that only the live path accepts is the asymmetry itself.
|
|
5256
|
+
sourceMarketId: match.id,
|
|
4668
5257
|
asOf,
|
|
4669
5258
|
fetchedAt: now.toISOString(),
|
|
4670
5259
|
outcomes,
|
|
@@ -4692,6 +5281,8 @@ var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
|
|
|
4692
5281
|
var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
|
|
4693
5282
|
var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
|
|
4694
5283
|
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
5284
|
+
var MAX_EVENT_MARKETS = 256;
|
|
5285
|
+
var DEFAULT_DEADLINE_MS = 15e3;
|
|
4695
5286
|
var WC_SERIES_SLUG = "soccer-fifwc";
|
|
4696
5287
|
var WC_SPORT = "fifwc";
|
|
4697
5288
|
var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
|
|
@@ -4704,49 +5295,81 @@ var PolymarketProvider = class {
|
|
|
4704
5295
|
opts;
|
|
4705
5296
|
name = "polymarket";
|
|
4706
5297
|
async findSignal(match, options) {
|
|
4707
|
-
const deadline =
|
|
4708
|
-
return (await this.resolveOne(match, options, deadline))
|
|
5298
|
+
const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
|
|
5299
|
+
return parsedValue(await this.resolveOne(match, options, deadline));
|
|
4709
5300
|
}
|
|
4710
5301
|
async findSignals(matches, options) {
|
|
4711
|
-
const
|
|
4712
|
-
const
|
|
4713
|
-
|
|
5302
|
+
const results = /* @__PURE__ */ new Map();
|
|
5303
|
+
const deadline = Date.now() + (options?.deadlineMs ?? DEFAULT_DEADLINE_MS);
|
|
5304
|
+
let complete = true;
|
|
4714
5305
|
for (const m of matches) {
|
|
4715
|
-
if (Date.now() >= deadline)
|
|
5306
|
+
if (Date.now() >= deadline) {
|
|
5307
|
+
results.set(m.id, unresolved("enrichment deadline expired"));
|
|
5308
|
+
complete = false;
|
|
5309
|
+
continue;
|
|
5310
|
+
}
|
|
4716
5311
|
const r = await this.resolveOne(m, options, deadline);
|
|
4717
|
-
if (r.
|
|
4718
|
-
|
|
5312
|
+
if (r.kind === "unresolved" || r.kind === "malformed") complete = false;
|
|
5313
|
+
results.set(m.id, r);
|
|
4719
5314
|
}
|
|
4720
|
-
return {
|
|
5315
|
+
return { results, complete };
|
|
4721
5316
|
}
|
|
4722
5317
|
/**
|
|
4723
|
-
* Resolve one match
|
|
4724
|
-
*
|
|
4725
|
-
*
|
|
4726
|
-
*
|
|
5318
|
+
* Resolve one match into a verdict.
|
|
5319
|
+
*
|
|
5320
|
+
* Every exit says which KIND of non-answer it is, because that decides
|
|
5321
|
+
* whether it may be remembered — see `isCacheable`: a conclusion we drew from
|
|
5322
|
+
* a payload we READ is cacheable (including an ambiguity, which is stable),
|
|
5323
|
+
* while a shape we could not read is not. Previously a single
|
|
5324
|
+
* `checked: boolean` collapsed five distinct situations into two, and the
|
|
5325
|
+
* ones that landed on the wrong side of it — an ambiguous payload, a
|
|
5326
|
+
* two-legged market, an incoherent 1X2 — were negative-cached as the fact
|
|
5327
|
+
* that this fixture has no market.
|
|
4727
5328
|
*/
|
|
4728
5329
|
async resolveOne(match, options, deadline = Number.POSITIVE_INFINITY) {
|
|
4729
|
-
const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
|
|
4730
|
-
const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
|
|
4731
|
-
if (slugs.length === 0) return { checked: true };
|
|
4732
5330
|
const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
4733
5331
|
try {
|
|
5332
|
+
const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
|
|
5333
|
+
const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
|
|
5334
|
+
if (slugs.length === 0) return definitiveNone("fixture has no derivable event slug");
|
|
5335
|
+
const RANK = {
|
|
5336
|
+
"definitive-none": 0,
|
|
5337
|
+
ambiguous: 1,
|
|
5338
|
+
unresolved: 2,
|
|
5339
|
+
malformed: 3
|
|
5340
|
+
};
|
|
5341
|
+
let worst;
|
|
5342
|
+
const keepWorst = (r) => {
|
|
5343
|
+
if (r.kind === "definitive-none" || r.kind === "valid") return;
|
|
5344
|
+
if (!worst || (RANK[r.kind] ?? 0) > (RANK[worst.kind] ?? 0)) worst = r;
|
|
5345
|
+
};
|
|
4734
5346
|
for (const slug of slugs) {
|
|
4735
5347
|
const remaining = deadline - Date.now();
|
|
4736
|
-
if (remaining <= 0) return
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
5348
|
+
if (remaining <= 0) return unresolved("deadline expired between candidate slugs");
|
|
5349
|
+
let found;
|
|
5350
|
+
try {
|
|
5351
|
+
found = await this.fetchEvent(slug, Math.min(configured, remaining));
|
|
5352
|
+
} catch {
|
|
5353
|
+
keepWorst(malformed("candidate request failed"));
|
|
5354
|
+
continue;
|
|
5355
|
+
}
|
|
5356
|
+
if (found.kind !== "valid") {
|
|
5357
|
+
keepWorst(found);
|
|
5358
|
+
continue;
|
|
5359
|
+
}
|
|
5360
|
+
const r = this.toSignal(match, slug, found.value, options);
|
|
5361
|
+
if (r.kind === "valid") return r;
|
|
5362
|
+
keepWorst(r);
|
|
4740
5363
|
}
|
|
4741
|
-
return
|
|
5364
|
+
return worst ?? definitiveNone("no candidate slug yielded a usable market");
|
|
4742
5365
|
} catch {
|
|
4743
|
-
return
|
|
5366
|
+
return malformed("provider request failed");
|
|
4744
5367
|
}
|
|
4745
5368
|
}
|
|
4746
5369
|
async fetchEvent(slug, timeoutMs) {
|
|
4747
5370
|
const base = this.opts.baseUrl ?? DEFAULT_BASE2;
|
|
4748
5371
|
assertAllowedHost(base);
|
|
4749
|
-
const url = `${base}/events
|
|
5372
|
+
const url = `${base}/events/slug/${encodeURIComponent(slug)}`;
|
|
4750
5373
|
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
4751
5374
|
const res = await doFetch(url, {
|
|
4752
5375
|
signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
|
|
@@ -4755,7 +5378,7 @@ var PolymarketProvider = class {
|
|
|
4755
5378
|
redirect: "error",
|
|
4756
5379
|
headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
|
|
4757
5380
|
});
|
|
4758
|
-
if (res.status === 404) return
|
|
5381
|
+
if (res.status === 404) return definitiveNone("slug returns 404");
|
|
4759
5382
|
if (!res.ok) {
|
|
4760
5383
|
throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
|
|
4761
5384
|
}
|
|
@@ -4764,63 +5387,148 @@ var PolymarketProvider = class {
|
|
|
4764
5387
|
throw new Error(`Polymarket response too large: ${length} bytes`);
|
|
4765
5388
|
}
|
|
4766
5389
|
const data = await res.json();
|
|
5390
|
+
if (Array.isArray(data) && data.length > 1) {
|
|
5391
|
+
return ambiguous("slug returned more than one event");
|
|
5392
|
+
}
|
|
5393
|
+
if (Array.isArray(data) && data.length === 0) return definitiveNone("slug returns no event");
|
|
4767
5394
|
const event = Array.isArray(data) ? data[0] : data;
|
|
4768
|
-
|
|
5395
|
+
if (!event || typeof event !== "object") return malformed("event body is not an object");
|
|
5396
|
+
return valid(event);
|
|
4769
5397
|
}
|
|
4770
5398
|
toSignal(match, eventSlug, event, options) {
|
|
4771
|
-
if (event.active
|
|
4772
|
-
|
|
4773
|
-
|
|
5399
|
+
if (typeof event.active !== "boolean" || typeof event.closed !== "boolean") {
|
|
5400
|
+
return malformed("event active/closed is not a boolean");
|
|
5401
|
+
}
|
|
5402
|
+
if (event.active === false || event.closed === true) {
|
|
5403
|
+
return definitiveNone("event is closed or inactive");
|
|
5404
|
+
}
|
|
5405
|
+
if (event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
|
|
5406
|
+
return definitiveNone("event is not in this competition");
|
|
4774
5407
|
}
|
|
4775
|
-
if (
|
|
4776
|
-
|
|
5408
|
+
if (typeof event.slug !== "string") {
|
|
5409
|
+
return malformed("event states no slug");
|
|
5410
|
+
}
|
|
5411
|
+
if (event.slug !== eventSlug) return definitiveNone("event is not the one requested");
|
|
5412
|
+
if (typeof event.startTime !== "string" || !canonicalTimestamp(event.startTime)) {
|
|
5413
|
+
return malformed("event startTime missing or unparseable");
|
|
5414
|
+
}
|
|
5415
|
+
const start = Date.parse(event.startTime);
|
|
4777
5416
|
const kick = Date.parse(match.kickoff);
|
|
4778
|
-
if (Number.isFinite(start)
|
|
4779
|
-
return
|
|
5417
|
+
if (!Number.isFinite(start) || !Number.isFinite(kick)) {
|
|
5418
|
+
return malformed("event or fixture kickoff is unreadable");
|
|
5419
|
+
}
|
|
5420
|
+
if (Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
|
|
5421
|
+
return definitiveNone("event kickoff does not match the fixture");
|
|
5422
|
+
}
|
|
5423
|
+
if (!Array.isArray(event.markets)) {
|
|
5424
|
+
return malformed("event markets is not an array");
|
|
4780
5425
|
}
|
|
4781
|
-
const
|
|
4782
|
-
|
|
5426
|
+
const marketsTruncated = Array.isArray(event.markets) && event.markets.length > MAX_EVENT_MARKETS;
|
|
5427
|
+
if (marketsTruncated) {
|
|
5428
|
+
return malformed("event market list exceeded the cap");
|
|
5429
|
+
}
|
|
5430
|
+
const marketList = takeBounded(event.markets, MAX_EVENT_MARKETS);
|
|
5431
|
+
if (marketList.some(
|
|
5432
|
+
(market) => !market || typeof market !== "object" || typeof market.sportsMarketType !== "string"
|
|
5433
|
+
)) {
|
|
5434
|
+
return malformed("event market is missing its market-type discriminator");
|
|
5435
|
+
}
|
|
5436
|
+
const moneyline = marketList.filter(
|
|
5437
|
+
(m) => m?.sportsMarketType === "moneyline"
|
|
4783
5438
|
);
|
|
4784
|
-
const
|
|
4785
|
-
const
|
|
4786
|
-
const
|
|
4787
|
-
|
|
5439
|
+
const homeSel = pickMarket(moneyline, match.home.code, match.home.name);
|
|
5440
|
+
const awaySel = pickMarket(moneyline, match.away.code, match.away.name);
|
|
5441
|
+
const drawSel = pickDraw(moneyline);
|
|
5442
|
+
for (const [side, sel] of [
|
|
5443
|
+
["home", homeSel],
|
|
5444
|
+
["away", awaySel],
|
|
5445
|
+
["draw", drawSel]
|
|
5446
|
+
]) {
|
|
5447
|
+
if (sel.kind === "ambiguous") {
|
|
5448
|
+
return ambiguous(`${sel.count} markets claim the ${side} outcome`);
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
if (homeSel.kind !== "one" || awaySel.kind !== "one" || drawSel.kind !== "one") {
|
|
5452
|
+
return definitiveNone("event does not carry all three 1X2 legs");
|
|
5453
|
+
}
|
|
5454
|
+
const homeMarket = homeSel.value;
|
|
5455
|
+
const awayMarket = awaySel.value;
|
|
5456
|
+
const drawMarket = drawSel.value;
|
|
4788
5457
|
const legIds = [homeMarket, awayMarket, drawMarket].filter((m) => m != null).map((m) => m.id ?? m.slug ?? "");
|
|
4789
|
-
if (new Set(legIds).size !== legIds.length)
|
|
5458
|
+
if (new Set(legIds).size !== legIds.length) {
|
|
5459
|
+
return ambiguous("two outcome legs are the same market");
|
|
5460
|
+
}
|
|
4790
5461
|
const legs = [
|
|
4791
5462
|
["home", homeMarket, match.home.code, match.home.name],
|
|
4792
5463
|
["draw", drawMarket, void 0, "Draw"],
|
|
4793
5464
|
["away", awayMarket, match.away.code, match.away.name]
|
|
4794
5465
|
];
|
|
4795
5466
|
const outcomes = [];
|
|
4796
|
-
let asOf = event.updatedAt;
|
|
5467
|
+
let asOf = canonicalTimestamp(event.updatedAt);
|
|
4797
5468
|
let liquidity;
|
|
4798
|
-
for (const [kind, market,
|
|
5469
|
+
for (const [kind, market, teamCode2, label] of legs) {
|
|
4799
5470
|
if (!market) continue;
|
|
4800
|
-
if (market.closed
|
|
4801
|
-
|
|
5471
|
+
if (typeof market.closed !== "boolean" || typeof market.active !== "boolean") {
|
|
5472
|
+
return malformed("market active/closed is not a boolean");
|
|
5473
|
+
}
|
|
5474
|
+
if (market.closed === true || market.active === false) {
|
|
5475
|
+
return definitiveNone("an outcome leg is closed or inactive");
|
|
5476
|
+
}
|
|
5477
|
+
if (market.description && NON_REGULAR_TIME.test(market.description)) {
|
|
5478
|
+
return definitiveNone("an outcome leg is not a regular-time market");
|
|
5479
|
+
}
|
|
4802
5480
|
const yes = yesPrice(market);
|
|
4803
|
-
if (yes == null) return
|
|
4804
|
-
outcomes.push({ kind, teamCode, label, probability: yes });
|
|
4805
|
-
|
|
4806
|
-
|
|
5481
|
+
if (yes == null) return malformed("market is not a readable Yes/No binary");
|
|
5482
|
+
outcomes.push({ kind, teamCode: teamCode2, label, probability: yes });
|
|
5483
|
+
const marketAsOf = canonicalTimestamp(market.updatedAt);
|
|
5484
|
+
if (!marketAsOf) {
|
|
5485
|
+
return malformed("market updatedAt missing or unparseable");
|
|
5486
|
+
}
|
|
5487
|
+
const nowMs = (options?.now ?? this.opts.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
5488
|
+
if (Date.parse(marketAsOf) - nowMs > FUTURE_SKEW_MS) {
|
|
5489
|
+
return malformed("market updatedAt is dated forward");
|
|
5490
|
+
}
|
|
5491
|
+
if (!asOf || Date.parse(marketAsOf) < Date.parse(asOf)) asOf = marketAsOf;
|
|
5492
|
+
const rawLiq = market.liquidityNum ?? market.liquidity;
|
|
5493
|
+
const liq = numberish(rawLiq);
|
|
5494
|
+
if (rawLiq != null && liq == null) {
|
|
5495
|
+
return malformed("market liquidity is unreadable");
|
|
5496
|
+
}
|
|
4807
5497
|
if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
|
|
4808
5498
|
}
|
|
4809
5499
|
const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
|
|
4810
|
-
if (rawSum < 0.9 || rawSum > 1.15)
|
|
5500
|
+
if (rawSum < 0.9 || rawSum > 1.15) {
|
|
5501
|
+
return ambiguous("outcome probabilities do not form a coherent 1X2");
|
|
5502
|
+
}
|
|
5503
|
+
if (!asOf) return malformed("no usable timestamp on the event or its markets");
|
|
4811
5504
|
const signal = buildMarketSignal({
|
|
4812
5505
|
match,
|
|
4813
5506
|
source: "polymarket",
|
|
4814
|
-
|
|
4815
|
-
|
|
5507
|
+
// Echoed into MCP structured content (tools.ts `market.id`), i.e. straight
|
|
5508
|
+
// into an agent's context. Stripping control characters is NOT sufficient
|
|
5509
|
+
// there: printable prose ("IGNORE PREVIOUS INSTRUCTIONS") survives that and
|
|
5510
|
+
// is precisely what matters for a model reading it. Gamma ids are short
|
|
5511
|
+
// opaque tokens, so validate that GRAMMAR and otherwise fall back to the
|
|
5512
|
+
// slug we derived ourselves.
|
|
5513
|
+
// The fallback is grammar-checked too. It is normally a slug we derived
|
|
5514
|
+
// ourselves, but `mapping.2026.json` can override it, so echoing it raw
|
|
5515
|
+
// was the one path around the agent-facing filter this line exists for.
|
|
5516
|
+
sourceMarketId: safeMarketId(event.id) ?? safeDerivedSlug(eventSlug),
|
|
5517
|
+
asOf,
|
|
4816
5518
|
outcomes,
|
|
4817
5519
|
liquidity,
|
|
4818
5520
|
now: options?.now ?? this.opts.now,
|
|
4819
5521
|
maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
|
|
4820
5522
|
});
|
|
4821
|
-
return signal.ambiguous ?
|
|
5523
|
+
return signal.ambiguous ? ambiguous("signal does not map cleanly onto this fixture") : valid(signal);
|
|
4822
5524
|
}
|
|
4823
5525
|
};
|
|
5526
|
+
function safeMarketId(id) {
|
|
5527
|
+
return typeof id === "string" && /^[0-9]{1,32}$/.test(id) ? id : void 0;
|
|
5528
|
+
}
|
|
5529
|
+
function safeDerivedSlug(slug) {
|
|
5530
|
+
return typeof slug === "string" && /^fifwc-[a-z]{2,3}-[a-z]{2,3}-\d{4}-\d{2}-\d{2}$/.test(slug) ? slug : void 0;
|
|
5531
|
+
}
|
|
4824
5532
|
var POLYMARKET_TOKEN = {
|
|
4825
5533
|
SUI: "che",
|
|
4826
5534
|
// Switzerland
|
|
@@ -4834,8 +5542,16 @@ var POLYMARKET_TOKEN = {
|
|
|
4834
5542
|
// Croatia
|
|
4835
5543
|
COD: "cdr",
|
|
4836
5544
|
// DR Congo
|
|
4837
|
-
CPV: "cvi"
|
|
5545
|
+
CPV: "cvi",
|
|
4838
5546
|
// Cabo Verde
|
|
5547
|
+
// TWO letters, not three — the one entry that is not ISO alpha-3. Verified
|
|
5548
|
+
// live: `fifwc-kor-cze-2026-06-11` is a 404, `fifwc-kr-cze-2026-06-11`
|
|
5549
|
+
// resolves to "Korea Republic vs. Czechia". Korea's three group fixtures
|
|
5550
|
+
// therefore had no market line at all. The `^[a-z]{3}$` guard in
|
|
5551
|
+
// `deriveEventSlugs` validates the FIFA CODE, not the token, so a two-letter
|
|
5552
|
+
// alias passes through it unharmed.
|
|
5553
|
+
KOR: "kr"
|
|
5554
|
+
// Korea Republic
|
|
4839
5555
|
};
|
|
4840
5556
|
function pmTokens(code) {
|
|
4841
5557
|
const c = code.toLowerCase();
|
|
@@ -4866,18 +5582,21 @@ function slugToken(m) {
|
|
|
4866
5582
|
return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
|
|
4867
5583
|
}
|
|
4868
5584
|
function isDrawMarket(m) {
|
|
4869
|
-
|
|
5585
|
+
const title = (m.groupItemTitle ?? "").trim().toLowerCase();
|
|
5586
|
+
return slugToken(m) === "draw" || title === "draw" || /^draw\s*\(/.test(title);
|
|
4870
5587
|
}
|
|
4871
|
-
function pickMarket(markets,
|
|
4872
|
-
const tokens = pmTokens(
|
|
5588
|
+
function pickMarket(markets, teamCode2, teamName) {
|
|
5589
|
+
const tokens = pmTokens(teamCode2);
|
|
4873
5590
|
const name = teamName.trim().toLowerCase();
|
|
4874
5591
|
const teamMarkets = markets.filter((m) => !isDrawMarket(m));
|
|
4875
|
-
const bySlug = teamMarkets.
|
|
4876
|
-
if (bySlug) return bySlug;
|
|
4877
|
-
|
|
5592
|
+
const bySlug = teamMarkets.filter((m) => tokens.includes(slugToken(m)));
|
|
5593
|
+
if (bySlug.length > 1) return { kind: "ambiguous", count: bySlug.length };
|
|
5594
|
+
const byTitle = name ? teamMarkets.filter((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name) : [];
|
|
5595
|
+
if (byTitle.length > 1) return { kind: "ambiguous", count: byTitle.length };
|
|
5596
|
+
return selectOne([.../* @__PURE__ */ new Set([...bySlug, ...byTitle])]);
|
|
4878
5597
|
}
|
|
4879
5598
|
function pickDraw(markets) {
|
|
4880
|
-
return markets.
|
|
5599
|
+
return selectOne(markets.filter(isDrawMarket));
|
|
4881
5600
|
}
|
|
4882
5601
|
function assertAllowedHost(base) {
|
|
4883
5602
|
let host;
|
|
@@ -4891,20 +5610,29 @@ function assertAllowedHost(base) {
|
|
|
4891
5610
|
}
|
|
4892
5611
|
}
|
|
4893
5612
|
function yesPrice(market) {
|
|
4894
|
-
const labels = parseJsonArray(market.outcomes);
|
|
4895
|
-
const
|
|
4896
|
-
if (
|
|
4897
|
-
const
|
|
4898
|
-
if (
|
|
4899
|
-
const
|
|
4900
|
-
|
|
5613
|
+
const labels = parseJsonArray(market.outcomes).map((l) => l.trim().toLowerCase());
|
|
5614
|
+
const raw = parseJsonArray(market.outcomePrices);
|
|
5615
|
+
if (raw.some((v) => v.trim() === "" || !Number.isFinite(Number(v)))) return void 0;
|
|
5616
|
+
const prices = raw.map((v) => Number(v));
|
|
5617
|
+
if (labels.length !== 2 || prices.length !== 2) return void 0;
|
|
5618
|
+
const i = labels.indexOf("yes");
|
|
5619
|
+
const j = labels.indexOf("no");
|
|
5620
|
+
if (i < 0 || j < 0) return void 0;
|
|
5621
|
+
const yes = prices[i];
|
|
5622
|
+
const no = prices[j];
|
|
5623
|
+
if (![yes, no].every((v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1)) {
|
|
5624
|
+
return void 0;
|
|
5625
|
+
}
|
|
5626
|
+
if (Math.abs(yes + no - 1) > 0.05) return void 0;
|
|
5627
|
+
return yes > 0 ? yes : void 0;
|
|
4901
5628
|
}
|
|
4902
5629
|
function parseJsonArray(v) {
|
|
4903
|
-
|
|
5630
|
+
const asText = (x) => typeof x === "string" || typeof x === "number" ? String(x) : "";
|
|
5631
|
+
if (Array.isArray(v)) return v.map(asText);
|
|
4904
5632
|
if (typeof v === "string") {
|
|
4905
5633
|
try {
|
|
4906
5634
|
const parsed = JSON.parse(v);
|
|
4907
|
-
return Array.isArray(parsed) ? parsed.map(
|
|
5635
|
+
return Array.isArray(parsed) ? parsed.map(asText) : [];
|
|
4908
5636
|
} catch {
|
|
4909
5637
|
return [];
|
|
4910
5638
|
}
|
|
@@ -4912,15 +5640,19 @@ function parseJsonArray(v) {
|
|
|
4912
5640
|
return [];
|
|
4913
5641
|
}
|
|
4914
5642
|
function numberish(v) {
|
|
4915
|
-
if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
|
|
5643
|
+
if (typeof v === "number") return Number.isFinite(v) && v >= 0 ? v : void 0;
|
|
4916
5644
|
if (typeof v === "string") {
|
|
4917
5645
|
const n = Number(v);
|
|
4918
|
-
return Number.isFinite(n) ? n : void 0;
|
|
5646
|
+
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
4919
5647
|
}
|
|
4920
5648
|
return void 0;
|
|
4921
5649
|
}
|
|
4922
5650
|
|
|
4923
5651
|
// src/markets/provider.ts
|
|
5652
|
+
var MARKET_COMPETITIONS = /* @__PURE__ */ new Set([DEFAULT_COMPETITION]);
|
|
5653
|
+
function marketsCoverCompetition(competition = resolveCompetition()) {
|
|
5654
|
+
return MARKET_COMPETITIONS.has(competition);
|
|
5655
|
+
}
|
|
4924
5656
|
function resolveMarketSource(explicit) {
|
|
4925
5657
|
if (explicit) return explicit;
|
|
4926
5658
|
if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
|
|
@@ -4937,6 +5669,7 @@ function makeMarketProvider(source) {
|
|
|
4937
5669
|
return new FakeMarketProvider();
|
|
4938
5670
|
// no synth → yields no signals, no network
|
|
4939
5671
|
default:
|
|
5672
|
+
if (!marketsCoverCompetition()) return new FakeMarketProvider();
|
|
4940
5673
|
return new PolymarketProvider();
|
|
4941
5674
|
}
|
|
4942
5675
|
}
|
|
@@ -4951,7 +5684,7 @@ async function getMarketSignals(provider, matches, options) {
|
|
|
4951
5684
|
try {
|
|
4952
5685
|
return await provider.findSignals(matches, options);
|
|
4953
5686
|
} catch {
|
|
4954
|
-
return
|
|
5687
|
+
return emptyBatch();
|
|
4955
5688
|
}
|
|
4956
5689
|
}
|
|
4957
5690
|
|
|
@@ -5034,6 +5767,9 @@ function formatShareSnippet(input, options = {}) {
|
|
|
5034
5767
|
if (input.degraded && input.matches.length > 0) {
|
|
5035
5768
|
blocks.push("(Live data unavailable \u2014 showing the bundled schedule, not live scores.)");
|
|
5036
5769
|
}
|
|
5770
|
+
if (includeMarkets && input.marketComplete === false) {
|
|
5771
|
+
blocks.push("(Market data unavailable or incomplete \u2014 not all fixtures were checked.)");
|
|
5772
|
+
}
|
|
5037
5773
|
blocks.push(
|
|
5038
5774
|
shareFooter({
|
|
5039
5775
|
source: input.source,
|
|
@@ -5064,7 +5800,9 @@ function formatShareTable(input, options = {}) {
|
|
|
5064
5800
|
const includeInstall = options.includeInstallLine !== false;
|
|
5065
5801
|
const blocks = [];
|
|
5066
5802
|
if (input.tables.length === 0) {
|
|
5067
|
-
blocks.push(
|
|
5803
|
+
blocks.push(
|
|
5804
|
+
input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
|
|
5805
|
+
);
|
|
5068
5806
|
} else {
|
|
5069
5807
|
for (const { group, rows } of input.tables) {
|
|
5070
5808
|
blocks.push(
|
|
@@ -5215,29 +5953,35 @@ export {
|
|
|
5215
5953
|
DEFAULT_FLAVOR,
|
|
5216
5954
|
DEFAULT_MAX_AGE_MS,
|
|
5217
5955
|
EspnAdapter,
|
|
5218
|
-
FEED_TEXT_MAX,
|
|
5219
5956
|
FLAVOR_LEVELS,
|
|
5220
5957
|
FakeMarketProvider,
|
|
5221
5958
|
KNOCKOUT_EXTRA_TIME_MS,
|
|
5222
5959
|
KNOWN_SOURCES,
|
|
5223
5960
|
LIVE_WINDOW_MS,
|
|
5961
|
+
MARKET_COMPETITIONS,
|
|
5962
|
+
MAX_LABEL_COLUMNS,
|
|
5224
5963
|
PolymarketProvider,
|
|
5225
5964
|
ProviderError,
|
|
5226
5965
|
SHARE_DISCLAIMER,
|
|
5227
5966
|
SHARE_HASHTAG,
|
|
5228
5967
|
allFixtures,
|
|
5229
5968
|
allTeams,
|
|
5969
|
+
ambiguous,
|
|
5230
5970
|
asFlavorLevel,
|
|
5971
|
+
bounded,
|
|
5231
5972
|
buildBracketTopology,
|
|
5232
5973
|
buildBracketView,
|
|
5233
5974
|
buildMarketSignal,
|
|
5234
5975
|
byKickoff,
|
|
5976
|
+
cacheableKeys,
|
|
5235
5977
|
competitionBase,
|
|
5236
5978
|
computeStandings,
|
|
5237
5979
|
countdown,
|
|
5238
5980
|
currentOrNextFixtureForTeam,
|
|
5981
|
+
definitiveNone,
|
|
5239
5982
|
deriveFavorite,
|
|
5240
5983
|
displayWidth,
|
|
5984
|
+
emptyBatch,
|
|
5241
5985
|
favoriteStrength,
|
|
5242
5986
|
fixturesByDate,
|
|
5243
5987
|
fixturesByGroup,
|
|
@@ -5265,6 +6009,8 @@ export {
|
|
|
5265
6009
|
getStandings,
|
|
5266
6010
|
groups,
|
|
5267
6011
|
hasSaneDistribution,
|
|
6012
|
+
humanLabel,
|
|
6013
|
+
isCacheable,
|
|
5268
6014
|
isFinished,
|
|
5269
6015
|
isFlavorLevel,
|
|
5270
6016
|
isLive,
|
|
@@ -5282,6 +6028,7 @@ export {
|
|
|
5282
6028
|
lookupTeam,
|
|
5283
6029
|
makeAdapter,
|
|
5284
6030
|
makeMarketProvider,
|
|
6031
|
+
malformed,
|
|
5285
6032
|
mapEspnEvent,
|
|
5286
6033
|
mapsCleanly,
|
|
5287
6034
|
marketAttributionText,
|
|
@@ -5293,6 +6040,7 @@ export {
|
|
|
5293
6040
|
marketRelevant,
|
|
5294
6041
|
marketSignalRendersFor,
|
|
5295
6042
|
marketSourceLabel,
|
|
6043
|
+
marketsCoverCompetition,
|
|
5296
6044
|
matchFlavor,
|
|
5297
6045
|
matchKey,
|
|
5298
6046
|
matchLocation,
|
|
@@ -5304,16 +6052,26 @@ export {
|
|
|
5304
6052
|
normalizeOutcomes,
|
|
5305
6053
|
outcomeFromScore,
|
|
5306
6054
|
padVisible,
|
|
6055
|
+
parseCachedMarketSignal,
|
|
6056
|
+
parseCachedMatch,
|
|
6057
|
+
parseCachedMatches,
|
|
5307
6058
|
parseStandings,
|
|
5308
6059
|
parseTeamSlot,
|
|
6060
|
+
parsedValue,
|
|
6061
|
+
productFlag,
|
|
5309
6062
|
resolveCompetition,
|
|
5310
6063
|
resolveMarketSource,
|
|
5311
6064
|
resolveTz,
|
|
6065
|
+
resolvedValues,
|
|
5312
6066
|
sanitizeBundledFixture,
|
|
5313
|
-
sanitizeFeedText,
|
|
5314
|
-
sanitizeMatchStrings,
|
|
5315
6067
|
scoreline,
|
|
6068
|
+
sealMarketSignal,
|
|
6069
|
+
sealMatch,
|
|
6070
|
+
selectOne,
|
|
5316
6071
|
stageLabel,
|
|
5317
6072
|
stageLabelI18n,
|
|
5318
|
-
t
|
|
6073
|
+
t,
|
|
6074
|
+
truncateVisible,
|
|
6075
|
+
unresolved,
|
|
6076
|
+
valid
|
|
5319
6077
|
};
|