@claudinho/core 0.11.2 → 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 +231 -18
  2. package/dist/index.js +293 -17
  3. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -153,11 +153,27 @@ type Stage = 'GROUP' | 'R32' | 'R16' | 'QF' | 'SF' | '3P' | 'F' | 'FRIENDLY' | '
153
153
  type Status = 'SCHEDULED' | 'LIVE' | 'HT' | 'FT' | 'POSTPONED' | 'CANCELLED';
154
154
  /** Match outcome from the home team's perspective. */
155
155
  type Outcome = 'H' | 'D' | 'A';
156
- /** A discrete in-match event (provider-dependent; often absent on free tiers). */
156
+ /**
157
+ * One of a match's goals or cards, as the provider listed it (ESPN's
158
+ * `competitions[0].details`, read by the grammar in `trust/espn.ts` and sealed
159
+ * by `sealEvent` in `trust/match.ts`, on the feed and the cache path alike).
160
+ * Key order is the serialized order.
161
+ */
157
162
  interface MatchEvent {
163
+ /** The kind. `SUB` stays in the type; ESPN's scoreboard never produces one. */
158
164
  type: 'GOAL' | 'OWN_GOAL' | 'PEN' | 'YELLOW' | 'RED' | 'SUB';
165
+ /** The minute the provider's clock showed: `90` for `90'+3'`. */
159
166
  minute: number;
167
+ /** Added time past `minute` (`3` for `90'+3'`), 1 to 30; absent when none. */
168
+ added?: number;
169
+ /**
170
+ * The side the event belongs to: the side a goal COUNTED for (an own goal
171
+ * included), the side a card was shown to. The identity; `teamCode` is a label.
172
+ */
173
+ side: 'home' | 'away';
174
+ /** That side's code, a display label. */
160
175
  teamCode: string;
176
+ /** The player, when the provider names exactly one of the expected team. */
161
177
  player?: string;
162
178
  }
163
179
  /** A single fixture/result — the central entity. */
@@ -201,7 +217,13 @@ interface Match {
201
217
  /** Live match minute when in progress. */
202
218
  minute?: number;
203
219
  status: Status;
204
- /** Goals/cards when the provider supplies them. */
220
+ /**
221
+ * The provider's goals and cards that could be read, in time order (minute,
222
+ * then added time). Never a claim that the list is every goal: a detail the
223
+ * grammar cannot read is left out, and a live timeline lags its score by
224
+ * nature. None on a match with no score (scheduled, postponed, cancelled);
225
+ * the key is absent when none was read.
226
+ */
205
227
  events?: MatchEvent[];
206
228
  /**
207
229
  * Winner's team code when the provider marks one (e.g. ESPN `competitor.winner`
@@ -279,6 +301,27 @@ type Lang = 'en' | 'es' | 'pt' | 'fr';
279
301
  declare function normalizeLang(lang?: string): Lang;
280
302
  /** Translate a message key with optional `{placeholder}` interpolation. */
281
303
  declare function t(lang: string | undefined, key: string, vars?: Record<string, string>): string;
304
+ /**
305
+ * The ten words of the plugin's moments, as `claudinho ambient --json` sends
306
+ * them (`words`), in this order: core's `moment.*` keys in one language.
307
+ * `goalFor` and `win` are templates holding `{team}` once, which the plugin
308
+ * fills with the record's own names; names, scores and minutes are never
309
+ * translated.
310
+ */
311
+ interface MomentWords {
312
+ readonly goal: string;
313
+ readonly ownGoal: string;
314
+ readonly ownGoalDetail: string;
315
+ readonly goalFor: string;
316
+ readonly goals: string;
317
+ readonly scoreUpdated: string;
318
+ readonly penalties: string;
319
+ readonly fullTime: string;
320
+ readonly win: string;
321
+ readonly draw: string;
322
+ }
323
+ /** The moment words in `lang` (an unsupported one is English), the templates left unfilled. */
324
+ declare function momentWords(lang: string | undefined): MomentWords;
282
325
  /**
283
326
  * Localized stage label, from the match (its stage, its group letter under the
284
327
  * group stage, and the words an OTHER stage carries). An `OTHER` stage prints
@@ -384,6 +427,42 @@ declare function padVisible(s: string, width: number): string;
384
427
  */
385
428
  declare function withFlag(text: string, flag: string | undefined, side: 'home' | 'away'): string;
386
429
 
430
+ interface TimelineOptions {
431
+ /** The reader's language (en/es/pt/fr); English when absent. */
432
+ readonly lang?: string;
433
+ /** Whether a nation's flag is printed beside its code (a club has none either way). */
434
+ readonly flags: boolean;
435
+ }
436
+ /** One part of a timeline row: its text, and `strong` on the player. */
437
+ interface TimelinePart {
438
+ readonly text: string;
439
+ readonly style?: 'strong';
440
+ }
441
+ /**
442
+ * The match's events as rows of parts; `[]` when it has none. Each row is
443
+ * the aligned columns as one plain part (two spaces, the minute, the marker,
444
+ * one space and the localized kind word, the side's label), then, when the
445
+ * provider named a player, two spaces and the player as one `strong` part.
446
+ * Joined, a row is its {@link timelineRows} line.
447
+ */
448
+ declare function timelineParts(match: Match, opts: TimelineOptions): TimelinePart[][];
449
+ /** The match's events as aligned text rows ({@link timelineParts} joined); `[]` when it has none. */
450
+ declare function timelineRows(match: Match, opts: TimelineOptions): string[];
451
+ /**
452
+ * The goal that made the record's score, PROVEN, or `null`. The record's
453
+ * scoring events, counted per side in their order (an own goal for the side
454
+ * it credited, which is the event's `side`), must equal its regulation score:
455
+ * then the timeline is whole and its last scoring event is the goal that made
456
+ * this score. Anything else proves nothing (a timeline that lags the score, a
457
+ * disallowed goal still listed, no score, no scoring event): the first scorer
458
+ * is never named for a second goal. Two scoring events identical in type,
459
+ * minute, added time, side and player prove nothing either: the same goal
460
+ * listed twice beside a timeline that lags would count as two. The ambient
461
+ * view's `lastGoal`, written by the refresher on each live record it
462
+ * publishes.
463
+ */
464
+ declare function provenLastGoal(match: Match): MatchEvent | null;
465
+
387
466
  /** Outcome (home perspective) from a final/looking scoreline. */
388
467
  declare function outcomeFromScore(home: number, away: number): Outcome;
389
468
  /** Is the match currently in play (including halftime)? */
@@ -871,8 +950,9 @@ declare function fixturesByGroup(group: string, fixtures?: Match[]): Match[];
871
950
  /**
872
951
  * Eligible to be "the next match": the kickoff is still ahead AND the match is
873
952
  * still going to be played. ONE predicate for every selector (`next`, the
874
- * statusline countdown, the knockout-fixture cache, the market fixture, the
875
- * refresher cadence) — a cancelled or postponed fixture keeps its kickoff, so
953
+ * knockout-fixture cache, the market fixture, the refresher cadence; the
954
+ * statusline countdown asks it for a kickoff strictly ahead and also names a
955
+ * DUE fixture, the CLI's `isDue`) — a cancelled or postponed fixture keeps its kickoff, so
876
956
  * a time-only check advertised it as the next game (audit A07). Finished is
877
957
  * deliberately NOT a clause: under a real clock a finished match never has a
878
958
  * future kickoff, and excluding it only changed seeded-clock replays of an
@@ -1683,7 +1763,7 @@ interface KnockoutFixturesResult {
1683
1763
  /**
1684
1764
  * The RESOLVED upcoming knockout fixtures from the live overlay — the data the
1685
1765
  * hot-path statusline can't fetch itself but needs to show a real next-match
1686
- * countdown (e.g. "🇲🇽 vs 🇪🇨 in 2d") instead of a 🏳️ placeholder. The cold-path
1766
+ * countdown (e.g. "⚽ 🇲🇽 vs 🇪🇨 in 2d") instead of a 🏳️ placeholder. The cold-path
1687
1767
  * refresher calls this and caches the result; the statusline reads the cache.
1688
1768
  *
1689
1769
  * Returns only fixtures where BOTH sides are resolved (neither is the 🏳️ placeholder) and kickoff
@@ -1794,6 +1874,8 @@ declare function getScheduleAhead(adapter: ProviderAdapter, now?: Date): Promise
1794
1874
  * an adjacent bucket. Adapters without a window fetch fall back to `fetchLive()`.
1795
1875
  */
1796
1876
  declare function getLiveMatches(adapter: ProviderAdapter, now?: Date): Promise<LiveResult>;
1877
+ /** The finished records a live read returns, at most (the refresher's record of play keeps as many). */
1878
+ declare const MAX_LIVE_FINISHED = 64;
1797
1879
  /** A live read with the answer's own account of itself: what the refresher decides from. */
1798
1880
  interface LiveReadResult extends LiveResult {
1799
1881
  /**
@@ -1802,14 +1884,30 @@ interface LiveReadResult extends LiveResult {
1802
1884
  * whole and holds no match in play does not prove none is.
1803
1885
  */
1804
1886
  complete: boolean;
1887
+ /**
1888
+ * The window's records with status `FT` that hold a score, as the adapter
1889
+ * sealed them, asked of the whole window before the in-play filter: the ids
1890
+ * the caller passed as `seen` first, in the window's order, then the others,
1891
+ * at most {@link MAX_LIVE_FINISHED}. `[]` for a failed read. The evidence of
1892
+ * full time for a match the refresher saw in play; never on
1893
+ * {@link getLiveMatches}' result.
1894
+ */
1895
+ finished: Match[];
1896
+ /**
1897
+ * The cap left finished records out: an id the caller saw that is absent
1898
+ * from `finished` may then be one of them, so its absence decides nothing.
1899
+ */
1900
+ finishedTruncated: boolean;
1805
1901
  }
1806
1902
  /**
1807
1903
  * {@link getLiveMatches}, keeping the window's `complete` verdict (read from
1808
- * the window's result BEFORE the in-play filter makes a new array). For the
1809
- * cold-path refresher, which keeps polling after a match was seen in play
1810
- * until a WHOLE read holds none.
1904
+ * the window's result BEFORE the in-play filter makes a new array), and the
1905
+ * window's finished records (`finished`, chosen ids-first by `seen`, the ids
1906
+ * the refresher's record of play holds as seen in play). For the cold-path
1907
+ * refresher, which keeps polling after a match was seen in play until a WHOLE
1908
+ * read holds none.
1811
1909
  */
1812
- declare function getLiveRead(adapter: ProviderAdapter, now?: Date): Promise<LiveReadResult>;
1910
+ declare function getLiveRead(adapter: ProviderAdapter, now?: Date, seen?: readonly string[]): Promise<LiveReadResult>;
1813
1911
 
1814
1912
  /** A roster team plus its group letter (A–L). */
1815
1913
  interface TeamInfo extends Team {
@@ -1951,6 +2049,15 @@ interface BoundedList<T> {
1951
2049
  * make an unbounded traversal safe. `takeBounded` is the one that bounds work.
1952
2050
  */
1953
2051
  declare function bounded<T>(items: readonly T[], max: number, complete?: boolean): BoundedList<T>;
2052
+ /**
2053
+ * Take at most `max` items from something that may not be an array at all,
2054
+ * BEFORE any per-item work happens.
2055
+ *
2056
+ * This is the work bound. Mapping, sorting or sanitizing first and slicing
2057
+ * afterwards is what made a 4,000-row group cost 300ms to return 32, and a
2058
+ * 100k-event record cost 5.3s on a 150ms surface.
2059
+ */
2060
+ declare function takeBounded<T>(value: unknown, max: number): T[];
1954
2061
 
1955
2062
  /**
1956
2063
  * The vocabulary for "we could not use this".
@@ -2514,7 +2621,7 @@ declare const DEFAULT_MAX_AGE_MS: number;
2514
2621
  * "markets favor X 100%" after full time reads as a bug, not information.
2515
2622
  * An unparseable kickoff fails open (the reliability gate still applies).
2516
2623
  */
2517
- declare function marketRelevant(match: Match, now?: Date): boolean;
2624
+ declare function marketRelevant(match: Match, now: Date): boolean;
2518
2625
  /**
2519
2626
  * Re-scale outcome probabilities so the positive ones sum to 1. This removes
2520
2627
  * market "vig" (raw prices sum to >1) and is scale-agnostic: inputs may be
@@ -2886,6 +2993,15 @@ declare function isPinnedSide(side: Team, pin: {
2886
2993
  readonly code: string;
2887
2994
  readonly name: string;
2888
2995
  }): boolean;
2996
+ /**
2997
+ * THE constructor of a `MatchEvent`, on the feed path (the candidates the ESPN
2998
+ * detail grammar built) and the cache path alike. An event with no side, a
2999
+ * side that is neither, an unreadable minute or an added time outside 1..30 is
3000
+ * refused (`null`); the match stands. Keys in the order `MatchEvent` declares
3001
+ * them. Exported for the one other reader of an event, the ambient view, which
3002
+ * re-seals the `lastGoal` the refresher stored on a cached live record.
3003
+ */
3004
+ declare function sealEvent(raw: unknown): MatchEvent | null;
2889
3005
  /**
2890
3006
  * Validate candidate parts into a Match, or say why not.
2891
3007
  *
@@ -3252,12 +3368,41 @@ declare function bracketCapability(competition: string, table?: readonly Competi
3252
3368
  * - a selected competition: an {@link AmbientView}.
3253
3369
  */
3254
3370
 
3371
+ /**
3372
+ * One part of the band's line (`lineParts`): a run of its text, `strong` for a
3373
+ * score token the fit kept whole. The parts' texts joined are the line.
3374
+ */
3375
+ interface AmbientLinePart {
3376
+ readonly text: string;
3377
+ readonly style?: 'strong';
3378
+ }
3379
+ /** The two names the hook's line prints for a record (a nation's roster name on the bundled competition). */
3380
+ interface AmbientNames {
3381
+ readonly home: string;
3382
+ readonly away: string;
3383
+ }
3384
+ /**
3385
+ * A finished match as the ambient view lists it (`finished`): the sealed
3386
+ * record (status `FT`, its final score), as a live one is serialized, with:
3387
+ */
3388
+ type AmbientFinishedMatch = Match & {
3389
+ /**
3390
+ * The side of the SAVED pin (`claudinho follow <alias> --team`, by core's
3391
+ * `isPinnedSide`), or `null`: never `CLAUDINHO_TEAM`, which only orders.
3392
+ */
3393
+ readonly pinnedSide: 'home' | 'away' | null;
3394
+ /** The names the hook's line prints for it. */
3395
+ readonly names: AmbientNames;
3396
+ /** The hook's own line for it, `(FT)` in the minute's place. */
3397
+ readonly line: string;
3398
+ };
3255
3399
  /**
3256
3400
  * A live match as the ambient view lists it: the sealed record, as the other
3257
3401
  * `--json` twins serialize a `Match` (`status`, the `score` object, `shootout`
3258
3402
  * after a shootout, `Team.id` where the cache holds it, a nation's generated
3259
3403
  * `flag`, no `flag` key on a club), with no `events` key (the ambient reader
3260
- * never seals a timeline), and two booleans about the reader's own team:
3404
+ * never seals a timeline), and what it says about the reader's own team, the
3405
+ * names and the last goal:
3261
3406
  */
3262
3407
  type AmbientMatch = Match & {
3263
3408
  /**
@@ -3271,6 +3416,10 @@ type AmbientMatch = Match & {
3271
3416
  * World Cup's nations); whatever `CLAUDINHO_TEAM` prefers.
3272
3417
  */
3273
3418
  readonly pinned: boolean;
3419
+ /** Which side is the SAVED pin's (by core's `isPinnedSide`), or `null`: never `CLAUDINHO_TEAM`'s. */
3420
+ readonly pinnedSide: 'home' | 'away' | null;
3421
+ /** The names the hook's line prints for it (a nation's roster name on the bundled competition). */
3422
+ readonly names: AmbientNames;
3274
3423
  /**
3275
3424
  * The hook's own line for this record, as `claudinho hook` prints it (the
3276
3425
  * names, pinned to the World Cup's roster on the bundle, each with its flag
@@ -3279,17 +3428,40 @@ type AmbientMatch = Match & {
3279
3428
  * never composes one.
3280
3429
  */
3281
3430
  readonly line: string;
3431
+ /**
3432
+ * The goal that made the record's score, PROVEN by the refresher (its
3433
+ * scoring events counted per side equal the score: core's
3434
+ * `provenLastGoal`), stored on the cached record and re-sealed by the
3435
+ * reader with core's `sealEvent` and kept only as a goal, an own goal or a
3436
+ * penalty whose `teamCode` is the code of the side its `side` names, a side
3437
+ * that has scored (its regulation score at least 1); `null`
3438
+ * when nothing is proven or the stored value is anything else.
3439
+ */
3440
+ readonly lastGoal: MatchEvent | null;
3282
3441
  };
3283
3442
  /** The ambient view of a selected competition, its keys in this order. */
3284
3443
  interface AmbientView {
3285
3444
  /**
3286
- * The statusline's line, as `claudinho prompt` prints it (the same reader,
3287
- * the same options), fitted to `--columns N` when given: whole matches are
3288
- * kept while they fit and the rest counted in the `+N` (or `+more`) suffix,
3289
- * which is never cut while the width holds it; below the suffix's own width
3290
- * the line is the width's cut, never wider than asked.
3445
+ * The statusline's line rendered in `lang` (the same reader and the same
3446
+ * options as `claudinho prompt`, which prints it in English), fitted to
3447
+ * `--columns N` when given: whole matches are kept while they fit and
3448
+ * the rest counted in the `+N` (or `+more`) suffix, which is never cut
3449
+ * while the width holds it; below the suffix's own width
3450
+ * the line is the width's cut, never wider than asked. Its own tokens are in
3451
+ * `lang` (the countdown, the syncing phrase, the live segment's half-time
3452
+ * and live words); the overflow marker (`+N`, `+more`), the World Cup's
3453
+ * sign-off and nothing known (`⚽ —`) are English in every language, so
3454
+ * `idle` and `empty` say what they say in every language.
3291
3455
  */
3292
3456
  readonly line: string;
3457
+ /**
3458
+ * The line as parts, built by the fitter as it places each shown score
3459
+ * token: their texts joined are `line` exactly, a score token the fit kept
3460
+ * whole is `strong`, one it cut is not. One plain part, the line itself, on
3461
+ * the countdown, the syncing line, nothing known and the sign-off, and
3462
+ * whenever the parts would not join to the line.
3463
+ */
3464
+ readonly lineParts: readonly AmbientLinePart[];
3293
3465
  /**
3294
3466
  * The line is the one long-lived line, the edition's sign-off (the World
3295
3467
  * Cup's, once every bundled window has closed): nothing changes until the
@@ -3322,6 +3494,18 @@ interface AmbientView {
3322
3494
  * listed, best effort, as the line shows it, and is never current.
3323
3495
  */
3324
3496
  readonly live: BoundedList<AmbientMatch>;
3497
+ /**
3498
+ * The matches the refresher SAW in play that a whole read then returned
3499
+ * finished (the snapshot's record of play), each for 10 minutes from the
3500
+ * read that first saw it finished, judged by the reader's clock (a stamp in
3501
+ * the future is not believed), whatever the snapshot's display window (the
3502
+ * refresher stops polling once the last match ends, so the snapshot ages
3503
+ * past it while an entry is still inside its own 10 minutes); never one the
3504
+ * live list holds. Listed only from a snapshot whose live read was whole
3505
+ * (`liveComplete: true`); otherwise `[]`, which then says nothing. At most
3506
+ * 64. Absence from it never means a match is over.
3507
+ */
3508
+ readonly finished: readonly AmbientFinishedMatch[];
3325
3509
  /**
3326
3510
  * The live list is a believed snapshot's inside its display window (its
3327
3511
  * stamp's age, by the reader's own rule, is finite and short of the window),
@@ -3337,7 +3521,9 @@ interface AmbientView {
3337
3521
  /**
3338
3522
  * The fixture the statusline's countdown names (its resolved pairing; the
3339
3523
  * preference's first), whether or not a match is in play now; `null` when
3340
- * there is none to count down to. A fixture ahead, never one in play.
3524
+ * there is none to count down to. A fixture ahead, or a DUE one (still
3525
+ * scheduled, its kickoff passed by at most 30 minutes, not held by the
3526
+ * snapshot's record of play: the line reads "now"); never one in play.
3341
3527
  */
3342
3528
  readonly next: Match | null;
3343
3529
  /**
@@ -3357,6 +3543,19 @@ interface AmbientView {
3357
3543
  readonly pickUnreadable?: true;
3358
3544
  /** The selection, as every `--json` answer carries it (core `selectionExtras`). */
3359
3545
  readonly competition: CompetitionKey;
3546
+ /**
3547
+ * The CLI's resolved language (`--lang`, `CLAUDINHO_LANG`, `LANG`'s language
3548
+ * part, each only when it is one of the four, else `en`), the language of
3549
+ * `line`, `lineParts` and `words`. `context`, `attribution`, `disclaimer`
3550
+ * and each record's `line` stay English in every language.
3551
+ */
3552
+ readonly lang: Lang;
3553
+ /**
3554
+ * The plugin's moment words in `lang` (core's `moment.*` keys, `momentWords`):
3555
+ * `goalFor` and `win` templates holding `{team}` once, for the plugin to
3556
+ * fill with a record's own `names`.
3557
+ */
3558
+ readonly words: MomentWords;
3360
3559
  /** The snapshot says its last refresh failed (`false` with no snapshot). */
3361
3560
  readonly degraded: boolean;
3362
3561
  /** The provider the snapshot was read from, whatever its stamp; `null` with no snapshot. */
@@ -3374,6 +3573,20 @@ interface AmbientView {
3374
3573
  readonly staleAfter: string | null;
3375
3574
  /** The non-affiliation sentence, core's `DISCLAIMER`. */
3376
3575
  readonly disclaimer: string;
3576
+ /**
3577
+ * The provider's attribution sentence, in English as every ambient string:
3578
+ * core's `live.data` copy with `liveSourceLabel(source)` (`Live data:
3579
+ * ESPN`), when the snapshot names a provider (`source` is a string) AND
3580
+ * the line is neither the sign-off (`idle`) nor nothing known (`empty`);
3581
+ * `null` otherwise: no snapshot, the sign-off (made from the bundled clock),
3582
+ * nothing known. `degraded` does not decide: a degraded snapshot still shows
3583
+ * a live row an earlier read served, or a countdown to a fixture discovery
3584
+ * read; nor does a read that was not whole. A limit: on the bundled
3585
+ * competition, a countdown made from the bundled skeleton (before its
3586
+ * knockout pairings resolve) would be attributed too; unreachable while that
3587
+ * edition is over.
3588
+ */
3589
+ readonly attribution: string | null;
3377
3590
  }
3378
3591
 
3379
3592
  /** The two v1 snippet shapes. `social` is the rich card; `compact` is one terse line per match. */
@@ -4067,4 +4280,4 @@ declare function lookAtEntry(path: string): FileEntry;
4067
4280
  */
4068
4281
  declare function lookAtOwnFile(path: string, maxBytes: number): OwnFile;
4069
4282
 
4070
- export { type AmbientMatch, type AmbientView, BRACKET_STAGE_ORDER, BUNDLE_COMPETITION, type BatchResolution, type BetweenEditions, type BoundedList, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketShareCard, type BracketTopology, type BracketView, type BuildSignalInput, COMPETITION_KIND, COUNTDOWN_NOW, type Capabilities, type Capability, type ChosenBy, type ClubResolution, type CompetitionEntry, type CompetitionKey, type CompetitionKind, type CompetitionSelection, DEFAULT_COMPETITION, DEFAULT_COOLDOWN_MS, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, DISCLAIMER, type DayAttribution, type DerivedTables, EARLIER_RECORD_NOTE, EspnAdapter, type EspnAdapterOptions, FAN_PROJECT, FLAVOR_BANKS, FLAVOR_LEVELS, FLAVOR_MOMENTS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FetchMeta, type FetchWindowOptions, type FileEntry, type Flair, type FlairOpts, type FlavorBank, type FlavorLevel, type Moment as FlavorMoment, type FlavorOpts, type FormatOpts, type GroupStandings, KNOCKOUT_EXTRA_TIME_MS, KNOWN_SOURCES, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type ListedCompetition, type LiveReadResult, type LiveResult, MARKETS_SCOPE_NOTE, MARKET_COMPETITIONS, MAX_CONFIG_BYTES, MAX_COOLDOWN_MS, MAX_LABEL_COLUMNS, MAX_RESPONSE_BYTES, MAX_SCHEDULE_INDEX, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchByIdResult, type MatchEvent, type MatchShareCard, NO_BRACKET, type NextFixtureResult, type NoConfigReason, type Outcome, type OwnFile, type ParseResult, type Pin, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, ProviderError, type ProviderErrorKind, type PunditPick, RALLY_CRIES, type RallyCry, type RallyPin, type ReadAccount, type ResolvedParticipant, ResponseTooLargeError, type Roster, SCHEDULE_AHEAD_DAYS, SCHEDULE_LOOKBACK_DAYS, SEASON_SLUG, SHARE_DISCLAIMER, SHARE_HASHTAG, STANDINGS_SHAPE, SUPPORTED, type ScheduleAheadResult, type ScheduleEntry, type SeasonInfo, type SelectedCompetition, type Selection, type ShareBracketInput, type ShareBracketOptions, type ShareCardContext, type ShareCardMarket, type ShareCardView, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type SmallFile, type Stage, type StandingRow, type StandingsResult, type Status, TABLE_KEY_ARG, TEAM_KIND, type TableData, type TableShareCard, type Team, type TeamInfo, type TeamKind, type TeamLookup, type UserConfig, type UserConfigRead, type VerdictExtras, type VerdictSource, allFixtures, allTeams, ambiguous, asFlavorLevel, attachFetchMeta, bounded, bracketCapability, bracketShareCard, buildBracketTopology, buildBracketView, buildMarketSignal, bundleApplies, bundleSeasonYear, byKickoff, cacheDirFor, cacheableKeys, capabilitiesOf, competitionBase, competitionKind, competitionLabel, competitionValue, computeStandings, configPath, countdown, countdownPhrase, currentOrNextFixtureForTeam, dateNoneRead, dateNoneReadSentence, dateShareCard, dateUnreached, dateUnreachedSentence, dayAttribution, definitiveNone, deriveFavorite, deriveTables, disclaimerLine, displayWidth, emptyBatch, entryOf, favoriteStrength, fetchMeta, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, flavorsFor, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getLiveRead, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getScheduleAhead, getStandings, groups, hasLiveWindow, hasSaneDistribution, humanLabel, isCacheable, isFinished, isFlavorLevel, isHumanLabel, isKnockoutStage, isKnownSource, isLive, isMexicoNationalTeam, isPinnedSide, isPlaceholderSide, isReliableMarketSignal, isStaleSignal, isTeam, isTournamentWindowOver, isUpcoming, isValidDate, isValidTimeZone, joinSegments, knockoutWindow, listCompetitions, liveNoneRead, liveNoneReadSentence, liveShareCard, liveSourceLabel, liveWindowMsFor, loadBracketTopology, localDate, lookAtEntry, lookAtOwnFile, lookAtSmallFile, lookupTeam, makeAdapter, makeMarketProvider, malformed, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketDisplayable, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketScopeVerdict, marketSignalRendersFor, marketSourceLabel, marketsCoverCompetition, marketsNoneRead, marketsNoneReadNote, marketsNoneReadSentence, matchFlair, matchFlairs, matchFlavor, matchKey, matchLocation, matchNoneReadSentence, matchShareCard, matchWindowSentence, mergeLive, modeLine, nationArg, nationToFlag, nationToRegion, nextFixtureForPin, nextFixtureForTeam, nextHorizonSentence, nextNoneReadSentence, nextShareCard, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseCachedMarketSignal, parseCachedMatch, parseCachedMatches, parseCachedScheduleIndex, parseStandings, parseTeamSlot, parsedValue, pinUnder, productFlag, rallyCryFor, rallyEntryFor, readJsonBounded, readSmallFile, readUserConfig, requestClock, resolveClub, resolveCompetition, resolveMarketSource, resolveTz, resolvedValues, retryAfterMs, rosterFor, sanitizeBundledFixture, savedSlug, scheduleEntryOf, scheduleSpan, scoreline, sealMarketSignal, sealMatch, sealScheduleEntry, sealSeason, selectOne, selectedCompetition, selectionExtras, selectionRefusal, selectionVerdict, servedExtras, stageLabel, stageLabelI18n, standingsShapeOf, statesPartial, t, tableData, tableKeyArg, tableShareCard, tableTitle, teamKind, truncateVisible, unresolved, unservedSentence, valid, verdictExtras, verdictNotice, verdictQualifiers, withFlag };
4283
+ export { type AmbientFinishedMatch, type AmbientLinePart, type AmbientMatch, type AmbientNames, type AmbientView, BRACKET_STAGE_ORDER, BUNDLE_COMPETITION, type BatchResolution, type BetweenEditions, type BoundedList, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketShareCard, type BracketTopology, type BracketView, type BuildSignalInput, COMPETITION_KIND, COUNTDOWN_NOW, type Capabilities, type Capability, type ChosenBy, type ClubResolution, type CompetitionEntry, type CompetitionKey, type CompetitionKind, type CompetitionSelection, DEFAULT_COMPETITION, DEFAULT_COOLDOWN_MS, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, DISCLAIMER, type DayAttribution, type DerivedTables, EARLIER_RECORD_NOTE, EspnAdapter, type EspnAdapterOptions, FAN_PROJECT, FLAVOR_BANKS, FLAVOR_LEVELS, FLAVOR_MOMENTS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FetchMeta, type FetchWindowOptions, type FileEntry, type Flair, type FlairOpts, type FlavorBank, type FlavorLevel, type Moment as FlavorMoment, type FlavorOpts, type FormatOpts, type GroupStandings, KNOCKOUT_EXTRA_TIME_MS, KNOWN_SOURCES, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type ListedCompetition, type LiveReadResult, type LiveResult, MARKETS_SCOPE_NOTE, MARKET_COMPETITIONS, MAX_CONFIG_BYTES, MAX_COOLDOWN_MS, MAX_LABEL_COLUMNS, MAX_LIVE_FINISHED, MAX_RESPONSE_BYTES, MAX_SCHEDULE_INDEX, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchByIdResult, type MatchEvent, type MatchShareCard, type MomentWords, NO_BRACKET, type NextFixtureResult, type NoConfigReason, type Outcome, type OwnFile, type ParseResult, type Pin, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, ProviderError, type ProviderErrorKind, type PunditPick, RALLY_CRIES, type RallyCry, type RallyPin, type ReadAccount, type ResolvedParticipant, ResponseTooLargeError, type Roster, SCHEDULE_AHEAD_DAYS, SCHEDULE_LOOKBACK_DAYS, SEASON_SLUG, SHARE_DISCLAIMER, SHARE_HASHTAG, STANDINGS_SHAPE, SUPPORTED, type ScheduleAheadResult, type ScheduleEntry, type SeasonInfo, type SelectedCompetition, type Selection, type ShareBracketInput, type ShareBracketOptions, type ShareCardContext, type ShareCardMarket, type ShareCardView, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type SmallFile, type Stage, type StandingRow, type StandingsResult, type Status, TABLE_KEY_ARG, TEAM_KIND, type TableData, type TableShareCard, type Team, type TeamInfo, type TeamKind, type TeamLookup, type TimelineOptions, type TimelinePart, type UserConfig, type UserConfigRead, type VerdictExtras, type VerdictSource, allFixtures, allTeams, ambiguous, asFlavorLevel, attachFetchMeta, bounded, bracketCapability, bracketShareCard, buildBracketTopology, buildBracketView, buildMarketSignal, bundleApplies, bundleSeasonYear, byKickoff, cacheDirFor, cacheableKeys, capabilitiesOf, competitionBase, competitionKind, competitionLabel, competitionValue, computeStandings, configPath, countdown, countdownPhrase, currentOrNextFixtureForTeam, dateNoneRead, dateNoneReadSentence, dateShareCard, dateUnreached, dateUnreachedSentence, dayAttribution, definitiveNone, deriveFavorite, deriveTables, disclaimerLine, displayWidth, emptyBatch, entryOf, favoriteStrength, fetchMeta, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, flavorsFor, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getLiveRead, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getScheduleAhead, getStandings, groups, hasLiveWindow, hasSaneDistribution, humanLabel, isCacheable, isFinished, isFlavorLevel, isHumanLabel, isKnockoutStage, isKnownSource, isLive, isMexicoNationalTeam, isPinnedSide, isPlaceholderSide, isReliableMarketSignal, isStaleSignal, isTeam, isTournamentWindowOver, isUpcoming, isValidDate, isValidTimeZone, joinSegments, knockoutWindow, listCompetitions, liveNoneRead, liveNoneReadSentence, liveShareCard, liveSourceLabel, liveWindowMsFor, loadBracketTopology, localDate, lookAtEntry, lookAtOwnFile, lookAtSmallFile, lookupTeam, makeAdapter, makeMarketProvider, malformed, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketDisplayable, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketScopeVerdict, marketSignalRendersFor, marketSourceLabel, marketsCoverCompetition, marketsNoneRead, marketsNoneReadNote, marketsNoneReadSentence, matchFlair, matchFlairs, matchFlavor, matchKey, matchLocation, matchNoneReadSentence, matchShareCard, matchWindowSentence, mergeLive, modeLine, momentWords, nationArg, nationToFlag, nationToRegion, nextFixtureForPin, nextFixtureForTeam, nextHorizonSentence, nextNoneReadSentence, nextShareCard, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseCachedMarketSignal, parseCachedMatch, parseCachedMatches, parseCachedScheduleIndex, parseStandings, parseTeamSlot, parsedValue, pinUnder, productFlag, provenLastGoal, rallyCryFor, rallyEntryFor, readJsonBounded, readSmallFile, readUserConfig, requestClock, resolveClub, resolveCompetition, resolveMarketSource, resolveTz, resolvedValues, retryAfterMs, rosterFor, sanitizeBundledFixture, savedSlug, scheduleEntryOf, scheduleSpan, scoreline, sealEvent, sealMarketSignal, sealMatch, sealScheduleEntry, sealSeason, selectOne, selectedCompetition, selectionExtras, selectionRefusal, selectionVerdict, servedExtras, stageLabel, stageLabelI18n, standingsShapeOf, statesPartial, t, tableData, tableKeyArg, tableShareCard, tableTitle, takeBounded, teamKind, timelineParts, timelineRows, truncateVisible, unresolved, unservedSentence, valid, verdictExtras, verdictNotice, verdictQualifiers, withFlag };
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",
@@ -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.2"} (+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;
@@ -7016,10 +7265,19 @@ async function getScheduleAhead(adapter, now = /* @__PURE__ */ new Date()) {
7016
7265
  };
7017
7266
  }
7018
7267
  async function getLiveMatches(adapter, now = /* @__PURE__ */ new Date()) {
7019
- const { complete: _complete, ...result } = await getLiveRead(adapter, now);
7268
+ const { complete: _complete, finished: _finished, finishedTruncated: _truncated, ...result } = await getLiveRead(adapter, now);
7020
7269
  return result;
7021
7270
  }
7022
- 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 = []) {
7023
7281
  try {
7024
7282
  const day = now.toISOString().slice(0, 10);
7025
7283
  const fetched = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1), { acrossSeasons: true }) : await adapter.fetchLive();
@@ -7027,6 +7285,7 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
7027
7285
  const season = meta?.season;
7028
7286
  const matches = fetched.filter((m) => isLive(m.status));
7029
7287
  const between = betweenEditionsOf(adapter, meta, fetched, providerDayOf(adapter, now));
7288
+ const done = finishedOf(fetched, seen);
7030
7289
  return {
7031
7290
  matches,
7032
7291
  degraded: false,
@@ -7038,10 +7297,12 @@ async function getLiveRead(adapter, now = /* @__PURE__ */ new Date()) {
7038
7297
  // boolean below is the refresher's, unchanged: false for that, for an
7039
7298
  // adapter that said nothing, and for a failed read alike.
7040
7299
  ...partialOfRead(meta),
7041
- complete: meta?.complete === true
7300
+ complete: meta?.complete === true,
7301
+ finished: done.finished,
7302
+ finishedTruncated: done.truncated
7042
7303
  };
7043
7304
  } catch {
7044
- return { matches: [], degraded: true, complete: false };
7305
+ return { matches: [], degraded: true, complete: false, finished: [], finishedTruncated: false };
7045
7306
  }
7046
7307
  }
7047
7308
 
@@ -7291,15 +7552,22 @@ function sealMarketSignal(raw, options = {}) {
7291
7552
  if (sourceMarketId) out.sourceMarketId = sourceMarketId;
7292
7553
  out.asOf = canonicalTimestamp(s.asOf) ?? "";
7293
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
+ }
7294
7563
  out.outcomes = outcomes;
7295
7564
  const isAmbiguous = s.ambiguous !== false;
7296
7565
  const favorite = isAmbiguous ? void 0 : deriveFavorite(outcomes);
7297
7566
  if (favorite) out.favorite = favorite;
7298
7567
  if (liquidity !== void 0) out.liquidity = liquidity;
7299
7568
  if (volume24h !== void 0) out.volume24h = volume24h;
7300
- out.stale = s.stale !== false;
7569
+ out.stale = isStaleSignal(out, { now: judgedAt, maxAgeMs: options.maxAgeMs });
7301
7570
  out.ambiguous = isAmbiguous || out.source === "";
7302
- out.stale = out.stale || isStaleSignal(out, { now: options.now, maxAgeMs: options.maxAgeMs });
7303
7571
  return valid(out);
7304
7572
  }
7305
7573
  function parseCachedMarketSignal(raw, options = {}) {
@@ -7308,7 +7576,7 @@ function parseCachedMarketSignal(raw, options = {}) {
7308
7576
 
7309
7577
  // src/markets/normalize.ts
7310
7578
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
7311
- function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
7579
+ function marketRelevant(match, now) {
7312
7580
  if (isLive(match.status)) return true;
7313
7581
  if (isFinished(match.status)) return false;
7314
7582
  const k = Date.parse(match.kickoff);
@@ -7397,12 +7665,13 @@ function buildMarketSignal(input) {
7397
7665
  const outcomes = normalizeOutcomes(input.outcomes);
7398
7666
  const ambiguous2 = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
7399
7667
  const favorite = ambiguous2 ? void 0 : deriveFavorite(outcomes);
7668
+ const clock = input.now ?? /* @__PURE__ */ new Date();
7400
7669
  const signal = {
7401
7670
  matchId: input.match.id,
7402
7671
  source: input.source,
7403
7672
  sourceMarketId: input.sourceMarketId,
7404
7673
  asOf: input.asOf,
7405
- fetchedAt: input.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7674
+ fetchedAt: input.fetchedAt ?? clock.toISOString(),
7406
7675
  outcomes,
7407
7676
  favorite,
7408
7677
  liquidity: input.liquidity,
@@ -7410,8 +7679,8 @@ function buildMarketSignal(input) {
7410
7679
  stale: false,
7411
7680
  ambiguous: ambiguous2
7412
7681
  };
7413
- signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
7414
- 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 });
7415
7684
  if (sealed.kind !== "valid") {
7416
7685
  return { ...signal, outcomes: [], favorite: void 0, stale: true, ambiguous: true };
7417
7686
  }
@@ -8687,6 +8956,7 @@ export {
8687
8956
  MAX_CONFIG_BYTES,
8688
8957
  MAX_COOLDOWN_MS,
8689
8958
  MAX_LABEL_COLUMNS,
8959
+ MAX_LIVE_FINISHED,
8690
8960
  MAX_RESPONSE_BYTES,
8691
8961
  MAX_SCHEDULE_INDEX,
8692
8962
  NO_BRACKET,
@@ -8836,6 +9106,7 @@ export {
8836
9106
  matchWindowSentence,
8837
9107
  mergeLive,
8838
9108
  modeLine,
9109
+ momentWords,
8839
9110
  nationArg,
8840
9111
  nationToFlag,
8841
9112
  nationToRegion,
@@ -8857,6 +9128,7 @@ export {
8857
9128
  parsedValue,
8858
9129
  pinUnder,
8859
9130
  productFlag,
9131
+ provenLastGoal,
8860
9132
  rallyCryFor,
8861
9133
  rallyEntryFor,
8862
9134
  readJsonBounded,
@@ -8875,6 +9147,7 @@ export {
8875
9147
  scheduleEntryOf,
8876
9148
  scheduleSpan,
8877
9149
  scoreline,
9150
+ sealEvent,
8878
9151
  sealMarketSignal,
8879
9152
  sealMatch,
8880
9153
  sealScheduleEntry,
@@ -8894,7 +9167,10 @@ export {
8894
9167
  tableKeyArg,
8895
9168
  tableShareCard,
8896
9169
  tableTitle,
9170
+ takeBounded,
8897
9171
  teamKind,
9172
+ timelineParts,
9173
+ timelineRows,
8898
9174
  truncateVisible,
8899
9175
  unresolved,
8900
9176
  unservedSentence,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/core",
3
- "version": "0.11.2",
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",