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:
@@ -38,6 +38,37 @@ const AGG_FETCH_LIMIT = 5000;
|
||||
/** Below this many settled rows, callers must not render a percentage. */
|
||||
const MIN_AGG_SAMPLE = 20;
|
||||
|
||||
/**
|
||||
* S6 (A1 board) — CLV distribution buckets (the MODEL tab strip). Signed CLV:
|
||||
* positive = beat the close. Outliers clamp into the edge buckets so every
|
||||
* settled clv lands somewhere. `side` drives the UI color (green = beat,
|
||||
* red = faded, dim = flat) — same meanings as clv_result.
|
||||
*/
|
||||
const CLV_BUCKETS = [
|
||||
{ label: '[-2,-1)', min: -2, max: -1, side: 'faded' },
|
||||
{ label: '[-1,-.5)', min: -1, max: -0.5, side: 'faded' },
|
||||
{ label: '[-.5,0)', min: -0.5, max: 0, side: 'faded' },
|
||||
{ label: '0', min: 0, max: 0, side: 'flat' },
|
||||
{ label: '(0,.5]', min: 0, max: 0.5, side: 'beat' },
|
||||
{ label: '(.5,1]', min: 0.5, max: 1, side: 'beat' },
|
||||
{ label: '(1,2]', min: 1, max: 2, side: 'beat' },
|
||||
];
|
||||
|
||||
/** Bucket index for one signed clv value (clamped into the edge buckets). */
|
||||
function clvBucketIndex(clv) {
|
||||
const v = numOrNull(clv); // strict — Number(null) is 0, a fabricated CLV
|
||||
if (v == null) return -1;
|
||||
if (v === 0) return 3;
|
||||
if (v < 0) {
|
||||
if (v >= -0.5) return 2;
|
||||
if (v >= -1) return 1;
|
||||
return 0; // ≤ -1 clamps into [-2,-1)
|
||||
}
|
||||
if (v <= 0.5) return 4;
|
||||
if (v <= 1) return 5;
|
||||
return 6; // > 1 clamps into (1,2]
|
||||
}
|
||||
|
||||
function isConfigured() {
|
||||
return Boolean(process.env.SUPABASE_URL
|
||||
&& (process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_KEY));
|
||||
@@ -397,6 +428,7 @@ async function getModelAggregate(opts = {}) {
|
||||
window_days: AGG_WINDOW_DAYS, min_sample: MIN_AGG_SAMPLE,
|
||||
settled: 0, hits: 0, misses: 0, pushes: 0, hit_pct: null,
|
||||
clv_sample: 0, clv_beat: 0, clv_faded: 0, clv_flat: 0, beat_close_pct: null,
|
||||
clv_distribution: null, // S6 — set past the n≥20 gate only
|
||||
pending: 0,
|
||||
};
|
||||
if (!opts.sb && !isConfigured()) return empty;
|
||||
@@ -405,7 +437,7 @@ async function getModelAggregate(opts = {}) {
|
||||
const since = new Date(nowMs - AGG_WINDOW_DAYS * 24 * 3600 * 1000).toISOString().slice(0, 10);
|
||||
|
||||
let settledQ = sb.from('ledger_entries')
|
||||
.select('outcome, clv_result, player_key, grade');
|
||||
.select('outcome, clv_result, clv, player_key, grade');
|
||||
settledQ = opts.userId ? settledQ.eq('user_id', opts.userId) : settledQ.is('user_id', null);
|
||||
settledQ = settledQ
|
||||
.not('outcome', 'is', null)
|
||||
@@ -471,6 +503,19 @@ async function getModelAggregate(opts = {}) {
|
||||
if (agg.settled >= MIN_AGG_SAMPLE && agg.clv_sample > 0) {
|
||||
agg.beat_close_pct = Math.round((agg.clv_beat / agg.clv_sample) * 100);
|
||||
}
|
||||
// S6 (A1 board) — clv_distribution rides the SAME n≥20 gate (this is the
|
||||
// single home of the gate — consumers never re-derive it). Null below the
|
||||
// sample floor or with zero settled clv values; the UI renders nothing.
|
||||
agg.clv_distribution = null;
|
||||
if (agg.settled >= MIN_AGG_SAMPLE && agg.clv_sample > 0) {
|
||||
const dist = CLV_BUCKETS.map((b) => ({ ...b, count: 0 }));
|
||||
let counted = 0;
|
||||
for (const r of settledRows || []) {
|
||||
const i = clvBucketIndex(r.clv);
|
||||
if (i >= 0) { dist[i].count += 1; counted += 1; }
|
||||
}
|
||||
if (counted > 0) agg.clv_distribution = dist;
|
||||
}
|
||||
return agg;
|
||||
}
|
||||
|
||||
@@ -486,6 +531,6 @@ module.exports = {
|
||||
__internals: {
|
||||
rowsFromSnapshot, computeClv, clvResultOf, indexProps, gameIdFor,
|
||||
dateET, sideOf, oddsForSide, isConfigured, CONFLICT,
|
||||
teamOpponentFor, teamsMatch,
|
||||
teamOpponentFor, teamsMatch, clvBucketIndex, CLV_BUCKETS,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user