40c61fbb0bb5ddf852eda1f8af502a89719c447d
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b33612675d |
Heal execute: quarantine markers, re-enable DNP voiding, two exclusion scopes
Order 2 Phases 2 + 4. Pre-heal rollback point secured first: vyndr-20260720-093821.dump (856,890 bytes) VERIFIED ON THE BOX, not just exit 0. MIGRATION 027 — two DISTINCT exclusion scopes, deliberately separate: - quarantine_reason: the row's GRADE is untrustworthy (wrong_opponent_grade). The row REMAINS a real public settled result — the bet happened, the outcome is real — but it must never train or validate, so getModelAggregate now excludes it from the denominator alongside void/unrecoverable. - analysis_flags: the row is VALID for settlement and the record but unattributable for PER-GAME analysis (doubleheader dates). Explicitly NOT filtered from aggregates. Collapsing these would either wrongly drop 166 doubleheader rows from the record or wrongly keep 25 wrong-opponent grades inside model validation. Tests assert both directions, including that analysis_flags is NOT filtered. Also adds re_settled_at + settlement_source to model_snapshots. DNP VOIDING RE-ENABLED — reversing my own Order 1.5 disable, with scrutiny, because its premise was FALSE. Order 1.5 assumed a missing player row meant the row's DATE was wrong. The Phase 0 dry-run disproved it: across every bindable row the stored date matched a real game (MIS-DATED: 0), and the players I had cited as counter-evidence were genuine DNPs on their true dates (Freeman 07-18; Kwan/Hedges/Davis 07-17 — their teams played, they did not). The evidence is positive: games FINAL + no line in a full-season log = no bet existed. I got this wrong twice tonight in opposite directions; the dry-run is what caught it. Recording the reasoning in the code so the next reader sees why the flag flipped back. Suite 282/3386 green, build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA |
||
|
|
270c4db47a |
STOP voiding on player-absence — it destroyed real results
Correctness fix to code I shipped minutes ago. The induced live settle pass voided 64 rows as 'player_dnp' and a large share of them are WRONG: the Jul 18 set is everyday starters (Freeman, Bellinger, Tucker, Chisholm, Conforto). They played. ROOT CAUSE — and my Phase 0 diagnosis was wrong. It is not DNP. The ledger row's game_date is WRONG. ledgerService derives game_date from the GRADE timestamp when the feed carries no game_time, and a 01:00/03:00 UTC snapshot is 21:00/23:00 ET the PREVIOUS day, so rows get labelled with the previous ET date. Verified against fresh season logs (cache disabled, so not staleness; found:true, so not name resolution): Freddie Freeman played Jul 17 and Jul 19 (x2, doubleheader) — NOT Jul 18 Steven Kwan played Jul 18 (x2) and Jul 19 — NOT Jul 17 Settlement was correct to find no game on the labelled date. My void logic then converted a data-labelling bug into destroyed results. FIX: never void on player-absence alone. Voiding now requires POSITIVE evidence — the games themselves postponed/cancelled. Absence returns 'unknown' (reason player_absent_unconfirmed), so the row retries and ages out to 'unrecoverable' at the cap. We cannot distinguish "did not play" from "mislabelled date", so we must not claim DNP. Both terminal states are excluded from the record denominator either way. Window-decay remains genuinely fixed (full season log vs a rolling window), and terminal states still prevent immortal rows. NOT DONE HERE: the 64 wrong voids are still in the table, and the game_date derivation is still wrong at the source. Both are reported for the table — no healing in this order. Suite green, build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA |
||
|
|
d4a6170ffa |
Settlement fix forward: date-targeted resolution + terminal states
Order 1 of 2. Push scoring UNTOUCHED — it is correct. No healing here.
PHASE 1 — DATE-TARGETED FETCH replaces the rolling window for settlement.
settleSource.resolveOutcome() resolves the SPECIFIC DATE and, when the
player is absent, reads GAME STATE to learn what the absence MEANS:
game final + player has a line -> SETTLE (a partial game is a real
result, never a void)
game final + player absent -> VOID (confirmed DNP)
postponed / cancelled -> VOID
scheduled / in progress / SUSPENDED -> PENDING (a suspended game resumes;
voiding it would destroy a real bet)
player played, stat missing -> unknown, NEVER void a real appearance
This is FREE for MLB: mlbStatsAdapter.getPlayerGameLog already returned the
full season log and getPlayerStats was discarding it with .slice(-10).
Settlement now reads fullLog — same request, same cache — which removes
window-decay entirely (the verified failure was a Jul 12 game outside a
last10 starting Jul 6). Projections keep using last10, unchanged.
PHASE 2 — TERMINAL STATES (migration 026 applied). outcome CHECK widened to
hit/miss/push/void/unrecoverable; added settle_attempts, settlement_source,
settlement_version, model_version. A row that cannot be resolved after
SETTLE_ATTEMPT_CAP (4) date-targeted attempts becomes 'unrecoverable'
rather than pending forever. CRITICAL: getModelAggregate now EXCLUDES void
and unrecoverable from the settled selection — it used
.not('outcome','is',null), so without this a void would have counted as a
settled row and silently moved the public record. Verified in the record
calc, not just the settle path.
PHASE 3 — SETTLEMENT-RATE ALARM. zeroSettleAlarm only caught a TOTAL zero
while ~30% of a slate failed quietly (Jul 17: 57/86). opsWatch
.settlementRateAlarm pages when resolved/attempted falls below
SETTLE_RATE_FLOOR (0.8). Voids count as RESOLVED — a void is a legitimate
terminal state — so healthy voiding never pages. Third silent-failure
surface of the night, now closed.
PHASE 4 — VERSION STAMPING. src/config/modelEras.js defines the cutoff
ONCE (2026-07-19T22:50:00Z); migration 026 backfilled pre-cutoff rows as
'pre-retention-unknown' (naming the uncertainty, not implying knowledge);
new rows carry model_version.
Regression caught pre-deploy: getScheduleFn was not injectable, so the
ledger suite hit the real network and HUNG. Now injectable via opts and a
no-op under NODE_ENV=test. The "no row -> pending" test was updated to the
new behaviour deliberately: a missing row on a FINAL game now voids.
Suite 281/3373 green, build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
|
||
|
|
89a2977f57 |
Item 7 — public accuracy reads the CLEAN ledger; BEAT CLOSE hidden until C4
Kev's call: the 30D accuracy surfaces must read TRUTH, not a cache that can't be filtered. My earlier degraded-row exclusion only touched getModelAggregate (Postgres); the public buckets/badge still read outcomeService (Redis outcome log), which counts degraded projection-0 outcomes and has no field to filter on. - /api/accuracy (AccuracyBadge) + /api/ledger/accuracy (buckets/ModelRecord) now source from the clean Postgres ledger aggregate via new ledgerService.getAccuracyView + accuracyBucketsFromAgg (model_value > 0 excludes degraded rows). Same response shapes → no frontend change. Redis outcome log is now read by nothing public; it can age out or be rebuilt. - BEAT CLOSE is a MEASURED-WRONG ZERO: captureClosing re-records the locked line as the "closing" line, so clv is flat on the whole sample and beat_close reads 0% (comparing a number to itself). Full write-up: specs/audit-data/ clv-capture-broken.md (the fix belongs to C4). Until then, beat_close_pct + clv_distribution are SUPPRESSED at the source (getModelAggregate, gated by clvCaptureReliable() / CLV_CAPTURE_RELIABLE=1). Every public surface already renders BEAT CLOSE only when non-null, so they all hide it now — no wrong zero anywhere. HIT RATE (real) is unaffected. Suite 271/3261 green, web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9fc4edf3a9 |
Blast radius: exclude projection<=0 grades from the public model record
The degraded grades (projection=0 → model_value=0) are already settled in the
append-only ledger and must NOT be deleted (Data Semantics law). But their
hit/miss is noise, not model skill — they never had a real projection. So
getModelAggregate now filters `.gt('model_value', 0)` on both the settled and
pending queries: the rows stay in ledger_entries, but leave the public hit_pct /
CLV / per-tier record. `.gt` also drops NULL model_value. Post-fix no such row
can be written (projection<=0 refuses), so this only sheds the historical set.
This is the functional form of the "marking" the work order asked for — the
degraded locks are effectively marked as non-counting without mutating history.
Test builder mocks gained `.gt`; a lock asserts the filter is applied to both
queries. Suite 269/3253 green, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9bafc76092 |
Wave 3: Record by grade tier — shared TierRecord across dashboard, /u, ledger
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> |
||
|
|
d3637e7abd |
S6 (a1): display — the full picture under the grammar
- specs/ROW-GRAMMAR.md: the row grammar law (slot order, color law, mark
law, mobile stacking, no-truncation), locked by tests/unit/rowGrammar.
StatStrip violations fixed: MovementChip before the grade (market
context before model output); ViabilityChips after the archetype
(identity is one contiguous run).
- Line-movement sparklines: intradayRefreshService.trackHistory captures
real {t,line} points per grade (seeded with the lock, deduped when
flat, capped 24) inside the snapshot write-back; StatStrip.LineSparkline
renders at >=3 points (green toward / amber against / dim flat).
- Last-10 dot strips: services/last10Dots (streaksService accessors) ->
/api/snapshot/:sport attaches last10_dots from rosterlogs:{sport};
StatStrip.DotStrip renders vs the LOCKED line, newest first.
- CLV distribution: getModelAggregate emits clv_distribution (7 signed
buckets, outliers clamped) only past the centralized n>=20 gate;
ledger MODEL header renders the bar strip.
- Global search: SearchModal (cmd-K via GlobalHosts + window.__search),
players via /api/players/search per sport + static lib/teams.js
(soccer deliberately absent); Nav search icon + Search first in the
mobile More sheet. Explore tab untouched.
- Landing LCP: fade-up floored at opacity .6 (hero h1 contentful on
first frame, S33 visible-floor rule) + IBM Plex Mono preload:false
(4 decorative font files off the slow-4G critical path).
2654 -> 2698 tests (226 suites) green; web build exit 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4d2b27d1d0 |
Session 61 (pre-merge): live-verification fixes from day-one rows
1. [settlement] armed boot line — settlement WAS scheduled (the tick runs settleAllOutcomes + settleAllLedgers FIRST at every snapshot slot, src/snapshotScheduler.js) but was invisible at boot. Now verifiable from logs forever. 2. Null locked_odds root cause (4 day-one rows, all betmgm SB unders): genuinely absent in the DB — the first-seen book row carried no juice for that side. indexOdds/indexProps now PREFER a book row with both sides priced (still a real row, never synthesized). Display was correct: absent beats wrong. 3. Verified retry-safe: 'Odds data temporarily unavailable' is the odds-api quota gate (oddsService:423) — thrown BEFORE any spend or cache write; runSnapshot errors write nothing; the next cron retries fresh with PropLine first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
d10bb4cce2 |
Session 59: Addendum + work-order 1.6 + Phase 2 + Phase 3 (2352 tests)
Overnight sprint for the Saturday 10 AM ET deploy gate — day one of the
public ledger record locks against freshly posted lines.
Task A — ledger team/opponent (migration 020, applied at 0 rows):
populated in both write paths from the real feed; opponent only when the
player's team matches a game participant (never guessed). Roadmap: Phase
4.5 WNBA ESPN-boxscore settlement (due ~Jul 24) + Phase 5 per-tier
calibration logged.
Task B — work-order 1.6 CLOSED (canonical player keys):
- searchPlayer resolves via nameKey; the old matcher deleted accents
("Sanchez" with acute -> "snchez") and substring-guessed onto the WRONG
player (the mismatched last-10 bug). Ambiguous -> null, never guess.
- Slate JOIN INVARIANT: a graded prop whose player's real team isn't in
the game is dropped (TB player can't render under MIL@PIT) — locked by
tests that fail the suite on regression.
- grades:{sport} TTL 2h -> 6h (expired between 5h cron gaps — the real
cause of /team "No active props" for slate players).
Task C — Phase 2 slate UX: tabs are THE filter (URL ?sport=, deep-linkable,
duplicate legacy tablist removed); cards cap at 6 graded props sorted
A+->F with ALL N READS in-place expander; waiting states show the real
next pipeline run ("Grades post ~6:00 PM ET").
Task D — Phase 3 mobile P0: root cause of vanished 390px nav was HIDE_ON
including '/' (landing had zero navigation) — fixed; html/body overflow-x
contained; GAME LINES collapses to best-line summary + "N BOOKS" expander
below 640px; venue drops before time/pitchers ever truncate.
Live verification: raw ESPN today STILL returns the Jun 13 NYK@SA Finals
game without a date pin; the pinned fetch returns 0 games, 0 off-date.
Backend 2327 -> 2352 tests (202 suites), web build exit 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
d296e40cb6 |
Session 58: Phase 1 — Truth Infrastructure (2327 tests)
ledger_entries is live (migration 019 applied to prod, RLS + NULLS NOT
DISTINCT dedupe verified against the real database). Every grade now
persists, settles against the real result, and carries closing-line value.
- ledgerService: pipeline pre-grade upserts (public model record, user_id
null, idempotent), closing capture on every snapshot (last write before
game start = the close), settlement with SIGNED CLV (over = locked -
closing; beat/faded/flat), 30d model aggregate with the hard n>=20 rule.
- Write paths: snapshotService -> ledger (priority path); Next /api/scan ->
ledger for authenticated users only (anon never touches the public
record). Refused reads write nothing and don't burn a scan.
- Honest refusal (work-order 1.5): no projection => insufficient_data,
grade null, "INSUFFICIENT DATA - no read" UI. The web gradeAdapter no
longer displays the line as the model projection (the audit's
model==line / +0% edge degenerate); the card renders absent states.
projectionFor is sport-aware (l5 -> l20 -> {stat}_per_90 -> xG).
- /ledger: MY READS | MODEL tabs; model header shows hit% + beat-close%
only at n>=20, else RECORD BUILDING + live pending count. ModelRecord
deferred-render strip on landing + player hero. CLV + outcome chips,
revised_from_grade strikethrough (Phase 2.5 ready).
- SYNC (Task 5): thresholds vs SNAPSHOT_EXPECTED_INTERVAL (normal <1.5x,
amber >=1.5x, STALE red >=3x) via /api/snapshot/summary.
- Phase 2.5 logged in specs/vyndr-roadmap.md (build after Phase 3).
- Data-semantics hardening: strict null-safe numeric parsing everywhere a
market value is handled (Number(null)===0 would have fabricated lines).
Backend 2309 -> 2327 tests (201 suites), web build exit 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|