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>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// S6 (A1 board) — ROW-GRAMMAR source locks (specs/ROW-GRAMMAR.md).
|
||||
// Density is unlimited when grammar is fixed: every element is DATA with ONE
|
||||
// meaning in the SAME slot on every row of its type. These tests fail if a
|
||||
// slot moves, a color changes meaning, or truncation sneaks onto a name.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
|
||||
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
|
||||
|
||||
const strip = read('components/vyndr/StatStrip.tsx');
|
||||
const spec = fs.readFileSync(path.join(__dirname, '..', '..', 'specs', 'ROW-GRAMMAR.md'), 'utf8');
|
||||
|
||||
/** Assert `markers` appear in this exact order in `src`, starting at `from`. */
|
||||
function assertOrder(src, markers, from = 0) {
|
||||
let cursor = from;
|
||||
for (const m of markers) {
|
||||
const i = src.indexOf(m, cursor);
|
||||
expect({ marker: m, found: i >= 0 }).toEqual({ marker: m, found: true });
|
||||
cursor = i + m.length;
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
describe('ROW-GRAMMAR §2 — canonical prop-row slot order (snapshot mode)', () => {
|
||||
test('stat+line → market context (dot, movement) → model output (revision, grade) → outcome → actions → provenance', () => {
|
||||
// Anchor on the graded row's stat+side+line span (unique to it).
|
||||
const base = strip.indexOf("<span style={{ color: '#fff' }}>{p.stat} {p.side}{p.line}</span>");
|
||||
expect(base).toBeGreaterThan(-1);
|
||||
assertOrder(strip, [
|
||||
'<BestPriceDot p={p} />', // slot 4a — best available price
|
||||
'<MovementChip p={p} />', // slot 4b — market movement
|
||||
'p.revisedFrom', // slot 5a — revision strikethrough
|
||||
'<GradeBadge grade={p.grade}', // slot 5b — the current grade
|
||||
'<OutcomeChip p={p} />', // slot 6 — settled result
|
||||
'<ParlayBtn p={p} />', // slot 7a — action
|
||||
'<BookItTeaser p={p} />', // slot 7b — action
|
||||
'Graded {p.gradedAt.ago}', // slot 8 — provenance, always last
|
||||
], base);
|
||||
});
|
||||
|
||||
test('sub-line order: last-10 dots → sparkline → current-line delta', () => {
|
||||
const base = strip.indexOf('ROW-GRAMMAR sub-line');
|
||||
expect(base).toBeGreaterThan(-1);
|
||||
assertOrder(strip, [
|
||||
'<DotStrip dots={p.last10Dots} />',
|
||||
'<LineSparkline p={p} />',
|
||||
'Current {p.delta.currentLine}',
|
||||
], base);
|
||||
});
|
||||
|
||||
test('strip header: identity run (name → team → archetype) then viability chips', () => {
|
||||
const base = strip.indexOf('// compact');
|
||||
expect(base).toBeGreaterThan(-1);
|
||||
assertOrder(strip, [
|
||||
'<PlayerName',
|
||||
'{team}</span>',
|
||||
'<ArchetypeBadge archetype={archetype.primary}',
|
||||
'<ViabilityChips lineup={lineup} injury={injury} />',
|
||||
], base);
|
||||
});
|
||||
|
||||
test('settled/dead rows suppress actions (the bet is over)', () => {
|
||||
expect(strip).toContain('{!p.outcome && !p.dead && <ParlayBtn p={p} />}');
|
||||
expect(strip).toContain('{!p.outcome && !p.dead && <BookItTeaser p={p} />}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ROW-GRAMMAR §3 — one meaning per color', () => {
|
||||
const section = (start, end) => {
|
||||
const a = strip.indexOf(start);
|
||||
const b = end ? strip.indexOf(end, a) : strip.length;
|
||||
expect(a).toBeGreaterThan(-1);
|
||||
return strip.slice(a, b === -1 ? strip.length : b);
|
||||
};
|
||||
|
||||
test('sparkline is green/amber/dim — never red (nothing has settled)', () => {
|
||||
const src = section('export function LineSparkline', 'export function ViabilityChips');
|
||||
expect(src).toContain("'var(--g-a)'");
|
||||
expect(src).toContain("'var(--amber)'");
|
||||
expect(src).not.toContain('--miss');
|
||||
});
|
||||
|
||||
test('dot strip: filled = signal green, hollow = dim (never red/amber)', () => {
|
||||
const src = section('export function DotStrip', 'export function LineSparkline');
|
||||
expect(src).toContain('var(--g-a');
|
||||
expect(src).toContain('var(--text-2');
|
||||
expect(src).not.toContain('--miss');
|
||||
expect(src).not.toContain('--amber');
|
||||
});
|
||||
|
||||
test('movement chips keep STEAM amber / VALUE green (QA.20 family)', () => {
|
||||
const src = section('function MovementChip', 'export interface StripArchetype');
|
||||
expect(src).toContain("steam ? 'var(--amber");
|
||||
expect(src).not.toContain('--miss');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ROW-GRAMMAR §1.4 — no truncation of names/times/pitchers', () => {
|
||||
test('StatStrip never ellipsizes', () => {
|
||||
expect(strip).not.toMatch(/textOverflow|text-overflow|ellipsis/);
|
||||
});
|
||||
test('SearchModal result names wrap, never truncate', () => {
|
||||
const sm = read('components/vyndr/SearchModal.tsx');
|
||||
expect(sm).not.toMatch(/textOverflow|ellipsis/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ROW-GRAMMAR — the law is written down and wired', () => {
|
||||
test('spec exists with the canonical slot order and color law', () => {
|
||||
expect(spec).toContain('CANONICAL PROP-ROW SLOT ORDER');
|
||||
expect(spec).toContain('one meaning per color');
|
||||
expect(spec).toContain('never');
|
||||
expect(spec).not.toMatch(/!/); // VOICE — no exclamation points
|
||||
});
|
||||
test('data marks are mono', () => {
|
||||
// DotStrip + sub-line render with the mono class (data never sans).
|
||||
const dot = strip.slice(strip.indexOf('export function DotStrip'), strip.indexOf('export function LineSparkline'));
|
||||
expect(dot).toContain('className="mono"');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user