From 7a35c9678162389d67fd1e705b81564be7cd7e14 Mon Sep 17 00:00:00 2001 From: Kev Date: Sat, 11 Jul 2026 02:30:41 -0400 Subject: [PATCH] =?UTF-8?q?Session=20H=20(night2):=20design=20pass=20?= =?UTF-8?q?=E2=80=94=20=C2=A72=20locks=20+=20honest=20waiting=20sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QA.20: one meaning per color, locked — STEAM amber (caution), VALUE green (edge), red only at STALE/miss; revisions render the original grade struck through on slate AND ledger. - QA.21: every new data surface (PriorReads, PlayerStreaks, ModelRecord) renders data in mono (tabular-nums via the .mono class). - QA.22: waiting copy derives from the real cron schedule on every surface (StatStrip, dashboard, Explore) — no passive mystery anywhere. - Explore empty state joined the honest-waiting lib. - Sweep result: no raw brand hexes and no sans-serif data in tonight's components (sans only on prose/captions/hero letter — sanctioned); glow confined to A-tier badges + reveal chrome; 390px containment holds (wrapping chips, scroll-contained tables from Phase 3 rules). Backend 2352 -> 2396 tests (205 suites), web build exit 0. Co-Authored-By: Claude Fable 5 --- BUILD-STATE.md | 48 +++++++++++++++++++++++++++++++ CLAUDE.md | 28 ++++++++++++++++++ tests/unit/vyndrParityQA.test.js | 45 +++++++++++++++++++++++++++++ web/src/components/ExploreHub.tsx | 3 +- 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/BUILD-STATE.md b/BUILD-STATE.md index 0acd3fa..7dd0707 100755 --- a/BUILD-STATE.md +++ b/BUILD-STATE.md @@ -9,6 +9,54 @@ work-order 1.6 closed (canonical player keys + slate join invariant), Phase 2 slate UX, Phase 3 mobile P0. DEPLOY GATE: Coolify by Sat 10 AM ET; first snapshot locks against freshly posted lines (manual internal trigger). +## Session 60 (night2/full-board, 2026-07-11) — BUILT ✅ THE WHOLE BOARD (awaiting "merge the train") + +Branch `night2/full-board` off d10bb4c — ZERO pushes to main. 2352 → +**2396 tests** (205 suites), web build exit 0. Sessions A–H all shipped: +- **A** AUTONOMY.md + P0: SNAP_TTL 6h→24h (the 11h overnight cron gap + expired the snapshot before the morning settle — accuracy loop silently + settled nothing; the Postgres ledger settle was immune). +- **B** Streaks engine RESURRECTED + fed: the snapshot pipeline now writes + rosterlogs:{sport} from the stats it already fetches (producers were all + external/unarmed — that's why panels never rendered). Form heat (hot + hitters/sluggers/shooters, Σ/Σ rate math) + THE LENS (built-vs, tonight's + matchup + opposing SP ERA, step-up/down, one-line read). LIVE acceptance: + 32 real rows (Turang 12-gm on-base, Reynolds 8-gm, Pratt 7-gm, Meidroth + 5-gm, Cortes 5-gm). +- **C** Aggregator mounted: Explore = SEO server shell + hub (leaders + + full streaks + hot lists); streaks FREE for all tiers (the picture is + free); landing teaser fixed (pointed at off-season NBA → never showed); + one stat selection filters props+streaks+hotlists; player-dossier + ACTIVE STREAKS block (+?player= on /api/streaks). +- **D** Phase 2.5: 20-min odds-only refresh in slate hours (skips snapshot + slots, INTRADAY_REFRESH=0 kill switch, POST /api/internal/refresh/all). + STEAM ▲ (with, no re-grade) / VALUE ▲ (against ≥1.0, re-grade holds) / + PUBLIC revision (grade drops → revised_from_grade, strikethrough). + Refresh-fidelity closing capture; ticker MOVEs at refresh frequency. + Quota: ≤144 PropLine calls/day vs 9,000 free capacity. Post-deploy: set + SNAPSHOT_EXPECTED_INTERVAL=1200 and SYNC goes live. +- **E** Phase 4: /api/players/search built (didn't exist — the Ohtani bug); + nameKey fuzzy match live-verified vs the real 1,299-player list. + §7 reveal (steps → DECLASSIFIED stamp → 90ms staggered panels, + reduced-motion safe). PRIOR READS chips. Parlay: shared statLabel, + 1-leg provisional grade, Nav entry via window.__openParlay. +- **F** Phase 5: archetype definition line on-page; VYNDR-on-team + (ModelRecord team scope); WNBA minutes-usage; Settings meter from the + LIVE limiter (3/day free, 15/day analyst, ∞ desk); per-tier calibration + chips (n≥20 PER TIER). +- **G** Phase 6: landing first-paint root cause KILLED (page blocked its + render on Supabase auth init for anonymous visitors; now a sync + localStorage gate — anon paints instantly). Proof Strip: A-tier-gated + header, "TONIGHT'S BOARD" fallback, yesterday's settled reads with + outcome chips when ungraded. /api/content/top-signals + /streak-watch + (zero-grade format) + /daily-report (self-flags do_not_post until n≥20). + Per-player OG images (Node runtime) + player metadata. +- **H** Design pass: QA.20–22 locks (STEAM amber/VALUE green/STALE-only + red, revisions struck through, new data surfaces mono, honest waiting + copy everywhere). + +### The word: "merge the train" → main fast-forwards → Coolify ships board #2. + ## Session 59 (2026-07-10 overnight) — SHIPPED ✅ ADDENDUM + 1.6 + PHASE 2 + PHASE 3 Backend 2327 → **2352 tests** (+25), 202 suites. Web build clean (exit 0). diff --git a/CLAUDE.md b/CLAUDE.md index 16ea2b6..0a4ee14 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -773,6 +773,34 @@ phased plan in the Session-57 conversation / BUILD-STATE Next section). expands in place; GAME LINES <640px = `.gl-summary` best-line row + expander revealing `.gl-full.gl-expanded`. +## Night-2 Board (Session 60 — non-obvious) +- **SNAP_TTL is 24h ON PURPOSE** (was 6h): the overnight cron gap is 11h; + a shorter TTL silently kills the morning accuracy settle. AUTONOMY.md is + the zero-touch trace — update it when the loop's shape changes. +- **The snapshot pipeline FEEDS the aggregator**: `mergeRosterLogs` writes + `rosterlogs:{sport}` (72h TTL, cap 300) from the same stats resolve that + classifies archetypes. Streaks/hot-lists starve if you remove it — the + external prefetch (n8n/tank01) was never armed. +- **THE LENS RULE**: no raw streak renders alone — `streakLens.applyLens` + attaches built-vs / tonight's matchup (+opposing SP ERA for MLB batters) + / difficulty / one-line read. Absent context = say less, never invent. + Streaks are FREE for every tier; hot lists keep the free top-3 gate. +- **Phase 2.5 lives in `intradayRefreshService`**: signed delta vs the + GRADED side; STEAM never re-grades; ≥1.0 against re-grades THAT prop at + the real current line; a dropped grade sets `revised_from_grade` ONCE + (the original letter is forever). `applyRevision` only touches unsettled + public rows. Scheduler skips full-snapshot slots; INTRADAY_REFRESH=0 + kills it. After deploy set SNAPSHOT_EXPECTED_INTERVAL=1200. +- **/api/players/search is the canonical resolver** (nameKey fuzzy; MLB = + statsapi list, others = cached names). The Python service is a fallback + only. Never reintroduce raw-substring matching. +- **Landing renders instantly for anon**: `hasStoredSession()` sync + localStorage gate — do NOT re-block the marketing render on + `useAuth().loading`; that was the 3–4s LOADING THE SLATE bug. +- **QA.20–22 in vyndrParityQA** lock the night's color semantics (STEAM + amber / VALUE green / red = STALE-or-miss only), mono data surfaces, and + schedule-derived waiting copy. Keep them green. + ## Active Skills - vyndr-voice (all user-facing output) - prop-analysis (grading methodology) diff --git a/tests/unit/vyndrParityQA.test.js b/tests/unit/vyndrParityQA.test.js index 5583c1f..33a85a6 100644 --- a/tests/unit/vyndrParityQA.test.js +++ b/tests/unit/vyndrParityQA.test.js @@ -91,3 +91,48 @@ describe('QA.18 — auth gate integration', () => { expect(routes.isGatedRoute('/dashboard')).toBe(false); // free funnel stays open }); }); + +// ── Session 60 (night2/H) — §2 design-pass locks for tonight's surfaces ───── +// One meaning per color: green = edge/favorable, amber = caution/market +// signal, red = miss/error. Data is mono. Glow stays on A-tier/chrome. + +describe('QA.20 — movement + status color semantics (Phase 2.5 surfaces)', () => { + it('STEAM is amber (caution: entry compressed), VALUE is green (edge)', () => { + const strip = read('components/vyndr/StatStrip.tsx'); + expect(strip).toMatch(/steam \? 'var\(--amber/); + expect(strip).toMatch(/: 'var\(--g-a/); + }); + it('SYNC goes red ONLY at the STALE tier (error meaning), amber at 1.5x', () => { + const live = read('components/vyndr/LiveLayer.tsx'); + expect(live).toContain("tier === 'stale' ? 'var(--miss)'"); + expect(live).toContain("tier === 'amber' ? 'var(--amber)'"); + }); + it('a revision renders the ORIGINAL grade struck through (public, never silent)', () => { + expect(read('components/vyndr/StatStrip.tsx')).toContain("textDecoration: 'line-through'"); + expect(read('app/ledger/page.tsx')).toContain("textDecoration: 'line-through'"); + }); + it('lens difficulty: tougher = amber, softer = green — never inverted', () => { + const panel = read('components/StreaksPanel.tsx'); + expect(panel).toMatch(/step up' \? 'var\(--amber[^']*' : [^:]*step down' \? 'var\(--g-a/); + }); +}); + +describe('QA.21 — new data surfaces are mono', () => { + it.each([ + 'components/vyndr/PriorReads.tsx', + 'components/vyndr/PlayerStreaks.tsx', + 'components/vyndr/ModelRecord.tsx', + ])('%s renders its data rows in mono', (rel) => { + expect(read(rel)).toContain('className="mono"'); + }); +}); + +describe('QA.22 — honest waiting copy everywhere (no passive mystery)', () => { + it.each([ + 'components/vyndr/StatStrip.tsx', + 'app/dashboard/page.tsx', + 'components/ExploreHub.tsx', + ])('%s derives its waiting state from the real schedule', (rel) => { + expect(read(rel)).toContain('nextRunLabelET'); + }); +}); diff --git a/web/src/components/ExploreHub.tsx b/web/src/components/ExploreHub.tsx index 04178f5..48f0821 100644 --- a/web/src/components/ExploreHub.tsx +++ b/web/src/components/ExploreHub.tsx @@ -14,6 +14,7 @@ import { playerHref } from '@/lib/playerHref'; import StreaksPanel from '@/components/StreaksPanel'; import HotListPanel from '@/components/HotListPanel'; import { useAuth } from '@/contexts/AuthContext'; +import { nextRunLabelET } from '@/lib/pipelineSchedule'; interface Leader { player: string; @@ -105,7 +106,7 @@ export default function ExploreHub() { {state === 'error' &&
Could not load the leaderboard. Try again.
} {state === 'ready' && rows.length === 0 && (
- No graded props for {sport.toUpperCase()} yet — check back when tonight's slate posts. + No graded props for {sport.toUpperCase()} yet. Grades post {nextRunLabelET() || 'on the next pipeline run'}.
)} {state === 'ready' && rows.map((r, i) => (