d3637e7abd
- specs/ROW-GRAMMAR.md: the row grammar law (slot order, color law, mark
law, mobile stacking, no-truncation), locked by tests/unit/rowGrammar.
StatStrip violations fixed: MovementChip before the grade (market
context before model output); ViabilityChips after the archetype
(identity is one contiguous run).
- Line-movement sparklines: intradayRefreshService.trackHistory captures
real {t,line} points per grade (seeded with the lock, deduped when
flat, capped 24) inside the snapshot write-back; StatStrip.LineSparkline
renders at >=3 points (green toward / amber against / dim flat).
- Last-10 dot strips: services/last10Dots (streaksService accessors) ->
/api/snapshot/:sport attaches last10_dots from rosterlogs:{sport};
StatStrip.DotStrip renders vs the LOCKED line, newest first.
- CLV distribution: getModelAggregate emits clv_distribution (7 signed
buckets, outliers clamped) only past the centralized n>=20 gate;
ledger MODEL header renders the bar strip.
- Global search: SearchModal (cmd-K via GlobalHosts + window.__search),
players via /api/players/search per sport + static lib/teams.js
(soccer deliberately absent); Nav search icon + Search first in the
mobile More sheet. Explore tab untouched.
- Landing LCP: fade-up floored at opacity .6 (hero h1 contentful on
first frame, S33 visible-floor rule) + IBM Plex Mono preload:false
(4 decorative font files off the slow-4G critical path).
2654 -> 2698 tests (226 suites) green; web build exit 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
195 lines
9.3 KiB
JavaScript
195 lines
9.3 KiB
JavaScript
// Session 60 (night2/D) — Phase 2.5 intraday refresh. Direction is the
|
||
// signal: WITH the grade = STEAM (no re-grade); AGAINST ≥1.0 = re-grade
|
||
// that prop only (holds → VALUE, drops → PUBLIC revision). All lines are
|
||
// real feed values; the refresh recaptures the close.
|
||
|
||
const { runIntradayRefresh, inSlateHours, __internals } = require('../../src/services/intradayRefreshService');
|
||
const { signedDelta, trackHistory, HISTORY_CAP } = __internals;
|
||
|
||
const NOW = '2026-07-11T20:00:00.000Z';
|
||
|
||
function harness({ grades, oddsProps, analyzeResult }) {
|
||
const store = new Map([[`snapshot:mlb:latest`, { sport: 'mlb', updated_at: NOW, grades, deltas: [] }]]);
|
||
const calls = { analyze: [], revisions: [], closing: 0, ticker: [] };
|
||
const deps = {
|
||
cacheGet: async (k) => store.get(k) ?? null,
|
||
cacheSet: async (k, v) => { store.set(k, v); },
|
||
getOdds: async () => ({ props: oddsProps }),
|
||
analyze: async (prop) => { calls.analyze.push(prop); return analyzeResult; },
|
||
ledger: {
|
||
applyRevision: async (sport, rev) => { calls.revisions.push(rev); return { revised: true }; },
|
||
captureClosing: async () => { calls.closing += 1; return { updated: 0 }; },
|
||
},
|
||
pushTickerItems: async (events) => { calls.ticker.push(...events); },
|
||
now: () => NOW,
|
||
};
|
||
return { deps, store, calls };
|
||
}
|
||
|
||
const GRADE = {
|
||
player: 'Aaron Judge', stat_type: 'home_runs', line: 0.5, direction: 'over',
|
||
grade: 'A', confidence: 78, gradedAt: { line: 0.5, odds: -115, timestamp: NOW },
|
||
};
|
||
const prop = (line) => ({ player: 'Aaron Judge', stat_type: 'home_runs', line, book: 'draftkings' });
|
||
|
||
describe('signedDelta — relative to the graded side', () => {
|
||
test('over: rising line = positive (market chasing the over)', () => {
|
||
expect(signedDelta('over', 1.5, 2.5)).toBe(1);
|
||
expect(signedDelta('over', 1.5, 1.0)).toBe(-0.5);
|
||
});
|
||
test('under: falling line = positive', () => {
|
||
expect(signedDelta('under', 1.5, 1.0)).toBe(0.5);
|
||
expect(signedDelta('under', 1.5, 2.5)).toBe(-1);
|
||
});
|
||
test('missing values → null, never a fabricated zero', () => {
|
||
expect(signedDelta('over', null, 2)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('runIntradayRefresh — directional handling', () => {
|
||
test('moved WITH the grade → STEAM, NO re-grade', async () => {
|
||
const { deps, store, calls } = harness({ grades: [GRADE], oddsProps: [prop(1.5)], analyzeResult: null });
|
||
const res = await runIntradayRefresh('mlb', deps);
|
||
expect(res).toMatchObject({ status: 'ok', steam: 1, value: 0, revised: 0 });
|
||
expect(calls.analyze).toHaveLength(0); // steam never re-grades
|
||
const snap = store.get('snapshot:mlb:latest');
|
||
expect(snap.grades[0].movement).toMatchObject({ kind: 'steam', delta: 1, currentLine: 1.5 });
|
||
expect(calls.ticker.length).toBeGreaterThan(0); // ≥1.0 move → ticker MOVE
|
||
expect(calls.ticker[0].text).toContain('o0.5 → o1.5');
|
||
});
|
||
|
||
test('moved AGAINST ≥1.0, re-grade HOLDS → VALUE (better entry)', async () => {
|
||
const { deps, store, calls } = harness({
|
||
grades: [{ ...GRADE, gradedAt: { line: 2.5, odds: -110, timestamp: NOW }, line: 2.5 }],
|
||
oddsProps: [prop(1.5)],
|
||
analyzeResult: { grade: 'A', confidence: 70 }, // same grade at the new line
|
||
});
|
||
const res = await runIntradayRefresh('mlb', deps);
|
||
expect(res).toMatchObject({ value: 1, revised: 0 });
|
||
expect(calls.analyze).toHaveLength(1);
|
||
expect(calls.analyze[0].line).toBe(1.5); // re-graded at the REAL current line
|
||
expect(store.get('snapshot:mlb:latest').grades[0].movement.kind).toBe('value');
|
||
expect(calls.revisions).toHaveLength(0);
|
||
});
|
||
|
||
test('moved AGAINST ≥1.0, grade DROPS → PUBLIC revision (original preserved)', async () => {
|
||
const { deps, store, calls } = harness({
|
||
grades: [{ ...GRADE, gradedAt: { line: 2.5, odds: -110, timestamp: NOW }, line: 2.5 }],
|
||
oddsProps: [prop(1.5)],
|
||
analyzeResult: { grade: 'C', confidence: 40 },
|
||
});
|
||
const res = await runIntradayRefresh('mlb', deps);
|
||
expect(res).toMatchObject({ revised: 1 });
|
||
const g = store.get('snapshot:mlb:latest').grades[0];
|
||
expect(g.grade).toBe('C');
|
||
expect(g.revised_from_grade).toBe('A'); // the original, forever
|
||
expect(calls.revisions[0]).toMatchObject({ newGrade: 'C', fromGrade: 'A', line: 2.5, side: 'over' });
|
||
});
|
||
|
||
test('a second drop keeps the ORIGINAL revised_from_grade', async () => {
|
||
const already = { ...GRADE, grade: 'B', revised_from_grade: 'A', gradedAt: { line: 2.5, odds: null, timestamp: NOW }, line: 2.5 };
|
||
const { deps, store } = harness({ grades: [already], oddsProps: [prop(1.5)], analyzeResult: { grade: 'C' } });
|
||
await runIntradayRefresh('mlb', deps);
|
||
expect(store.get('snapshot:mlb:latest').grades[0].revised_from_grade).toBe('A');
|
||
});
|
||
|
||
test('an insufficient re-read HOLDS the original grade (never blanks it)', async () => {
|
||
const { deps, store } = harness({
|
||
grades: [{ ...GRADE, gradedAt: { line: 2.5, odds: null, timestamp: NOW }, line: 2.5 }],
|
||
oddsProps: [prop(1.5)],
|
||
analyzeResult: { grade: null, insufficient_data: true },
|
||
});
|
||
const res = await runIntradayRefresh('mlb', deps);
|
||
expect(res.value).toBe(1);
|
||
expect(store.get('snapshot:mlb:latest').grades[0].grade).toBe('A');
|
||
});
|
||
|
||
test('prop gone from the feed → frozen untouched; close recaptured every run', async () => {
|
||
const { deps, store, calls } = harness({ grades: [GRADE], oddsProps: [prop(0.5)], analyzeResult: null });
|
||
const res = await runIntradayRefresh('mlb', deps);
|
||
expect(res.checked).toBe(1);
|
||
expect(store.get('snapshot:mlb:latest').grades[0].movement).toBeNull(); // <0.5 noise
|
||
expect(calls.closing).toBe(1); // refresh-fidelity closing capture
|
||
});
|
||
|
||
test('no snapshot → honest skip, zero quota spent', async () => {
|
||
const calls = { odds: 0 };
|
||
const res = await runIntradayRefresh('mlb', {
|
||
cacheGet: async () => null,
|
||
cacheSet: async () => {},
|
||
getOdds: async () => { calls.odds += 1; return { props: [] }; },
|
||
ledger: {}, pushTickerItems: async () => {}, now: () => NOW,
|
||
});
|
||
expect(res.status).toBe('skipped');
|
||
expect(calls.odds).toBe(0);
|
||
});
|
||
});
|
||
|
||
// S6 (A1 board) — sparkline fuel: {t, line} history capture on each grade's
|
||
// movement tracking, persisted inside the snapshot write-back (no new keys).
|
||
describe('trackHistory — real {t, line} points, seeded, deduped, capped', () => {
|
||
const LOCK_TS = '2026-07-11T14:00:00.000Z';
|
||
const g = (extra = {}) => ({ ...GRADE, gradedAt: { line: 0.5, odds: -115, timestamp: LOCK_TS }, ...extra });
|
||
|
||
test('first capture seeds the LOCKED line at its own timestamp, then the move', () => {
|
||
const h = trackHistory(g(), 1.5, NOW);
|
||
expect(h).toEqual([{ t: LOCK_TS, line: 0.5 }, { t: NOW, line: 1.5 }]);
|
||
});
|
||
|
||
test('a flat line never appends a duplicate point (a point means it MOVED)', () => {
|
||
const h = trackHistory(g(), 0.5, NOW); // current == locked
|
||
expect(h).toEqual([{ t: LOCK_TS, line: 0.5 }]);
|
||
const again = trackHistory(g({ history: h }), 0.5, '2026-07-11T21:00:00.000Z');
|
||
expect(again).toHaveLength(1);
|
||
});
|
||
|
||
test('an unparseable current line appends nothing (real points only)', () => {
|
||
expect(trackHistory(g(), null, NOW)).toBeUndefined();
|
||
const prior = [{ t: LOCK_TS, line: 0.5 }];
|
||
expect(trackHistory(g({ history: prior }), 'n/a', NOW)).toBe(prior);
|
||
});
|
||
|
||
test('capped at 24 points, newest kept', () => {
|
||
const prior = Array.from({ length: HISTORY_CAP }, (_, i) => ({ t: `t${i}`, line: i }));
|
||
const h = trackHistory(g({ history: prior }), 99, NOW);
|
||
expect(h).toHaveLength(HISTORY_CAP);
|
||
expect(h[HISTORY_CAP - 1]).toEqual({ t: NOW, line: 99 });
|
||
expect(h[0]).toEqual({ t: 't1', line: 1 }); // oldest point aged out
|
||
});
|
||
});
|
||
|
||
describe('runIntradayRefresh — history rides the snapshot write-back', () => {
|
||
test('a steam move persists history on the grade (locked point + current)', async () => {
|
||
const { deps, store } = harness({ grades: [GRADE], oddsProps: [prop(1.5)], analyzeResult: null });
|
||
await runIntradayRefresh('mlb', deps);
|
||
const snap = store.get('snapshot:mlb:latest');
|
||
expect(snap.grades[0].history).toEqual([
|
||
{ t: NOW, line: 0.5 }, // seeded from the lock (gradedAt.timestamp === NOW here)
|
||
{ t: NOW, line: 1.5 },
|
||
]);
|
||
// grades:{sport} carries the same enriched grades.
|
||
expect(store.get('grades:mlb').grades[0].history).toHaveLength(2);
|
||
});
|
||
|
||
test('a flat run keeps a single seeded point — the sparkline stays hidden (<3)', async () => {
|
||
const { deps, store } = harness({ grades: [GRADE], oddsProps: [prop(0.5)], analyzeResult: null });
|
||
await runIntradayRefresh('mlb', deps);
|
||
expect(store.get('snapshot:mlb:latest').grades[0].history).toEqual([{ t: NOW, line: 0.5 }]);
|
||
});
|
||
|
||
test('a prop gone from the feed leaves prior history untouched', async () => {
|
||
const withHist = { ...GRADE, stat_type: 'hits', history: [{ t: 'x', line: 1 }] };
|
||
const { deps, store } = harness({ grades: [withHist], oddsProps: [prop(1.5)], analyzeResult: null });
|
||
await runIntradayRefresh('mlb', deps);
|
||
const frozen = store.get('snapshot:mlb:latest').grades.find((g) => g.stat_type === 'hits');
|
||
expect(frozen.history).toEqual([{ t: 'x', line: 1 }]);
|
||
});
|
||
});
|
||
|
||
describe('inSlateHours — noon–midnight ET', () => {
|
||
test('3 PM ET yes, 4 AM ET no', () => {
|
||
expect(inSlateHours(new Date('2026-07-11T19:00:00Z'))).toBe(true); // 3 PM EDT
|
||
expect(inSlateHours(new Date('2026-07-11T08:00:00Z'))).toBe(false); // 4 AM EDT
|
||
});
|
||
});
|