Session 39: Design system Phase H — QA pass, §13 parity verified, conversion COMPLETE (1907 tests)
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) <noreply@anthropic.com>
This commit is contained in:
+74
-2
@@ -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 `<html data-*>`, 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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
@@ -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<Sport, string> = {
|
||||
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 }> }) {
|
||||
|
||||
@@ -13,10 +13,10 @@ function MiniBrain({ size = 56 }: { size?: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 60 56" aria-hidden style={{ flexShrink: 0 }}>
|
||||
{links.map(([a, b], i) => (
|
||||
<line key={i} className="brain-link" x1={nodes[a][0]} y1={nodes[a][1]} x2={nodes[b][0]} y2={nodes[b][1]} stroke="#00ffb8" strokeWidth="1" opacity="0.5" />
|
||||
<line key={i} className="brain-link" x1={nodes[a][0]} y1={nodes[a][1]} x2={nodes[b][0]} y2={nodes[b][1]} stroke="var(--g-ap)" strokeWidth="1" opacity="0.5" />
|
||||
))}
|
||||
{nodes.map(([x, y], i) => (
|
||||
<circle key={i} className="brain-node" cx={x} cy={y} r="2.6" fill="#00ffb8" style={{ animationDelay: `${(i % 5) * 0.2}s` }} />
|
||||
<circle key={i} className="brain-node" cx={x} cy={y} r="2.6" fill="var(--g-ap)" style={{ animationDelay: `${(i % 5) * 0.2}s` }} />
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
@@ -84,7 +84,7 @@ export default function ProcessingGrade({ data, replayKey = 0, onShare, onAddToP
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto', textAlign: 'right' }}>
|
||||
<div className="mono" style={{ fontSize: 30, fontWeight: 800, color: '#00ffb8', textShadow: '0 0 16px rgba(0,255,184,.5)' }}>{pct}%</div>
|
||||
<div className="mono" style={{ fontSize: 30, fontWeight: 800, color: 'var(--g-ap)', textShadow: '0 0 16px rgba(0,255,184,.5)' }}>{pct}%</div>
|
||||
<div className="mono" style={{ fontSize: 9.5, letterSpacing: '0.12em', color: 'rgba(232,255,244,.5)' }}>WEIGHING 40+ FACTORS</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,16 +94,16 @@ export default function ProcessingGrade({ data, replayKey = 0, onShare, onAddToP
|
||||
const on = i < lit;
|
||||
return (
|
||||
<div key={i} className={on ? 'factor-ignite' : ''} style={{ display: 'flex', alignItems: 'center', gap: 12, opacity: on ? 1 : 0.18, transition: 'opacity .2s' }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', flexShrink: 0, background: on ? '#00ffb8' : 'rgba(232,255,244,.3)', boxShadow: on ? '0 0 10px rgba(0,255,184,.8)' : 'none' }} />
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', flexShrink: 0, background: on ? 'var(--g-ap)' : 'rgba(232,255,244,.3)', boxShadow: on ? '0 0 10px rgba(0,255,184,.8)' : 'none' }} />
|
||||
<span className="mono" style={{ fontSize: 13, color: on ? '#e8fff4' : 'rgba(232,255,244,.4)' }}>{f}</span>
|
||||
{on && <span className="mono" style={{ marginLeft: 'auto', fontSize: 10.5, color: '#00ffb8', letterSpacing: '0.08em' }}>✓ WEIGHED</span>}
|
||||
{on && <span className="mono" style={{ marginLeft: 'auto', fontSize: 10.5, color: 'var(--g-ap)', letterSpacing: '0.08em' }}>✓ WEIGHED</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 24, height: 4, background: 'rgba(0,0,0,.35)', borderRadius: 3, overflow: 'hidden' }}>
|
||||
<div style={{ width: `${pct}%`, height: '100%', background: 'linear-gradient(90deg, var(--acc-1), #00ffb8)', borderRadius: 3, transition: 'width .18s ease-out', boxShadow: '0 0 10px rgba(0,255,184,.6)' }} />
|
||||
<div style={{ width: `${pct}%`, height: '100%', background: 'linear-gradient(90deg, var(--acc-1), var(--g-ap))', borderRadius: 3, transition: 'width .18s ease-out', boxShadow: '0 0 10px rgba(0,255,184,.6)' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user