Commit Graph

36 Commits

Author SHA1 Message Date
builtbykev e5b20b0509 Software watches coverage now, and ancestry becomes a product contract
Three closeout items, traced before building.

THE OBSERVER WAS DIAGNOSTICS, NOT MONITORING. Traced from the deployed tree:
exactly two callsites, both manual internal routes, and nothing in the scheduler
or ops path consumed it. A persistent lineage failure could have sat unnoticed
until a human asked.

The monitor now runs on the scheduler's per-minute tick, throttled to 30
minutes. It is placed there rather than after a snapshot on purpose: an
in-snapshot audit structurally cannot report that no snapshot ran, which is the
failure mode that matters most, and it would run under peak write contention
where the audit already demonstrably times out. It reads only the durable
observer and never `attachLineage`'s counters, and every failure path is
swallowed — a monitor that can take down the pipeline it watches is worse than
no monitor.

HEALTHY IS SILENCE; EVERYTHING ELSE SPEAKS. `coverageAlarm` is pure, so the
policy is testable and cannot drift into the scheduler. AUDIT_UNAVAILABLE says
"could not be measured — the audit did not run", deliberately worded so it can
never be read as "coverage is zero": those are different claims and collapsing
them is how a monitor starts lying in the reassuring direction. Alerts dedupe on
(health, cohort) so a standing fault states itself once and a NEW cohort with
the same fault speaks again.

ANCESTRY BECOMES A PRODUCT CONTRACT. It was internal-only. `GET
/api/ancestry/ledger/:id` (requireAuth, rate-limited) plus the Next proxy that
makes it browser-reachable, keyed on the LEDGER ROW ID — a stable identifier the
ledger API already returns — rather than a raw natural key exposed because it
was convenient. Its own router, so `routes/ledger.js` stays free of lineage
entirely and the grade-badge guard keeps its teeth. Every response declares
`authority: LOCAL, authority_scope: ANCESTRY_ONLY`.

THREE TEETH CAME BACK GREEN AND ALL THREE WERE MY TESTS, NOT SAFE DEFECTS.
The badge guard was CASE-SENSITIVE, so `LINEAGE_ANCESTRY` and `readAncestry`
walked straight past it. `try/finally` is valid JavaScript, so removing the
monitor's catch produced no load error and nothing asserted the containment.
And the multi-date cohort check was a grep for `.gte('game_date'` that the
head query satisfied on its own. All three replaced with behavioural tests,
including a scheduler double whose fake client HONOURS its filters — a
pass-through would have made a narrowed cohort walk look correct.

Suite 398/5,522/0 · tsc 0 · web build 0 · teeth 14/14 and 15/15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
2026-08-30 23:44:46 -04:00
builtbykev 71d3b7b786 E10 Report issue template + E12 /report archive, to spec
PHASE 0 — the spec, read not recalled. E10: "Hybrid: dark billboard header
that survives every client, light paper body Gmail can't wreck. 600px,
stacked, no webfont dependence." Content law: "One email per slate day.
Top read, what changed, the record. Nothing else." E12: "EVERY ISSUE SHOWS
ITS OWN DAY RECORD -- THE ARCHIVE IS A LEDGER TOO."

COMPOSED, NOT FORKED. The audit had E10 as PARTIAL, not absent:
newsletterService already builds the daily report's CONTENT and lints its
voice. What was missing is the designed hybrid SHELL, so reportTemplate.js
is a template over that builder rather than a second report -- the same
call made for the movement strip, and for the same reason.

PHASE 1 — the hybrid shell is an ENGINEERING constraint, not a look, and
the tests say so: Gmail strips style blocks, Outlook ignores flexbox, and
a dark body renders as a black rectangle in several clients. Hence tables,
inline styles, 600px fixed, system fonts, no image required to read, and
the green SHIFTS from #00D4A0 to #00A57D on paper because the dark-mode
green is unreadable there.

FACT-CONTRACTED: a section whose data is absent is OMITTED and NAMED in
`omitted`, never filled. There is no code path producing a placeholder
figure. The honesty block carries the real numbers -- graded count,
cleared-ceiling count, the realized rate against baseline, and that we do
not issue A grades.

E1'S LAW TRAVELS EVEN THOUGH ITS RENDERING CANNOT. An SVG strip is not
reliable in email, so movementText carries the RULE: green only when the
move favours the read, and a flat market says FLAT · [N]D rather than
showing nothing.

NO DESIGNER SAMPLE DATA. Nabers 1,120.5, No 128, DAY RECORD 9-4 are a spec
for what a live issue renders; pasting them in would be fabrication
carrying a designer's authority and would look entirely correct. Tested.

PHASE 2 — /report is now the real archive, REPLACING the S41 redirect to
/blog. That redirect existed because the surface did not; E12 built it, so
the placeholder is correctly gone and the S41 test is updated rather than
worked around. Every row carries its own day record, and an unknown record
says UNSETTLED -- never a dash that reads as zero. Empty archive is an
honest state.

Backend: public read-only /api/report over Redis issues, plus the Next
proxy. Both surfaces registered under the reachability guard.

A test bug I made twice now: my check for forbidden sample values matched
the template's own doc block, which NAMES those values as things never to
paste. Documentation worth keeping, so both suites strip comments before
matching -- a guard that reads its own warning is not reading the code.

WAVE-2 STATUS: E1, F9-F11, E10, E12 done. Still gated -- F5 article media
and E16/F8 on the card-system reconciliation; the in-season hub IA on the
social chat's formula; E9/E15 on model; E2/E6 on licensing.

Read-only throughout; serving fingerprint unchanged including
newsletterService; accrual clock unchanged at 0 eligible dates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
2026-08-07 22:17:59 -04:00
builtbykev c575a708c7 Content studio API + preview page; widen the reachability guard; correct
two inventory errors

INVENTORY CORRECTION, and it was mine. Phase 2's two "orphans" are NOT
orphans -- my board grepped only web/src/app and missed component-level
mounting. The transitive check says both are already mounted:

  BookComparisonPanel -> GradeResultCard -> app/scan/page.tsx
  NewsWire            -> ExploreHub      -> app/explore/page.tsx

So book comparison is DONE (wired to /api/books, rendering on the grade
card) and THE WIRE is DONE-BY-DESIGN, mounted in ExploreHub. Its header
names an "Offseason Hub" as its home, and that hub genuinely does not
exist -- but that is board item #8, not a mounting bug, and inventing a
surface to satisfy a comment would be the wrong fix.

The lesson is the same one this session keeps teaching: I checked one
directory and reported a conclusion the check could not support.

ALSO CAUGHT: I overwrote src/routes/content.js, which was the Session-29
content-templates route, by picking a filename without looking. Restored
from git with no work lost; the new surface lives at
/api/content-studio and both now coexist.

PHASE 0/1 — /api/content-studio serves finished posts (copy, branded card,
card_svg, the fact_contract each was REQUIRED to have, and the facts that
actually backed it) plus a POST for editorial status in Redis. Private via
internal key; the Next proxy holds the key server-side so the browser
never does. /studio renders it as a thin client -- copy and card side by
side with the fact contract visible, because reviewing copy by reading it
is exactly how a wrong number ships. Never-blank: a night with nothing
generated says so.

API-FIRST is the point: the endpoint an autonomous poster will call is the
one the page already renders, so the agent handoff is a pointer change,
not a rebuild. Contract documented at docs/CONTENT-STUDIO-API.md.

EXPRESS 5 BROKE 23 SUITES at first: `router.get('/:date?')` throws at
mount time in Express 5, taking down everything that imports app.js. Two
explicit routes instead.

PHASE 3 — the reachability guard is widened from grade-fields-only to a
general built-but-unread check. Book comparison, THE WIRE and the content
studio are now registered surfaces; a page counts as its own entry point
(Next mounts it by convention) while everything else must trace to one.
22 checks green; a registered-but-unimported surface still goes red.

FULLY ISOLATED: read-only on model/slate/ledger, serving fingerprint
verified unchanged, accrual clock unchanged at 0 eligible dates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
2026-08-07 18:51:52 -04:00
builtbykev cb3237cdce Item 6 — Desk showcase renders REAL data (or hides), kills the mocked ladder
The pricing Desk showcase hardcoded an alt-line ladder (1.5 A +7.1% / 2.5 A+
+11.4% / 4.5 C -3.8%), QUARTER-KELLY 2.4%, and PARLAY φ 0.34 — a mocked demo
selling something we weren't proving.

- deskShowcaseService reads the pre-graded snapshot for a real A/B prop's
  alt-line ladder (prefers the one with the most grade variation — the most
  compelling real example). Edge per rung shows only when it's a plausible
  market value; the inflated (model-line)/line artifact on small lines is
  guarded to "—" rather than shown as a fake +91%.
- PARLAY φ is now REAL: the model's same-team correlation (0.34, mirroring the
  frontend parlayMath team constant) computed for TWO REAL same-team legs,
  named. No real same-team pair on the board → the tile hides, never an
  invented number.
- QUARTER-KELLY tile is REMOVED: the snapshot has no odds, so a real
  quarter-Kelly % can't be computed here — a fabricated 2.4% is worse than
  nothing. Kelly stays a real in-app Desk feature; the showcase just doesn't
  fake it.
- DeskShowcase is now a client component fetching /api/desk-showcase; when the
  board has no real ladder the whole visuals column hides (real-or-hidden, same
  law as the hero). The pitch copy is unchanged.

5 service tests. Change-affected suites green, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 01:42:16 -04:00
builtbykev 9b9aab4262 Item 5 — daily hero prop is a live RULE (biggest model-vs-market disagreement)
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>
2026-07-18 01:29:55 -04:00
builtbykev 41fc2b90e2 Item 2 — founder counter is REAL or hidden (kills the hardcoded 47/100)
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>
2026-07-17 15:32:18 -04:00
builtbykev f0752b804b Wave 2A: offseason data feeds — news wire + quota-disciplined futures
FREE ESPN news wire + championship-winner futures for the never-dark
offseason hub. Both graceful/empty, never fabricate a market value.

- newsService (mirrors injuryService): per-sport ESPN /news FEEDS, pure
  parseNews → { sport, items:[{id,headline,description,published,type,
  athlete?{name,key},team?,href}] }; athlete/team from categories[] only
  (absent when not present). Cache 15m, injectable, offline-tested.
- oddsNormalizer.normalizeOutrights: NEW branch — outrights outcomes are
  {name,price} with no point, so normalizeProps drops them; keeps them with
  best-price-across-allowed-books per selection. + americanToDecimal.
- oddsService.FUTURES_KEYS: separate map (mlb/nba/wnba championship winner),
  OUT of the daily SPORT_KEYS/snapshot budget.
- futuresService: getFutures(sport,deps) → { sport, updated_at, markets:
  [{key,title,selections:[{name,price,prevPrice?,move?}]}] }. One outrights
  call per 12h TTL (quota-disciplined), FUTURES_ENABLED gate. Price-move
  (shortening/drifting/flat) mirrors computeLineDeltas SHAPE on odds not
  line; prev prices persisted inside the futures:{sport} value (no new key).
  linkNewsToMoves pure causal-tie helper.
- Routes /api/news/:sport + /api/futures/:sport (registered) + Next proxies.
- Tests: newsService, futuresService, oddsNormalizerOutrights (fail→pass,
  no network). Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:33:26 -04:00
builtbykev 54fa5853f5 Wave 6: Combat Intelligence Layer (honest free v1)
Net-new MMA/UFC vertical — fight-card discovery, tale-of-the-tape,
style-blend archetypes, ML + round-total odds, and a style-edge VERDICT
(a MODEL read, explicitly NOT a settled grade). Built to
specs/combat-intelligence.md.

Backend:
- combatAdapter: ESPN MMA scoreboard (date-pinned, free JSON) -> fight
  cards + tale-of-tape (record/weight class/rounds/ESPN athlete id);
  defensive parse (null on unknown shape, never throws); injectable
  fetchImpl + cache; pure normalizeCombatOdds (odds-api h2h/totals ->
  ML + round total, allow-listed books, best price). Number(null) guard.
- archetypeService: 6 pinned combat styles in a SEPARATE COMBAT_ARCHETYPES
  registry (FINISHER collides with soccer + its green trips the signal-
  green gate); classify('mma') blends range/tempo/outcome, honest-empty on
  thin data (no forced fallback); styleMatchup() honest verdict.
- oddsService: SPORT_KEYS.mma + MMA_MARKETS=['h2h','totals'] + SPORT_MARKETS
  (no spreads suffix). oddsNormalizer MARKET_MAP h2h/totals.
- config/sports.js + web mirror: mma.active=true (collectData stays false;
  NOT in the graded-props pipeline SPORT_CONFIG or snapshot/settle loop).
- routes/combat.js: GET /api/combat/:date + GET /api/fight/:id (public,
  cached, honest empty off-card) + Next proxies.

Frontend:
- FightCard: two-fighter tale-of-the-tape (initials monogram — no photos),
  GRAPPLER/STRIKER blend bars, discipline pedigree tags, shared
  ArchetypeBadge (sport="mma", unicode glyphs), CENTER VERDICT, ML +
  round-total real; method/round/KO = honest "data-limited", never
  fabricated. Self-hides on a non-two-fighter bout.
- /fight/[id] page (server wrapper + client), EmptyState off-season.
- MMA SportBadge token (#D4AF37); archetypes.js sport-aware resolution.

DEFERRED (per spec, NOT built): matchup-GRADE engine, method/round/props
board, combat settlement, ufcstats scraping.

Tests: +3 suites (31 tests) — combat archetype cross-file color/glyph
match, classify blends, styleMatchup honesty, adapter defensive parse +
odds normalize, FightCard honesty grep; extended oddsNormalizer +
sportMarkets. Full suite 253/253 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:58:22 -04:00
builtbykev b5d3fd14bb S11 (a1): live tracking — the read locked, the game watched
MLB statsapi + WNBA ESPN live boxscores -> per-player current values
(live:{sport}:{date} TTL 90s, /api/live/:sport + Next proxy). Pure
propState math (HIT / ON PACE / NEEDS N / HOLDS / LINE PASSED — never
red in-progress), attachLiveProgress strip join on nameKey+statType,
proximity-to-hit slate float, StatStrip LiveTracker in the ROW-GRAMMAR
outcome slot (spec amended + lock test updated). Grades never change
in-game — tracking, labeled as such. 2698 -> 2757 tests, web build 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 00:45:29 -04:00
builtbykev 1d46b446c9 Merge S10 (a1): public ledger profiles v1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	BUILD-STATE.md
#	CLAUDE.md
2026-07-11 19:40:43 -04:00
builtbykev b20145c215 S10 (a1): public ledger profiles v1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:32:22 -04:00
builtbykev 4e49ee0990 S9 (a1): slip reader — zero-API OCR
tesseract.js (self-hosted WASM, Apache-2.0) + pure per-book layout
parsers (DK/FD/MGM/Caesars) with per-field confidence and needs_review
honesty — the reader never guesses. POST /api/slips/parse (auth, free
1/day paid 10/day, 4MB cap) + Next proxy. Gated /slip page: upload or
paste, manual-correct UI, per-leg grades through the normal engine
(refusals render honestly), add-all to Parlay Lab, share card. Vision
model upgrade logged post-revenue. 2574 -> 2608 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:32:17 -04:00
builtbykev fc0c7bfd18 Merge S7 (a1): newsletter — THE VYNDR REPORT
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	BUILD-STATE.md
#	CLAUDE.md
2026-07-11 14:32:18 -04:00
builtbykev 48eb420f9e Merge S3 (a1): affiliate + partner plumbing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:31:53 -04:00
builtbykev 0e7871ac0e S4 (a1): the media engine — VOICE templates, /desk, Ghost drafts
4a VOICE v1.1 committed (board start); lint is EXECUTABLE — banned list +
   no-exclamation law enforced in the engine (throws in test, drops in
   prod) and locked by tests. Curly-apostrophe variants covered.
4b mediaEngine: deterministic templates (MORNING WIRE, SIGNAL, STREAK
   WATCH, THE SETTLE, RECEIPTS, ARCHETYPE WATCH, LINE DISPATCH) filled
   ONLY from snapshot/ledger/streaks JSON. Record percentages never render
   under n>=20 (counts + 'Record building' below). Stark layer = curated
   committed library (content/stark-lines.json), day-rotated selection —
   selected, never generated.
4c /desk (founder-only: requireAuth + DESK_OWNERS email allowlist,
   deny-by-default): all formats as text + <=280-char pre-segmented tweets
   with per-tweet copy buttons + char counts, wire/numbers-only variants,
   DATA BRIEF block (structured day numbers) with copy-for-claude.ai.
   ntfy ping after the day's first snapshot: 'Desk pack ready'.
4d ghostPublisher: DRAFTS ONLY (status:'draft' test-locked), env-gated
   no-op, HS256 JWT via node crypto (zero new deps). POST
   /api/internal/ghost/drafts saves slate preview + settle drafts.
   Nothing anywhere auto-posts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:31:35 -04:00
builtbykev 32d7200571 S7 (a1): newsletter — THE VYNDR REPORT
Email capture + daily report assembly + operator-triggered Listmonk send.

- NewsletterCapture (dark terminal, mono) on landing (below FAQ) + /welcome
  (the real signup success surface); double-opt-in note; 'Signups open soon'
  when Listmonk env is unset.
- POST /api/newsletter/subscribe: public, 10/min IP limit, honeypot,
  server-side email validation, forwards to Listmonk subscribers API with
  preconfirm_subscriptions:false (Listmonk sends the confirmation).
  No env -> calm 200 { ok:false, reason:'not configured' }. Next proxy
  web/src/app/api/newsletter/subscribe/route.ts (S25 rule).
- newsletterService.buildDailyReport: signals from snapshot:{sport}:latest,
  STREAK WATCH via rosterLogs -> streaksService -> streakLens, THE RECORD via
  ledgerService.getModelAggregate (percentage only when hit_pct != null —
  n>=20 gate — else 'RECORD BUILDING · N pending'). RG footer (21+,
  1-800-GAMBLER, Listmonk-native {{ UnsubscribeURL }}) in html + text.
  VOICE v1.1 lint locked by tests: no '!', no banned vocabulary, numbers
  only from injected pipeline data.
- sendDailyReport: creates + starts a Listmonk campaign; env-gated no-op;
  refuses an empty report. Deliberately UNSCHEDULED — only
  POST /api/internal/newsletter/send (internal key) triggers it.
- docs/NEWSLETTER.md: box-side Listmonk runbook (install, double-opt-in
  list, API user, Coolify env, test-send).
- Spec: specs/feature-a1-s7-newsletter.md.

Tests 2398 -> 2429 (207 suites, all green); next build exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:29:22 -04:00
builtbykev 0996320bd1 S3 (a1): affiliate + partner plumbing
Zero out-of-pocket; everything config-flip-ready but DISABLED/organic.

- BOOK IT deep links: web/src/lib/bookLinks.js + affiliateConfig.js
  (all books enabled:false, Impact/Partnerize param shapes documented,
  empty params skipped). Wired into StatStrip BookItTeaser (real anchor
  now) + scan hand-off links. Every book anchor renders
  rel="sponsored noopener noreferrer" (BOOK_LINK_REL).
- Best-price marker: slateAdapter.detectBestBook (only when >=2 books
  post the SAME line and prices differ — absent beats wrong) + subtle
  signal-green dot in StatStrip. Slate.groupByGame threads the grouped
  per-book rows (books[]) onto PropRowProp instead of discarding them.
- Partner refs: ?ref=CODE -> vyndr_ref cookie (90d, first-touch,
  PartnerRefCapture in layout) -> signup metadata partner_ref ->
  internal GET /api/partners/report/:code (requireInternalAuth; honest
  zeros + note until the TODO migration in docs/PARTNERS.md adds
  user_profiles.partner_ref — NOT run). Stripe promo-code convention:
  partner code == promotion code, verbatim.
- Tests: +41 (2398 -> 2439, 209 suites); bookItTeaser + vyndrCoreScreens
  invariants updated to the new (stronger) rel contract. Web build exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:28:18 -04:00
builtbykev f110bd63f1 Session F (night2): Phase 5 — records + dossier
5.1 Archetype defined on-page: one line from the archetype library under
    ARCHETYPE DNA (BOMBER — elite raw power…); expander keeps the long form.
5.2 VYNDR-on-team live: getModelAggregate team scope (migration-020 column)
    + /api/ledger/model?team= + ModelRecord mounted on the Team Hub header.
5.3 WNBA/NBA profile parity: minutes-based usage (+MIN season cell) when
    the feed carries minutes — absent beats invented.
5.4 Settings read meter: real rolling-24h usage from the SAME store the
    limiter enforces (GET /api/user/scan-meter + proxy). Metered tiers see
    'X of N reads today' + bar; unlimited tiers see nothing. Corrects the
    stale '5 scans / month' copy.
5.5 Per-tier calibration on the MODEL tab: A+/A/B/C chips with hit% at
    n>=20 PER TIER, 'building (n/20)' below — the separation between tiers
    is the proof the grades mean something.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 01:05:29 -04:00
builtbykev 5d19660f8e Session E (night2): Phase 4 — scan + parlay polish
4.1 ROOT CAUSE of 'Ohtani returns nothing': no backend
    /api/players/search existed (MLB 404'd; NBA/WNBA hit the offline
    Python service). New Express route + mlbStatsAdapter.matchPlayers —
    canonical nameKey fuzzy match (exact > last-name prefix > folded
    substring). LIVE-VERIFIED vs the real 1,299-player list: Ohtani /
    Aaron Judge / Sánchez / sanchez / Chisholm Jr all resolve; accented
    and unaccented return identical results. Non-MLB matches the
    platform's cached names (rosterlogs + grades), cache-only.
4.2 Reveal choreography per §7: analyzing steps → DECLASSIFIED stamp →
    90ms-staggered context panels (entrance floors visible per the
    Phase-0 rule); prefers-reduced-motion skips straight to the card.
4.3 PRIOR READS chips on scan results — the model's public ledger
    history for the player (deferred-render, outcomes + pending, never
    invented). /api/ledger/model gains ?player= on entries.
4.4 Parlay Lab: humanized stat labels via the ONE shared formatter
    (lib/gradeAdapter.statLabel); 1-leg provisional grade ('Leg grade:
    B — add a leg for the combined read'); discoverable entry — Nav
    'Parlay Lab' item opens the drawer via window.__openParlay, and the
    open drawer now renders an honest empty state at 0 legs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:59:17 -04:00
builtbykev d09a06c054 Session 55: Self-learning loop + real-time layer (2274 tests)
Product overhaul core — the two transformative, differentiated systems:

Self-learning loop (Phase 2): outcomeService settles locked snapshot grades
against real MLB Stats API results → hit/miss/push, rolling accuracy by grade
tier (30d window). Idempotent, injectable, unit-tested. New GET /api/accuracy +
/api/ledger/accuracy + internal settle triggers + cron hook. AccuracyBadge
(dashboard/scan/landing) is honest — "LEARNING" below MIN_SAMPLE, never a fake
number. Settled HIT/MISS chips overlay the live slate.

Real-time layer (Phase 1): Slate silent 60s auto-refresh (no flash, no wipe on
transient blips) + "SIGNAL LIVE · UPDATED Xs ago" freshness strip; Ticker LIVE
badge that flashes on fresh events.

Landing (Phase 3): TopSignals shows tonight's real top-3 A-rated grades + live
accuracy — the product shown, not described.

Founder pricing: FOUNDER_CODE_EXPIRY default 2026-06-30 → 2026-12-31 (had
lapsed, disabling every founder code + the ClaimMeter pitch). That expiry — not
a tier change — was the real cause of the 4 stripe test failures.

Backend 2255 (4 failing) → 2274 (all green; +19 new, +4 fixed). Web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 15:39:13 -04:00
builtbykev f0674ca07d Session 51: Complete Team Hub (2234 tests)
Research-depth team view: /team/[abbr] with roster, archetypes, stats, props.

- Team API: mlbStatsAdapter.getTeams/resolveTeam/getTeamRoster (statsapi, abbr→id
  + active roster, cached). teamService.getTeamHub assembles roster → per-player
  season stats (bounded concurrency) + archetype (snapshot grade or classify) +
  tonight's graded props from grades:{sport}; whole hub cached 15min. MLB real;
  NBA/WNBA graceful snapshot roster. GET /api/team/:abbr (404 unknown) + proxy.
- Team Hub page: server page.tsx (generateMetadata) + TeamHub client — header,
  sort (archetype/graded/A-Z), archetype filter chips, roster rows (archetype +
  player link + position + stats + graded props + parlay "+"), "No active props"
  greyed state, loading/error.
- Game cards: team abbreviations are now TeamLinks → /team/:abbr?sport= (green
  hover, stops propagation). Team Hub has "← Back to Slate".

Backend 2215 -> 2234 tests (+19), 190 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:55:10 -04:00
builtbykev 3b47b783dc Session 49: Complete onboarding flow + name micro-fixes (2185 tests)
Name micro-fixes (close the normalization arc):
- collapseInitials merges "J C Escarra" -> "JC Escarra" (display + key); both
  playerName.js copies. Added mickey:michael nickname.

Onboarding flow (end-to-end, complete):
- Storage: Supabase user_metadata.preferences (no migration).
- API: src/routes/preferences.js GET/POST (requireAuth, admin getUserById/
  updateUserById, partial merge + sanitize) + Next /api/preferences proxy.
- Page: web onboarding/page.tsx — 3 steps (sports >=1 / books skip / bankroll
  presets+custom+skip) -> SIGNAL ACTIVE -> POST onboarding_complete:true -> 2s
  -> /dashboard. Redirects to login when unauthenticated.
- Redirect: dashboard fetches /api/preferences fresh; new+incomplete users
  (created_at >= cutoff) -> /onboarding; never while auth loading; existing
  users exempt.
- Personalization: Slate default tab = prefs.sports[0]; preferred books glow in
  the card lines grid (lib/books isPreferredBook, threaded dash->Slate->GameCard).
- Settings: PREFERENCES section loads + edits + saves sports/books/limit.

Backend 2156 -> 2185 tests (+29), 184 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:20:55 -04:00
builtbykev f8b120c0aa Session 45: Snapshot pipeline + GameCard swap + live ticker (2100 tests)
The on-demand "Read" grade model is RETIRED. A scheduled pipeline pre-grades the
slate, locks grades to the line, tracks movement; the dashboard shows them already
there. Orchestrates existing services — nothing rebuilt.

- snapshotService.runSnapshot(sport): getOdds → gradeAndCacheSlate → classify
  archetype per player → lock gradedAt → line deltas vs previous snapshot → write
  snapshot:{sport}:latest/previous + grades:{sport} → ticker events. Fully
  injectable, zero-network unit tests. runAllSnapshots = cron entrypoint.
- Internal trigger POST /api/internal/snapshot/:sport + /all (requireInternalAuth).
  In-process cron (SNAPSHOT_CRON=1, UTC 14,19,22,1,3) in server.js, no new dep.
- Public reads: GET /api/snapshot/:sport (cache-only) + GET /api/ticker (merges
  TICKER_MANUAL pins) + Next proxies.
- GameCard swap: live Slate renders vyndr/GameCard (legacy kept for types only),
  overlays locked grades onto game props → player name once + archetype badge +
  "Graded Xh ago at -115 · Current 2.5 · ▲ TOWARD +1.0". Ungraded → "Awaiting next
  scan", NO Read button. On-demand onGrade flow deleted.
- Ticker polls /api/ticker every 30s, graceful fallback to hardcoded items.
- NBA/WNBA: espnStatsAdapter free fallback (defensive parse → found:false on shape
  mismatch) wired into resolvePlayerStats after the offline Python service.

Env: PROPLINE_API_KEY_1/2/3, VYNDR_INTERNAL_KEY, SNAPSHOT_CRON=1, TICKER_MANUAL.
Backend 2061 -> 2100 tests (+39), 173 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:34:29 -04:00
builtbykev 927c4a5c65 Session 29: Content generation templates — slate threads, POTD, recaps, matchup previews (1660 tests) 2026-06-13 21:30:57 -04:00
builtbykev c48aecd510 Session 28: Parlay builder, line movement tracker, book comparison — 3 features, zero credits (1623 tests) 2026-06-13 12:37:08 -04:00
builtbykev 0538205fab Session 23: All-day intelligence layer — schedule, game lines, streaks, hot lists, stat filtering, ParlayAPI dead (1567 tests) 2026-06-12 11:16:58 -04:00
builtbykev 0e3839a90a Session 18: Admin dashboard + Tank01 prefetch endpoint (1443 tests) 2026-06-11 22:29:38 -04:00
builtbykev e5c45ecc8e Session 10: Internal auth refactor, prefetch cascade keys, Sentry, welcome email (1286 tests) 2026-06-10 20:45:05 -04:00
builtbykev 5c44922937 Session 7b: Fix pipeline - body parser, Redis queueing, poller visibility, auto-start 2026-06-10 01:22:55 -04:00
builtbykev 1fa04dc776 Sessions 5-7a: 955 tests, deployment ready 2026-06-08 18:35:13 -04:00
builtbykev eb443232fb feat: Feature 3.4 — Stripe integration with founder code system
Subscription billing:
- POST /api/stripe/checkout — creates Stripe Checkout session
- POST /api/stripe/webhook — handles lifecycle events (raw body)
- POST /api/stripe/portal — customer self-service management
- GET /api/stripe/status — current subscription info

Founder code system:
- Validates codes against env var list with expiry date
- Routes to founder Stripe Price IDs (locked rate for life)
- 4 price objects: analyst, analyst-founder, desk, desk-founder

Webhook events handled:
- checkout.session.completed → tier update + founder status
- customer.subscription.deleted → revert to free tier
- invoice.payment_failed → logged

Lazy Stripe init (no API key required at import time).
Raw body middleware for webhook signature verification.

16 new tests, 237 total (210 Node.js + 27 Python), all passing.
Phase 3 Web MVP COMPLETE. All roadmap features shipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 10:30:42 -04:00
builtbykev ed6502a880 feat: Feature 1.5 — Bet Submission with 3 methods + performance tracking
Three submission methods:
- POST /api/bets/quickslip — structured bet entry
- POST /api/bets/screenshot — stub OCR with confirm flow
- POST /api/bets/sync — coming soon stub

Full bet lifecycle:
- PATCH /api/bets/:id/settle — settle with outcome, recalculates performance
- GET /api/bets — list with status/book/pagination filters
- GET /api/bets/performance — ROI, win rate, profit (weekly/monthly/all_time)

Payout calculator handles straight bets (American odds) and parlays
(multiplied leg payouts). Performance service recalculates on each
settlement and upserts into performance table.

33 new tests, 221 total (194 Node.js + 27 Python), all passing.
All backend features for Phase 1 + Phase 2 now complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 05:11:42 -04:00
builtbykev 2366660f5e feat: Feature 2.2 — Line Movement + Cascade Detection
Line movement system:
- Baseline capture on first odds fetch of the day
- Movement detection >= 0.5 points with direction (up/down)
- Sharp money heuristic (sharp_action/public_action/unknown)
- GET /api/movements with player, stat_type, min_movement filters
- Movements included in GET /api/odds/nba live responses

Cascade detection system:
- Scratch detection: player props disappear from 2+ books
- Affected user lookup via scan_sessions + picks
- Parlay re-grade without scratched legs
- cascade_alerts created for affected users
- GET /api/alerts (Analyst/Desk only), PATCH /api/alerts/:id/read

Zero extra Odds API credits — all detection piggybacks on existing fetches.
Migration 002: line_baselines, line_movements, cascade_alerts tables.

30 new tests, 188 total (161 Node.js + 27 Python), all passing.
Phase 2 Core Product COMPLETE.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:21:34 -04:00
builtbykev 411cb6f196 feat: Feature 2.1 — Parlay Scan with correlation detection + monetization
POST /api/scan/parlay — authenticated parlay analysis:
- Supabase JWT auth middleware (auth.getUser verification)
- 5 correlation types detected between legs (same_game, same_team,
  same_player_conflicting, positive_correlation, blowout_cascade)
- Overall parlay grading (A/B/C/D) with correlation penalty adjustments
- Free tier: 5 scans/month, atomic scan count increment
- Scan 5: full analysis + personalized upgrade pitch
- Scan 6+: 403 block with upgrade pitch
- Pitch personalization from scan history (top stats, grades, tier rec)
- DB writes: picks + scan_sessions per scan

30 new tests, 158 total (131 Node.js + 27 Python), all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 12:45:15 -04:00
builtbykev c8c0962e56 feat: Feature 1.3 — Prop Analysis Engine with 6-step grading pipeline
Core intelligence for BetonBLK prop analysis:
- POST /api/analyze/prop — single prop analysis
- POST /api/analyze/batch — multi-prop analysis for parlay scanner
- 6-step pipeline: season avg → recent form → situational splits →
  cross-book lines → kill conditions → grade (A/B/C/D)
- 6 kill conditions: low_minutes, small_sample, b2b_high_usage,
  blowout_risk, split_conflict, no_opponent_data
- Composite scoring with confidence (30-95), bonuses, penalties
- Added spreads market to Odds API fetch (zero extra credits)
- Full reasoning output with step-by-step breakdown

36 new tests (unit + integration), 128 total across all features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:41:18 -04:00
builtbykev 00409fd6cd feat: Feature 1.1 — Odds API integration complete, 28 tests passing 2026-03-21 08:31:15 -04:00