No grade, ledger or scoring change. Push scoring untouched.
REVIEW ZERO 0.3/0.4 — THE RESOLUTION TAIL DOES NOT FIRE. The resolver is
POST /api/grading/resolve (routes/grading.js:208), and its fanout at :356-371
covers webPush, telegram and discord — but:
- share-card generation: SPEC'D-NOT-BUILT. Not in the fanout at all (grep
shareCard in grading.js = 0). shareCards/renderer.js exists with ZERO
callers, so the component is built but no step would ever invoke it.
- push notifications: BUILT-NOT-FIRING. In the fanout but gated on
webPush.configured() (VAPID). push_subscriptions = 0 rows and
user_notifications = 0 rows — nothing ever subscribed or delivered.
- Telegram result posts: BUILT-NOT-FIRING (gated on BOT_TOKEN + CHANNEL_ID).
- Discord result posts: BUILT-NOT-FIRING (gated on webhookFor('results')).
- recap (all-Final trigger): SPEC'D-NOT-BUILT. No recap file exists in src/.
AND THE WHOLE TAIL IS UNREACHABLE: nothing calls /api/grading/resolve — there is
no ESPN poller in the repo. The live settlement path is the scheduler's
settleAllOutcomes + settleAllLedgers, which fans out to opsNotify only (ops
alerts), with no user-facing output. So even the built channels have no trigger.
Per the order's own rule, ShareCard, /notifications, result posts and recap are
therefore ALL SCOPED, none shipped — no dead shells over a silent pipeline.
BUILT — /compare. Semantics (0.2): a same-market head-to-head, two players with
every row a measure BOTH sides are scored on, aligned via alignRows so the
numbers are comparable — deliberately not two disconnected graded props. Reads
the live /api/stats/player/:name?sport= aggregate. Honest-absent three ways: an
unresolved side reads NO DATA while the other still renders; a measure only one
side has renders a dash, never 0; if neither resolves the page refuses to
compare. NO VERDICT — it shows measures and says the reader draws the call.
Two pre-existing tests (vyndrPhaseE, vyndrParityQA) asserted the in-development
placeholder; both superseded rather than deleted — they now assert the stronger
properties against the real page (live fetch, no sample players, NO VERDICT,
NO DATA, "not a zero").
Floor: 316 suites / 3930 tests green (10 new), web build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
Six live untruths corrected — no grade/snapshot/scorer/pipeline touched:
1. /compare — hardcoded Jokic A+/Wembanyama A + fake VERDICT replaced with an
honest in-development state; removed from Nav + BottomTabBar (route still
resolves, never the sample). Real two-player build is later.
2. Pricing — founder Desk $34.99→$44.99 (matches lib/checkout.js), Analyst
$14.99; removed the struck $19.99/$44.99 "regular" numbers and DeskShowcase's
stale $34.99. First-100 counter is real (ClaimMeter → Stripe countFounderSeats);
no fake "first 50" desk claim added (no such counter exists).
3. FAQ "NexaPay" → Stripe (verified: live checkout is Next→Express→checkout.stripe.com).
4. FAQ + Features "Brier/CLV published from day one" removed (not surfaced yet) —
returns when real. Backend Brier compute untouched.
5. MobileEdgeBoard removed from the Slate — its edge% feed was a miscalibrated
placeholder (masked >40% as "—"); phones now show the real game cards.
6. Price triplet — never-computed model/EV now derives NO_MODEL (honest absent,
MODEL "—" / "NOT PRICED", no verdict) instead of QUARANTINE's false "we
suppressed our price / a leg is poisoned" copy. Fixes grade card + LiveHeroProp.
Full suite 3833 green, web build exit 0. Tests updated to the new honest contracts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsztNChZ7vEvSR61AuMhD1
The product argued with itself: FAB/nav said "Scan", Free tier "5 scans",
ticker "MLB slate scanned" — while the Ledger says "MY READS". Swept every
user-visible surface to READ:
- BottomTabBar FAB + Nav link: 'Scan' → 'Read'
- Pricing free tier: '5 scans to try the model' → '5 reads …'
- StatStrip: 'Awaiting next scan' → 'Awaiting next read'
- Ticker badge + snapshotService event: tag 'SCAN' → 'READ',
'slate scanned' → 'slate read' (readSportOf parses BOTH old and new so
cached ticker items dedupe cleanly through the rollover)
- upgradePitch: 'You've scanned N parlays' / 'unlimited scans' → read/reads
Internal untouched (not user-visible): /api/scan routes, scan_count column,
scanning state, DemoScan/ScanIcon, scanlines CSS, the transitional SCAN
color-map key.
tests/unit/verbLaw.test.js is the enforcement: it fails on user-visible
scan/scanned/scans copy across web/src + src/services (skips comments). Suite
270/3254 green, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phone audit: read cards were huge, only 1-2 fit per screen. Compressed the
vertical spacing — article padding 16->12, header margin 8->5, name 15->14px,
ladder-rungs margin 10->8, book/date line 12->8.
Kept the archetype showDesc: it renders INLINE (same row as the badge), so it
adds zero vertical height — dropping it wouldn't help density and would break
the ds5 design lock ("the badge shows its one-line meaning where it leads").
Locked the density in vyndrParityQA (P2-10).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phone audit: the Compare verdict read "the edge tonight tilts his way" for
Jokić vs Wembanyama — an NBA claim in July, when NBA has 0 games. The page is
sample/form data with no game resolution, so "tonight" can never be verified.
Reframed to "on current form" (the rows ARE L10 form) — always honest, in or
out of season.
Also fixed the cited dimensions: the verdict claimed "usage", but in the sample
Jokić's Usage% (29.1) is LOWER than Wemby's (31.0) — he wins scoring, boards,
and playmaking, not usage. Copy now matches the data.
Locks both P1-7 and P1-8 in vyndrParityQA.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phone audit: CONSENSUS VS MODEL bled off the right edge ('2 BO…', 'MO…') and
STARTING-pitcher lines truncated ('2.64 E…'). The M4 lock only hid the DOCUMENT
scroll (html/body overflow-x) — content still clipped inside cards. Now contained:
- .breadth-row stacks (flex-direction:column) at <430px, each field on its own
line with overflow-wrap:anywhere — no bleed.
- the game-card starting-pitcher inner spans wrap + shrink (flexWrap + minWidth:0)
so name/ERA/archetype flow onto a second line instead of clipping.
- STRENGTHENED the lock: vyndrParityQA now asserts the CONTAINMENT patterns
(breadth-row stacks, pitcher spans wrap), not just document overflow.
MLB stat pills: the game-lines grid already scrolls-within-card (<640 M1); if
the audit still shows pill clipping elsewhere, it's a follow-up targeted pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phone audit: at 390px we still rendered the full desktop 3-row header (nav +
TOP MOVES ticker + SYNC line) eating ~20% of the viewport, and its height
clipped page titles under it (MY READS tabs, HEAD TO HEAD). Implemented Design's
mobile app bar <768px:
- New MobileSyncClock (extracted from HeartbeatBar) lives in the Nav's right
cluster — wall clock rests, amber/STALE reacts off the shared freshness tier.
- <768px: the ticker row (.nav-ticker) AND the whole heartbeat bar are hidden;
only the nav row shows (logo + clock + search). main padding-top → 62px and
the Slate sticky tabs → top:60px, so nothing clips under the bar.
- Locked in vyndrParityQA (P0-4): ticker+heartbeat hidden, nav clock shown,
paddings collapsed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phone audit found LEDGER READ CARDS rendering B badges with BLUE borders + C
with AMBER right now in prod — GradePill (components/GradeCard.tsx) hardcoded the
OLD palette (rgba(74,158,255) blue-B, rgba(255,179,71) amber-C) for bg/border
while the text used the migrated token. Migrated bg/border to color-mix on the
grade token, so B renders neutral-white and C grey (matching the board).
- globals.css .grade-*-bg → token-derived color-mix (was raw blue/amber rgba).
- DELETED glow from .grade-glow-b/c/d (glow is A-tier ONLY, by law) — B/C/D keep
their token color, no text-shadow.
- Purged the last dead grade-blue #4A9EFF fallbacks (SoccerGradeResult, the
intelligence INFO dot).
- REGRESSION LOCK: vyndrParityQA fails if #4a9eff / rgba(74,158,255) reappears
anywhere in web/src, if GradePill hardcodes blue/amber rgba, or if grade-glow
B/C/D grow a text-shadow again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Structural mobile rules become failing tests (M4 'test-lock what's lockable'):
the flat EDGE BOARD shows <768px and game cards are desktop-only; the board
renders TeamChips + tier GradeBadge + sign-colored hero edge% + the ranked
opacity ramp; document never scrolls sideways at 390px. Source assertions —
they lock the RULES, not the pixels (that's the master audit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design's mobile app bar shows a wall clock, no SYNC/STALE readout. Building it
literally would drop the Ship-A staleness signal on the device most users are
on. Kev's ruling: Hybrid — the wall clock is the RESTING state (the stillness),
STALE/amber is the REACTION (the punctuation), driven by the SAME real signal
as desktop (refreshed_at vs expected_interval_s, thresholds 1.5×/3×). Never
silently stale on mobile. Design drew the happy path; we keep the failure state.
- web/src/lib/freshness.js — extracted the freshness tier as ONE shared source
(CommonJS, unit-tested); desktop HeartbeatBar + mobile clock both key off it,
so mobile can't silently disagree with desktop about staleness.
- LiveLayer: <768px hides SIGNAL LIVE + EKG + graded + the SYNC label and shows
a single right-aligned clock — ticking wall clock when calm, amber SYNC / red
STALE when the tier reacts. Resting dot is static (the clock is the pulse).
- Test-lock: freshness.test.js (tier thresholds, absent≠false-stale, no negative
age) + vyndrParityQA (mobile clock wired, one shared freshness source).
Built to Design's mobile spec, VISUALLY UNVERIFIED at 390px. NEXT shell
increments (still M1b): merge the clock into the logo row (Design's single-row
app bar), the breadth strip (EDGES/AVG CLV/GAMES — replaces the graded count
mobile lost here), and relocate the wire to the bottom of the board.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SETUP (required first step): overwrote specs/design-reference/ with the CURRENT
authoritative mockup ("Vyndr System.dc.html" from claude.ai/design, the version
with the combat card / pitcher identity / live grade-shift / correlation builder
/ FREE|PRO pricing / TRANSMISSION QUIET). 12 surfaces. Confirmed it has NO media
queries — desktop-only, so the 390px expression is a deliberate design decision,
not a shrink.
M1a — mobile foundation (built to spec, VISUALLY UNVERIFIED at 390px; WSL2↔Chrome
unreachable, the Chrome audit is the eyes):
- Header-zone collapse (the concrete audit finding: ticker + SIGNAL LIVE + STALE
stacking in ~110px). The decorative EKG is dropped <768px so the heartbeat
reads as ONE clean status line; signal pulse + ticker are the single animated
element (DESIGN-SPEC §4). LiveLayer gains .heartbeat-bar / .hb-ekg hooks.
- Primary CTA (.vbtn) meets the 44px touch target on mobile; dense `small`
buttons opt out (density is a feature).
- .m-hero mono-hero clamp for the one-figure-per-card grammar at 390px.
- M4 test-lock: vyndrParityQA asserts the header collapse, tap target, overflow
containment, and the honest "UNVERIFIED at 390px" label are all in source.
Per-surface stacking (M1b), billboards (M2), desktop parity (M3) continue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Semantics preserved (step-up amber / step-down green); DS4 moved the
coloring into a sev object, so the source-lock regex is updated to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>
Work-order Phase 0 (Jul 10 live audit): every fabricated UI element deleted
or rewired to real data. Deletion sprint — no new product features.
0.1 Fake NBA game: root cause was scheduleService fetching the ESPN
scoreboard with no ?dates= param or date filter — off-season ESPN
returns the NEAREST slate (Jun 13 NYK@SA Finals rendered as tonight).
Now pinned to the requested ET date + defensive filter; undated events
dropped. Honest month-aware per-sport empty states (lib/emptyState.js).
0.2 Fake header counters: liveTick stripped to a bare 1s pulse (the
auto-incrementing "247 graded", sin-driven brain-%, aPlus/cascades are
dead). New GET /api/snapshot/summary (cache-only, before /:sport) +
Next proxy; HeartbeatBar shows the real graded count and SYNC =
elapsed since the last pipeline run (amber past 5 min).
0.3 Ticker: hardcoded fallback items deleted (real snapshot exhaust only);
MOVE kept — computeLineDeltas is real movement. <4 real items → no
ticker; bar publishes --ticker-h so the fixed header collapses cleanly.
0.4 /terminal retired: route redirects to /dashboard; VVI/injury-wire/
leaders layouts preserved unrouted as §12 content-engine templates.
Nav PRIMARY = Slate/Scan/Ledger; BottomTabBar Terminal→Explore; PWA
shortcut Terminal→Ledger; #terminal alias → /dashboard.
0.5 ›Query nav pill deleted (duplicate /scan link).
Backend 2289 → 2309 tests (199 suites), web build exit 0.
Spec: specs/phase-0-kill-the-lies.md. Next: work-order Phase 1 (ledger
persistence + settlement).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>