@claudinho/core 0.3.0 → 0.4.1

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
@@ -22,8 +22,9 @@ npm i @claudinho/core
22
22
  - **Static schedule** — all 104 fixtures (groups, venues, host cities, kickoffs) bundled; query with `allFixtures`, `fixturesByDate` (groups by your timezone), `fixturesByTeam`, `fixturesByGroup`, `nextFixtureForTeam`, `groups`
23
23
  - **Live overlay** — `makeAdapter`, `getMatchesForDate`, `getLiveMatches`, `mergeLive` (static base + live state, with graceful degradation)
24
24
  - **Standings** — `computeStandings` (points / GD / GF tiebreak)
25
- - **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
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.
27
28
 
28
29
  ## Example
29
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. */
@@ -709,4 +713,54 @@ declare class PolymarketProvider implements MarketProvider {
709
713
  private toSignal;
710
714
  }
711
715
 
712
- 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, type Stage, type StandingRow, type Status, type Team, allFixtures, asFlavorLevel, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, deriveFavorite, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, flagEmoji, formatKickoff, 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 };
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";
@@ -3306,6 +3325,89 @@ async function getMarketSignals(provider, matches, options) {
3306
3325
  return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
3307
3326
  }
3308
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
+ }
3309
3411
  export {
3310
3412
  DEFAULT_COMPETITION,
3311
3413
  DEFAULT_FLAVOR,
@@ -3314,6 +3416,8 @@ export {
3314
3416
  FLAVOR_LEVELS,
3315
3417
  FakeMarketProvider,
3316
3418
  PolymarketProvider,
3419
+ SHARE_DISCLAIMER,
3420
+ SHARE_HASHTAG,
3317
3421
  allFixtures,
3318
3422
  asFlavorLevel,
3319
3423
  buildMarketSignal,
@@ -3327,7 +3431,10 @@ export {
3327
3431
  fixturesByGroup,
3328
3432
  fixturesByTeam,
3329
3433
  flagEmoji,
3434
+ formatDate,
3330
3435
  formatKickoff,
3436
+ formatShareSnippet,
3437
+ formatTime,
3331
3438
  getLiveMatches,
3332
3439
  getMarketSignal,
3333
3440
  getMarketSignals,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
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",