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,109 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* last10Dots — S6 (A1 board, ROW-GRAMMAR §4).
|
||||
*
|
||||
* ●●○●● — a grade's last 10 REAL games scored against TONIGHT'S locked line:
|
||||
* per game, hit = stat value ≥ line → boolean, newest first. Pure math over
|
||||
* the `rosterlogs:{sport}` blob the snapshot pipeline already writes (games
|
||||
* are stored most-recent-first) — zero API calls, zero new keys.
|
||||
*
|
||||
* Stat-field access reuses the SAME accessors as streaksService (one source
|
||||
* of truth for the several field spellings each feed uses). A stat type with
|
||||
* no accessor, an empty log, or a non-finite line → null: absent beats wrong,
|
||||
* the UI renders nothing.
|
||||
*/
|
||||
|
||||
const { __internals: streakAccessors } = require('./streaksService');
|
||||
const { nameKey } = require('../utils/playerName');
|
||||
|
||||
const { nba, mlb, soccer } = streakAccessors;
|
||||
|
||||
// grade stat_type → streaksService accessor, per sport. Keep keyed on the
|
||||
// grade-gate stat names (routes/analyze.js vocabulary), not market keys.
|
||||
const STAT_ACCESSORS = {
|
||||
mlb: {
|
||||
hits: mlb.hits,
|
||||
total_bases: mlb.totalBases,
|
||||
home_runs: mlb.homeRuns,
|
||||
rbi: mlb.rbi,
|
||||
runs: mlb.runs,
|
||||
stolen_bases: mlb.stolenBases,
|
||||
walks: mlb.walks,
|
||||
strikeouts: mlb.strikeouts,
|
||||
earned_runs: mlb.earnedRuns,
|
||||
hits_allowed: mlb.hits, // pitching log's hits = hits allowed
|
||||
innings_pitched: mlb.inningsPitched,
|
||||
doubles: mlb.doubles,
|
||||
triples: mlb.triples,
|
||||
outs: mlb.outs,
|
||||
},
|
||||
nba: {
|
||||
points: nba.points,
|
||||
rebounds: nba.rebounds,
|
||||
assists: nba.assists,
|
||||
threes: nba.threes,
|
||||
steals: nba.steals,
|
||||
blocks: nba.blocks,
|
||||
pra: nba.pra,
|
||||
},
|
||||
soccer: {
|
||||
goals: soccer.goals,
|
||||
assists: soccer.assists,
|
||||
shots_on_target: soccer.shotsOnTarget,
|
||||
},
|
||||
};
|
||||
STAT_ACCESSORS.wnba = STAT_ACCESSORS.nba;
|
||||
|
||||
const MAX_DOTS = 10;
|
||||
|
||||
/**
|
||||
* Compute the dot booleans for one player's game log vs a line.
|
||||
* `games` most-recent-first (the rosterlogs order). Returns boolean[]
|
||||
* (newest first, ≤10) or null when it can't be computed honestly.
|
||||
*/
|
||||
function computeLast10Dots(games, sport, statType, line) {
|
||||
const sp = String(sport || '').toLowerCase();
|
||||
const accessor = (STAT_ACCESSORS[sp] || {})[String(statType || '').toLowerCase()];
|
||||
// Strict: Number(null) is 0 — a fabricated line (Data Semantics Rule).
|
||||
const ln = line == null ? NaN : Number(line);
|
||||
if (!accessor || !Number.isFinite(ln)) return null;
|
||||
const rows = Array.isArray(games) ? games.filter(Boolean).slice(0, MAX_DOTS) : [];
|
||||
if (rows.length === 0) return null;
|
||||
return rows.map((g) => accessor(g) >= ln);
|
||||
}
|
||||
|
||||
/** Index a rosterlogs blob by nameKey → games (most-recent-first). */
|
||||
function indexRosterLogs(blob) {
|
||||
const map = {};
|
||||
for (const e of Array.isArray(blob) ? blob : []) {
|
||||
if (e && e.name && Array.isArray(e.games) && e.games.length > 0) {
|
||||
map[nameKey(e.name)] = e.games;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach `last10_dots` to each grade that has a real log + locked line.
|
||||
* Non-destructive: grades without a computable strip pass through untouched.
|
||||
*/
|
||||
function attachLast10Dots(grades, rosterIndex, sport) {
|
||||
if (!rosterIndex || Object.keys(rosterIndex).length === 0) return grades;
|
||||
return (grades || []).map((g) => {
|
||||
const player = g.player || g.player_name;
|
||||
if (!player) return g;
|
||||
const games = rosterIndex[nameKey(player)];
|
||||
if (!games) return g;
|
||||
const line = g.gradedAt && g.gradedAt.line != null ? g.gradedAt.line : g.line;
|
||||
const dots = computeLast10Dots(games, sport, g.stat_type || g.stat, line);
|
||||
return dots ? { ...g, last10_dots: dots } : g;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
computeLast10Dots,
|
||||
indexRosterLogs,
|
||||
attachLast10Dots,
|
||||
__internals: { STAT_ACCESSORS, MAX_DOTS },
|
||||
};
|
||||
Reference in New Issue
Block a user