DELTA MEASURED on live prod grades (live ordering unchanged): MLB 7/8
props move (87.5%), mean 2.5 places, TOP READ CHANGES (corey seager hits
1.5 under -> jake burger hits 0.5 over). WNBA 25/25 move, mean 4.1, max 12.
This is a large re-ordering, not a tweak.
Caveat recorded rather than buried: MLB had only 8 graded props at
measurement time. The percentages are real; the sample is one small slate.
Re-run before the flip -- it is one call.
PER-SPORT DOCTRINE ENFORCED IN CODE. WNBA moves the most and must NOT
adopt this: its p_win is anti-predictive, so ranking that board by p_win
would sort it by a signal measured to point the WRONG WAY -- worse than
the incumbent, not better. A comment would not have stopped a future flip
from going global, so FORECAST_RANKED_SPORTS = Set(['mlb']) gates the
forecast_rank stamp, with tests asserting no sport inherits MLB's result.
A sport joins only by passing its own holdout.
EDGE IS NOW DIAGNOSTIC-ONLY IN DISPLAY. MobileEdgeBoard.EdgeCell rendered
green (--g-a) for positive edge and red (--miss) for negative. Two things
were wrong: green/red IS a quality claim on a quantity that does not
predict, and ROW-GRAMMAR reserves red for settled-negative ONLY -- a
negative diagnostic is not a settled loss. Now neutral mono with a
diagnostic tooltip; header reads "MKT GAP · DIAGNOSTIC". The number is
still shown -- no display went blank. DeskShowcase neutralised likewise.
PINNACLE LOGGED, NOT ENSHRINED. Per the order, "market-not-sharp" is
PENDING-RECOVERY rather than a confirmed permanent limitation. The single
question for PropLine is in BLOCKERS.md with its evidence, and MASTER-PLAN
now carries the pending status instead of the permanent claim.
Live sorts remain byte-identical: selectTopGrades, flattenToEdgeBoard and
topGradedService all still call the incumbent.
Gates: 4,041 tests / 323 suites green; next build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
Additive frontend/visual. Backend untouched (git diff src/ = empty): no grade,
model, classifier or ledger change. The 41->74 registry expansion is HELD for
D1-B. Push scoring untouched.
REVIEW ZERO — classifier coverage bounded the glyph wiring. Three buckets, and
the computation was redone three times before it was right (the frontend keys
GLYPHS by ARCHETYPE NAME while the backend keys `glyph:` by SHAPE NAME, and most
registry keys are unquoted identifiers — the first two passes mis-parsed both):
(a) classifier-backed, already wired: 38
(b) classifier-backed, package SVG exists, NOT wired -> WIRED HERE: 6
striker, grappler, pressure, counter, grinder, finisher — all MMA/combat
archetypes in archetypeService.js that were rendering EMOJI fallbacks
('*', 'x', '>', '<>') where the package ships real 24-grid duotone marks.
(c) package SVG with no classifier -> HELD for D1-B: 39 (wiring them would
render nothing)
(d) classifier-backed but NO package SVG: 2 ('dual threat', 'paint boss') —
a DESIGN gap, not a build gap; flagged for D1-B.
GLYPHS map 38 -> 44 keys, deliberately far short of the package's 83.
BOUNDARY CHANNEL — the blue tokens already existed (--priced-out set) and were
applied on NoMarketState and the scan void box, but PriceTriplet's NO_MODEL
("line not priced") still rendered in neutral text, so the channel was applied
inconsistently. NO_MODEL now renders in the channel, completing "every boundary
state or none". Token-only (no hex fallback and no hex in prose — PriceTriplet's
own test forbids literal hex, and it caught both).
REACTION PRIMITIVES — new web/src/lib/reactions.js + globals.css keyframes at the
exact HANDOFF timings: flash .75s ease-out, boot stagger 60ms steps, reactions
gated 1.5s, WIRE hold 6s. nudge() REFUSES a no-op (null/absent direction -> no
flash) so the primitive cannot be attached to an idle loop — a flash without a
new datum is the UI lying about the feed. Reduced-motion honoured.
READ-FAB — aligned to the exact package geometry: 50px circle, translateY(-14px),
6px void ring (was 46px, marginTop -16, 3px ring).
CARD TOKEN — audit correction: #0E0E14 was already tokenised as --bg-1/--card;
the audit's "1 file" was counting the raw hex, not the token. No change needed.
Floor: 317 suites / 3946 tests green (16 new), web build exit 0.
NOT DONE THIS ORDER (reported, not silently dropped): row-hover rationale and
IntersectionObserver reveal (Phase 3 item 8) and team-gradient chips (Phase 4
item 10) are not implemented — they need the System artboard's row anatomy,
which is a larger port than the rest of D1-A.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
Wiring + one copy pass. No grade, ledger, model or scoring change (diff empty
across intelligence/, ledgerService, outcomeService, gradeSlateService).
REVIEW ZERO — each surface proven with real data BEFORE wiring:
0.1 /intelligence vs /system are NOT duplicates. System.dc.html is a
multi-surface artboard (TERMINAL + INTELLIGENCE + WIRE sections), not the
design for a distinct /system route; its INTELLIGENCE section is already
realised as the live app/intelligence/page.tsx. No /system page exists and
none should be built as a second copy — the prod 404 is correct.
0.2 /intelligence renders live and gates SERVER-side, not by blur: the proxy
requires auth and limits by tier (desk 50 signals / non-desk 8), and
returns 401 to an anonymous caller (verified live). No leak.
0.3 /slip parses a real DraftKings slip end to end: 3/3 legs,
needs_review false, Aaron Judge total_bases over 1.5 @ -115. Honest limit
recorded: parsers are layout-rigid, an unsupported layout yields ZERO legs
rather than wrong ones (never-guess), so real-world OCR hit-rate across
layouts is CANNOT DETERMINE until user slips arrive.
0.4 /parlay direct route hits the real correlation builder on the same
ParlayContext the drawer uses.
0.5 /marketplace advertised four unbuilt things but made NO performance or
profit claim, and its capture was already real (/api/waitlist upserts to a
waitlist table). The gap was tense, not fabrication.
WIRED: Nav MORE gains Intelligence, Slip Reader and Marketplace; Parlay Lab
re-pointed from the drawer hash to /parlay (the drawer is unaffected —
ParlayPanel stays mounted with its floating badge).
GATING: /intelligence added to GATED_ROUTES because its feed 401s signed-out, so
an ungated link would land visitors on a permanently empty page. /parlay stays
OPEN deliberately — it is the free parlay funnel and gating it would be a
monetization regression.
/marketplace honesty pass: every item body now opens "Not built yet." /
"Not written yet." / "Not produced yet." with what is planned; the subhead states
it is not a purchase, not a pre-order and not a promise of a ship date; the
playbook item carries "No profit claim, no promised return". The capture stays
real — no fake button. Unit-locked.
Floor: 315 suites / 3920 tests green (12 new), web build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
THE PROMOTION WAS NOT PERFORMED. Champion grade path byte-identical (diff empty
across intelligence/, gradeSlateService, snapshotService). Projection, p_win and
the CLV instrument untouched.
REVIEW ZERO IS A GATE AND THREE OF FOUR PREREQUISITES FAIL:
0.1 scores are ESTIMATED priors from the founding spec, not measured. The
premise's cited values are not in the code either — the module holds
nba:points .80 and mlb:total_bases .55; there is no NBA 0.72 and no WNBA
score at all.
0.2 VERSION-BOUNDARY TAG DID NOT LAND — config/modelEras.js has zero shading
references. It was deliberately not applied twice (nothing had been
promoted) and reported both times. The order's own rule says STOP.
0.3 NO ROLLBACK FLAG EXISTS — zero occurrences of SHADING_ENABLED /
EDGE_SHADING / shadingEnabled anywhere in src/.
0.4 takeable tags DID land (migration 034, 1246/1254 rows). PASS.
AND THE APPROVED DELTA DOES NOT MATCH THE MEASURED ONE. Approved: 43.6% of
grades re-letter, efficient markets tighten and soft hold. Measured on all 1250
live rows: 97.4% change (1217), 79.8% move UP, 17.6% down, resulting in 79.0%
A-family (MLB 93.4%) against the champion's 0.2%. And rows_actually_shaded = 0
of 1250 — 96.5% of markets are unscored (f=1) and the one scored market present
is the anchor (f=1.0 by construction). The entire re-letter comes from switching
to edge-vs-fixed-bar grading, NOT from efficiency shading, which is inert on
this board. That is an unapproved grading-basis change riding along, which the
order's own "no new scaling changes riding along" guardrail forbids.
Flipping would re-letter 97.4% of an append-only public record, move 79.8% of
grades UP and mint A's on 79% of the board, on a letter whose measured
correlation with outcomes is r ~ 0.005 — the exact scenario the permanent
founder ruling forbids.
SHIPPED — ORDER B (independent of the promotion, and a live falsehood):
edge_pct display retired from GradeResultCard (confidence strip, EDGE stat cell
now honest-absent, alt-ladder rung) and SoccerGradeResult. DeskShowcase kept
(already honest). Computation and the board's signed-edge sort fallback SURVIVE
— deleting them would re-break the sort fixed on 2026-07-29; a test asserts all
three survive and the sort still orders agrees -> disagrees -> absent.
Fixed two build-breakers the retirement caused (orphaned edgeColor import,
orphaned edge_pct destructure; edge_pct stays on the props contract). Two
pre-existing tests superseded rather than deleted: they asserted the edge figure
is sign-coloured, and now assert the stronger property that no edge percentage
renders at all.
Floor: 314 suites / 3908 tests green (9 new), web build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
Review Zero found the hero's ACTUAL behavior was worse than "unknown": it ranks
on ev_pct (heroPropService v2), but ev_pct is NULL on served grades and
Number(null)===0 made Number.isFinite(Number(null)) TRUE — so every prop tied at
EV 0 and the "top read" was really the FIRST takeable A/B prop in cache order
(arbitrary, dressed as ranked).
v3: rank by the CHAMPION's p_win (the only signal with a promising, not proven,
edge — its takeable-MLB-over CLV survived the skew audit) among A/B, TAKEABLE-
priced reads (isTakeable band -160..+200, same as the proof/audit). Strict
null guard kills the Number(null)=0 bug. Takeable filter is mandatory (raw p_win
crowns -300 chalk). NO backfill: nothing qualifies → honest empty state
(available:false, reason:'no_qualifying_read'), never a weak recent read.
p_win is RANKING-ONLY, server-side — toHero never exposes it and the route strips
it. Framing unchanged in substance (model number vs book number, grade,
timestamp) — no proven-edge / +EV / best-bet claim, no CLV/ROI/edge number.
Display-only: reads snapshot caches, writes to nothing (no grade/ledger/lock_lines).
Full suite 3852 green, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsztNChZ7vEvSR61AuMhD1
BookComparison.tsx was built but UNROUTED (dead). Route it to the GradeResultCard
via a new self-fetching BookComparisonPanel that reads the live /api/books feed
(source:'bookprices' — the snapshot-locked, fenced, byte-identical store).
Contract fix (Review Zero 0.1): books frequently sit at DIFFERENT lines (WNBA DK
21.5 / FD 18.5; MLB 2/3), so BookComparison now renders EACH book's own line
per-row — never one shared header line implying a false same-number comparison.
Honest states: single-book (the common case for MLB) → one book, "One book
posting this prop.", NO crown/second row; multi-book → all books' own line+price,
NONE crowned (BOOK_CROWN_ENABLED=false — no best-price claim, verified live
crowned:false); no books → renders NULL (panel self-hides), never a placeholder.
No regression: only the always-empty inline d.books section was replaced; grade,
projection, PropLine line, and PriceTriplet price are untouched (wiring test
asserts them). Freshness (0.4): bookprices is written in the SAME snapshot that
locks the grade (intraday refresh touches neither gradedAt.line nor bookprices) —
same fresh, no stale-label needed. Web-only → grade byte-identical trivially.
Full suite 3851 green, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsztNChZ7vEvSR61AuMhD1
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
Semantic COLOR fix, not cosmetic. The S6/S7 build predated the current Scanner
States spec: it used AMBER (the quarantine / model-suppressed channel) for the
"no market / line not priced" case, telling users "model suppressed" when the
truth is "the board never priced this." Corrected to the HANDOFF Session-3
blue-boundary law: BLUE (--priced-out #8FB2DE) = no-market boundary; amber stays
QUARANTINE; red stays REFUSAL.
Phase 1 (reskin): NoMarketState → dashed BLUE void box + blue header/copy; the
S7 rows → spec format (o 27.5 · BK −114 · ◆ −105 · OPEN READ ▸) at 44px,
390-legible. Input-area surfacer pills reskinned to the blue channel too.
Phase 2 (never-built states, only those Phase 0 confirmed against live data):
- GREEN CTA with LIVE player count ("PLAYER · N PRICED PROPS ▸"), degrading
honestly to the board path ("N PROPS LIVE · TONIGHT'S BOARD ▸") at 0 — count
from the SAME fresh index as the rows (pricedCountForPlayer), can't disagree.
- CASE A none-priced DEFAULT: "WE PRICE THESE FOR [player]" — the player's other
priced stats (pricedStatsForPlayer, filter by nameKey).
- Typed-line-mismatch blue fact line ("o X ISN'T PRICED · NEAREST ↓").
FLAGGED / not built (no shells): Case C off-slate quiet-stop needs schedule/
roster membership the pricedLines index doesn't carry (out of the presentation
fence). Spec CONTRADICTION: Case B says "fair previews amber," but the law
reserves amber for quarantine — fair renders NEUTRAL ◆ (blue-dim), not amber, to
avoid blurring the channel.
Free-tier gate VERIFIED before rendering FAIR: fair_odds is the de-vigged MARKET
price (valueState: "never hide the honest fair number"), NOT the gated
model_odds — no paid leak. Carried through indexPricedLines (additive; keying/
refresh/onPick/stale-tap all unchanged — the proven S7 data path is untouched).
Scan A byte-identical; PRICED_NUDGE_ENABLED still the kill switch; reversible.
Build exit 0; priced + parity suites green (67).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
Migrate the Landing hero (Landing-only component; LiveHeroProp/triplet
untouched) to the design bundle's visual language: mono "SPORTS
INTELLIGENCE TERMINAL" eyebrow, tightened 56px/800 headline "Every line,
graded before you bet it.", dual mono CTAs (OPEN THE TERMINAL / SEE THE
PUBLIC RECORD), proof chips.
Claims audited against the held list — the bundle's marketing copy carries
claims we have not earned; those did NOT ship:
- "CLV-VERIFIED" chip + "verified against closing lines" — HELD (C4 broken)
- "312 props / 47 games" count — fabricated demo → ABSENT (no count)
Shipped copy is earned only: pre-graded, edge-ranked, settled in public,
misses included; PUBLIC LEDGER / MISSES INCLUDED / 5 FREE READS chips.
HELD + FLAGGED for a design pass (fabrication-backed, no honest designed
state — check-don't-freelance): the edge-board demo (A+/A don't emit), the
"grades calibrate · A+ hit 80%" claim (calibration unmeasured), and the
tier-record band (real data is B/C-only, no ROI). Not built with fake data.
Tokens via var(--g-a/--void/--border/…) with bundle-hex fallbacks
(documented-intentional pattern). Build exit 0; parity QA green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
The Session-78 diagnosis stands: the join works, and a marketless scan rightly
shows no triplet. This makes that absence legible and points the user at what IS
priced, without fabricating a market.
PHASE 0 gate — design-check, reachability, timing, all clear. Design-check: the
bundle has the triplet's own REFUSAL language ("we'd rather show nothing than a
number we can't stand behind") as the honesty precedent, and a designed
EmptyState component whose actions give a path forward — so the empty state is
built in the established visual language, not freelanced. Reachability: the
scanner already fetches games/odds/search per selection; the snapshot is one
more public, 30s-cached fetch per sport, re-run when the sport changes.
Staleness: the snapshot rotates 5x/day and every scan re-validates the market
server-side at submit time, so a surfaced line that goes stale degrades to the
empty state on tap rather than a vanishing triplet — the stale-tap guard is
inherent, not bolted on.
Reversibility was the design constraint. The working card, price triplet, grade
adapter, valueState and the scan route are BYTE-IDENTICAL — a test asserts none
of them even reference the new empty state. Everything new lives in two added
files (lib/pricedLines.js, components/vyndr/NoMarketState.tsx) and additive
blocks in the scan page. Removing them leaves the Scan-A path untouched.
Non-fabricating by construction: indexPricedLines only keeps snapshot rows that
carry a real book price, keyed by exact player+stat via nameKey. A different
stat priced for the same player surfaces nothing for the picked stat; an
off-slate player surfaces nothing; nothing is suggested, interpolated, or
rounded to a nearest line. The empty state shows no market numbers of its own —
only real priced lines as one-tap chips, or a link to the live board when there
are none.
Framing is help, not restriction: a "PRICED TONIGHT" chip row sits under the
free-typed line input, and the scanner still accepts any player, stat and line.
Tapping a chip pre-fills the priced line and re-scans it — the market is
re-resolved server-side, so the tap either yields a real triplet or degrades to
the honest empty state.
Path forward, not a wall: a marketless scan no longer dead-ends in blank space.
It states truthfully that the board didn't price that line, keeps the grade, and
routes the user to the priced lines for that exact player+stat or to tonight's
board.
Tests 3760 passed / 303 suites, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
Live induction on the deployed landing page caught this; markup review and the
unit suite both passed it. With the model leg stripped for anonymous visitors,
LiveHeroProp forwarded book/fair/model/ev/quarantine to PriceTriplet but NOT
model_price_locked, so deriveValueState fell through to the missing-model-price
branch and the card rendered "MODEL READ WITHHELD" — the quarantine state,
whose copy says we suppressed our own price because a leg is poisoned. Nothing
was poisoned. The real reason was the paywall, and the two must never share a
face: one says our data is untrustworthy, the other says you don't have this
tier.
Now forwarded, with a test asserting both the separation in deriveValueState
and the forwarding at the call site.
Tests 3559 passed / 291 suites, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
PHASE 0 finding, reported before building: the token layer this order asked
me to establish ALREADY EXISTS and already matches HANDOFF.md exactly.
web/src/app/globals.css :root carries the design's surfaces, borders, text
ramp, fonts and grade colours byte-for-byte (aligned 2026-07-16), and
lib/colorContract.js already encodes the green-is-edge-only and
glow-is-A-tier-only laws with a test enforcing them. The stack is Tailwind v4
CSS-first (no config file) with components styled by inline style={{}} reading
var(--x) — 1,916 such reads — so CSS custom properties are the only vehicle
the stack natively consumes. Creating a second parallel layer would have meant
two competing sources of truth, so this EXTENDS the existing one.
PHASE 1 — additive only. globals.css gains one colour the system did not have,
the priced-out blue (#8fb2de + tints), plus a tokenized A-tier glow and the
fair-leg tints. The block writes the LAWS into the token layer itself — green
= takeable edge only, glow = A-tier only, amber = caution + the fair leg, red
= miss/negative only, blue = edge priced out, JetBrains Mono = all data — and
a test asserts every newly-declared name is new (zero collisions, zero
overrides). No existing hardcoded style was touched and no live surface was
migrated: the diff over existing files is 149 insertions, 0 deletions.
PHASE 2 — lib/valueState.js is the single verdict function; the component
renders what it returns and never re-derives one. VALUE fires only on
ev >= 2 AND a takeable price, mirroring src/config/valueEngine.js with a test
that cross-checks both files and fails on drift. Five states: VALUE (green),
EDGE-NOT-TAKEABLE (blue), NO EDGE (grey, stated at full voice), QUARANTINE
(model leg withheld, book+fair stand), REFUSAL (nothing rendered). Free tier
is gated at the wire — tierGating strips model_odds and sets
model_price_locked, so the lock is real rather than a blur over data already
sent; book and fair pass through on every tier because the fair leg is never
the paywall. Wired into the landing hero (data was already on /api/hero-prop)
and the read card, where the projection block reads first and the triplet sits
beside it, not in place of it. Ledger and public profile are out of scope —
no fair-odds columns exist there.
PHASE 3 — induced all six states in a real browser and read back computed
styles, not just markup. Green resolves on VALUE alone: rgb(0,212,160) on the
model leg and verdict; the +11.7%-EV-at-210 row renders rgb(143,178,222) blue
and a white model leg; quarantine shows MODEL "—" with book and fair intact;
refusal renders no legs at all; free tier renders a lock bar with book -120 and
fair -104 still honest. Landing hero on live data: book -153, fair -129, model
-343, VALUE +21.1% vs fair at +28.1% EV. At a real 390px column the three legs
hold at 117px each with no horizontal overflow and fair no smaller than its
neighbours.
Induction caught a real bug that markup review would not have: the "VS FAIR"
figure compared BOOK to fair, printing "VALUE · -6.5% VS FAIR" — a
contradiction on screen. The design's own two worked examples pin the formula
as MODEL minus FAIR in implied-probability percentage points; modelVsFair now
reproduces both exactly (+2.9 and -1.8) and a test locks them. The figure is
shown only when its sign agrees with the verdict, so a row that clears the EV
bar on the book price while our price sits level with fair leads with the EV
instead of a number that reads as a contradiction.
Tests 3539 passed / 290 suites, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
TRUTH FIX FIRST. row.clv/clv_result derive from the overwritable
closing_line — 637/699 rows had closing == locked — so of the 578 rendered
chips, 522 "flat"s encoded a CAPTURE FAILURE as a held line. That number
was live on FIVE surfaces, not the one the order assumed:
1. ledger card ClvChip@319 (authed)
2. public profile /u/ duplicate chip@260 (PUBLIC, shareable)
3. dashboard "· CLV BEAT"@508 (authed)
4. board / Slate "CLV BEAT/FADED"@422 (FREE surface)
5. board / Slate "✓ HIT · CLV BEAT"@517 (FREE surface)
Removing it from the ledger alone would have left the lie live on three
surfaces including two public ones, defeating the stated PRIMARY GOAL, so
the removal covers all five. That is a deliberate extension beyond the
"scoped to ledger card" guardrail and is flagged as such — the guardrail
protected against feature creep, and this is the same defect at four more
addresses.
DIRECTIONAL BADGE installed in the old ledger slot. Three time states now
read distinctly on the row: entry (locked_odds, at-grade) · close (badge,
at-close) · outcome (hit/miss, final). The RESULT stays the row hero.
The PUBLIC profile deliberately gets NO badge — it never receives dclv
data (Analyst+Desk, server-gated), so that surface now shows the settled
result alone.
HONEST ABSENCE, tested: the 578 formerly-chipped rows now render NOTHING —
not a "flat", which is the old chip's lie in subtler form. Rows settled
before capture existed will never get dclv, and permanent silence is the
correct output. All six states verified in place; a 40-row dense ledger
stays legible (27 badges, max 40 chars, one line, consistent slot after
OutcomeChip); no CLV sort or filter exists.
FIELD REMOVAL DEFERRED as a separate scoped cleanup: /api/ledger/mine and
/api/profiles still SEND clv/clv_result, and dashboard/Slate still type
them. Display removal is local and safe; stripping the fields mid-swap
could break a response consumer.
Suite 289/3485 green, build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
CARD BADGE ONLY. Ticker-CLV explicitly DEFERRED (named, not lost).
PHASE 1 — SERVER-SIDE GATE AT THE DATA LAYER. A Free request never
RECEIVES dclv data: the CLV columns are appended to the SELECT only behind
canAccess(tier,'clv_badge') (new capability, analyst+desk), and responses
are ALSO stripped as defence in depth so a future SELECT change cannot
quietly leak. No CSS/client gate — data that reaches the browser has left
the building. dclv_fair_lock/fair_close are de-vig internals and are never
sent at all.
SURFACE AUDIT, all six channels, each test-locked to contain no CLV:
public profile (share link), snapshot/card feed, ticker feed, share
card/OG, embeddable widget, newsletter. A test also asserts no
ledger_entries read uses select('*') — a star would auto-leak every new
column, which is exactly how a gate becomes theatre.
PHASE 2 — IMMUTABLE ONCE COMPUTED. A settle can re-run (stat correction,
protested game) and a badge that flips positive->negative AFTER a user saw
or screenshotted it is a credibility failure. First computation wins: dclv
is only computed when dclv_computed_at is null, so a re-settle can never
rewrite a shown badge. Same discipline as the locked grade.
PHASE 3 — RENDER, test-first, ABSENCE IS HONEST. unknown / flat / null /
missing-receipt all render NOTHING — no element, no placeholder, no
"pending". Proven on an ALL-NULL board (today: 0 badges) and a MIXED board
(tomorrow: 1 of 4 badged, badge-less cards clean). Binary states only:
positive -> MOVED TOWARD US "graded -110 · closed -145"
negative -> MOVED AWAY "graded -110 · closed +120"
The RECEIPT is the persuasive part, so a badge with no numbers is
suppressed rather than shown as a bare claim. Negative is neutral context
and NEVER touches the locked grade — no back-door re-grading.
NO aggregate, count or rollup exists by construction: the module exports
exactly {clvBadge, fmtPrice} and a badge payload carries exactly
{tone,label,receipt} — asserted by test, because an on-screen tally would
be the held aggregate claim through the side door.
Build gotcha hit and fixed: clvBadge is CommonJS (allowJs) with no TS
types, so the .tsx needed an explicit cast at the call site — the build
worker exits 1 on type errors even though compilation "succeeds".
Suite 288/3473 green, build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
1+2. Checkout price was CODE-gated (founder price only with a valid founder
code) — so "Claim a Founder Desk" would have charged the $44.99 standard
price, not the advertised $34.99. Now it's SEAT-gated: resolveCheckoutPrice()
attaches the founder price while founder seats remain (< FOUNDER_SEATS_TOTAL,
read from the SAME countFounderSeats() truth as the ClaimMeter), and flips to
standard at seat 100. createCheckoutSession uses it; the founderCode param is
kept for back-compat but no longer drives price. The meter flips to "SOLD
OUT" at capacity. When the count can't be verified we honor the advertised
founder price (never overcharge).
- Also hardened countFounderSeats to manual pagination (the for-await form
broke on non-async-iterable list mocks).
3. Tests: resolveCheckoutPrice at seat 0 → founder, seat 100 → standard, the
99/100 boundary, null-count → advertised founder price.
4. Grace: invoice.payment_failed now sets a 14-DAY grace (spans Stripe's Smart
Retry window) instead of 48h — a transient decline no longer revokes access
mid-retry. Access is revoked only when Stripe actually cancels
(customer.subscription.deleted keeps its 48h grace). Test updated.
Stripe + founders suites green, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The landing hero was a static Jokic "Example" card with a name-length pick and a
hardcoded A- 73% +6.2% fallback. Now it's deterministic and live:
- heroPropService.pickHeroProp reads the pre-graded snapshot and selects the
prop with the LARGEST |projection - line| gap among A/B grades (conviction,
not noise) — the read where VYNDR disagrees most with the market, the card
that makes a stranger argue. No curation, no grading (reads cache → no API
credits). GET /api/hero-prop (backend) + repointed Next proxy.
- The card shows the disagreement EXPLICITLY: the book's line vs VYNDR's model,
side by side (model in green), with the real grade timestamp ("Graded 2:14
PM"). The EXAMPLE chip is gone.
- Empty slate → the MOST RECENT real graded read (flagged "LATEST READ", real
date). Nothing cached → { available:false } and the card HIDES. No
hand-written fallback — the Jokic card is deleted. Survives a dead night: a
live rule shows tonight's real MLB read, never a phantom July NBA card.
7 service tests lock the rule (max-gap, A/B gate, projection/line required,
empty→recent, hidden, cross-sport). colorContract updated to the new
disagreement display. Change-affected suites green, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ClaimMeter rendered a fabricated "47 / 100 CLAIMED" (a hardcoded default; the
comment even said "Cosmetic conversion driver… Static here"). Now:
- GET /api/founders/count counts ONLY real paying founders — user_profiles
where founder_pricing = true AND subscription_status = 'active' (the
Stripe-webhook-synced mirror, so we never hammer the Stripe API). Cached 5
min in Redis on top of that.
- If the source is unavailable (Supabase unconfigured, query error, column not
migrated, client throws) the endpoint returns { available: false } and the
ClaimMeter renders NOTHING — counter and progress bar both hidden. We never
fall back to a number.
- A low real count is shown honestly (0 → "0 / 100"); the truth is the feature.
Next proxy at app/api/founders/count. 6 route tests cover real count, low
count, error/unconfigured/throw → hidden, and cache-hit. Suite 271/3260 green,
web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
DISPLAY FIX (shipped): the league leaderboard rendered raw snake_case
("stolen_bases U0.5", "earned_runs U2.5"). New canonical short-label lib
web/src/lib/statAbbrev.js (one source, CommonJS + unit-tested) maps stat_type
to SB/ER/TB/HR/K/PTS/… and ExploreHub routes through it. Unknown ids upper-case
their words so raw snake_case can never leak again.
FLAG (reported, NOT silently changed — per the audit's instruction): the "B at
45% confidence" is a BACKEND grading issue, diagnosed against live snapshot:
- 25/25 grades mismatch their own confidence vs grade_thresholds.json (B shown
at conf 55 = the B- band; a systematic one-sub-tier gap on every prop). The
surfaced `confidence` is not the probability that derived the letter (likely
the data-sufficiency penalty applied to display-only).
- 9/25 have projection=0 — the MLB feature path feeds 0 instead of refusing
(S58 insufficient_data), which also produces the P1-7 broken edge_pct.
Full write-up + do-not list: specs/audit-data/mlb-grade-degradation.md. NOT
re-lettering or shifting thresholds on the frontend — that would hide the bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phone audit called the board 'mostly-empty'. Two causes, both now addressed:
1. Dead images (P0-2, already fixed) → the matchup/team chips render logos now.
2. Degraded edge data. Live snapshot edge_pct is on a broken scale (distinct
values 20/60/100/140 — not a market %), with projection=0 and confidence
35-55%. A real prop-market edge is single-digit, never past ~40%. Leading
the board with '+140%' fabricates a signal (Data Semantics Rule).
Fix: an edge whose |value| > 40 is treated as ABSENT at BOTH layers — the
data layer (flattenToEdgeBoard nulls it, so it can't RANK a fake +140% above a
real +8.4%) and the display (EdgeCell shows '—'). Board falls through to the
grade-rank tiebreak when edges are unreliable. Real edges (≤40) are untouched.
The root cause — edge_pct/projection/confidence degradation — is a BACKEND
grading issue (same family as the P2-9 '45% B' flag), reported separately; this
is the honest frontend guard, not a fix for the data.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phone audit: the same screen showed green 'SIGNAL LIVE · UPDATED 0s ago' AND
amber 'SYNC 46:03' — two components reading different fields. The Slate's
'UPDATED Xs ago' measured the CLIENT poll time (always ~0s, since it refetches
every 60s), while the app-bar clock honestly measured the pipeline's data age
(refreshed_at). '0s ago' claimed a freshness the data didn't have.
Now the Slate captures snap.refreshed_at from the snapshot it already fetches
(newest across sports) and 'UPDATED' shows THAT — the same source of truth as
the clock. The clock still owns the amber/STALE reaction; the strip just states
the honest data age. No more contradiction.
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>
Completes P0-3 across all three surfaces:
- CONSENSUS VS MODEL (MarketBreadth): dedupe by player+market so Ben Williamson's
alt-line variants show as ONE consensus row (no per-market cap — a consensus
table just shouldn't repeat a player).
- LEDGER cards: group by player+market via groupIntoLadders — Alec Bohm's
strikeout ladder (U1.6/U1.3/O1.5) is now ONE card with the rungs nested (each
its own side/line + tier-colored grade), not three separate cards.
- playerGrouping reads player OR player_name (ledger rows use player_name) —
regression-tested so the ledger doesn't silently empty.
The Alt Line Ladder shape is what /pricing already demos; the record now uses it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phone audit: leaderboard flooded with 9 consecutive identical 'stolen_bases U0.5
45% B' rows. New shared lib/playerGrouping (dedupeLeaders + groupIntoLadders,
name-key aware, 7 unit tests): ONE row per (player, market family) keeping the
best-ranked, then a per-market cap (4) so no single prop type floods the board.
Applied to ExploreHub. Ledger cards + Consensus grouping follow in P0-3b/c using
the same lib.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phone audit: 78 ESPN <img> at naturalWidth:0, ZERO espncdn/mlbstatic requests
ever fire — the browser never attempts the fetch. CSP was NOT the cause (img-src
already allows a.espncdn.com + img.mlbstatic.com, confirmed in the live header).
Root cause = native loading="lazy" on the tiny entity <img>s never triggering on
device (the audit's suspect b). TeamLogo + PlayerAvatar now load="eager"
(+ decoding="async") — they're 10-40px core-visual entities, so eager is correct
and cheap, and it forces the fetch on parse regardless of size/viewport.
Browser-request confirmation is the audit's Chrome domain (I can't observe
network from server curls); eager semantics guarantee the fetch once the src
(which 200s server-side) is in the DOM.
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>
Design colors every LIVE indicator green (active = green; the live-dots and
LIVE·Q3 labels are all green). The slate header's 'N LIVE' count was red
(--live #ff3b3b) — contradicts the one-meaning-per-color contract (red = miss/
error, which live isn't). Aligned to --g-a.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PitcherArsenal's meta line showed team + 'vs OPP' as plain text; Rev 3 anchors
the pitcher's team and opponent with TeamChips (real logo / monogram). Combat
FightCard intentionally keeps fighter monograms (no photos — likeness rule).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The one genuinely-new mobile screen. Design's mobile BOARD is a FLAT edge-ranked
list (all graded props across every game on one list, sorted by edge) — not the
desktop's game-grouped cards. Implemented to the drawing with REAL snapshot data:
- slateAdapter.flattenToEdgeBoard(cards) — pure transform of the assembled
GameCardData[] (grade→game join already done) into ranked rows, edge desc.
STRICT null edge sorts LAST (never 0-coerced to the top — Data Semantics Rule).
Threaded edge_pct through buildPlayerStripsFromProps (was dropped). 6 unit tests.
- MobileEdgeBoard component — Design's exact screen-01 rows: rank (green #1),
player + prop, matchup sub-line with TeamChips + live-dot, tier grade chip,
and the edge% as the one bold mono hero (green +, red −). Ranked opacity ramp
(1 → .55) + green inset border on the top reads. Breadth strip EDGES/AVG CLV/
GAMES — CLV honest '—' (per-slate CLV isn't computed; never fabricated).
- Slate: <768px renders the flat board, ≥768px keeps game cards (same data,
toggled by width). Ungraded slate still shows game cards on phones (no blank).
Built to Design's screen-01 drawing, VISUALLY UNVERIFIED at 390px — the core
mobile screen, top of the master-audit list.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GradeResultCard already carried Design's §7 grade-reveal structure (hero +
identity + 3-col grid + intel box) — the screen was built to the same contract
Design's mobile follows. Applied the Rev 3 gaps: team context now a TeamChip
(real logo/monogram), and the mobile grade hero → 74px (Design's exact mobile
size; M1a had 80). Desktop hero unchanged (116px).
Built to Design's mobile spec, VISUALLY UNVERIFIED at 390px.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design Rev 3 anchors every matchup/context abbr with a 10-12px team tile. New
reusable TeamChip renders the real TeamLogo (licensed ESPN logo where it
resolves, team-colored monogram otherwise — the resolver is already built) at
that size + the abbr, sitting inside the row so it inherits the ranked opacity
ramp. First placement: StatStrip's player/team context (name → team-chip →
archetype). ROW-GRAMMAR identity-run test updated to the chip marker.
Remaining Rev 3 placements to thread TeamChip into (reusable, mechanical):
parlay legs, grade-shift header, pitcher "vs", /u recent-settled, other
matchup context lines. Game-card headers already carry TeamLogo (TeamLink).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Token alignment (M3.1) orphaned a few bare literals: PlayerAvatar's team-less
monogram accent still fell back to the retired B-blue #4A9EFF → Design neutral
#B8BCC8; the player/u OG + portrait billboards painted old text-0 #E8E8F0 →
Design #F0F0F0 (satori canvas needs literals).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The only surface still showing a raw book key: BookComparison rendered
{b.book} capitalized ('Betmgm'). Now uses the entity-layer BookChip (branded
wordmark + name), matching ledger/GameCard. No lowercase/raw book text anywhere.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The share/OG card (a billboard) still painted grade B blue (#4A9EFF) on canvas;
Design's B is neutral-bright white. Canvas needs a literal, so #F0F0F0 direct.
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>
The "SIGNAL LIVE vs STALE 8h" contradiction was a field mismatch, not a dead
pipeline. updated_at is the grade-LOCK time (advances only on a full snapshot,
5×/day — grades never change in-game, so it is intentionally stable). The
SYNC badge measured the 20-min intraday cadence (expected_interval_s=1200)
against that 5×/day field → structurally guaranteed STALE between slots even
when intraday refreshes lines perfectly.
- snapshotService: full snapshot now seeds refreshed_at at lock time
- intradayRefresh already bumps refreshed_at every ~20 min (unchanged)
- /api/snapshot/summary + GET /:sport now expose refreshed_at (was written to
Redis but never serialized → no public liveness signal existed)
- LiveLayer SYNC badge measures freshness from refreshed_at (fallback updated_at)
Exposing refreshed_at also gives a public heartbeat probe: it advances every
intraday slot, so pipeline liveness is verifiable without container logs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend /explore (ExploreHub) into the 365-day never-dark hub. Two new
self-hiding sections feed REAL always-available data into the offseason:
- NewsWire (components/vyndr/NewsWire.tsx): real ESPN headlines from
/api/news/:sport (newest-first, mono timestamps, type chips, player/team
links) + real injuries from /api/schedule/:sport/injuries (OUT/GTD chips,
token colors). Reuses the retired TerminalTemplates INJURY_WIRE layout but
never routes its sample constants. Self-hides when both feeds are empty.
- FuturesBoard (components/vyndr/FuturesBoard.tsx): real futures from
/api/futures/:sport — championship/win-total/award markets, mono tabular
prices + movement colored by the contract (shortening=green / drifting=amber
/ flat=dim, NEVER red; move shown ONLY when the backend supplies one).
Carries the honest "TRACKED · NOT GRADED" label — no fabricated grades on
futures. Self-hides when markets:[].
- ExploreHub is offseason-aware (via emptyState OFF_SEASON month check): the
hub LEADS with futures + wire when the board is dark, COMPLEMENTS the live
board in-season. Sport selector kept; each section self-hides independently.
- Testable pure helpers: lib/futuresMove.js (move→color, never red) +
lib/newsFormat.js (timeAgo mono-stamp, ESPN type labels).
Contracts consumed (Wave 2A owns the proxy/service files); code self-hides on
fetch failure if a proxy isn't present yet.
Tests: tests/unit/newsWire.test.js + futuresBoard.test.js (26 new). Full suite
259 suites / 3144 green; web build exit 0. vyndrParityQA stays green
(mono data, no glitch on data surfaces).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The NBA/WNBA espnId was captured only from espnStatsAdapter (the offline-
Python fallback), unreliable in prod. Add espnAthleteIndex — a pure,
defensive harvester that builds { nameKey -> {espnId, headshotHref} } from
the ESPN schedule->summary/boxscore/leaders/injuries/roster feeds the
pipeline already calls (free, bounded mapLimit, cached, MLB->{}).
snapshotService now fills any player the primary stats-resolve left without
an espnId from this index, and stores a DIRECT headshotHref as headshotUrl
on the enriched grade (the exact URL, never 404s on a constructed path).
Threaded headshotUrl through slateAdapter.buildPlayerStripsFromProps ->
GameCard -> StatStrip -> PlayerAvatar/getHeadshotUrl (direct href wins over
the constructed one). MLB's MLBAM path is untouched. Soccer resolves only
via a direct href; absent -> honest monogram (API_FOOTBALL_KEY remains the
reliable soccer path, unwired).
getGameSummary now also passes through ESPN `rosters` (pre-game lineups
carry id + headshot). Everything graceful: any miss -> absent -> monogram.
Tests: tests/unit/espnHeadshotIndex.test.js (11) — fixture->index, snapshot
merge fallback, direct-href-wins, soccer honest monogram, malformed/cyclic
parse never throws. Full suite 3080 green; web next build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 3 — OUTLOOK MODE. The game grid no longer dead-ends in a "NO SLATE" CTA.
When there are no live games (and it's not a network failure) it shows REAL,
always-available data: yesterday's PROVEN A-tier receipts (/api/ledger/model)
+ tomorrow's date-pinned ESPN schedule preview (free/cached). A network
fetchError stays a distinct ERROR state — never a fabricated outlook.
- lib/outlook.js (new, CommonJS, unit-tested): buildOutlook selection +
mapTomorrowPreview (upcoming-only, drops incomplete matchups, never invents).
- Slate.tsx: OutlookSurface replaces the empty-grid CTA (dateOffset 0 only).
- dashboard/page.tsx: DashboardOutlook replaces the "Today's games" NO-SLATE CTA.
Step 4 — MARKET-BREADTH / CONSENSUS vs MODEL. Makes the DeskShowcase
"consensus vs model" claim REAL. Consensus = median book line across a prop's
per-book rows; the model's position is model_value vs consensus, signed by the
graded side. <2 distinct books → null (never fabricate a consensus); a
non-numeric line is ignored, never coerced to 0.
- lib/marketBreadth.js (new, CommonJS, unit-tested): median/computeBreadth/
collectBreadth (strict null guards).
- components/vyndr/MarketBreadth.tsx (new): mono/tabular strip, colored by sign
via colorContract.edgeColor, self-hides when nothing has >=2 books.
- Slate.tsx renders it above the grid (joins books + snapshot model_value).
- slateAdapter.js exports gradeKey for the join.
- DeskShowcase.tsx: the consensus claim is now backed by the shipped feature.
Tests: tests/unit/outlook.test.js + tests/unit/marketBreadth.test.js (23 cases).
Full suite 2984 passing (245 suites); next build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TASK 1 — Parlay Lab (/parlay): a dedicated Correlation Builder with a
leg source INDEPENDENT of the live slate. Browses tonight's pre-graded
props from /api/snapshot/:sport (resolves players via /api/players/search),
adds legs through useParlay().addLeg (deduped by legKey), and renders the
PARLAY SLIP — combined grade, correlation, and payout read straight off
ParlayContext. Surfaces parlayService's correlation warning as the
CAUTION · CORRELATION FLAG, honors the tier leg-cap (free 2 / analyst 4 /
desk 6) and blurs the payout for free tier with the __goPaywall upsell.
Added /parlay to OPEN_ROUTES (free funnel, like scan/dashboard). Retired
the cleanly-dead ParlayTray.tsx (unmounted since Session 50). No new
proxies — reuses existing snapshot/search/parlay-grade endpoints.
TASK 2 — Live Grade-Shift timeline: web/src/lib/gradeShift.js (pure,
testable) builds a line/grade-movement timeline from already-emitted data
(intraday {t,line} history + revised_from_grade). Color law mirrors
ROW-GRAMMAR / StatStrip.LineSparkline: green = toward the graded side,
amber = against, dim = flat (never red). GradeShift.tsx renders it, shows
the original grade struck-through on a revision, and self-hides below 3
real points. Mounted in GradeResultCard (self-hides on the scan path,
which carries no captured history — honest, never fabricated).
Tests: tests/unit/gradeShift.test.js (15) + tests/unit/parlayLab.test.js
(13). Full suite 245 suites / 2989 tests green (baseline 243/2961).
Next build EXIT=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addition 2 (non-negotiable): the model's record must show PER GRADE TIER
(A+ went X-Y, A X-Y, …) everywhere the record appears. A blended % hides the
proof that higher grades win more — the tier calibration IS the credibility.
The backend already computed `by_tier` in getModelAggregate; this is display
propagation via ONE shared component (the class fix, not five one-offs).
- web/src/lib/tierRecord.js — testable CommonJS row-builder. W-L counts ALWAYS
(honest at any n); hit-% only when the upstream n≥20 gate passed (hit_pct !=
null), else "RECORD BUILDING · N settled". Order A+ A B C D F. A-tier is the
only edge (green) tier — no glow below A, red reserved for outcomes.
- web/src/components/vyndr/TierRecord.tsx — the ONE shared table. Presentational
(byTier) for /u + ledger; self-fetch (/api/ledger/model, sport-scoped) for the
dashboard. Fully self-hides until a tier has a settled read.
- Ledger swaps its inline TierCalibration for the shared component (single
source). /u PublicProfile renders it below the blended hero (by_tier added to
the aggregate type; it already flows through the route + proxy untouched).
Dashboard gains a compact per-tier surface.
- Endpoint/proxy audit: profiles.js + ledger.js return the full aggregate
(by_tier included); both Next proxies pass the body through — no threading
needed. No change to the gate or math in getModelAggregate.
Tests: tests/unit/tierRecord.test.js (row logic + tier order + edge contract +
source-assert all three surfaces render the shared component). ledgerService
test gains an A+-stands-alone bucketing case. Full suite green (243 suites /
2961 tests); web next build exits 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Threads a REAL athlete id from the snapshot's per-player stats resolve (zero
new I/O) → enriched grade → grades:{sport} → slate strip → PlayerAvatar. Real
photo where an id resolves; team-colored monogram (never a gray silhouette,
never a broken image) where it can't. Ids are never fabricated.
Ingestion (Addition 1):
- espnStatsAdapter.getSeasonAverages now RETURNS the resolved ESPN athlete id
(was discarded) as espnId; non-numeric uid degrades to null.
- playerIntelService surfaces MLBAM playerId (MLB) / ESPN espnId (NBA/WNBA).
- snapshotService captures both per player and stores them on the enriched
grade beside archetype/team (null when unresolved → monogram path).
Thread → component:
- slateAdapter.buildPlayerStripsFromProps carries playerId/espnId onto each
strip; StatStrip → PlayerAvatar (accepts both ids; getHeadshotUrl routes by
sport: MLB→mlbstatic, NBA/WNBA→a.espncdn).
- Silhouette surfaces rewired to PlayerAvatar (branded monogram on null):
scan search dropdown (guarded MLBAM p.id) + tonight chips, SearchModal,
HotListPanel, GradeResultCard header. Scan grade card feeds the picked
MLBAM id through gradeAdapter.
- playerHeadshot pure URL logic extracted to CommonJS playerHeadshotUrl.js
(unit-testable; the .ts re-exports it). nfl/nhl added to ESPN_SPORT_PATH.
Tests: tests/unit/headshotThread.test.js (per-league URL + id thread + monogram
null path) + extended snapshotService/espnStatsAdapter suites. Full suite
241 suites / 2915 green; next build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MISSION 1 — real sportsbook wordmarks (kills lowercase "betmgm"):
- books.js: add the 6 missing ALLOWED_BOOKS keys (fanatics/bet365/
hardrockbet/betrivers/pointsbet/pinnacle) with real brand names +
colors — no live book falls to neutral gray. Add `slug` fields +
bookSlug()/hasBookSvg() + BUNDLED_BOOK_SVGS.
- Bundle 8 self-authored styled-text wordmark SVGs under
web/public/books/{slug}.svg (draftkings/fanduel/betmgm/caesars/
bet365/pinnacle/hardrockbet/betrivers). NOT copied trademarked logo
glyphs — the book's NAME in brand weight+color; official press-kit
art can drop into the same paths with zero code change.
- BookWordmark: render the local SVG when bundled, else the brand-color
styled-text fallback (never a broken image; never a lowercase key).
- Import BookWordmark into the ledger row (page.tsx:368) + the identical
public-profile row, replacing bare {row.book} text. vyndr/GameCard
line-grid book cell now proper-cases via bookInfo().name (keeps the
preferred-book green highlight).
MISSION 2 — team-logo coverage gaps:
- teamMeta.js: add ESPN-schedule ball-sport abbr aliases the feed emits
that fell to monograms — SA→SAS, NY→NYK, WSH→WAS, BRK→BKN (NBA),
CONN→CON (WNBA). Real-abbr-first lookup means MLB WSH (Nationals) +
WNBA NY (Liberty) still resolve directly; NY in MLB stays null.
Tests: new tests/unit/bookWordmark.test.js (all 10 ALLOWED_BOOKS resolve
to a real brand+non-gray color; 8 bundled SVGs exist; BookWordmark
SVG-first + no-lowercase-leak; ledger/profile import + use BookWordmark).
entityLayer.test.js extended for the new aliases. Full suite green
(239 suites / 2891 tests); next build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FIX 1 — Honest billing renewal render. VYNDR tiers are monthly, so a
`subscription_end` far in the future (the manually-seeded "RENEWS 6/9/2036"
founder row) is a comped/lifetime/seed value, not a renewal. New
web/src/lib/billingDisplay.js `classifyRenewal()` → date | none | lapsed |
unknown (strict Date.parse guard, MONTHLY_RENEWAL_MAX_DAYS=60). Profile page
renders the classified label for both the "Renews" stat and the
cancel-scheduled "Access ends" line — no raw far-future date. No DB row mutated.
FIX 2 — MLB namesake collision (James Wood → "Chicago Cubs"). searchPlayer now
collects ALL exact-nameKey matches instead of first-`.find`; a ≥2 collision
resolves ONLY via a confident teamHint (the prop's game participants, matched
against the cached /teams list with ESPN↔statsapi abbr reconciliation), else
refuses (null) — never guesses. The hint threads getPlayerStats →
resolvePlayerStats → snapshotService (built from each prop's home/away team).
Join invariant: a single-exact player whose team isn't in the hinted game has
its team DROPPED (null), so streaks/rosterlogs never tag a foreign team. Full
teamHint recovery shipped (not just the refuse fallback).
FIX 3 — DeskShowcase headline "A $1M terminal." → deadpan value-showing copy
"Every grade, every alt line, live." Prices ($44.99 / $34.99) unchanged.
Tests: billingDisplay.test.js (7), mlbNamesakeResolve.test.js (12,
disambiguation + join invariant + pure helpers), ds5PricingStates updated to
assert the new headline and no "$1M". Full suite green (237 suites / 2863
tests); web `next build` exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>