From e453c24d2c7563f71f1befe282171f5572251199 Mon Sep 17 00:00:00 2001 From: Kev Date: Tue, 16 Jun 2026 14:37:07 -0400 Subject: [PATCH] =?UTF-8?q?Session=2039:=20Design=20system=20Phase=20H=20?= =?UTF-8?q?=E2=80=94=20QA=20pass,=20=C2=A713=20parity=20verified,=20conver?= =?UTF-8?q?sion=20COMPLETE=20(1907=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final phase of the VYNDR 2.0 conversion (Sessions 33-39). Verify -> fix -> lock. Frontend-only; zero backend changes. §13 automated checklist: all PASS or FIXED. - QA.1 token resolution FIXED: ProcessingGrade #00ffb8 -> var(--g-ap); game/[id] sport literals -> var(--s-*). Remaining hex documented as intentional (var-with-fallback, Next metadata, bespoke intel-surface shades). - QA.6 glitch discipline: ZERO glitch on data components. - QA.4/5/8/11/16/18 verified; QA.9 (cmd palette) documented deferred; QA.17 (AI slop) flagged for Kev's manual browser review. De-flake: soccerFeatureExtractorCascade hit Jest's 5s default under full-suite load (falls through to live adapters on cache miss) -> jest.setTimeout(20000), same family as the S32 pipeline test. Verified stable across 3 full-suite runs. New: tests/unit/vyndrParityQA.test.js (17 tests locking the parity invariants). Backend 1890 -> 1907, 146 suites, zero regressions (stable x3). Web build clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- BUILD-STATE.md | 76 ++++++++++++++- CLAUDE.md | 17 ++++ .../soccerFeatureExtractorCascade.test.js | 7 ++ tests/unit/vyndrParityQA.test.js | 92 +++++++++++++++++++ web/src/app/game/[id]/page.tsx | 7 +- web/src/components/vyndr/ProcessingGrade.tsx | 12 +-- 6 files changed, 200 insertions(+), 11 deletions(-) create mode 100644 tests/unit/vyndrParityQA.test.js diff --git a/BUILD-STATE.md b/BUILD-STATE.md index f678878..fefaebc 100755 --- a/BUILD-STATE.md +++ b/BUILD-STATE.md @@ -4,8 +4,80 @@ 2026-06-16 ## Current Phase -SHIP BUILD v38.0 — VYNDR 2.0 design system, Phase G: systems — living layer, -i18n/odds, a11y toggles, paywall/checkout, parlay correlation math (Session 38) +SHIP BUILD v39.0 — VYNDR 2.0 design system, Phase H: QA pass — §13 parity +verified, conversion COMPLETE (Session 39) + +## Session 39 (2026-06-16) — SHIPPED ✅ DESIGN CONVERSION COMPLETE + +Phase H: the QA pass against the §13 parity checklist. Verify → fix → lock. +Frontend-only; ZERO backend changes. Backend 1890 → **1907 tests** (+17), 146 +suites, **stable across 3 consecutive full-suite runs** (the S38 flaky test is +fixed). Web build clean (compiled successfully, exit 0). + +This completes the 7-session VYNDR 2.0 conversion (Sessions 33–39): tokens → +components → shell → screens → mobile → systems → QA. + +### §13 AUTOMATED CHECKLIST — RESULTS +- **QA.1 Token resolution** — FIXED. `#00ffb8` (= the A+ token) → `var(--g-ap)` + in ProcessingGrade (7 sites); `game/[id]` sport literals (`#E94B3C/#1E90FF/ + #FFB347`) → `var(--s-nba/mlb/wnba)`. Remaining hex are INTENTIONAL + + documented: `var(--token, #fallback)` belt-and-suspenders (soccer/offline/ + admin), Next metadata `themeColor` (can't be a var), and bespoke intel-surface + / red-tint text shades (`#e8fff4`,`#bdf5e2`,`#ff8a8a`,`#ff8b7a`,`#ffb0a4`, + `#ffd9a8`,`#04140f`) ported verbatim from the prototype — no token equivalent; + retokenizing would visibly deviate from the design. +- **QA.2 Typography** — PASS. Data rows/chips use `var(--mono)`; the GradeResult + hero LETTER is `var(--sans)` (display) faithful to the prototype's grade-card. +- **QA.3 Grade prominence** — PASS. Hero 92–116px (80px mobile), GradeBadge hero=100. +- **QA.4 Best/worst lines** — PASS. Legacy GameCard (live slate) + vyndr/GameCard + tint best green (`rgba(0,212,160,.13)` + green left border) / worst red. +- **QA.5 Wordmark everywhere** — PASS. Nav, Footer, login, about, 404 (+ social cards). +- **QA.6 Glitch discipline** — PASS. ZERO glitch classes in GradeResultCard / + GameCard(s) / ProcessingGrade. Glitch stays on chrome only. +- **QA.7 Heartbeat/ticker/live counters** — PASS (Phase G; under the nav). +- **QA.8 Scan reveal** — PASS. ProcessingGrade factor-ignite → CRT-sweep → card. +- **QA.9 Command palette (⌘K)** — DEFERRED (documented). Nav `›` Query links to + /scan; a true ⌘K palette was never in scope for 33–39. +- **QA.10 Parlay correlation** — PASS (Phase G `lib/parlayMath.js`, tested). +- **QA.11 Mobile parity** — PASS. 5-tab bar (Slate/Terminal/Scan/Ledger/More). +- **QA.12 Auth gate / deep-links / paywall** — PASS (AuthGate, HashRedirect, read-meter→paywall). +- **QA.13 i18n / odds** — PASS. `fmtOdds` converts ML; totals pass through (tested). +- **QA.14 Accessibility** — PASS. Prefs modal sets ``, persists. +- **QA.15 PWA** — PASS (S27 SW + S37 manifest/shortcuts/viewport-fit). Not touched. +- **QA.16 No dead buttons** — PASS. Zero `onClick={}` (asserted by a tree-walk test). +- **QA.17 No AI slop** — MANUAL (flagged for Kev's browser review — see below). +- **QA.18 Auth-gate integration** — PASS. Gates via lib/routes → `/login?next=`. + +### De-flake (completion-quality) +The S38-flagged flaky test (`soccerFeatureExtractorCascade › nextMatch cascade`) +intermittently hit Jest's 5s default under full-suite concurrency (the extractor +falls through to live adapters on a cache miss). Added `jest.setTimeout(20000)` +to that test file — same fix family as S32's CPU-bound pipeline test. Test-only, +no service change. Verified: 3 consecutive clean full-suite runs (1907/1907). + +### Files created +- `tests/unit/vyndrParityQA.test.js` (17 tests locking QA.1/4/5/6/11/16/18 — + glitch-free data, token resolution, best-line tint, wordmark presence, 5 tabs, + no dead buttons, auth-gate list) + +### Files modified +- `web/src/components/vyndr/ProcessingGrade.tsx` (#00ffb8 → --g-ap) +- `web/src/app/game/[id]/page.tsx` (sport literals → tokens) +- `tests/unit/soccerFeatureExtractorCascade.test.js` (de-flake timeout) + +### ⚠️ MANUAL CHECKS for Kev (browser, post-deploy) +- [ ] No AI slop (gradient bg / rounded-pill SaaS cards / stray emoji / exposed- + algorithm copy). NOTE: 🔥 STREAKS + ◎ scan glyph are deliberate data markers. +- [ ] North-star energy — every page belongs with the 404. +- [ ] Auth gate end-to-end: incognito → /ledger → /login?next=/ledger → sign in → /ledger. +- [ ] Mobile: tab bar native feel, More sheet slide, touch targets. +- [ ] Scan grade reveal plays; ticker scrolls; best/worst line tints; PWA installs. +- [ ] Still-open operator item (since S31): rotate the leaked GitHub PAT in the + `origin` remote URL and scrub it from `.git/config`. + +--- + +## Session 38 (2026-06-16) — SHIPPED ## Session 38 (2026-06-16) — SHIPPED diff --git a/CLAUDE.md b/CLAUDE.md index ed81a2a..29b5cdf 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -276,6 +276,23 @@ Testable CommonJS modules in `lib/` + thin React glue: - GOTCHA: don't return a `Set.delete`-based unsub directly from `useEffect` (returns boolean ≠ valid cleanup) — wrap as `() => { unsub(); }`. +## VYNDR 2.0 Conversion COMPLETE (Session 39 — Phase H QA) +The 7-session design conversion (33–39) is done and parity-verified against §13. +- Parity invariants are locked by `tests/unit/vyndrParityQA.test.js` — if you + later add a glitch class to a data component, a raw `#00ffb8`/sport hex, a dead + `onClick={}`, or break the gated-route list, that suite fails. Keep it green. +- INTENTIONAL hex (do NOT "fix" to tokens): `var(--token, #fallback)` fallbacks, + Next metadata `themeColor`, and the bespoke intel-surface/red-tint text shades + (#e8fff4/#bdf5e2/#ff8a8a/#ff8b7a/#ffb0a4/#ffd9a8/#04140f) ported from the + prototype — no token equivalent. +- The GradeResultCard hero LETTER is intentionally `var(--sans)` (display), + matching the prototype; all grade DATA rows + the GradeBadge chip are mono. +- DEFERRED (never in 33–39 scope): a true ⌘K command palette (Nav `›` Query + currently links to /scan). +- TEST DE-FLAKE: `soccerFeatureExtractorCascade` gets `jest.setTimeout(20000)` — + it falls through to live adapters on cache miss and flaked at Jest's 5s default + under full-suite load (same family as the S32 pipeline test). + ## Active Skills - vyndr-voice (all user-facing output) - prop-analysis (grading methodology) diff --git a/tests/unit/soccerFeatureExtractorCascade.test.js b/tests/unit/soccerFeatureExtractorCascade.test.js index e4c9fee..38aa5e8 100644 --- a/tests/unit/soccerFeatureExtractorCascade.test.js +++ b/tests/unit/soccerFeatureExtractorCascade.test.js @@ -19,6 +19,13 @@ jest.mock('../../src/utils/redis', () => ({ const { normalizeName } = require('../../src/utils/normalize'); const extractor = require('../../src/services/intelligence/soccerFeatureExtractor'); +// Session 39 (QA) — de-flake. When a cache key misses, the extractor falls +// through to live network adapters; under full-suite concurrency that async +// chain occasionally exceeds Jest's 5s default (intermittent timeout on the +// nextMatch/referee cascade tests). Same fix family as Session 32's CPU-bound +// pipeline test — give the suite headroom. Test-only; no service change. +jest.setTimeout(20000); + beforeEach(() => { mockCacheStore.clear(); }); describe('soccerFeatureExtractor — source cascade (Session 9)', () => { diff --git a/tests/unit/vyndrParityQA.test.js b/tests/unit/vyndrParityQA.test.js new file mode 100644 index 0000000..6b1e4f4 --- /dev/null +++ b/tests/unit/vyndrParityQA.test.js @@ -0,0 +1,92 @@ +// VYNDR 2.0 — Phase H parity QA (Session 39). Locks the automated §13 +// checklist invariants so the design conversion can't silently regress. + +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'); + +describe('QA.6 — glitch discipline (data NEVER glitches)', () => { + const GLITCH = /wm-tear|glitch-shift|head-tear|glitch-hover/; + it.each([ + 'components/vyndr/GradeResultCard.tsx', + 'components/vyndr/GameCard.tsx', + 'components/GameCard.tsx', + 'components/vyndr/ProcessingGrade.tsx', + ])('%s contains no glitch classes', (rel) => { + expect(GLITCH.test(read(rel))).toBe(false); + }); +}); + +describe('QA.1 — token resolution (no literals that duplicate a token)', () => { + it('ProcessingGrade uses the A+ token, not the raw #00ffb8 literal', () => { + const src = read('components/vyndr/ProcessingGrade.tsx'); + expect(src).not.toContain('#00ffb8'); + expect(src).toContain('var(--g-ap)'); + }); + it('game detail uses sport tokens, not duplicated sport hex', () => { + const src = read('app/game/[id]/page.tsx'); + expect(src).toContain('var(--s-nba)'); + expect(src).not.toMatch(/#E94B3C/); + }); +}); + +describe('QA.4 — Bloomberg best/worst line pattern', () => { + it('legacy GameCard (live slate) tints best green / worst red', () => { + const src = read('components/GameCard.tsx'); + expect(src).toContain('rgba(0,212,160,.13)'); + expect(src).toContain('rgba(255,82,82,.07)'); + expect(src).toMatch(/bestAway|bestHome/); + }); +}); + +describe('QA.5 — wordmark everywhere', () => { + it.each([ + 'components/Nav.tsx', + 'components/Footer.tsx', + 'app/login/page.tsx', + 'app/about/page.tsx', + 'app/not-found.tsx', + ])('%s renders the Wordmark', (rel) => { + expect(read(rel)).toContain('Wordmark'); + }); +}); + +describe('QA.11 — mobile parity (5-tab bar)', () => { + it('BottomTabBar declares Slate/Terminal/Scan/Ledger/More', () => { + const src = read('components/BottomTabBar.tsx'); + ['Slate', 'Terminal', 'Scan', 'Ledger', 'More'].forEach((t) => expect(src).toContain(`'${t}'`)); + }); +}); + +describe('QA.16 — no dead buttons', () => { + it.each(['components', 'app'])('no empty onClick handlers under web/src/%s', (dir) => { + const root = path.join(WEB, dir); + const offenders = []; + const walk = (d) => { + for (const e of fs.readdirSync(d, { withFileTypes: true })) { + const fp = path.join(d, e.name); + if (e.isDirectory()) walk(fp); + else if (e.name.endsWith('.tsx') && /onClick=\{\}/.test(fs.readFileSync(fp, 'utf8'))) offenders.push(fp); + } + }; + walk(root); + expect(offenders).toEqual([]); + }); +}); + +describe('QA.18 — auth gate integration', () => { + it('AuthGate gates via lib/routes and bounces to /login?next=', () => { + const src = read('components/AuthGate.tsx'); + expect(src).toContain('isGatedRoute'); + expect(src).toContain('/login?next='); + }); + it('gated route list covers the personal surfaces', () => { + const routes = require('../../web/src/lib/routes'); + ['/ledger', '/tracker', '/account', '/notifications'].forEach((r) => + expect(routes.isGatedRoute(r)).toBe(true), + ); + expect(routes.isGatedRoute('/dashboard')).toBe(false); // free funnel stays open + }); +}); diff --git a/web/src/app/game/[id]/page.tsx b/web/src/app/game/[id]/page.tsx index 25d45cd..9f7cbd9 100644 --- a/web/src/app/game/[id]/page.tsx +++ b/web/src/app/game/[id]/page.tsx @@ -49,10 +49,11 @@ interface PropEntry { alt_lines?: { line: number; grade: string; hit_rate?: number }[]; } +// Session 39 (QA.1) — resolve to sport tokens instead of duplicate literals. const SPORT_COLOR: Record = { - NBA: '#E94B3C', - MLB: '#1E90FF', - WNBA: '#FFB347', + NBA: 'var(--s-nba)', + MLB: 'var(--s-mlb)', + WNBA: 'var(--s-wnba)', }; export default function GamePage({ params }: { params: Promise<{ id: string }> }) { diff --git a/web/src/components/vyndr/ProcessingGrade.tsx b/web/src/components/vyndr/ProcessingGrade.tsx index 56473e3..f1881d9 100644 --- a/web/src/components/vyndr/ProcessingGrade.tsx +++ b/web/src/components/vyndr/ProcessingGrade.tsx @@ -13,10 +13,10 @@ function MiniBrain({ size = 56 }: { size?: number }) { return ( {links.map(([a, b], i) => ( - + ))} {nodes.map(([x, y], i) => ( - + ))} ); @@ -84,7 +84,7 @@ export default function ProcessingGrade({ data, replayKey = 0, onShare, onAddToP
-
{pct}%
+
{pct}%
WEIGHING 40+ FACTORS
@@ -94,16 +94,16 @@ export default function ProcessingGrade({ data, replayKey = 0, onShare, onAddToP const on = i < lit; return (
- + {f} - {on && ✓ WEIGHED} + {on && ✓ WEIGHED}
); })}
-
+