@claudinho/core 0.11.1 → 0.12.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +550 -181
  2. package/dist/index.js +300 -20
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -332,6 +332,13 @@ var EN = {
332
332
  "status.fullTime": "full-time",
333
333
  "status.postponed": "postponed",
334
334
  "status.cancelled": "cancelled",
335
+ // A timeline row's kind word (core `timelineRows`).
336
+ "event.goal": "Goal",
337
+ "event.ownGoal": "Own goal",
338
+ "event.penalty": "Penalty",
339
+ "event.yellow": "Yellow card",
340
+ "event.red": "Red card",
341
+ "event.sub": "Substitution",
335
342
  "live.title": "Live now",
336
343
  "today.title": "Today's matches",
337
344
  "today.on": "Matches",
@@ -340,6 +347,23 @@ var EN = {
340
347
  "next.in": "in {countdown}",
341
348
  // A kickoff that has passed, where the countdown would say "in" (core `countdownPhrase`).
342
349
  "countdown.now": "now",
350
+ // The ambient line's syncing phrase (`claudinho ambient --json` alone; the
351
+ // statusline's own `prompt` stays English).
352
+ "ambient.syncing": "live \xB7 syncing\u2026",
353
+ // The plugin's moment words, sent by `claudinho ambient --json` as `words`
354
+ // (`{team}` a template the plugin fills with the record's own names). Each
355
+ // at most 40 characters; `{team}` once in `moment.goalFor` and `moment.win`
356
+ // and nowhere else, or the plugin falls back to its English words.
357
+ "moment.goal": "GOAL!",
358
+ "moment.ownGoal": "Own goal",
359
+ "moment.ownGoalDetail": "own goal",
360
+ "moment.goalFor": "Goal for {team}",
361
+ "moment.goals": "Goals",
362
+ "moment.scoreUpdated": "Score updated",
363
+ "moment.penalties": "Penalties",
364
+ "moment.fullTime": "Full time",
365
+ "moment.win": "{team} win",
366
+ "moment.draw": "Draw",
343
367
  // A title that introduces the lines below it (the MCP text).
344
368
  heading: "{title}:",
345
369
  // A list's empty state and the outage lines: the CLI's sentences, the MCP
@@ -351,6 +375,7 @@ var EN = {
351
375
  "feed.earlierRecord": "Live state could not be refreshed \u2014 showing the provider's earlier record.",
352
376
  "next.none": "No upcoming fixture found for {team}.",
353
377
  "match.none": "No match found with id {id}.",
378
+ "match.kickoff": "Kickoff",
354
379
  // The MCP text's own sentences for these states (the CLI says `live.degraded`
355
380
  // there): they keep the team or the id the answer is about.
356
381
  "next.unreachable": "Couldn't reach the data provider \u2014 no upcoming fixture confirmed for {team}.",
@@ -428,6 +453,12 @@ var ES = {
428
453
  "status.fullTime": "finalizado",
429
454
  "status.postponed": "aplazado",
430
455
  "status.cancelled": "cancelado",
456
+ "event.goal": "Gol",
457
+ "event.ownGoal": "Gol en contra",
458
+ "event.penalty": "Penal",
459
+ "event.yellow": "Tarjeta amarilla",
460
+ "event.red": "Tarjeta roja",
461
+ "event.sub": "Cambio",
431
462
  "live.title": "En vivo",
432
463
  "today.title": "Partidos de hoy",
433
464
  "today.on": "Partidos",
@@ -435,6 +466,17 @@ var ES = {
435
466
  "next.label": "Pr\xF3ximo partido de {team}",
436
467
  "next.in": "en {countdown}",
437
468
  "countdown.now": "ahora",
469
+ "ambient.syncing": "en vivo \xB7 sincronizando\u2026",
470
+ "moment.goal": "\xA1GOL!",
471
+ "moment.ownGoal": "Gol en contra",
472
+ "moment.ownGoalDetail": "gol en contra",
473
+ "moment.goalFor": "Gol de {team}",
474
+ "moment.goals": "Goles",
475
+ "moment.scoreUpdated": "Marcador corregido",
476
+ "moment.penalties": "Penales",
477
+ "moment.fullTime": "Final",
478
+ "moment.win": "Gana {team}",
479
+ "moment.draw": "Empate",
438
480
  heading: "{title}:",
439
481
  "today.none": "No hay partidos para esta fecha.",
440
482
  "live.none": "No hay partidos en juego ahora mismo.",
@@ -443,6 +485,7 @@ var ES = {
443
485
  "feed.earlierRecord": "No se pudo actualizar el estado en vivo \u2014 se muestra el registro anterior del proveedor.",
444
486
  "next.none": "No se encontr\xF3 pr\xF3ximo partido para {team}.",
445
487
  "match.none": "No se encontr\xF3 partido con id {id}.",
488
+ "match.kickoff": "Inicio",
446
489
  "next.unreachable": "No se pudo contactar al proveedor de datos \u2014 ning\xFAn pr\xF3ximo partido confirmado para {team}.",
447
490
  "match.unreachable": "No se pudo contactar al proveedor de datos \u2014 no se pudo consultar el partido {id}.",
448
491
  "team.ambiguous": '"{query}" es ambiguo. \xBFQuisiste decir:',
@@ -517,6 +560,12 @@ var PT = {
517
560
  "status.fullTime": "encerrado",
518
561
  "status.postponed": "adiado",
519
562
  "status.cancelled": "cancelado",
563
+ "event.goal": "Gol",
564
+ "event.ownGoal": "Gol contra",
565
+ "event.penalty": "P\xEAnalti",
566
+ "event.yellow": "Cart\xE3o amarelo",
567
+ "event.red": "Cart\xE3o vermelho",
568
+ "event.sub": "Substitui\xE7\xE3o",
520
569
  "live.title": "Ao vivo",
521
570
  "today.title": "Jogos de hoje",
522
571
  "today.on": "Jogos",
@@ -524,6 +573,17 @@ var PT = {
524
573
  "next.label": "Pr\xF3ximo jogo de {team}",
525
574
  "next.in": "em {countdown}",
526
575
  "countdown.now": "agora",
576
+ "ambient.syncing": "ao vivo \xB7 sincronizando\u2026",
577
+ "moment.goal": "GOL!",
578
+ "moment.ownGoal": "Gol contra",
579
+ "moment.ownGoalDetail": "gol contra",
580
+ "moment.goalFor": "Gol \xB7 {team}",
581
+ "moment.goals": "Gols",
582
+ "moment.scoreUpdated": "Placar corrigido",
583
+ "moment.penalties": "P\xEAnaltis",
584
+ "moment.fullTime": "Fim de jogo",
585
+ "moment.win": "{team} vence",
586
+ "moment.draw": "Empate",
527
587
  heading: "{title}:",
528
588
  "today.none": "Nenhum jogo para esta data.",
529
589
  "live.none": "Nenhum jogo em andamento agora.",
@@ -532,6 +592,7 @@ var PT = {
532
592
  "feed.earlierRecord": "N\xE3o foi poss\xEDvel atualizar o estado ao vivo \u2014 mostrando o registro anterior do provedor.",
533
593
  "next.none": "Nenhum pr\xF3ximo jogo encontrado para {team}.",
534
594
  "match.none": "Nenhum jogo encontrado com id {id}.",
595
+ "match.kickoff": "In\xEDcio",
535
596
  "next.unreachable": "N\xE3o foi poss\xEDvel contatar o provedor de dados \u2014 nenhum pr\xF3ximo jogo confirmado para {team}.",
536
597
  "match.unreachable": "N\xE3o foi poss\xEDvel contatar o provedor de dados \u2014 o jogo {id} n\xE3o p\xF4de ser consultado.",
537
598
  "team.ambiguous": '"{query}" \xE9 amb\xEDguo. Voc\xEA quis dizer:',
@@ -606,6 +667,12 @@ var FR = {
606
667
  "status.fullTime": "termin\xE9",
607
668
  "status.postponed": "report\xE9",
608
669
  "status.cancelled": "annul\xE9",
670
+ "event.goal": "But",
671
+ "event.ownGoal": "But contre son camp",
672
+ "event.penalty": "Penalty",
673
+ "event.yellow": "Carton jaune",
674
+ "event.red": "Carton rouge",
675
+ "event.sub": "Remplacement",
609
676
  "live.title": "En direct",
610
677
  "today.title": "Matchs d'aujourd'hui",
611
678
  "today.on": "Matchs",
@@ -613,6 +680,17 @@ var FR = {
613
680
  "next.label": "Prochain match de {team}",
614
681
  "next.in": "dans {countdown}",
615
682
  "countdown.now": "maintenant",
683
+ "ambient.syncing": "en direct \xB7 synchronisation\u2026",
684
+ "moment.goal": "BUT !",
685
+ "moment.ownGoal": "But contre son camp",
686
+ "moment.ownGoalDetail": "contre son camp",
687
+ "moment.goalFor": "But pour {team}",
688
+ "moment.goals": "Buts",
689
+ "moment.scoreUpdated": "Score corrig\xE9",
690
+ "moment.penalties": "Tirs au but",
691
+ "moment.fullTime": "Fin du match",
692
+ "moment.win": "{team} gagne",
693
+ "moment.draw": "Match nul",
616
694
  heading: "{title} :",
617
695
  "today.none": "Aucun match pr\xE9vu pour cette date.",
618
696
  "live.none": "Aucun match en cours pour l'instant.",
@@ -621,6 +699,7 @@ var FR = {
621
699
  "feed.earlierRecord": "L\u2019\xE9tat en direct n\u2019a pas pu \xEAtre actualis\xE9 \u2014 affichage de l\u2019enregistrement pr\xE9c\xE9dent du fournisseur.",
622
700
  "next.none": "Aucun prochain match trouv\xE9 pour {team}.",
623
701
  "match.none": "Aucun match trouv\xE9 avec id {id}.",
702
+ "match.kickoff": "Coup d\u2019envoi",
624
703
  "next.unreachable": "Impossible de joindre le fournisseur de donn\xE9es \u2014 aucun prochain match confirm\xE9 pour {team}.",
625
704
  "match.unreachable": "Impossible de joindre le fournisseur de donn\xE9es \u2014 le match {id} n\u2019a pas pu \xEAtre consult\xE9.",
626
705
  "team.ambiguous": '"{query}" est ambigu. Vouliez-vous dire :',
@@ -654,6 +733,20 @@ function t(lang, key, vars) {
654
733
  (whole, name) => Object.hasOwn(vars, name) ? vars[name] : whole
655
734
  );
656
735
  }
736
+ function momentWords(lang) {
737
+ return {
738
+ goal: t(lang, "moment.goal"),
739
+ ownGoal: t(lang, "moment.ownGoal"),
740
+ ownGoalDetail: t(lang, "moment.ownGoalDetail"),
741
+ goalFor: t(lang, "moment.goalFor"),
742
+ goals: t(lang, "moment.goals"),
743
+ scoreUpdated: t(lang, "moment.scoreUpdated"),
744
+ penalties: t(lang, "moment.penalties"),
745
+ fullTime: t(lang, "moment.fullTime"),
746
+ win: t(lang, "moment.win"),
747
+ draw: t(lang, "moment.draw")
748
+ };
749
+ }
657
750
  var STAGE_KEYS = Object.freeze({
658
751
  GROUP: "stage.groupStage",
659
752
  R32: "stage.r32",
@@ -763,7 +856,7 @@ function formatTime(iso, opts = {}) {
763
856
  }).format(when);
764
857
  }
765
858
  var COUNTDOWN_NOW = "now";
766
- function countdown(iso, from = /* @__PURE__ */ new Date()) {
859
+ function countdown(iso, from) {
767
860
  const when = parsedDate(iso);
768
861
  if (!when) return UNKNOWN_TIME;
769
862
  const ms = when.getTime() - from.getTime();
@@ -776,7 +869,7 @@ function countdown(iso, from = /* @__PURE__ */ new Date()) {
776
869
  if (hours > 0) return `${hours}h${mins}m`;
777
870
  return `${mins}m`;
778
871
  }
779
- function countdownPhrase(lang, iso, from = /* @__PURE__ */ new Date()) {
872
+ function countdownPhrase(lang, iso, from) {
780
873
  const left = countdown(iso, from);
781
874
  return left === COUNTDOWN_NOW ? t(lang, "countdown.now") : t(lang, "next.in", { countdown: left });
782
875
  }
@@ -842,6 +935,70 @@ function withFlag(text, flag2, side) {
842
935
  return side === "home" ? `${flag2} ${text}` : `${text} ${flag2}`;
843
936
  }
844
937
 
938
+ // src/timeline.ts
939
+ var KIND_KEYS = Object.freeze({
940
+ GOAL: "event.goal",
941
+ OWN_GOAL: "event.ownGoal",
942
+ PEN: "event.penalty",
943
+ YELLOW: "event.yellow",
944
+ RED: "event.red",
945
+ SUB: "event.sub"
946
+ });
947
+ var KIND_MARKS = Object.freeze({
948
+ GOAL: "\u26BD",
949
+ OWN_GOAL: "\u26BD",
950
+ PEN: "\u26BD",
951
+ YELLOW: "\u{1F7E8}",
952
+ RED: "\u{1F7E5}",
953
+ SUB: "\u{1F504}"
954
+ });
955
+ function timelineParts(match, opts) {
956
+ const events = match.events ?? [];
957
+ if (events.length === 0) return [];
958
+ const cells = events.map((e) => {
959
+ const team = e.side === "home" ? match.home : match.away;
960
+ return {
961
+ minute: e.added !== void 0 ? `${e.minute}+${e.added}'` : `${e.minute}'`,
962
+ mark: KIND_MARKS[e.type],
963
+ kind: t(opts.lang, KIND_KEYS[e.type]),
964
+ label: withFlag(team.code, opts.flags ? team.flag : void 0, e.side),
965
+ player: e.player
966
+ };
967
+ });
968
+ const widest = (pick2) => Math.max(...cells.map((c) => displayWidth(pick2(c))));
969
+ const minuteW = widest((c) => c.minute);
970
+ const kindW = widest((c) => c.kind);
971
+ const labelW = widest((c) => c.label);
972
+ return cells.map((c) => {
973
+ const columns = ` ${padVisible(c.minute, minuteW)} ${c.mark} ${padVisible(c.kind, kindW)} ${padVisible(c.label, labelW)}`;
974
+ if (!c.player) return [{ text: columns.trimEnd() }];
975
+ return [{ text: `${columns} ` }, { text: c.player, style: "strong" }];
976
+ });
977
+ }
978
+ function timelineRows(match, opts) {
979
+ return timelineParts(match, opts).map((row) => row.map((p) => p.text).join(""));
980
+ }
981
+ var SCORING = /* @__PURE__ */ new Set(["GOAL", "OWN_GOAL", "PEN"]);
982
+ function provenLastGoal(match) {
983
+ const score = match.score;
984
+ if (!score) return null;
985
+ let home = 0;
986
+ let away = 0;
987
+ let last = null;
988
+ const identities = /* @__PURE__ */ new Set();
989
+ for (const e of match.events ?? []) {
990
+ if (!SCORING.has(e.type)) continue;
991
+ const identity = JSON.stringify([e.type, e.minute, e.added ?? 0, e.side, e.player ?? null]);
992
+ if (identities.has(identity)) return null;
993
+ identities.add(identity);
994
+ if (e.side === "home") home++;
995
+ else if (e.side === "away") away++;
996
+ else continue;
997
+ last = e;
998
+ }
999
+ return last !== null && home === score.home && away === score.away ? last : null;
1000
+ }
1001
+
845
1002
  // src/normalize.ts
846
1003
  function outcomeFromScore(home, away) {
847
1004
  if (home > away) return "H";
@@ -1132,18 +1289,33 @@ function sealScorePair(raw) {
1132
1289
  const away = count(v.away, MAX_GOALS);
1133
1290
  return home !== void 0 && away !== void 0 ? { home, away } : void 0;
1134
1291
  }
1292
+ var MAX_ADDED_MINUTES = 30;
1293
+ var SIDES = /* @__PURE__ */ new Set(["home", "away"]);
1135
1294
  function sealEvent(raw) {
1136
- if (!raw || typeof raw !== "object") return void 0;
1295
+ if (!raw || typeof raw !== "object") return null;
1137
1296
  const e = raw;
1138
1297
  const type = member(e.type, EVENT_TYPES);
1139
- if (!type) return void 0;
1298
+ if (!type) return null;
1140
1299
  const minute = count(e.minute, MAX_MINUTE);
1141
- if (minute === void 0) return void 0;
1142
- const out = { type, minute, teamCode: humanLabel(e.teamCode, TEAM_CODE_COLUMNS) };
1300
+ if (minute === void 0) return null;
1301
+ let added;
1302
+ if (e.added !== void 0) {
1303
+ added = count(e.added, MAX_ADDED_MINUTES);
1304
+ if (added === void 0 || added < 1) return null;
1305
+ }
1306
+ const side = member(e.side, SIDES);
1307
+ if (!side) return null;
1308
+ const out = { type, minute };
1309
+ if (added !== void 0) out.added = added;
1310
+ out.side = side;
1311
+ out.teamCode = humanLabel(e.teamCode, TEAM_CODE_COLUMNS);
1143
1312
  const player = humanLabel(e.player);
1144
1313
  if (player) out.player = player;
1145
1314
  return out;
1146
1315
  }
1316
+ function byClock(a, b) {
1317
+ return a.minute - b.minute || (a.added ?? 0) - (b.added ?? 0);
1318
+ }
1147
1319
  function sealMatch(parts, opts = {}) {
1148
1320
  if (!parts || typeof parts !== "object") return malformed("match is not an object");
1149
1321
  const id = opaqueId(parts.id, ESPN_ID);
@@ -1188,7 +1360,8 @@ function sealMatch(parts, opts = {}) {
1188
1360
  winnerCode = claimedWinner;
1189
1361
  }
1190
1362
  }
1191
- const events = opts.events === false ? [] : takeBounded(parts.events, MAX_MATCH_EVENTS).map(sealEvent).filter((e) => !!e);
1363
+ const timelined = status === "LIVE" || status === "HT" || status === "FT";
1364
+ const events = opts.events === false || !timelined ? [] : takeBounded(parts.events, MAX_MATCH_EVENTS).map(sealEvent).filter((e) => !!e).sort(byClock);
1192
1365
  const out = { id, stage };
1193
1366
  if (stageLabel2) out.stageLabel = stageLabel2;
1194
1367
  if (group) out.group = group;
@@ -4446,6 +4619,75 @@ function parseMinute(st) {
4446
4619
  }
4447
4620
  return void 0;
4448
4621
  }
4622
+ var DETAIL_FLAGS = ["scoringPlay", "yellowCard", "redCard", "penaltyKick", "ownGoal", "shootout"];
4623
+ var DETAIL_MINUTE = /^(\d{1,3})'$/;
4624
+ var DETAIL_MINUTE_ADDED = /^(\d{1,3})'\+(\d{1,2})'$/;
4625
+ function detailKind(d) {
4626
+ const f = {};
4627
+ for (const name of DETAIL_FLAGS) {
4628
+ const v = flag(d[name]);
4629
+ if (v === void 0) return void 0;
4630
+ f[name] = v;
4631
+ }
4632
+ if (f.shootout) return void 0;
4633
+ const { scoringPlay: s, yellowCard: y, redCard: r, penaltyKick: p, ownGoal: o } = f;
4634
+ if (s && !y && !r) {
4635
+ if (!p && !o) return "GOAL";
4636
+ if (o && !p) return "OWN_GOAL";
4637
+ if (p && !o) return "PEN";
4638
+ return void 0;
4639
+ }
4640
+ if (p || o || s) return void 0;
4641
+ if (y && !r) return "YELLOW";
4642
+ if (r && !y) return "RED";
4643
+ return void 0;
4644
+ }
4645
+ function detailMinute(clock) {
4646
+ if (!clock || typeof clock !== "object") return void 0;
4647
+ const dv = clock.displayValue;
4648
+ if (typeof dv !== "string") return void 0;
4649
+ const plain = DETAIL_MINUTE.exec(dv);
4650
+ if (plain) return { minute: Number(plain[1]) };
4651
+ const extra = DETAIL_MINUTE_ADDED.exec(dv);
4652
+ if (!extra) return void 0;
4653
+ return { minute: Number(extra[1]), added: Number(extra[2]) };
4654
+ }
4655
+ function rawTeamId(holder) {
4656
+ if (!holder || typeof holder !== "object") return void 0;
4657
+ const team = holder.team;
4658
+ if (!team || typeof team !== "object") return void 0;
4659
+ return opaqueId(team.id, ESPN_ID);
4660
+ }
4661
+ function detailEvents(details, sides) {
4662
+ const out = [];
4663
+ for (const raw of takeBounded(details, MAX_MATCH_EVENTS)) {
4664
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
4665
+ const d = raw;
4666
+ const type = detailKind(d);
4667
+ if (!type) continue;
4668
+ const when = detailMinute(d.clock);
4669
+ if (!when) continue;
4670
+ const teamId = rawTeamId(d);
4671
+ const side = teamId === void 0 ? void 0 : teamId === sides.homeId ? "home" : teamId === sides.awayId ? "away" : void 0;
4672
+ if (!side) continue;
4673
+ const event = { type, minute: when.minute };
4674
+ if (when.added !== void 0) event.added = when.added;
4675
+ event.side = side;
4676
+ event.teamCode = side === "home" ? sides.homeCode : sides.awayCode;
4677
+ const player = detailPlayer(d.athletesInvolved, type === "OWN_GOAL" ? side === "home" ? sides.awayId : sides.homeId : teamId);
4678
+ if (player) event.player = player;
4679
+ out.push(event);
4680
+ }
4681
+ return out;
4682
+ }
4683
+ function detailPlayer(athletes, expectedTeamId) {
4684
+ if (!Array.isArray(athletes) || athletes.length !== 1 || expectedTeamId === void 0) return void 0;
4685
+ const a = athletes[0];
4686
+ if (rawTeamId(a) !== expectedTeamId) return void 0;
4687
+ const name = a.displayName;
4688
+ if (!isHumanLabel(name)) return void 0;
4689
+ return humanLabel(name) || void 0;
4690
+ }
4449
4691
  var MAX_SLUG_UNITS2 = 64;
4450
4692
  var SLUG = /^[a-z0-9-]+$/;
4451
4693
  var EXACT_STAGE = Object.freeze({
@@ -4567,7 +4809,14 @@ function parseEspnEvent(raw, ctx = {}) {
4567
4809
  status,
4568
4810
  winnerCode,
4569
4811
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4570
- events: ev.events
4812
+ // The goals and cards: ESPN's `details`, read by the detail grammar; the
4813
+ // seal keeps them only on a status that holds a score.
4814
+ events: detailEvents(comp?.details, {
4815
+ homeId: rawTeamId(homeRaw),
4816
+ awayId: rawTeamId(awayRaw),
4817
+ homeCode: home.code,
4818
+ awayCode: away.code
4819
+ })
4571
4820
  },
4572
4821
  // The final reseal states the same kind the competitors were built with.
4573
4822
  { teamKind: kind }
@@ -4890,7 +5139,7 @@ function fetchMeta(result) {
4890
5139
  // src/adapters/espn.ts
4891
5140
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
4892
5141
  var DEFAULT_COMPETITION = BUNDLED_SLUG;
4893
- var USER_AGENT = `claudinho/${"0.11.1"} (+https://github.com/arturogarrido/claudinho)`;
5142
+ var USER_AGENT = `claudinho/${"0.12.0"} (+https://github.com/arturogarrido/claudinho)`;
4894
5143
  var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
4895
5144
  var DEFAULT_COOLDOWN_MS = 5 * 6e4;
4896
5145
  var MAX_COOLDOWN_MS = 15 * 6e4;
@@ -6606,6 +6855,9 @@ function mergeLive(base, live) {
6606
6855
  function clockOf(adapter) {
6607
6856
  return new Date(adapter.now?.() ?? Date.now());
6608
6857
  }
6858
+ function requestClock(adapter, given) {
6859
+ return given ?? clockOf(adapter);
6860
+ }
6609
6861
  function providerDayOf(adapter, instant) {
6610
6862
  if (Number.isNaN(instant.getTime())) return "";
6611
6863
  return adapter.bucketDay?.(instant) || instant.toISOString().slice(0, 10);
@@ -6780,7 +7032,7 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
6780
7032
  });
6781
7033
  let refreshRead = {};
6782
7034
  if (candidate) {
6783
- const { partial, served: _served, ...r } = await getMatchById(adapter, candidate.id);
7035
+ const { partial, served: _served, ...r } = await getMatchById(adapter, candidate.id, now);
6784
7036
  refreshRead = partial ? { partial } : {};
6785
7037
  const m = r.degraded ? candidate : r.match ?? candidate;
6786
7038
  if (!isFinished(m.status)) return { ...r, match: m, ...bothReads(windowRead, refreshRead) };
@@ -7013,10 +7265,19 @@ async function getScheduleAhead(adapter, now = /* @__PURE__ */ new Date()) {
7013
7265
  };
7014
7266
  }
7015
7267
  async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
7016
- const { complete: _complete, ...result } = await getLiveRead(adapter, now);
7268
+ const { complete: _complete, finished: _finished, finishedTruncated: _truncated, ...result } = await getLiveRead(adapter, now);
7017
7269
  return result;
7018
7270
  }
7019
- async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
7271
+ var MAX_LIVE_FINISHED = 64;
7272
+ function finishedOf(records, seen) {
7273
+ const wanted = new Set(takeBounded(seen, MAX_LIVE_FINISHED));
7274
+ const done = records.filter((m) => m.status === "FT" && m.score !== void 0);
7275
+ const first = done.filter((m) => wanted.has(m.id));
7276
+ const rest = done.filter((m) => !wanted.has(m.id));
7277
+ const all = [...first, ...rest];
7278
+ return { finished: all.slice(0, MAX_LIVE_FINISHED), truncated: all.length > MAX_LIVE_FINISHED };
7279
+ }
7280
+ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date(), seen = []) {
7020
7281
  try {
7021
7282
  const day = now.toISOString().slice(0, 10);
7022
7283
  const fetched = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1), { acrossSeasons: true }) : await adapter.fetchLive();
@@ -7024,6 +7285,7 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
7024
7285
  const season = meta?.season;
7025
7286
  const matches = fetched.filter((m) => isLive(m.status));
7026
7287
  const between = betweenEditionsOf(adapter, meta, fetched, providerDayOf(adapter, now));
7288
+ const done = finishedOf(fetched, seen);
7027
7289
  return {
7028
7290
  matches,
7029
7291
  degraded: false,
@@ -7035,10 +7297,12 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
7035
7297
  // boolean below is the refresher's, unchanged: false for that, for an
7036
7298
  // adapter that said nothing, and for a failed read alike.
7037
7299
  ...partialOfRead(meta),
7038
- complete: meta?.complete === true
7300
+ complete: meta?.complete === true,
7301
+ finished: done.finished,
7302
+ finishedTruncated: done.truncated
7039
7303
  };
7040
7304
  } catch {
7041
- return { matches: [], degraded: true, complete: false };
7305
+ return { matches: [], degraded: true, complete: false, finished: [], finishedTruncated: false };
7042
7306
  }
7043
7307
  }
7044
7308
 
@@ -7288,15 +7552,22 @@ function sealMarketSignal(raw, options = {}) {
7288
7552
  if (sourceMarketId) out.sourceMarketId = sourceMarketId;
7289
7553
  out.asOf = canonicalTimestamp(s.asOf) ?? "";
7290
7554
  out.fetchedAt = canonicalTimestamp(s.fetchedAt) ?? "";
7555
+ const judgedAt = options.now ?? /* @__PURE__ */ new Date();
7556
+ const fetchedMs = Date.parse(out.fetchedAt);
7557
+ if (out.fetchedAt === "" || !Number.isFinite(fetchedMs)) {
7558
+ return malformed("signal carries no readable fetch time");
7559
+ }
7560
+ if (fetchedMs > judgedAt.getTime()) {
7561
+ return unresolved("signal was fetched after the clock it is judged by");
7562
+ }
7291
7563
  out.outcomes = outcomes;
7292
7564
  const isAmbiguous = s.ambiguous !== false;
7293
7565
  const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
7294
7566
  if (favorite) out.favorite = favorite;
7295
7567
  if (liquidity !== void 0) out.liquidity = liquidity;
7296
7568
  if (volume24h !== void 0) out.volume24h = volume24h;
7297
- out.stale = s.stale !== false;
7569
+ out.stale = isStaleSignal(out, { now: judgedAt, maxAgeMs: options.maxAgeMs });
7298
7570
  out.ambiguous = isAmbiguous || out.source === "";
7299
- out.stale = out.stale || isStaleSignal(out, { now: options.now, maxAgeMs: options.maxAgeMs });
7300
7571
  return valid(out);
7301
7572
  }
7302
7573
  function parseCachedMarketSignal(raw, options = {}) {
@@ -7305,7 +7576,7 @@ function parseCachedMarketSignal(raw, options = {}) {
7305
7576
 
7306
7577
  // src/markets/normalize.ts
7307
7578
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
7308
- function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
7579
+ function marketRelevant(match, now) {
7309
7580
  if (isLive(match.status)) return true;
7310
7581
  if (isFinished(match.status)) return false;
7311
7582
  const k = Date.parse(match.kickoff);
@@ -7394,12 +7665,13 @@ function buildMarketSignal(input) {
7394
7665
  const outcomes = normalizeOutcomes(input.outcomes);
7395
7666
  const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
7396
7667
  const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
7668
+ const clock = input.now ?? /* @__PURE__ */ new Date();
7397
7669
  const signal = {
7398
7670
  matchId: input.match.id,
7399
7671
  source: input.source,
7400
7672
  sourceMarketId: input.sourceMarketId,
7401
7673
  asOf: input.asOf,
7402
- fetchedAt: input.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7674
+ fetchedAt: input.fetchedAt ?? clock.toISOString(),
7403
7675
  outcomes,
7404
7676
  favorite,
7405
7677
  liquidity: input.liquidity,
@@ -7407,8 +7679,8 @@ function buildMarketSignal(input) {
7407
7679
  stale: false,
7408
7680
  ambiguous: ambiguous2
7409
7681
  };
7410
- signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
7411
- const sealed = sealMarketSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
7682
+ signal.stale = isStaleSignal(signal, { now: clock, maxAgeMs: input.maxAgeMs });
7683
+ const sealed = sealMarketSignal(signal, { now: clock, maxAgeMs: input.maxAgeMs });
7412
7684
  if (sealed.kind !== "valid") {
7413
7685
  return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
7414
7686
  }
@@ -8684,6 +8956,7 @@ export {
8684
8956
  MAX_CONFIG_BYTES,
8685
8957
  MAX_COOLDOWN_MS,
8686
8958
  MAX_LABEL_COLUMNS,
8959
+ MAX_LIVE_FINISHED,
8687
8960
  MAX_RESPONSE_BYTES,
8688
8961
  MAX_SCHEDULE_INDEX,
8689
8962
  NO_BRACKET,
@@ -8833,6 +9106,7 @@ export {
8833
9106
  matchWindowSentence,
8834
9107
  mergeLive,
8835
9108
  modeLine,
9109
+ momentWords,
8836
9110
  nationArg,
8837
9111
  nationToFlag,
8838
9112
  nationToRegion,
@@ -8854,11 +9128,13 @@ export {
8854
9128
  parsedValue,
8855
9129
  pinUnder,
8856
9130
  productFlag,
9131
+ provenLastGoal,
8857
9132
  rallyCryFor,
8858
9133
  rallyEntryFor,
8859
9134
  readJsonBounded,
8860
9135
  readSmallFile,
8861
9136
  readUserConfig,
9137
+ requestClock,
8862
9138
  resolveClub,
8863
9139
  resolveCompetition,
8864
9140
  resolveMarketSource,
@@ -8871,6 +9147,7 @@ export {
8871
9147
  scheduleEntryOf,
8872
9148
  scheduleSpan,
8873
9149
  scoreline,
9150
+ sealEvent,
8874
9151
  sealMarketSignal,
8875
9152
  sealMatch,
8876
9153
  sealScheduleEntry,
@@ -8890,7 +9167,10 @@ export {
8890
9167
  tableKeyArg,
8891
9168
  tableShareCard,
8892
9169
  tableTitle,
9170
+ takeBounded,
8893
9171
  teamKind,
9172
+ timelineParts,
9173
+ timelineRows,
8894
9174
  truncateVisible,
8895
9175
  unresolved,
8896
9176
  unservedSentence,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/core",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "The engine behind Claudinho: the domain model, the provider adapters (ESPN), live scores and standings for the competition you follow (16 supported), the bundled 2026 World Cup schedule with its knockout bracket and read-only Polymarket sidecar. Independent fan project. Not affiliated with FIFA, any confederation, league or club, or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,7 +67,7 @@
67
67
  "club-world-cup"
68
68
  ],
69
69
  "devDependencies": {
70
- "@types/node": "^22.20.4",
70
+ "@types/node": "^22.20.5",
71
71
  "@vitest/coverage-v8": "^5.0.3",
72
72
  "tsup": "^8.0.0",
73
73
  "tsx": "^4.23.15",