Files
vyndr/tests/unit/last10Dots.test.js
T
builtbykev d3637e7abd S6 (a1): display — the full picture under the grammar
- 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>
2026-07-11 20:08:24 -04:00

97 lines
4.3 KiB
JavaScript

// S6 (A1 board) — ●●○●● last-10 vs tonight's locked line (ROW-GRAMMAR §4).
// Pure math over the rosterlogs blob; stat access reuses streaksService's
// accessors (one source of truth for every field spelling). Real logs only.
const fs = require('fs');
const path = require('path');
const { computeLast10Dots, indexRosterLogs, attachLast10Dots, __internals } = require('../../src/services/last10Dots');
describe('computeLast10Dots', () => {
const games = [
{ date: '2026-07-10', hits: 2, totalBases: 5 },
{ date: '2026-07-09', hits: 0, totalBases: 0 },
{ date: '2026-07-08', hits: 1, totalBases: 2 },
];
test('hit = value ≥ line, boolean per game, newest first', () => {
expect(computeLast10Dots(games, 'mlb', 'hits', 1.5)).toEqual([true, false, false]);
expect(computeLast10Dots(games, 'mlb', 'total_bases', 1.5)).toEqual([true, false, true]);
});
test('tolerates the alternate field spellings (streaksService accessors)', () => {
const alt = [{ H: 3 }, { h: 0 }];
expect(computeLast10Dots(alt, 'mlb', 'hits', 0.5)).toEqual([true, false]);
});
test('nba/wnba stats work too (points, pra)', () => {
const log = [{ pts: 31, reb: 8, ast: 7 }, { pts: 12, reb: 2, ast: 1 }];
expect(computeLast10Dots(log, 'wnba', 'points', 20.5)).toEqual([true, false]);
expect(computeLast10Dots(log, 'nba', 'pra', 40.5)).toEqual([true, false]);
});
test('caps at 10 games', () => {
const long = Array.from({ length: 14 }, (_, i) => ({ hits: i % 2 }));
expect(computeLast10Dots(long, 'mlb', 'hits', 0.5)).toHaveLength(10);
});
test('absent → nothing: unknown stat, no games, bad line all return null', () => {
expect(computeLast10Dots(games, 'mlb', 'exit_velocity', 1.5)).toBeNull();
expect(computeLast10Dots([], 'mlb', 'hits', 1.5)).toBeNull();
expect(computeLast10Dots(games, 'mlb', 'hits', null)).toBeNull();
expect(computeLast10Dots(games, 'unknown_sport', 'hits', 1.5)).toBeNull();
});
});
describe('indexRosterLogs + attachLast10Dots', () => {
const blob = [
{ name: 'A.J. Ewing', team: 'NYY', games: [{ hits: 2 }, { hits: 0 }, { hits: 1 }] },
{ name: 'No Games Guy', team: 'BOS', games: [] },
];
test('indexes by nameKey (variants merge) and skips empty logs', () => {
const idx = indexRosterLogs(blob);
expect(idx['aj ewing']).toHaveLength(3);
expect(Object.keys(idx)).toHaveLength(1);
});
test('attaches last10_dots against the LOCKED line; untouched when absent', () => {
const idx = indexRosterLogs(blob);
const grades = [
{ player: 'AJ Ewing', stat_type: 'hits', line: 2.5, gradedAt: { line: 0.5 }, grade: 'A' },
{ player: 'Someone Else', stat_type: 'hits', line: 0.5, grade: 'B' },
{ player: 'AJ Ewing', stat_type: 'exit_velocity', line: 90, grade: 'C' },
];
const out = attachLast10Dots(grades, idx, 'mlb');
// locked line (0.5) is used, not the current line (2.5).
expect(out[0].last10_dots).toEqual([true, false, true]);
expect(out[1].last10_dots).toBeUndefined(); // no log → nothing
expect(out[2].last10_dots).toBeUndefined(); // no accessor → nothing
expect(out[1]).toBe(grades[1]); // pass-through untouched
});
test('empty roster index is a no-op', () => {
const grades = [{ player: 'AJ Ewing', stat_type: 'hits', line: 0.5 }];
expect(attachLast10Dots(grades, {}, 'mlb')).toBe(grades);
});
});
describe('wiring locks', () => {
test('the public snapshot route reads rosterlogs and attaches dots', () => {
const route = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'routes', 'snapshot.js'), 'utf8');
expect(route).toContain('rosterlogs:${sport}');
expect(route).toContain('attachLast10Dots');
});
test('the slate adapter threads last10_dots + history onto strip props', () => {
const adapter = fs.readFileSync(path.join(__dirname, '..', '..', 'web', 'src', 'lib', 'slateAdapter.js'), 'utf8');
expect(adapter).toContain('rec.last10_dots');
expect(adapter).toContain('rec.history');
});
test('every accessor is a function (the streaksService contract holds)', () => {
for (const [sport, map] of Object.entries(__internals.STAT_ACCESSORS)) {
for (const [stat, fn] of Object.entries(map)) {
expect({ sport, stat, isFn: typeof fn === 'function' }).toEqual({ sport, stat, isFn: true });
}
}
});
});