@claudinho/core 0.2.0 → 0.4.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 CHANGED
@@ -1,8 +1,9 @@
1
1
  # @claudinho/core ⚽
2
2
 
3
- Shared domain model, data-provider adapters, and helpers for **Claudinho** —
4
- the 2026 men's football tournament in your dev environment. This is the engine
5
- behind [`@claudinho/cli`](https://www.npmjs.com/package/@claudinho/cli) and
3
+ Shared domain model, data-provider adapters, a read-only market-signal sidecar,
4
+ and helpers for **Claudinho** — the 2026 men's football tournament in your dev
5
+ environment. This is the engine behind
6
+ [`@claudinho/cli`](https://www.npmjs.com/package/@claudinho/cli) and
6
7
  [`@claudinho/mcp`](https://www.npmjs.com/package/@claudinho/mcp).
7
8
 
8
9
  > ⚠️ **Not affiliated with, endorsed by, or connected to FIFA or Anthropic.**
@@ -21,7 +22,9 @@ npm i @claudinho/core
21
22
  - **Static schedule** — all 104 fixtures (groups, venues, host cities, kickoffs) bundled; query with `allFixtures`, `fixturesByDate` (groups by your timezone), `fixturesByTeam`, `fixturesByGroup`, `nextFixtureForTeam`, `groups`
22
23
  - **Live overlay** — `makeAdapter`, `getMatchesForDate`, `getLiveMatches`, `mergeLive` (static base + live state, with graceful degradation)
23
24
  - **Standings** — `computeStandings` (points / GD / GF tiebreak)
24
- - **Helpers** — emoji flags (`nationToFlag`), TZ-aware time (`formatKickoff`, `countdown`, `localDate`), location strings (`matchLocation`), localized commentary flair (`matchFlavor` / `FlavorLevel`), validators (`isValidDate`, `isValidTimeZone`)
25
+ - **Helpers** — emoji flags (`nationToFlag`), TZ-aware time (`formatKickoff`, `formatDate`, `formatTime`, `countdown`, `localDate`), location strings (`matchLocation`), localized commentary flair (`matchFlavor` / `FlavorLevel`), validators (`isValidDate`, `isValidTimeZone`)
26
+ - **Prediction-market signals (sidecar)** — read-only market odds kept *separate* from `Match`: the `MarketSignal` / `MarketProvider` model, the `PolymarketProvider` (public Gamma data only — no auth/trading/links; event slugs auto-derived per fixture, validation fails closed), a `FakeMarketProvider`, `makeMarketProvider`, `getMarketSignal` / `getMarketSignals`, the `isReliableMarketSignal` gate, and approved-copy formatters (`marketFavoriteText`, `marketProbabilityText`, `marketBlock`). Informational only — never betting advice.
27
+ - **Shareable snippets** — `formatShareSnippet` builds pure, deterministic, plain-text match cards (composing `Match` + the market copy bank) for the CLI's `share` command and future MCP/site reuse. The non-affiliation disclaimer is non-optional; market lines come from the approved bank.
25
28
 
26
29
  ## Example
27
30
 
package/dist/index.d.ts CHANGED
@@ -135,6 +135,10 @@ interface FormatOpts {
135
135
  }
136
136
  /** Format a kickoff like "Thu 19:00" in the target timezone/locale. */
137
137
  declare function formatKickoff(iso: string, opts?: FormatOpts): string;
138
+ /** A short calendar date like "Jun 11" in the target timezone/locale. */
139
+ declare function formatDate(iso: string, opts?: FormatOpts): string;
140
+ /** Time of day like "19:00" (24-hour) in the target timezone/locale. */
141
+ declare function formatTime(iso: string, opts?: FormatOpts): string;
138
142
  /** Compact human countdown until kickoff: "3d4h", "2h10m", "45m", or "now". */
139
143
  declare function countdown(iso: string, from?: Date): string;
140
144
  /** The calendar date (YYYY-MM-DD) of a kickoff in the target timezone. */
@@ -394,7 +398,15 @@ interface LiveResult {
394
398
  matches: Match[];
395
399
  /** True when the provider call failed and we fell back to static data. */
396
400
  degraded: boolean;
401
+ /**
402
+ * The live-data provider that served this result (e.g. "espn"), for
403
+ * attribution. Absent when `degraded` — the bundled static schedule, served
404
+ * by no live provider, must not be attributed to one.
405
+ */
406
+ source?: string;
397
407
  }
408
+ /** Human label for a live-data provider name (attribution). Text only. */
409
+ declare function liveSourceLabel(source: string): string;
398
410
  /**
399
411
  * Matches for a date, preferring live provider data, falling back to the static
400
412
  * schedule on any provider/network error (graceful degradation).
@@ -403,4 +415,352 @@ declare function getMatchesForDate(adapter: ProviderAdapter, dateISO: string): P
403
415
  /** Currently-live matches; empty + degraded on error. */
404
416
  declare function getLiveMatches(adapter: ProviderAdapter): Promise<LiveResult>;
405
417
 
406
- export { DEFAULT_COMPETITION, DEFAULT_FLAVOR, EspnAdapter, type EspnAdapterOptions, FLAVOR_LEVELS, type FlavorLevel, type FormatOpts, type LedgerRow, type LiveResult, type MapContext, type Match, type MatchEvent, type Outcome, type ProviderAdapter, type ProviderCapabilities, type PunditPick, type Stage, type StandingRow, type Status, type Team, allFixtures, asFlavorLevel, byKickoff, competitionBase, computeStandings, countdown, fixturesByDate, fixturesByGroup, fixturesByTeam, flagEmoji, formatKickoff, getLiveMatches, getMatchesForDate, groups, isFinished, isFlavorLevel, isLive, isValidDate, isValidTimeZone, localDate, makeAdapter, mapEspnEvent, matchFlavor, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, outcomeFromScore, resolveCompetition, resolveTz, scoreline };
418
+ /**
419
+ * Prediction-market "signal" model — a *sidecar* to Match, deliberately never
420
+ * embedded in it. Market data has different freshness, reliability, failure,
421
+ * and legal semantics than tournament facts, so it lives in its own folder and
422
+ * is keyed back to a match by id. This keeps prediction-market context off the
423
+ * hot paths (statusline, hook) by construction rather than by remembering a flag.
424
+ *
425
+ * Read-only by design: providers fetch public market data only — no wallet,
426
+ * no auth, no order placement. Nothing here models trading.
427
+ */
428
+
429
+ /** Which match result a priced line refers to (home win / draw / away win). */
430
+ type MarketOutcomeKind = 'home' | 'draw' | 'away' | 'other';
431
+ /** One priced outcome of a match market. */
432
+ interface MarketOutcome {
433
+ kind: MarketOutcomeKind;
434
+ /** Team code for 'home'/'away' (e.g. "MEX"); absent for draw/other. */
435
+ teamCode?: string;
436
+ /** Label as the source displays it (e.g. "Mexico", "Draw"). */
437
+ label: string;
438
+ /** Market-implied probability, normalized to [0,1] (sums to ~1 across a market). */
439
+ probability: number;
440
+ }
441
+ /** How strongly the market leans toward its top outcome. */
442
+ type FavoriteStrength = 'close' | 'slight' | 'clear';
443
+ /** The top outcome plus its strength bucket. */
444
+ interface MarketFavorite {
445
+ kind: 'home' | 'draw' | 'away';
446
+ teamCode?: string;
447
+ probability: number;
448
+ strength: FavoriteStrength;
449
+ }
450
+ /** A normalized prediction-market reading for a single match. */
451
+ interface MarketSignal {
452
+ /** Claudinho match id this signal maps to. */
453
+ matchId: string;
454
+ /** Provider name (e.g. "polymarket"). */
455
+ source: string;
456
+ /**
457
+ * Opaque source market id, for debugging and mapping only. Deliberately never
458
+ * surfaced as a clickable link in v1 (see the no-outbound-links guardrail).
459
+ */
460
+ sourceMarketId?: string;
461
+ /** When the source last priced the market (ISO 8601 UTC). */
462
+ asOf: string;
463
+ /** When Claudinho fetched it (ISO 8601 UTC). */
464
+ fetchedAt: string;
465
+ /** Priced outcomes, normalized so positive probabilities sum to ~1. */
466
+ outcomes: MarketOutcome[];
467
+ /** Top outcome, when one can be determined from a clean mapping. */
468
+ favorite?: MarketFavorite;
469
+ /** Source liquidity, when available (provider units; used only for gating). */
470
+ liquidity?: number;
471
+ /** Source 24h volume, when available (provider units). */
472
+ volume24h?: number;
473
+ /** True when the snapshot is older than the freshness window. */
474
+ stale: boolean;
475
+ /** True when the market does not map cleanly to the displayed home/draw/away result. */
476
+ ambiguous: boolean;
477
+ }
478
+ /** Tunables for reliability gating and provider fetch behavior. */
479
+ interface MarketSignalOptions {
480
+ /** "Now" for staleness math; defaults to the current time. */
481
+ now?: Date;
482
+ /** Minimum source liquidity required to treat a signal as reliable. */
483
+ minLiquidity?: number;
484
+ /** Max age (ms) before a signal is considered stale. */
485
+ maxAgeMs?: number;
486
+ /**
487
+ * Bypass reliability gates — used by the dedicated `markets` surface, which
488
+ * may show a thin/stale market *with a caveat*. Default surfaces never set this.
489
+ */
490
+ includeUnreliable?: boolean;
491
+ /**
492
+ * Max total wall-clock (ms) for a batch `findSignals` before it stops early.
493
+ * Keeps optional market enrichment from blocking core fixture output.
494
+ */
495
+ deadlineMs?: number;
496
+ /** Per-request fetch timeout (ms) override for the provider. */
497
+ timeoutMs?: number;
498
+ }
499
+ /**
500
+ * Result of a batch lookup. `checked` is the set of match ids the provider
501
+ * DEFINITIVELY resolved (reached the source and found no usable market, or the
502
+ * fixture is unmappable) — distinct from matches that errored or were skipped by
503
+ * the deadline. Callers negative-cache only `checked` ids, so a transient
504
+ * provider/network failure never suppresses a valid signal.
505
+ */
506
+ interface MarketSignalsResult {
507
+ signals: Map<string, MarketSignal>;
508
+ checked: Set<string>;
509
+ }
510
+ /**
511
+ * A prediction-market provider. A *separate* swap-point from ProviderAdapter
512
+ * (which supplies match data): different cadence, reliability, and legal
513
+ * posture. Implementations fetch public market data only.
514
+ */
515
+ interface MarketProvider {
516
+ readonly name: string;
517
+ /** Signal for one match, or undefined when nothing maps cleanly. */
518
+ findSignal(match: Match, options?: MarketSignalOptions): Promise<MarketSignal | undefined>;
519
+ /** Batch form; signals plus the set of definitively-checked ids. */
520
+ findSignals(matches: Match[], options?: MarketSignalOptions): Promise<MarketSignalsResult>;
521
+ }
522
+
523
+ /**
524
+ * Probability normalization + the reliability gate. `isReliableMarketSignal` is
525
+ * the single primitive every default-on surface keys off: "show only when
526
+ * reliable, otherwise omit silently."
527
+ */
528
+
529
+ /** Default freshness window: a signal older than this is stale (15 minutes). */
530
+ declare const DEFAULT_MAX_AGE_MS: number;
531
+ /**
532
+ * Re-scale outcome probabilities so the positive ones sum to 1. This removes
533
+ * market "vig" (raw prices sum to >1) and is scale-agnostic: inputs may be
534
+ * 0..1 or 0..100 and the result is a clean [0,1] distribution. Non-finite or
535
+ * non-positive inputs collapse to 0.
536
+ */
537
+ declare function normalizeOutcomes(outcomes: MarketOutcome[]): MarketOutcome[];
538
+ /** Bucket a favorite's probability into a strength label. */
539
+ declare function favoriteStrength(probability: number): FavoriteStrength;
540
+ /** Pick the top home/draw/away outcome as the favorite, if one exists. */
541
+ declare function deriveFavorite(outcomes: MarketOutcome[]): MarketFavorite | undefined;
542
+ /**
543
+ * Does this market cleanly price the displayed home/draw/away result? Rejects
544
+ * "to advance"/"to win tournament"-style markets (an 'other' outcome), markets
545
+ * whose team codes don't match the fixture, and group-stage markets missing a
546
+ * draw line (a 90' group result must price the draw).
547
+ */
548
+ declare function mapsCleanly(match: Match, outcomes: MarketOutcome[]): boolean;
549
+ /** Sanity check: ≥2 priced outcomes whose probabilities sum to ~1. */
550
+ declare function hasSaneDistribution(outcomes: MarketOutcome[]): boolean;
551
+ /** Is the signal older than the freshness window? Unparseable timestamps are stale. */
552
+ declare function isStaleSignal(signal: MarketSignal, options?: MarketSignalOptions): boolean;
553
+ /**
554
+ * The load-bearing gate. A signal is reliable only when it is unambiguous, has
555
+ * a determinable favorite, is a sane distribution, is fresh, and (when a floor
556
+ * is configured) liquid enough. `includeUnreliable` bypasses all of it.
557
+ */
558
+ declare function isReliableMarketSignal(signal: MarketSignal, options?: MarketSignalOptions): boolean;
559
+ /** Inputs a provider supplies to build a normalized, gated signal. */
560
+ interface BuildSignalInput {
561
+ match: Match;
562
+ source: string;
563
+ sourceMarketId?: string;
564
+ asOf: string;
565
+ fetchedAt?: string;
566
+ /** Raw outcomes (any positive scale); normalized internally. */
567
+ outcomes: MarketOutcome[];
568
+ liquidity?: number;
569
+ volume24h?: number;
570
+ /** Force-flag as ambiguous (e.g. the source title didn't parse to a clean result). */
571
+ ambiguous?: boolean;
572
+ now?: Date;
573
+ maxAgeMs?: number;
574
+ }
575
+ /**
576
+ * Construct a normalized MarketSignal from a provider's raw parts. Centralizes
577
+ * normalization, clean-mapping detection, favorite derivation, and staleness so
578
+ * every provider produces identically-shaped, gate-ready signals.
579
+ */
580
+ declare function buildMarketSignal(input: BuildSignalInput): MarketSignal;
581
+
582
+ /**
583
+ * Provider-neutral text snippets for market signals — the single home for the
584
+ * approved copy bank, shared verbatim by CLI and MCP. Keeping copy here (not in
585
+ * each client) is a legal control: the language stays factual and never says
586
+ * "bet", "value", "edge", "lock", etc. Display precision is whole-number
587
+ * percent on purpose, to avoid implying false precision.
588
+ */
589
+
590
+ /** Human label for the data source (text only — never a logo). */
591
+ declare function marketSourceLabel(source: string): string;
592
+ /** One-line favorite read, drawn only from the approved copy bank. */
593
+ declare function marketFavoriteText(signal: MarketSignal, match: Match): string;
594
+ /** "Mexico 56% · Draw 25% · South Africa 19%" in home·draw·away reading order. */
595
+ declare function marketProbabilityText(signal: MarketSignal, match: Match): string;
596
+ /** "Source: Polymarket · updated 14:32 UTC". */
597
+ declare function marketAttributionText(signal: MarketSignal): string;
598
+ /** Compact one-liner for an inline annotation under a match row. */
599
+ declare function marketLine(signal: MarketSignal, match: Match): string;
600
+ /**
601
+ * Multi-line detail block (the caller indents/colorizes). Used by `match <id>`
602
+ * and the dedicated `markets` command. Always carries attribution and the
603
+ * informational-only caveat; prepends a stale warning when applicable.
604
+ */
605
+ declare function marketBlock(signal: MarketSignal, match: Match): string[];
606
+
607
+ /**
608
+ * Provider factory + graceful-degradation wrappers, mirroring `live.ts`'s
609
+ * getMatchesForDate contract: a market signal is optional enrichment, so any
610
+ * provider/network/parse error degrades to "no signal" and never throws.
611
+ */
612
+
613
+ /**
614
+ * Resolve the market-data source: explicit arg > CLAUDINHO_MARKETS_SOURCE env >
615
+ * 'polymarket' (mirrors resolveCompetition). Set CLAUDINHO_MARKETS_SOURCE=fake
616
+ * to preview the UX with synthetic, clearly-labeled "demo data" odds.
617
+ */
618
+ declare function resolveMarketSource(explicit?: string): string;
619
+ /**
620
+ * Construct a market-signal provider. Defaults to the Polymarket public-data
621
+ * adapter; honors CLAUDINHO_MARKETS_SOURCE ('fake' = network-free synthetic
622
+ * demo data; 'none'/'off' = network-free no-op). Tests usually inject directly.
623
+ */
624
+ declare function makeMarketProvider(source?: string): MarketProvider;
625
+ /** Fetch one match's signal; never throws — undefined on any error. */
626
+ declare function getMarketSignal(provider: MarketProvider, match: Match, options?: MarketSignalOptions): Promise<MarketSignal | undefined>;
627
+ /** Batch fetch; never throws — empty result (nothing checked) on any error. */
628
+ declare function getMarketSignals(provider: MarketProvider, matches: Match[], options?: MarketSignalOptions): Promise<MarketSignalsResult>;
629
+
630
+ /**
631
+ * A network-free MarketProvider for tests and local UX validation. Returns
632
+ * explicitly-provided signals and (optionally) deterministically synthesizes
633
+ * plausible-but-fake odds so every surface can be exercised without any live
634
+ * API. Always labeled `source: 'fake'` so it can never masquerade as real data.
635
+ */
636
+
637
+ interface FakeMarketProviderOptions {
638
+ /** Pre-built signals keyed by matchId; returned verbatim when present. */
639
+ signals?: Record<string, MarketSignal>;
640
+ /** When true, synthesize a deterministic signal for any unmapped match. */
641
+ synthesize?: boolean;
642
+ /** "Now" used when synthesizing timestamps (keeps tests deterministic). */
643
+ now?: Date;
644
+ }
645
+ declare class FakeMarketProvider implements MarketProvider {
646
+ private readonly opts;
647
+ readonly name = "fake";
648
+ constructor(opts?: FakeMarketProviderOptions);
649
+ findSignal(match: Match, options?: MarketSignalOptions): Promise<MarketSignal | undefined>;
650
+ findSignals(matches: Match[], options?: MarketSignalOptions): Promise<MarketSignalsResult>;
651
+ private synthesize;
652
+ }
653
+
654
+ /**
655
+ * Polymarket public-data adapter — read-only prediction-market signals.
656
+ *
657
+ * STRICT read-only by design: it touches only the public Gamma events/markets
658
+ * data endpoint. No auth, no wallet, no CLOB/order endpoints, no trading, and no
659
+ * outbound links (sourceMarketId is opaque, never a URL). Any network/parse/host
660
+ * error degrades to "no signal" — it never throws.
661
+ *
662
+ * Payload model (verified against the live Gamma API, see
663
+ * docs/POLYMARKET_MARKET_PREDICTIONS.md): a World Cup match is a Gamma EVENT
664
+ * (`fifwc-{home}-{away}-{date}`) whose payload carries the three moneyline
665
+ * BINARY markets — home win / draw / away win. Each is `outcomes: ["Yes","No"]`
666
+ * and the outcome's probability is its "Yes" price.
667
+ *
668
+ * Mapping is by event slug, which is deterministic — so by default the slug is
669
+ * DERIVED from the fixture (team codes + UTC date) and every match attempts
670
+ * enrichment. A hand-curated entry in mapping.2026.json overrides the derived
671
+ * slug for the rare fixture whose slug doesn't follow the pattern. Because the
672
+ * slug is a guess, validation FAILS CLOSED: the returned event must match the
673
+ * requested slug, line up on kickoff, expose the right moneyline markets, and
674
+ * resolve in regular time — otherwise no signal is produced.
675
+ */
676
+
677
+ /**
678
+ * Optional override of the derived event slug for a fixture whose Polymarket
679
+ * slug doesn't follow `fifwc-{home}-{away}-{date}` (e.g. an abbreviation that
680
+ * differs from the FIFA code). Most matches need no entry — the slug is derived.
681
+ */
682
+ interface MarketMapping {
683
+ /** Gamma event slug, e.g. "fifwc-mex-rsa-2026-06-11". */
684
+ eventSlug: string;
685
+ /** Optional Gamma event id (diagnostics; the slug is the lookup key). */
686
+ eventId?: string;
687
+ }
688
+ type MarketMappingTable = Record<string, MarketMapping>;
689
+ interface PolymarketProviderOptions {
690
+ fetchImpl?: typeof fetch;
691
+ timeoutMs?: number;
692
+ /** Base URL; must resolve to an allow-listed host. */
693
+ baseUrl?: string;
694
+ /** Override the bundled mapping table (tests / future gateway). */
695
+ mapping?: MarketMappingTable;
696
+ now?: Date;
697
+ maxAgeMs?: number;
698
+ }
699
+ declare class PolymarketProvider implements MarketProvider {
700
+ private readonly opts;
701
+ readonly name = "polymarket";
702
+ constructor(opts?: PolymarketProviderOptions);
703
+ findSignal(match: Match, options?: MarketSignalOptions): Promise<MarketSignal | undefined>;
704
+ findSignals(matches: Match[], options?: MarketSignalOptions): Promise<MarketSignalsResult>;
705
+ /**
706
+ * Resolve one match. `checked` distinguishes a DEFINITIVE result (reached the
707
+ * source and found no usable market, or the fixture is unmappable) from a
708
+ * provider/network error — so transient failures are retried, not
709
+ * negative-cached.
710
+ */
711
+ private resolveOne;
712
+ private fetchEvent;
713
+ private toSignal;
714
+ }
715
+
716
+ /** The two v1 snippet shapes. `social` is the rich card; `compact` is one terse line per match. */
717
+ type ShareStyle = 'compact' | 'social';
718
+ /** The project social tag — a distribution unit, default-on but removable. */
719
+ declare const SHARE_HASHTAG = "#VibingLaVidaLoca";
720
+ /**
721
+ * The non-affiliation line. Non-optional in every snippet: a shared artifact is
722
+ * decontextualized, so the legal disclaimer must travel with every paste.
723
+ */
724
+ declare const SHARE_DISCLAIMER = "Independent fan project \u00B7 not affiliated with FIFA or Anthropic.";
725
+ interface ShareSnippetOptions {
726
+ /** Snippet shape; defaults to `social`. */
727
+ style?: ShareStyle;
728
+ /** Include the reliable market block/line when a signal is present (default true). */
729
+ includeMarkets?: boolean;
730
+ /** Include the #VibingLaVidaLoca tag (default true). */
731
+ includeHashtag?: boolean;
732
+ /** Include the "Try it: …" install/run cue (default true). */
733
+ includeInstallLine?: boolean;
734
+ }
735
+ interface ShareSnippetInput {
736
+ /** Pre-resolved, English title line, e.g. "Next up for Mexico". */
737
+ title: string;
738
+ /** Matches to render (0..n). An empty set still yields a valid titled card. */
739
+ matches: Match[];
740
+ /**
741
+ * Reliable, display-ready market signals keyed by match id (sidecar — never
742
+ * embedded in Match). Callers gate these; the formatter only renders.
743
+ */
744
+ marketSignals?: Map<string, MarketSignal>;
745
+ /** Live-data provider name (e.g. "espn") for attribution; omit when static/degraded. */
746
+ source?: string;
747
+ /**
748
+ * Body line shown when `matches` is empty (e.g. "No upcoming fixture found for
749
+ * ZZZ."), so an unknown/empty target yields a clear card instead of a void.
750
+ */
751
+ emptyNote?: string;
752
+ /** Exact run cue to advertise, e.g. "npx @claudinho/cli next MEX". */
753
+ installLine?: string;
754
+ /** Timezone for kickoff date/time (date/time only — copy stays English). */
755
+ tz?: string;
756
+ /** Locale for kickoff date/time. */
757
+ locale?: string;
758
+ }
759
+ /**
760
+ * Render a shareable snippet. Pure and deterministic: identical input yields
761
+ * identical output. Blocks (title, each match, footer) are separated by a blank
762
+ * line; lines within a block by a single newline.
763
+ */
764
+ declare function formatShareSnippet(input: ShareSnippetInput, options?: ShareSnippetOptions): string;
765
+
766
+ export { type BuildSignalInput, DEFAULT_COMPETITION, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, EspnAdapter, type EspnAdapterOptions, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type LedgerRow, type LiveResult, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchEvent, type Outcome, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, type PunditPick, SHARE_DISCLAIMER, SHARE_HASHTAG, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type Stage, type StandingRow, type Status, type Team, allFixtures, asFlavorLevel, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, deriveFavorite, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, flagEmoji, formatDate, formatKickoff, formatShareSnippet, formatTime, getLiveMatches, getMarketSignal, getMarketSignals, getMatchesForDate, groups, hasSaneDistribution, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isStaleSignal, isValidDate, isValidTimeZone, liveSourceLabel, localDate, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketLine, marketProbabilityText, marketSourceLabel, matchFlavor, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeOutcomes, outcomeFromScore, resolveCompetition, resolveMarketSource, resolveTz, scoreline };
package/dist/index.js CHANGED
@@ -310,6 +310,25 @@ function formatKickoff(iso, opts = {}) {
310
310
  timeZone: tz
311
311
  }).format(new Date(iso));
312
312
  }
313
+ function formatDate(iso, opts = {}) {
314
+ const tz = resolveTz(opts.tz);
315
+ const locale = safeLocale(opts.locale);
316
+ return new Intl.DateTimeFormat(locale, {
317
+ month: "short",
318
+ day: "numeric",
319
+ timeZone: tz
320
+ }).format(new Date(iso));
321
+ }
322
+ function formatTime(iso, opts = {}) {
323
+ const tz = resolveTz(opts.tz);
324
+ const locale = safeLocale(opts.locale);
325
+ return new Intl.DateTimeFormat(locale, {
326
+ hour: "2-digit",
327
+ minute: "2-digit",
328
+ hour12: false,
329
+ timeZone: tz
330
+ }).format(new Date(iso));
331
+ }
313
332
  function countdown(iso, from = /* @__PURE__ */ new Date()) {
314
333
  const ms = new Date(iso).getTime() - from.getTime();
315
334
  if (ms <= 0) return "now";
@@ -2848,12 +2867,16 @@ function mergeLive(base, live) {
2848
2867
  for (const m of live) byId.set(m.id, m);
2849
2868
  return [...byId.values()];
2850
2869
  }
2870
+ function liveSourceLabel(source) {
2871
+ const known = { espn: "ESPN" };
2872
+ return known[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
2873
+ }
2851
2874
  async function getMatchesForDate(adapter, dateISO) {
2852
2875
  const base = allFixtures();
2853
2876
  const day = dateISO.slice(0, 10);
2854
2877
  try {
2855
2878
  const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
2856
- return { matches: mergeLive(base, live), degraded: false };
2879
+ return { matches: mergeLive(base, live), degraded: false, source: adapter.name };
2857
2880
  } catch {
2858
2881
  return { matches: base, degraded: true };
2859
2882
  }
@@ -2864,46 +2887,589 @@ function shiftUtcDate(dateISO, days) {
2864
2887
  }
2865
2888
  async function getLiveMatches(adapter) {
2866
2889
  try {
2867
- return { matches: await adapter.fetchLive(), degraded: false };
2890
+ return { matches: await adapter.fetchLive(), degraded: false, source: adapter.name };
2868
2891
  } catch {
2869
2892
  return { matches: [], degraded: true };
2870
2893
  }
2871
2894
  }
2895
+
2896
+ // src/markets/normalize.ts
2897
+ var DEFAULT_MAX_AGE_MS = 15 * 6e4;
2898
+ function normalizeOutcomes(outcomes) {
2899
+ const sum = outcomes.reduce(
2900
+ (s, o) => s + (Number.isFinite(o.probability) && o.probability > 0 ? o.probability : 0),
2901
+ 0
2902
+ );
2903
+ if (sum <= 0) return outcomes.map((o) => ({ ...o, probability: 0 }));
2904
+ return outcomes.map((o) => ({
2905
+ ...o,
2906
+ probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
2907
+ }));
2908
+ }
2909
+ function favoriteStrength(probability) {
2910
+ if (probability >= 0.65) return "clear";
2911
+ if (probability >= 0.52) return "slight";
2912
+ return "close";
2913
+ }
2914
+ function deriveFavorite(outcomes) {
2915
+ let top;
2916
+ for (const o of outcomes) {
2917
+ if (o.kind === "other") continue;
2918
+ if (!top || o.probability > top.probability) top = o;
2919
+ }
2920
+ if (!top || top.probability <= 0 || top.kind === "other") return void 0;
2921
+ return {
2922
+ kind: top.kind,
2923
+ teamCode: top.teamCode,
2924
+ probability: top.probability,
2925
+ strength: favoriteStrength(top.probability)
2926
+ };
2927
+ }
2928
+ function mapsCleanly(match, outcomes) {
2929
+ if (outcomes.some((o) => o.kind === "other")) return false;
2930
+ const home = outcomes.find((o) => o.kind === "home");
2931
+ const away = outcomes.find((o) => o.kind === "away");
2932
+ const draw = outcomes.find((o) => o.kind === "draw");
2933
+ if (!home || !away) return false;
2934
+ if (home.teamCode && home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
2935
+ return false;
2936
+ }
2937
+ if (away.teamCode && away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
2938
+ return false;
2939
+ }
2940
+ if (match.stage === "GROUP" && !draw) return false;
2941
+ return true;
2942
+ }
2943
+ function hasSaneDistribution(outcomes) {
2944
+ const priced = outcomes.filter((o) => Number.isFinite(o.probability) && o.probability > 0);
2945
+ if (priced.length < 2) return false;
2946
+ const sum = priced.reduce((s, o) => s + o.probability, 0);
2947
+ return sum > 0.97 && sum < 1.03;
2948
+ }
2949
+ function isStaleSignal(signal, options = {}) {
2950
+ const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
2951
+ const asOf = Date.parse(signal.asOf);
2952
+ if (!Number.isFinite(asOf)) return true;
2953
+ const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
2954
+ return now - asOf > maxAge;
2955
+ }
2956
+ function isReliableMarketSignal(signal, options = {}) {
2957
+ if (options.includeUnreliable) return true;
2958
+ if (signal.ambiguous) return false;
2959
+ if (!signal.favorite) return false;
2960
+ if (!hasSaneDistribution(signal.outcomes)) return false;
2961
+ if (signal.stale || isStaleSignal(signal, options)) return false;
2962
+ if (options.minLiquidity != null) {
2963
+ if (signal.liquidity == null || signal.liquidity < options.minLiquidity) return false;
2964
+ }
2965
+ return true;
2966
+ }
2967
+ function buildMarketSignal(input) {
2968
+ const outcomes = normalizeOutcomes(input.outcomes);
2969
+ const ambiguous = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
2970
+ const favorite = ambiguous ? void 0 : deriveFavorite(outcomes);
2971
+ const signal = {
2972
+ matchId: input.match.id,
2973
+ source: input.source,
2974
+ sourceMarketId: input.sourceMarketId,
2975
+ asOf: input.asOf,
2976
+ fetchedAt: input.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2977
+ outcomes,
2978
+ favorite,
2979
+ liquidity: input.liquidity,
2980
+ volume24h: input.volume24h,
2981
+ stale: false,
2982
+ ambiguous
2983
+ };
2984
+ signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
2985
+ return signal;
2986
+ }
2987
+
2988
+ // src/markets/format.ts
2989
+ function pct(p) {
2990
+ return Math.round(p * 100);
2991
+ }
2992
+ function marketSourceLabel(source) {
2993
+ if (source === "polymarket") return "Polymarket";
2994
+ if (source === "fake") return "demo data";
2995
+ return source.charAt(0).toUpperCase() + source.slice(1);
2996
+ }
2997
+ function outcomeLabel(o, match) {
2998
+ if (o.kind === "home") return match.home.name;
2999
+ if (o.kind === "away") return match.away.name;
3000
+ if (o.kind === "draw") return "Draw";
3001
+ return o.label;
3002
+ }
3003
+ function utcHhmm(iso) {
3004
+ const t = Date.parse(iso);
3005
+ if (!Number.isFinite(t)) return "";
3006
+ return `${new Date(t).toISOString().slice(11, 16)} UTC`;
3007
+ }
3008
+ function marketFavoriteText(signal, match) {
3009
+ const fav = signal.favorite;
3010
+ if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
3011
+ if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
3012
+ const name = fav.kind === "home" ? match.home.name : match.away.name;
3013
+ return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
3014
+ }
3015
+ function marketProbabilityText(signal, match) {
3016
+ const order = ["home", "draw", "away"];
3017
+ const parts = [];
3018
+ for (const kind of order) {
3019
+ const o = signal.outcomes.find((x) => x.kind === kind);
3020
+ if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
3021
+ }
3022
+ for (const o of signal.outcomes) {
3023
+ if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
3024
+ }
3025
+ return parts.join(" \xB7 ");
3026
+ }
3027
+ function marketAttributionText(signal) {
3028
+ const time = utcHhmm(signal.asOf);
3029
+ const src = `Source: ${marketSourceLabel(signal.source)}`;
3030
+ return time ? `${src} \xB7 updated ${time}` : src;
3031
+ }
3032
+ function marketLine(signal, match) {
3033
+ return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
3034
+ signal.source
3035
+ )} \xB7 informational only`;
3036
+ }
3037
+ function marketBlock(signal, match) {
3038
+ const lines = [];
3039
+ if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
3040
+ lines.push(marketFavoriteText(signal, match));
3041
+ lines.push(marketProbabilityText(signal, match));
3042
+ lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
3043
+ return lines;
3044
+ }
3045
+
3046
+ // src/markets/fake.ts
3047
+ var FakeMarketProvider = class {
3048
+ constructor(opts = {}) {
3049
+ this.opts = opts;
3050
+ }
3051
+ opts;
3052
+ name = "fake";
3053
+ async findSignal(match, options) {
3054
+ const preset = this.opts.signals?.[match.id];
3055
+ if (preset) return preset;
3056
+ if (this.opts.synthesize) return this.synthesize(match, options);
3057
+ return void 0;
3058
+ }
3059
+ async findSignals(matches, options) {
3060
+ const signals = /* @__PURE__ */ new Map();
3061
+ const checked = /* @__PURE__ */ new Set();
3062
+ for (const m of matches) {
3063
+ checked.add(m.id);
3064
+ const s = await this.findSignal(m, options);
3065
+ if (s) signals.set(m.id, s);
3066
+ }
3067
+ return { signals, checked };
3068
+ }
3069
+ synthesize(match, options) {
3070
+ const seed = hash(`${match.home.code}-${match.away.code}`);
3071
+ const home = 0.3 + seed % 33 / 100;
3072
+ const away = 0.18 + (seed >> 3) % 23 / 100;
3073
+ const draw = Math.max(0.05, 1 - home - away);
3074
+ const outcomes = [
3075
+ { kind: "home", teamCode: match.home.code, label: match.home.name, probability: home },
3076
+ { kind: "draw", label: "Draw", probability: draw },
3077
+ { kind: "away", teamCode: match.away.code, label: match.away.name, probability: away }
3078
+ ];
3079
+ const now = this.opts.now ?? options?.now ?? /* @__PURE__ */ new Date();
3080
+ const asOf = new Date(now.getTime() - 6e4).toISOString();
3081
+ return buildMarketSignal({
3082
+ match,
3083
+ source: "fake",
3084
+ sourceMarketId: `fake-${match.id}`,
3085
+ asOf,
3086
+ fetchedAt: now.toISOString(),
3087
+ outcomes,
3088
+ liquidity: 5e4,
3089
+ now,
3090
+ maxAgeMs: options?.maxAgeMs
3091
+ });
3092
+ }
3093
+ };
3094
+ function hash(s) {
3095
+ let h = 0;
3096
+ for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) >>> 0;
3097
+ return h % 1e5;
3098
+ }
3099
+
3100
+ // src/markets/mapping.2026.json
3101
+ var mapping_2026_default = {
3102
+ version: 1,
3103
+ note: "Optional matchId -> Polymarket EVENT-slug OVERRIDES. By default the slug is DERIVED from each fixture (fifwc-{home}-{away}-{UTC-date}, e.g. fifwc-mex-rsa-2026-06-11), so most matches need NO entry here. Add an entry only for a fixture whose real Polymarket slug differs (e.g. a team abbreviation that isn't the FIFA code, or a tz-shifted date). The event payload carries the three moneyline binary markets; each outcome's probability is its 'Yes' price; validation fails closed. Entry: { eventSlug, eventId? }.",
3104
+ markets: {}
3105
+ };
3106
+
3107
+ // src/markets/polymarket.ts
3108
+ var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
3109
+ var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
3110
+ var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
3111
+ var DEFAULT_TIMEOUT_MS = 8e3;
3112
+ var WC_SERIES_SLUG = "soccer-fifwc";
3113
+ var WC_SPORT = "fifwc";
3114
+ var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
3115
+ var NON_REGULAR_TIME = /extra time|penalt|to advance|to qualif|win the (group|tournament|cup|title)/i;
3116
+ var BUNDLED_MAPPING = mapping_2026_default.markets;
3117
+ var PolymarketProvider = class {
3118
+ constructor(opts = {}) {
3119
+ this.opts = opts;
3120
+ }
3121
+ opts;
3122
+ name = "polymarket";
3123
+ async findSignal(match, options) {
3124
+ return (await this.resolveOne(match, options)).signal;
3125
+ }
3126
+ async findSignals(matches, options) {
3127
+ const signals = /* @__PURE__ */ new Map();
3128
+ const checked = /* @__PURE__ */ new Set();
3129
+ const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
3130
+ for (const m of matches) {
3131
+ if (Date.now() >= deadline) break;
3132
+ const r = await this.resolveOne(m, options);
3133
+ if (r.checked) checked.add(m.id);
3134
+ if (r.signal) signals.set(m.id, r.signal);
3135
+ }
3136
+ return { signals, checked };
3137
+ }
3138
+ /**
3139
+ * Resolve one match. `checked` distinguishes a DEFINITIVE result (reached the
3140
+ * source and found no usable market, or the fixture is unmappable) from a
3141
+ * provider/network error — so transient failures are retried, not
3142
+ * negative-cached.
3143
+ */
3144
+ async resolveOne(match, options) {
3145
+ const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
3146
+ const eventSlug = entry?.eventSlug ?? deriveEventSlug(match);
3147
+ if (!eventSlug) return { checked: true };
3148
+ try {
3149
+ const event = await this.fetchEvent(eventSlug, options?.timeoutMs);
3150
+ const signal = event ? this.toSignal(match, eventSlug, event, options) : void 0;
3151
+ return { signal, checked: true };
3152
+ } catch {
3153
+ return { checked: false };
3154
+ }
3155
+ }
3156
+ async fetchEvent(slug, timeoutMs) {
3157
+ const base = this.opts.baseUrl ?? DEFAULT_BASE2;
3158
+ assertAllowedHost(base);
3159
+ const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
3160
+ const doFetch = this.opts.fetchImpl ?? fetch;
3161
+ const res = await doFetch(url, {
3162
+ signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
3163
+ headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
3164
+ });
3165
+ if (res.status === 404) return void 0;
3166
+ if (!res.ok) {
3167
+ throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
3168
+ }
3169
+ const data = await res.json();
3170
+ const event = Array.isArray(data) ? data[0] : data;
3171
+ return event && typeof event === "object" ? event : void 0;
3172
+ }
3173
+ toSignal(match, eventSlug, event, options) {
3174
+ if (event.active === false || event.closed === true) return void 0;
3175
+ if (event.seriesSlug != null && event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
3176
+ return void 0;
3177
+ }
3178
+ if (event.slug != null && event.slug !== eventSlug) return void 0;
3179
+ const start = event.startTime ? Date.parse(event.startTime) : Number.NaN;
3180
+ const kick = Date.parse(match.kickoff);
3181
+ if (Number.isFinite(start) && Number.isFinite(kick) && Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
3182
+ return void 0;
3183
+ }
3184
+ const moneyline = (event.markets ?? []).filter(
3185
+ (m) => (m.sportsMarketType ?? "moneyline") === "moneyline"
3186
+ );
3187
+ const homeMarket = pickMarket(moneyline, match.home.code, match.home.name);
3188
+ const awayMarket = pickMarket(moneyline, match.away.code, match.away.name);
3189
+ const drawMarket = pickDraw(moneyline);
3190
+ if (!homeMarket || !awayMarket) return void 0;
3191
+ const legIds = [homeMarket, awayMarket, drawMarket].filter((m) => m != null).map((m) => m.id ?? m.slug ?? "");
3192
+ if (new Set(legIds).size !== legIds.length) return void 0;
3193
+ const legs = [
3194
+ ["home", homeMarket, match.home.code, match.home.name],
3195
+ ["draw", drawMarket, void 0, "Draw"],
3196
+ ["away", awayMarket, match.away.code, match.away.name]
3197
+ ];
3198
+ const outcomes = [];
3199
+ let asOf = event.updatedAt;
3200
+ let liquidity;
3201
+ for (const [kind, market, teamCode, label] of legs) {
3202
+ if (!market) continue;
3203
+ if (market.closed === true || market.active === false) return void 0;
3204
+ if (market.description && NON_REGULAR_TIME.test(market.description)) return void 0;
3205
+ const yes = yesPrice(market);
3206
+ if (yes == null) return void 0;
3207
+ outcomes.push({ kind, teamCode, label, probability: yes });
3208
+ if (market.updatedAt && (!asOf || market.updatedAt < asOf)) asOf = market.updatedAt;
3209
+ const liq = numberish(market.liquidityNum ?? market.liquidity);
3210
+ if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
3211
+ }
3212
+ const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
3213
+ if (rawSum < 0.9 || rawSum > 1.15) return void 0;
3214
+ const signal = buildMarketSignal({
3215
+ match,
3216
+ source: "polymarket",
3217
+ sourceMarketId: event.id ?? eventSlug,
3218
+ asOf: asOf ?? (/* @__PURE__ */ new Date()).toISOString(),
3219
+ outcomes,
3220
+ liquidity,
3221
+ now: options?.now ?? this.opts.now,
3222
+ maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
3223
+ });
3224
+ return signal.ambiguous ? void 0 : signal;
3225
+ }
3226
+ };
3227
+ function deriveEventSlug(match) {
3228
+ const home = match.home.code.toLowerCase();
3229
+ const away = match.away.code.toLowerCase();
3230
+ if (home === away || home === "tbd" || away === "tbd") return void 0;
3231
+ if (!/^[a-z]{3}$/.test(home) || !/^[a-z]{3}$/.test(away)) return void 0;
3232
+ const date = match.kickoff.slice(0, 10);
3233
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return void 0;
3234
+ return `fifwc-${home}-${away}-${date}`;
3235
+ }
3236
+ function slugToken(m) {
3237
+ return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
3238
+ }
3239
+ function isDrawMarket(m) {
3240
+ return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
3241
+ }
3242
+ function pickMarket(markets, teamCode, teamName) {
3243
+ const code = teamCode.toLowerCase();
3244
+ const name = teamName.trim().toLowerCase();
3245
+ const teamMarkets = markets.filter((m) => !isDrawMarket(m));
3246
+ const bySlug = teamMarkets.find((m) => slugToken(m) === code);
3247
+ if (bySlug) return bySlug;
3248
+ return teamMarkets.find((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name);
3249
+ }
3250
+ function pickDraw(markets) {
3251
+ return markets.find(isDrawMarket);
3252
+ }
3253
+ function assertAllowedHost(base) {
3254
+ let host;
3255
+ try {
3256
+ host = new URL(base).host;
3257
+ } catch {
3258
+ throw new Error(`Invalid Polymarket base URL: ${base}`);
3259
+ }
3260
+ if (!ALLOWED_HOSTS.has(host)) {
3261
+ throw new Error(`Polymarket host not allow-listed: ${host}`);
3262
+ }
3263
+ }
3264
+ function yesPrice(market) {
3265
+ const labels = parseJsonArray(market.outcomes);
3266
+ const prices = parseJsonArray(market.outcomePrices).map((p2) => Number(p2));
3267
+ if (labels.length === 0 || labels.length !== prices.length) return void 0;
3268
+ const i = labels.findIndex((l) => l.trim().toLowerCase() === "yes");
3269
+ if (i < 0) return void 0;
3270
+ const p = prices[i];
3271
+ return typeof p === "number" && Number.isFinite(p) && p > 0 && p <= 1 ? p : void 0;
3272
+ }
3273
+ function parseJsonArray(v) {
3274
+ if (Array.isArray(v)) return v.map((x) => String(x));
3275
+ if (typeof v === "string") {
3276
+ try {
3277
+ const parsed = JSON.parse(v);
3278
+ return Array.isArray(parsed) ? parsed.map((x) => String(x)) : [];
3279
+ } catch {
3280
+ return [];
3281
+ }
3282
+ }
3283
+ return [];
3284
+ }
3285
+ function numberish(v) {
3286
+ if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
3287
+ if (typeof v === "string") {
3288
+ const n = Number(v);
3289
+ return Number.isFinite(n) ? n : void 0;
3290
+ }
3291
+ return void 0;
3292
+ }
3293
+
3294
+ // src/markets/provider.ts
3295
+ function resolveMarketSource(explicit) {
3296
+ if (explicit) return explicit;
3297
+ if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
3298
+ return process.env.CLAUDINHO_MARKETS_SOURCE;
3299
+ }
3300
+ return "polymarket";
3301
+ }
3302
+ function makeMarketProvider(source) {
3303
+ switch (resolveMarketSource(source)) {
3304
+ case "fake":
3305
+ return new FakeMarketProvider({ synthesize: true });
3306
+ case "none":
3307
+ case "off":
3308
+ return new FakeMarketProvider();
3309
+ // no synth → yields no signals, no network
3310
+ default:
3311
+ return new PolymarketProvider();
3312
+ }
3313
+ }
3314
+ async function getMarketSignal(provider, match, options) {
3315
+ try {
3316
+ return await provider.findSignal(match, options);
3317
+ } catch {
3318
+ return void 0;
3319
+ }
3320
+ }
3321
+ async function getMarketSignals(provider, matches, options) {
3322
+ try {
3323
+ return await provider.findSignals(matches, options);
3324
+ } catch {
3325
+ return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
3326
+ }
3327
+ }
3328
+
3329
+ // src/share/format.ts
3330
+ var SHARE_HASHTAG = "#VibingLaVidaLoca";
3331
+ var SHARE_DISCLAIMER = "Independent fan project \xB7 not affiliated with FIFA or Anthropic.";
3332
+ function mid(m) {
3333
+ return isLive(m.status) || m.status === "FT" ? scoreline(m) : "vs";
3334
+ }
3335
+ function statusTail(m) {
3336
+ switch (m.status) {
3337
+ case "LIVE":
3338
+ return m.minute ? `${m.minute}'` : "LIVE";
3339
+ case "HT":
3340
+ return "HT";
3341
+ case "FT":
3342
+ return "FT";
3343
+ case "POSTPONED":
3344
+ return "postponed";
3345
+ case "CANCELLED":
3346
+ return "cancelled";
3347
+ default:
3348
+ return "";
3349
+ }
3350
+ }
3351
+ function compactLine(m, input, single) {
3352
+ const home = `${m.home.flag} ${m.home.code}`;
3353
+ const away = `${m.away.code} ${m.away.flag}`;
3354
+ const opts = { tz: input.tz, locale: input.locale };
3355
+ let tail;
3356
+ if (m.status === "SCHEDULED") {
3357
+ const time = formatTime(m.kickoff, opts);
3358
+ tail = single ? `${formatDate(m.kickoff, opts)} ${time}` : time;
3359
+ } else {
3360
+ tail = statusTail(m);
3361
+ }
3362
+ return `${home} ${mid(m)} ${away}${tail ? ` \xB7 ${tail}` : ""}`;
3363
+ }
3364
+ function socialCard(m, input) {
3365
+ const lines = [];
3366
+ const head = `${m.home.flag} ${m.home.name} ${mid(m)} ${m.away.name} ${m.away.flag}`;
3367
+ if (m.status === "SCHEDULED") {
3368
+ lines.push(head);
3369
+ const date = formatDate(m.kickoff, { tz: input.tz, locale: input.locale });
3370
+ const time = formatTime(m.kickoff, { tz: input.tz, locale: input.locale });
3371
+ const zone = input.tz ? ` ${input.tz}` : "";
3372
+ lines.push(`${date} \xB7 ${time}${zone}`);
3373
+ } else {
3374
+ const tail = statusTail(m);
3375
+ lines.push(tail ? `${head} \xB7 ${tail}` : head);
3376
+ }
3377
+ const loc = matchLocation(m);
3378
+ if (loc) lines.push(loc);
3379
+ return lines;
3380
+ }
3381
+ function formatShareSnippet(input, options = {}) {
3382
+ const style = options.style ?? "social";
3383
+ const includeMarkets = options.includeMarkets !== false;
3384
+ const includeHashtag = options.includeHashtag !== false;
3385
+ const includeInstall = options.includeInstallLine !== false;
3386
+ const signals = input.marketSignals ?? /* @__PURE__ */ new Map();
3387
+ const single = input.matches.length === 1;
3388
+ const blocks = [input.title];
3389
+ if (input.matches.length === 0) {
3390
+ if (input.emptyNote) blocks.push(input.emptyNote);
3391
+ } else if (style === "compact") {
3392
+ blocks.push(input.matches.map((m) => compactLine(m, input, single)).join("\n"));
3393
+ } else {
3394
+ for (const m of input.matches) {
3395
+ const card = socialCard(m, input);
3396
+ const sig = includeMarkets ? signals.get(m.id) : void 0;
3397
+ if (sig) {
3398
+ if (single) card.push("", ...marketBlock(sig, m));
3399
+ else card.push(marketLine(sig, m));
3400
+ }
3401
+ blocks.push(card.join("\n"));
3402
+ }
3403
+ }
3404
+ const footer = [];
3405
+ if (input.source) footer.push(`Live data: ${liveSourceLabel(input.source)}`);
3406
+ footer.push([includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 "));
3407
+ if (includeInstall && input.installLine) footer.push(`Try it: ${input.installLine}`);
3408
+ blocks.push(footer.join("\n"));
3409
+ return blocks.join("\n\n");
3410
+ }
2872
3411
  export {
2873
3412
  DEFAULT_COMPETITION,
2874
3413
  DEFAULT_FLAVOR,
3414
+ DEFAULT_MAX_AGE_MS,
2875
3415
  EspnAdapter,
2876
3416
  FLAVOR_LEVELS,
3417
+ FakeMarketProvider,
3418
+ PolymarketProvider,
3419
+ SHARE_DISCLAIMER,
3420
+ SHARE_HASHTAG,
2877
3421
  allFixtures,
2878
3422
  asFlavorLevel,
3423
+ buildMarketSignal,
2879
3424
  byKickoff,
2880
3425
  competitionBase,
2881
3426
  computeStandings,
2882
3427
  countdown,
3428
+ deriveFavorite,
3429
+ favoriteStrength,
2883
3430
  fixturesByDate,
2884
3431
  fixturesByGroup,
2885
3432
  fixturesByTeam,
2886
3433
  flagEmoji,
3434
+ formatDate,
2887
3435
  formatKickoff,
3436
+ formatShareSnippet,
3437
+ formatTime,
2888
3438
  getLiveMatches,
3439
+ getMarketSignal,
3440
+ getMarketSignals,
2889
3441
  getMatchesForDate,
2890
3442
  groups,
3443
+ hasSaneDistribution,
2891
3444
  isFinished,
2892
3445
  isFlavorLevel,
2893
3446
  isLive,
3447
+ isReliableMarketSignal,
3448
+ isStaleSignal,
2894
3449
  isValidDate,
2895
3450
  isValidTimeZone,
3451
+ liveSourceLabel,
2896
3452
  localDate,
2897
3453
  makeAdapter,
3454
+ makeMarketProvider,
2898
3455
  mapEspnEvent,
3456
+ mapsCleanly,
3457
+ marketAttributionText,
3458
+ marketBlock,
3459
+ marketFavoriteText,
3460
+ marketLine,
3461
+ marketProbabilityText,
3462
+ marketSourceLabel,
2899
3463
  matchFlavor,
2900
3464
  matchLocation,
2901
3465
  mergeLive,
2902
3466
  nationToFlag,
2903
3467
  nationToRegion,
2904
3468
  nextFixtureForTeam,
3469
+ normalizeOutcomes,
2905
3470
  outcomeFromScore,
2906
3471
  resolveCompetition,
3472
+ resolveMarketSource,
2907
3473
  resolveTz,
2908
3474
  scoreline
2909
3475
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@claudinho/core",
3
- "version": "0.2.0",
4
- "description": "Domain model, data-provider adapters, and helpers for Claudinho — the 2026 football tournament in your dev environment.",
3
+ "version": "0.4.0",
4
+ "description": "Domain model, provider adapters, standings, and a read-only market-signal sidecar for Claudinho — the 2026 men's football tournament. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Arturo Garrido",