@claudinho/core 0.4.3 → 0.5.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 +3 -3
- package/dist/index.d.ts +107 -2
- package/dist/index.js +119 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,13 +18,13 @@ npm i @claudinho/core
|
|
|
18
18
|
## What's inside
|
|
19
19
|
|
|
20
20
|
- **Domain model** — `Match` (incl. `venue`/`city`/`country`), `Team`, `Stage`, `Status`, `PunditPick`, `LedgerRow`
|
|
21
|
-
- **`ProviderAdapter`** — the swappable data-vendor interface; `EspnAdapter` included
|
|
21
|
+
- **`ProviderAdapter`** — the swappable data-vendor interface (`fetchByDate`, `fetchLive`, optional `fetchWindow`/`fetchStandings`); `EspnAdapter` included
|
|
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
|
-
- **Standings** — `computeStandings` (points / GD / GF tiebreak)
|
|
24
|
+
- **Standings** — `getStandings` fetches authoritative, cumulative group tables from the provider (`GroupStandings`), failing closed to a roster-at-zero when none is available; `computeStandings` derives a table from a set of matches (points / GD / GF tiebreak)
|
|
25
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 signals 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
|
|
27
|
+
- **Shareable snippets** — `formatShareSnippet` builds pure, deterministic, plain-text match cards (composing `Match` + the market copy bank); `formatShareTable` does the same for a group's standings (facts + emoji flags only, no market line). For the CLI's `share` command and MCP/site reuse. The non-affiliation disclaimer is non-optional; market lines come from the approved bank.
|
|
28
28
|
|
|
29
29
|
## Example
|
|
30
30
|
|
package/dist/index.d.ts
CHANGED
|
@@ -254,6 +254,11 @@ interface StandingRow {
|
|
|
254
254
|
goalDiff: number;
|
|
255
255
|
points: number;
|
|
256
256
|
}
|
|
257
|
+
/** A group's table: the group letter ("A".."L") and its rows in standings order. */
|
|
258
|
+
interface GroupStandings {
|
|
259
|
+
group: string;
|
|
260
|
+
rows: StandingRow[];
|
|
261
|
+
}
|
|
257
262
|
/**
|
|
258
263
|
* Compute a group table from a set of matches. Teams are seeded from every
|
|
259
264
|
* match (so a pre-tournament table still lists all four teams at 0), and only
|
|
@@ -286,6 +291,13 @@ interface ProviderAdapter {
|
|
|
286
291
|
fetchLive(): Promise<Match[]>;
|
|
287
292
|
/** Optional inclusive date-range fetch (used for schedule generation). */
|
|
288
293
|
fetchWindow?(startDate: string, endDate: string): Promise<Match[]>;
|
|
294
|
+
/**
|
|
295
|
+
* Optional authoritative group tables (cumulative across the group stage).
|
|
296
|
+
* Returned in standings order per group. Providers that can't supply a real
|
|
297
|
+
* table omit this; callers then fall back (degraded) to a roster at zero —
|
|
298
|
+
* never a wrong, partial table computed from a narrow live window.
|
|
299
|
+
*/
|
|
300
|
+
fetchStandings?(): Promise<GroupStandings[]>;
|
|
289
301
|
/** Optional push subscription (websocket/SSE providers). Returns an unsubscribe fn. */
|
|
290
302
|
subscribe?(onBatch: (matches: Match[]) => void): () => void;
|
|
291
303
|
}
|
|
@@ -375,6 +387,34 @@ interface EspnAdapterOptions {
|
|
|
375
387
|
*/
|
|
376
388
|
enrichGroups?: boolean;
|
|
377
389
|
}
|
|
390
|
+
interface EspnStandingsStat {
|
|
391
|
+
name?: string;
|
|
392
|
+
value?: number;
|
|
393
|
+
}
|
|
394
|
+
interface EspnStandingsEntry {
|
|
395
|
+
team?: EspnTeam;
|
|
396
|
+
stats?: EspnStandingsStat[];
|
|
397
|
+
}
|
|
398
|
+
interface EspnStandingsChild {
|
|
399
|
+
name?: string;
|
|
400
|
+
abbreviation?: string;
|
|
401
|
+
standings?: {
|
|
402
|
+
entries?: EspnStandingsEntry[];
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
interface EspnStandings {
|
|
406
|
+
children?: EspnStandingsChild[];
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Parse the standings payload into group tables. Pure (exported for tests).
|
|
410
|
+
*
|
|
411
|
+
* Two non-obvious robustness points, both verified against the live response:
|
|
412
|
+
* - ESPN's `entries` array is NOT in rank order, so we sort by the `rank` stat
|
|
413
|
+
* (falling back to points → GD → GF → code when rank is absent).
|
|
414
|
+
* - Non-group `children` (knockout brackets) are skipped by the "Group X" name
|
|
415
|
+
* test, so this stays correct once the bracket phase begins.
|
|
416
|
+
*/
|
|
417
|
+
declare function parseStandings(data: EspnStandings): GroupStandings[];
|
|
378
418
|
declare class EspnAdapter implements ProviderAdapter {
|
|
379
419
|
private readonly opts;
|
|
380
420
|
readonly name = "espn";
|
|
@@ -385,9 +425,18 @@ declare class EspnAdapter implements ProviderAdapter {
|
|
|
385
425
|
fetchByDate(dateISO: string): Promise<Match[]>;
|
|
386
426
|
fetchWindow(startDate: string, endDate: string): Promise<Match[]>;
|
|
387
427
|
fetchLive(): Promise<Match[]>;
|
|
428
|
+
/** Standings endpoint URL (lives under apis/v2, not site/v2; derived from base). */
|
|
429
|
+
private standingsUrl;
|
|
430
|
+
/**
|
|
431
|
+
* Authoritative, cumulative group tables from the standings endpoint. Throws
|
|
432
|
+
* on fetch/parse failure (the caller decides the fallback). Group-stage only:
|
|
433
|
+
* non-group `children` are filtered out by {@link parseStandings}.
|
|
434
|
+
*/
|
|
435
|
+
fetchStandings(): Promise<GroupStandings[]>;
|
|
388
436
|
/**
|
|
389
437
|
* Build (and cache) a team-code -> group-letter map from the standings
|
|
390
|
-
* endpoint. Best-effort: returns {} if standings are unavailable.
|
|
438
|
+
* endpoint. Best-effort: returns {} if standings are unavailable. Reuses the
|
|
439
|
+
* same parse as {@link fetchStandings}, so the two never drift.
|
|
391
440
|
*/
|
|
392
441
|
fetchGroupMap(force?: boolean): Promise<Record<string, string>>;
|
|
393
442
|
private fetchScoreboard;
|
|
@@ -427,6 +476,27 @@ declare function liveSourceLabel(source: string): string;
|
|
|
427
476
|
* schedule on any provider/network error (graceful degradation).
|
|
428
477
|
*/
|
|
429
478
|
declare function getMatchesForDate(adapter: ProviderAdapter, dateISO: string): Promise<LiveResult>;
|
|
479
|
+
interface StandingsResult {
|
|
480
|
+
/** Group tables in group-letter order; each table's rows in standings order. */
|
|
481
|
+
tables: GroupStandings[];
|
|
482
|
+
/** True when no authoritative table was available and rows are a static roster. */
|
|
483
|
+
degraded: boolean;
|
|
484
|
+
/** The provider that served a real table (absent when degraded). */
|
|
485
|
+
source?: string;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Authoritative group tables, preferring the provider's cumulative standings and
|
|
489
|
+
* FAILING CLOSED to a roster-at-zero (degraded) when none is available.
|
|
490
|
+
*
|
|
491
|
+
* Deliberately does NOT compute a table from a live-match window: that silently
|
|
492
|
+
* drops earlier matchdays and reports a wrong, partial table (e.g. all-zeros for
|
|
493
|
+
* a group not playing today) — the bug this replaced. A degraded roster is
|
|
494
|
+
* honestly empty; a confidently-wrong table is the failure mode we refuse.
|
|
495
|
+
*
|
|
496
|
+
* An empty `tables` with `degraded: false` means the fetch succeeded but the
|
|
497
|
+
* asked-for group isn't in it (caller renders "no such group").
|
|
498
|
+
*/
|
|
499
|
+
declare function getStandings(adapter: ProviderAdapter, group?: string): Promise<StandingsResult>;
|
|
430
500
|
interface MatchByIdResult {
|
|
431
501
|
match?: Match;
|
|
432
502
|
degraded: boolean;
|
|
@@ -796,6 +866,14 @@ interface ShareSnippetInput {
|
|
|
796
866
|
emptyNote?: string;
|
|
797
867
|
/** Exact run cue to advertise, e.g. "npx @claudinho/cli next MEX". */
|
|
798
868
|
installLine?: string;
|
|
869
|
+
/**
|
|
870
|
+
* True when the live fetch failed and these are static fixtures (no live
|
|
871
|
+
* scores). A pasted card must say so — otherwise a "no matches" / scheduled
|
|
872
|
+
* card reads as authoritative when the feed is actually down. When set with
|
|
873
|
+
* matches present, a not-live notice is appended; for the empty case the
|
|
874
|
+
* caller picks a feed-down `emptyNote`.
|
|
875
|
+
*/
|
|
876
|
+
degraded?: boolean;
|
|
799
877
|
/** Timezone for kickoff date/time (date/time only — copy stays English). */
|
|
800
878
|
tz?: string;
|
|
801
879
|
/** Locale for kickoff date/time. */
|
|
@@ -807,5 +885,32 @@ interface ShareSnippetInput {
|
|
|
807
885
|
* line; lines within a block by a single newline.
|
|
808
886
|
*/
|
|
809
887
|
declare function formatShareSnippet(input: ShareSnippetInput, options?: ShareSnippetOptions): string;
|
|
888
|
+
interface ShareTableInput {
|
|
889
|
+
/** Group tables to render (1..n); each in standings order. */
|
|
890
|
+
tables: {
|
|
891
|
+
group: string;
|
|
892
|
+
rows: StandingRow[];
|
|
893
|
+
}[];
|
|
894
|
+
/** Live-data provider name for attribution; omit when degraded/static. */
|
|
895
|
+
source?: string;
|
|
896
|
+
/** Exact run cue, e.g. "npx @claudinho/cli table A". */
|
|
897
|
+
installLine?: string;
|
|
898
|
+
/** Body line when there are no tables (e.g. "No group Z."). */
|
|
899
|
+
emptyNote?: string;
|
|
900
|
+
/**
|
|
901
|
+
* True when the rows are a static roster (no live results), not an
|
|
902
|
+
* authoritative table. A shared card is pasted into public/social, so this
|
|
903
|
+
* MUST be surfaced — otherwise a roster-at-zero reads as a real "nobody has
|
|
904
|
+
* played yet" table. The card then carries an explicit not-live notice.
|
|
905
|
+
*/
|
|
906
|
+
degraded?: boolean;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Render a shareable group-standings card. Pure, plain-text, deterministic. Like
|
|
910
|
+
* {@link formatShareSnippet} but for tables: facts + emoji flags only, no market
|
|
911
|
+
* lines (standings carry no market read), disclaimer non-optional, hashtag and
|
|
912
|
+
* run cue toggleable via {@link ShareSnippetOptions}.
|
|
913
|
+
*/
|
|
914
|
+
declare function formatShareTable(input: ShareTableInput, options?: ShareSnippetOptions): string;
|
|
810
915
|
|
|
811
|
-
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, LIVE_WINDOW_MS, 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 MatchByIdResult, 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, currentOrNextFixtureForTeam, deriveFavorite, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, formatDate, formatKickoff, formatShareSnippet, formatTime, getLiveMatches, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, groups, hasSaneDistribution, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isStaleSignal, isValidDate, isValidTimeZone, liveSourceLabel, localDate, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSourceLabel, matchFlavor, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeOutcomes, outcomeFromScore, resolveCompetition, resolveMarketSource, resolveTz, scoreline };
|
|
916
|
+
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 GroupStandings, LIVE_WINDOW_MS, 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 MatchByIdResult, type MatchEvent, type Outcome, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, type PunditPick, SHARE_DISCLAIMER, SHARE_HASHTAG, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type Stage, type StandingRow, type StandingsResult, type Status, type Team, allFixtures, asFlavorLevel, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, deriveFavorite, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, formatDate, formatKickoff, formatShareSnippet, formatShareTable, formatTime, getLiveMatches, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getStandings, groups, hasSaneDistribution, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isStaleSignal, isValidDate, isValidTimeZone, liveSourceLabel, localDate, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSourceLabel, matchFlavor, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeOutcomes, outcomeFromScore, parseStandings, resolveCompetition, resolveMarketSource, resolveTz, scoreline };
|
package/dist/index.js
CHANGED
|
@@ -2787,6 +2787,43 @@ function mapEspnEvent(ev, ctx = {}) {
|
|
|
2787
2787
|
function toEspnDate(d) {
|
|
2788
2788
|
return d.replace(/\D/g, "").slice(0, 8);
|
|
2789
2789
|
}
|
|
2790
|
+
function statVal(stats, name) {
|
|
2791
|
+
const v = stats?.find((s) => s.name === name)?.value;
|
|
2792
|
+
return typeof v === "number" && Number.isFinite(v) ? Math.round(v) : 0;
|
|
2793
|
+
}
|
|
2794
|
+
function entryToRow(e) {
|
|
2795
|
+
return {
|
|
2796
|
+
team: toTeam(e.team),
|
|
2797
|
+
played: statVal(e.stats, "gamesPlayed"),
|
|
2798
|
+
won: statVal(e.stats, "wins"),
|
|
2799
|
+
drawn: statVal(e.stats, "ties"),
|
|
2800
|
+
lost: statVal(e.stats, "losses"),
|
|
2801
|
+
goalsFor: statVal(e.stats, "pointsFor"),
|
|
2802
|
+
goalsAgainst: statVal(e.stats, "pointsAgainst"),
|
|
2803
|
+
goalDiff: statVal(e.stats, "pointDifferential"),
|
|
2804
|
+
points: statVal(e.stats, "points")
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
function parseStandings(data) {
|
|
2808
|
+
const out = [];
|
|
2809
|
+
for (const child of data.children ?? []) {
|
|
2810
|
+
const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
|
|
2811
|
+
if (!letter) continue;
|
|
2812
|
+
const ranked = (child.standings?.entries ?? []).map((e) => ({
|
|
2813
|
+
row: entryToRow(e),
|
|
2814
|
+
rank: statVal(e.stats, "rank")
|
|
2815
|
+
}));
|
|
2816
|
+
ranked.sort((a, b) => {
|
|
2817
|
+
if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
|
|
2818
|
+
const r = a.row;
|
|
2819
|
+
const s = b.row;
|
|
2820
|
+
return s.points - r.points || s.goalDiff - r.goalDiff || s.goalsFor - r.goalsFor || r.team.code.localeCompare(s.team.code);
|
|
2821
|
+
});
|
|
2822
|
+
out.push({ group: letter, rows: ranked.map((x) => x.row) });
|
|
2823
|
+
}
|
|
2824
|
+
out.sort((a, b) => a.group.localeCompare(b.group));
|
|
2825
|
+
return out;
|
|
2826
|
+
}
|
|
2790
2827
|
var EspnAdapter = class {
|
|
2791
2828
|
constructor(opts = {}) {
|
|
2792
2829
|
this.opts = opts;
|
|
@@ -2806,25 +2843,30 @@ var EspnAdapter = class {
|
|
|
2806
2843
|
const today = await this.fetchScoreboard();
|
|
2807
2844
|
return today.filter((m) => m.status === "LIVE" || m.status === "HT");
|
|
2808
2845
|
}
|
|
2846
|
+
/** Standings endpoint URL (lives under apis/v2, not site/v2; derived from base). */
|
|
2847
|
+
standingsUrl() {
|
|
2848
|
+
const base = this.opts.baseUrl ?? DEFAULT_BASE;
|
|
2849
|
+
return `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* Authoritative, cumulative group tables from the standings endpoint. Throws
|
|
2853
|
+
* on fetch/parse failure (the caller decides the fallback). Group-stage only:
|
|
2854
|
+
* non-group `children` are filtered out by {@link parseStandings}.
|
|
2855
|
+
*/
|
|
2856
|
+
async fetchStandings() {
|
|
2857
|
+
return parseStandings(await this.get(this.standingsUrl()));
|
|
2858
|
+
}
|
|
2809
2859
|
/**
|
|
2810
2860
|
* Build (and cache) a team-code -> group-letter map from the standings
|
|
2811
|
-
* endpoint. Best-effort: returns {} if standings are unavailable.
|
|
2861
|
+
* endpoint. Best-effort: returns {} if standings are unavailable. Reuses the
|
|
2862
|
+
* same parse as {@link fetchStandings}, so the two never drift.
|
|
2812
2863
|
*/
|
|
2813
2864
|
async fetchGroupMap(force = false) {
|
|
2814
2865
|
if (this.groupMap && !force) return this.groupMap;
|
|
2815
|
-
const base = this.opts.baseUrl ?? DEFAULT_BASE;
|
|
2816
|
-
const standingsUrl = `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
|
|
2817
2866
|
const map = {};
|
|
2818
2867
|
try {
|
|
2819
|
-
const
|
|
2820
|
-
for (const
|
|
2821
|
-
const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
|
|
2822
|
-
if (!letter) continue;
|
|
2823
|
-
for (const e of child.standings?.entries ?? []) {
|
|
2824
|
-
const code = e.team?.abbreviation?.toUpperCase();
|
|
2825
|
-
if (code) map[code] = letter;
|
|
2826
|
-
}
|
|
2827
|
-
}
|
|
2868
|
+
const tables = parseStandings(await this.get(this.standingsUrl()));
|
|
2869
|
+
for (const t of tables) for (const r of t.rows) map[r.team.code] = t.group;
|
|
2828
2870
|
} catch {
|
|
2829
2871
|
}
|
|
2830
2872
|
this.groupMap = map;
|
|
@@ -2897,6 +2939,22 @@ async function getMatchesForDate(adapter, dateISO) {
|
|
|
2897
2939
|
return { matches: base, degraded: true };
|
|
2898
2940
|
}
|
|
2899
2941
|
}
|
|
2942
|
+
async function getStandings(adapter, group) {
|
|
2943
|
+
const want = group?.toUpperCase();
|
|
2944
|
+
if (adapter.fetchStandings) {
|
|
2945
|
+
try {
|
|
2946
|
+
const all = await adapter.fetchStandings();
|
|
2947
|
+
const tables2 = (want ? all.filter((t) => t.group === want) : all).sort(
|
|
2948
|
+
(a, b) => a.group.localeCompare(b.group)
|
|
2949
|
+
);
|
|
2950
|
+
return { tables: tables2, degraded: false, source: adapter.name };
|
|
2951
|
+
} catch {
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
const letters = want ? [want] : groups();
|
|
2955
|
+
const tables = letters.map((g) => ({ group: g, rows: computeStandings(fixturesByGroup(g)) })).filter((t) => t.rows.length > 0);
|
|
2956
|
+
return { tables, degraded: true };
|
|
2957
|
+
}
|
|
2900
2958
|
function shiftUtcDate(dateISO, days) {
|
|
2901
2959
|
const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
|
|
2902
2960
|
return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
|
|
@@ -3450,11 +3508,53 @@ function formatShareSnippet(input, options = {}) {
|
|
|
3450
3508
|
blocks.push(card.join("\n"));
|
|
3451
3509
|
}
|
|
3452
3510
|
}
|
|
3511
|
+
if (input.degraded && input.matches.length > 0) {
|
|
3512
|
+
blocks.push("(Live data unavailable \u2014 showing the bundled schedule, not live scores.)");
|
|
3513
|
+
}
|
|
3514
|
+
blocks.push(
|
|
3515
|
+
shareFooter({
|
|
3516
|
+
source: input.source,
|
|
3517
|
+
installLine: input.installLine,
|
|
3518
|
+
includeHashtag,
|
|
3519
|
+
includeInstall
|
|
3520
|
+
})
|
|
3521
|
+
);
|
|
3522
|
+
return blocks.join("\n\n");
|
|
3523
|
+
}
|
|
3524
|
+
function shareFooter(opts) {
|
|
3453
3525
|
const footer = [];
|
|
3454
|
-
if (
|
|
3455
|
-
footer.push(
|
|
3456
|
-
|
|
3457
|
-
|
|
3526
|
+
if (opts.source) footer.push(`Live data: ${liveSourceLabel(opts.source)}`);
|
|
3527
|
+
footer.push(
|
|
3528
|
+
[opts.includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 ")
|
|
3529
|
+
);
|
|
3530
|
+
if (opts.includeInstall && opts.installLine) footer.push(`Try it: ${opts.installLine}`);
|
|
3531
|
+
return footer.join("\n");
|
|
3532
|
+
}
|
|
3533
|
+
function gd(n) {
|
|
3534
|
+
return n > 0 ? `+${n}` : `${n}`;
|
|
3535
|
+
}
|
|
3536
|
+
function tableRow(r, rank) {
|
|
3537
|
+
return `${rank}. ${r.team.flag} ${r.team.code} ${r.points} pts \xB7 ${r.won}-${r.drawn}-${r.lost} \xB7 ${gd(r.goalDiff)}`;
|
|
3538
|
+
}
|
|
3539
|
+
function formatShareTable(input, options = {}) {
|
|
3540
|
+
const includeHashtag = options.includeHashtag !== false;
|
|
3541
|
+
const includeInstall = options.includeInstallLine !== false;
|
|
3542
|
+
const blocks = [];
|
|
3543
|
+
if (input.tables.length === 0) {
|
|
3544
|
+
blocks.push(input.emptyNote ?? "No standings available.");
|
|
3545
|
+
} else {
|
|
3546
|
+
for (const { group, rows } of input.tables) {
|
|
3547
|
+
blocks.push(
|
|
3548
|
+
[`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, i + 1))].join("\n")
|
|
3549
|
+
);
|
|
3550
|
+
}
|
|
3551
|
+
if (input.degraded) {
|
|
3552
|
+
blocks.push("(Live standings unavailable \u2014 group roster, not live results.)");
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
blocks.push(
|
|
3556
|
+
shareFooter({ source: input.source, installLine: input.installLine, includeHashtag, includeInstall })
|
|
3557
|
+
);
|
|
3458
3558
|
return blocks.join("\n\n");
|
|
3459
3559
|
}
|
|
3460
3560
|
export {
|
|
@@ -3486,12 +3586,14 @@ export {
|
|
|
3486
3586
|
formatDate,
|
|
3487
3587
|
formatKickoff,
|
|
3488
3588
|
formatShareSnippet,
|
|
3589
|
+
formatShareTable,
|
|
3489
3590
|
formatTime,
|
|
3490
3591
|
getLiveMatches,
|
|
3491
3592
|
getMarketSignal,
|
|
3492
3593
|
getMarketSignals,
|
|
3493
3594
|
getMatchById,
|
|
3494
3595
|
getMatchesForDate,
|
|
3596
|
+
getStandings,
|
|
3495
3597
|
groups,
|
|
3496
3598
|
hasSaneDistribution,
|
|
3497
3599
|
isFinished,
|
|
@@ -3523,6 +3625,7 @@ export {
|
|
|
3523
3625
|
nextFixtureForTeam,
|
|
3524
3626
|
normalizeOutcomes,
|
|
3525
3627
|
outcomeFromScore,
|
|
3628
|
+
parseStandings,
|
|
3526
3629
|
resolveCompetition,
|
|
3527
3630
|
resolveMarketSource,
|
|
3528
3631
|
resolveTz,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claudinho/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Domain model, provider adapters (ESPN), standings, bundled schedule, and the read-only Polymarket market-signal sidecar powering Claudinho. Not affiliated with FIFA or Anthropic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|