Session H (night2): design pass — §2 locks + honest waiting sweep

- 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 <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-11 02:30:41 -04:00
parent 1b4f2772d6
commit 7a35c96781
4 changed files with 123 additions and 1 deletions
+48
View File
@@ -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 AH 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.2022 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).
+28
View File
@@ -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 34s LOADING THE SLATE bug.
- **QA.2022 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)
+45
View File
@@ -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');
});
});
+2 -1
View File
@@ -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' && <div className="mono" style={{ padding: '30px 16px', textAlign: 'center', fontSize: 12, color: 'var(--miss)' }}>Could not load the leaderboard. Try again.</div>}
{state === 'ready' && rows.length === 0 && (
<div className="mono" style={{ padding: '30px 16px', textAlign: 'center', fontSize: 12, color: 'var(--text-2)' }}>
No graded props for {sport.toUpperCase()} yet check back when tonight&apos;s slate posts.
No graded props for {sport.toUpperCase()} yet. Grades post {nextRunLabelET() || 'on the next pipeline run'}.
</div>
)}
{state === 'ready' && rows.map((r, i) => (