main
19 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
08e5c908e6 |
Takeable audit: the ledger is contaminated, and I caused it
READ-ONLY. Nothing enforced or fixed; the five challengers untouched. VERDICT: gaps exist, and one is LIVE CONTAMINATION of the ledger -- the exact table every accruing holdout resolves against. book, locked_odds and the takeable flag ITSELF are being stamped from books you cannot bet: DFS dabble (707 rows, 24% of all rows), offshore bovada (214), onexbet (42), exchange kalshi (7, mean |odds| 1120). 0% before 2026-08-01. 47.9% on 08-01. 42.5% on 08-02. It began the day I widened the books for display. LEAK LOCATED, not inferred: recordPipelineGrades indexes byKey over the FULL display-widened props list, then prefers that prop -- book: (prop && prop.book) || g.book, and locked_odds/takeable both fall back to oddsForSide(prop). The grade is computed on a MODEL book and the ledger row is then re-stamped from whatever book indexed first. The takeable flag is therefore not merely mislabelled: it is computed FROM the contaminated price, so it is wrong on its own terms. The served grade path is clean TODAY (428 grades, 100% MODEL books), so dedupeProps' gate works. But MODEL_BOOKS is NOT a subset of TAKEABLE_BOOKS -- pinnacle is model-eligible and correctly not takeable -- so the projection may anchor to a reference line by design. Harmless while pinnacle returns nothing; live again when it recovers. BLAST RADIUS bounded but growing: 47 contaminated rows have already settled (21% of settled rows since 08-01) and ~700 are still pending and will settle into the holdouts. The damage is mostly ahead of us, which is what makes this urgent rather than historical. NOT VERIFIED and not claimed either way: whether the stored `line` is also contaminated. It traces to the graded prop, but I did not check it end-to-end; the enforcement order should. The prediction-vs-reference distinction HOLDS and must not be collapsed: the prediction target must be takeable, while fair_prob / consensus / edge stay reference. The bug is not the three-way split -- it is that one write path ignores it. Stack sequenced in the plan: (a) takeable enforcement, (b) structural Number(null)===0 guard (hits will re-trigger it -- its 0.5 lines make P(0) the whole game), (c) hits. Carry-forward: tb-v1 verdict, the third pre-registered branch, and the 100s Cloudflare timeout vs a ~115s snapshot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
aa1228ec42 |
tb-v1 report + plan: diagnosis on trial, branch pre-registered
Firing verified on a real prod snapshot: 10/10 total_bases props carry proj_tb_p_over. The snapshot HTTP call returned 524 (Cloudflare's 100s origin timeout vs a ~115s snapshot) but the work completed server-side -- confirmed from the ledger rather than assumed. Face validity is good and diagnostic: means agree almost exactly with the ladder (1.813 vs 1.833), so this is a SHAPE-ONLY intervention, which is what was intended. Component rates are plausible, and Carroll's triples rate (0.112, far above his peers) is a clean check -- he is a speed player and the model sees it. AN OBSERVATION I AM NOT RESOLVING BY EYE: tb-v1 reads systematically LOWER than the ladder (0.424 vs 0.540 at the same mean). That is the expected DIRECTION, since the NB overstates P(>=2) by treating a home run as four accumulating events -- but whether 0.424 is right or an overcorrection is not knowable from face validity. A ~1.8-TB hitter clearing 1.5 empirically sits nearer 45-50%, between the two. I am not claiming tb-v1 is better; the holdout decides. BRANCH PRE-REGISTERED, before the result, so the verdict cannot be reinterpreted afterward: improves -> family-mismatch HOLDS, similarity stays off the critical path, hits is next; does not improve -> hypothesis WRONG and the mean-weakness/similarity branch REOPENS. Also recorded: I hit Number(null)===0 in my own new module -- a null component rate treated as a measured zero, the difference between "never triples" and "we don't know his triple rate". A test caught it. Sixth appearance of this trap in this codebase, and it caught the person writing the warnings about it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
48706210fe |
Diagnose proj-v1.1: concentrated mean failure, NOT a similarity problem
READ-ONLY. Nothing built or fixed; the four challengers untouched. 41% OF THE REPORTED GAP WAS A MEASUREMENT ARTIFACT. p_win is P(graded side); proj_p_over_line is P(over); 31.4% of settled rows are UNDER-graded, so comparing them raw measures the ladder backwards on a third of the sample. Matched + direction-aligned (n=437): 0.252 vs champion 0.352, not 0.108 vs 0.331. The PRODUCT is not making this mistake -- I checked; projectionChallenger normalises both to the over basis deliberately. The error was in the measurement. THE LOSS IS CONCENTRATED. hits (n=245, res 0.060) and total_bases (n=49, res 0.009) are 67% of rows and carry essentially no signal. Everything else is fine or better: walks 0.519 vs champion 0.544, runs mean 0.345 vs 0.392, and on DOUBLES the ladder's mean BEATS the champion's (0.207 vs -0.062). IT IS THE MEAN, NOT THE SHAPE. On the two failing families the mean itself carries no signal (0.052, -0.019) against the champion's 0.158 and 0.085. Where the mean is good the probability is good -- shape follows mean. A HYPOTHESIS I TESTED AND DISPROVED: prediction compression. I expected P(>=1 hit) to sit in a narrow band and fail to rank. It does not -- spread ratio 0.94 overall, 0.80 for hits, 0.94 for total_bases. The ladder has comparable spread; it is spread in a direction uncorrelated with outcomes. Recorded because it was a plausible story the data refused. PRIORS AND PLUMBING CLEAN. proj_factors carries form_rate, combined_multiplier and breakdown on every row; proj_point 100% populated with sane centres (hits 0.830 vs line 0.578). Not the environment-style silent-null failure. NAMED CAUSE (structural, flagged as hypothesis not finding): the count model mismatches those two stats. total_bases is a WEIGHTED SUM (1B..HR = 1..4), so an NB treats one home run as four events and mis-states variance -- and TB has the worst result in the table. hits is BOUNDED BY AT-BATS and mostly traded at 0.5, so almost everything rides on P(0), the region where the wrong family hurts most. walks/runs/doubles ARE genuine low-rate counts and are exactly the ones that work. FIX BRANCH: targeted per-stat fix for hits and total_bases. THIS REMOVES THE MLB SIMILARITY BUILD FROM THE CRITICAL PATH -- that branch assumed a GLOBAL mean weakness, and the mean is fine or better on three of six stat families. Similarity may be worth building later, on evidence, not on this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
f5997778a2 |
Dormant-layer audit: nothing to connect; proj-v1.1 is live and losing
READ-ONLY. Nothing connected, built or wired; the accruing challengers were not touched. "Dormant" meant three different things and in no case is the answer "connect it". DISTRIBUTION LADDER IS NOT DORMANT. projection/distribution.js is consumed by projectionChallenger (proj-v1.1), live on every snapshot at 94.2% coverage (276/293) with 437 settled rows since 2026-07-23. It is a FOURTH accruing challenger, and it is LOSING: resolution 0.108 vs the champion's 0.331. That verdict is no longer thin. It is also PER-STAT and doctrine-correct -- nine distinct league priors (hits 0.90, total_bases 1.45, home_runs 0.15, ...) each feeding a gamma-Poisson posterior into a negative binomial. Correcting the plan: §10.3's "single additive index across hits/Ks/TB" is engine1's GRADE, not this ladder, which made a solved problem look open. SIMILARITY IS WRONG-SPORT. Zero callers, and its weights are NBA vocabulary: pace 0.15, referee_tendency 0.06, lineup_context 0.12, score_state_context 0.05, travel_fatigue 0.08. MLB has no pace and no referees. Connecting it would be the sport-stubbed-in-on-another-sport's- template breach, and it would fail QUIETLY -- missing factors are skipped, so the score would silently collapse onto whatever few dimensions happened to exist. CONSTRUCT, not connect. BAYESIAN WOULD REGRESS THE MODEL. Zero callers, and DISTRIBUTION_SHAPES keys on rbis / runs_scored / strikeouts_batter / outs_recorded / pitcher_strikeouts / walks_allowed / pitches_thrown -- NONE of which are live stat keys (S41: they are rbi / runs / outs / strikeouts). getDistributionShape defaults to 'normal' on an unknown key, so wiring it as-is would model COUNT stats as Gaussian, silently, on most MLB props. It is also superseded by distribution.js. Do not connect; retire or rewrite. DEPENDENCY, inverted: a better mean would help the ladder, but the ladder is already connected and both would-be foundations are unusable -- so this is not "connect similarity first", it is "the ladder is live and underperforming, and strengthening its mean requires BUILDING an MLB similarity layer that does not exist". Next-order pointer moved to diagnosing proj-v1.1: the only candidate already carrying settled evidence, and its diagnosis decides whether the similarity build is worth doing at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
ec815b0e37 |
Matchup axis report + plan reconciled: three challengers now accruing
Records the verification that matters: firing measured on a real prod snapshot rather than inferred. environment 248/293 (84.6%) -- also its FIRST confirmed ledger write, which the previous session could only infer -- and matchup 243/293 (82.9%) on tier batter_own_split. Both were 0/634. Collinearity guard passed at n=243: r = -0.003 vs the projection, +0.074 vs p_win, +0.003 vs line, -0.068 vs environment, -0.150 vs opportunity. The axis is not re-encoding recent form. The nudge distribution is also the right SHAPE -- mean +0.0007, 123 positive / 120 negative -- a balanced two-sided signal; a one-sided distribution would have suggested a sign or baseline error. Plan reconciled in place: arch-v1 condition axes marked firing, three challengers listed with coverage and their own holdout queries, and the next-order pointer moved to connecting the still-dormant layers (similarity, Bayesian, distribution ladder) with archetype_x_archetype as the named alternative. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
cfda597fb5 |
Reconcile MASTER-PLAN to true state; next order = matchup axis
Reconciled in place, not regenerated. Next-order pointer now MATCHUP AXIS with its verified sourcing table, and an explicit note that SOURCE-LINEUPS-first is NOT needed. Marked DONE with their evidence: p_win ranking + edge retirement, calibration DECIDED, MLB isotonic DECIDED (provisional label retracted), grade cap 25->500 (board 7->365+), book widening, S59 invariant armed, environment axis repaired. Records the honest shape of Phase 1: it is further along than the phase table implied, but mostly because the work turned out to be CONNECTION AND REPAIR rather than construction -- the ladder question dissolved, the cap was discarding 95.7% of the slate, and two condition axes were wired but firing on zero rows. Carried forward without softening: WNBA is NOT BUILT rather than failed, and the ruler is MARKET-not-SHARP with PENDING-RECOVERY status until PropLine answers the Pinnacle question -- not to be enshrined as permanent. Remaining ~19 orders, ~9 unblocked. The two accruing verdicts are time, not code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
0d43fb7db8 |
arch-v1 axis audit: env/matchup were dead; environment fixed
Report for the audit + fix already committed. Records the two things worth carrying forward: 1. The environment axis has NOT yet been observed writing to the ledger, and I am not claiming it has. recordPipelineGrades upserts with ignoreDuplicates and dedupes on (user_id, player_key, stat, line, side, game_id) -- correctly, so a re-run never overwrites the original lock. Today's 429 rows predate the fix, so the axis cannot backfill onto them; first ledger observation is tomorrow's slate. What IS directly verified is the resolver (105/120) and the join key (416/416) -- the two things that were actually broken. 2. Matchup is not fixed and is not claimed as fixed. It needs the opposing starter and BOTH hands, and the audit shows three separate absences: oppPitcherByTeam 0, handById 0, bats 0/120. Fixing the pitcher feed without the hands, or the hands without the feed, still produces an axis that fires on zero rows. Also noted: the S59 slate JOIN INVARIANT keys off the same null `team` field, so it is currently inert too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
8a02c75aec |
Step 0 input check: stop before wiring opportunity, and why
READ-ONLY. Live grade path byte-identical -- no layer wired, no threshold
moved, no challenger added, no holdout run.
INPUTS ARE 100% POPULATED (n=80 real MLB props, through the grader's own
path): ab_per_game, rest_days, l5_avg, l20_avg, l10_stddev and
game_count_in_7d all 100%; opp_rank_stat 65% overall and 0% on
stolen_bases. So there is no honest-degradation problem to solve.
FOUR FINDINGS THAT STOP THE WIRING, three of which would have made the
work unmeasurable or wrong:
1. THE PREMISE IS WRONG. There is no built opportunity layer to connect.
ab_per_game is consumed in exactly one place -- analyzeViaEngine1:379,
which renders "4.3 AB/G" on the grade card. engine1 has NO opportunity
or usage factor at all. A projected opportunity was never built;
building one is construction, not connection.
2. THE INPUT IS THE WRONG SHAPE. ab_per_game = season atBats/games. It is
a per-player CONSTANT (measured: varies for 3 of 20 players, and those
cannot be legitimate since the value can't depend on stat_type), so it
can only move all of a player's props together, never separate them.
And it is collinear with the projection: l20_avg = seasonTotal/games,
the SAME denominator, so l20_avg already embeds opportunity
multiplicatively. Adding it additively double-counts.
3. THE REAL INPUT DOES NOT EXIST. depthChartService returns battingOrder:
null for MLB ("the one lineup slot the free schedule feed exposes") and
PropLine /context carries lineup_confirmed as a BOOLEAN, not the order.
4. ARCHITECTURE: wiring it into engine1 would be unmeasurable BY THIS
ORDER'S OWN TEST. Step 2 proves reliability and resolution, both
measured on p_win. engine1 factors move the grade LETTER and never
touch p_win. The layer belongs in probabilityEstimator, which already
adjusts on opp_rank_stat, home_away and a consistency pull.
SEQUENCING IS ALSO STALE: challengerProjection (arch-v1) is already live
with archetype, matchup (platoon) and environment (park) axes, writing
p_win_challenger to the ledger. Step 2 of the order's sequence is partly
done -- and the harness this order needed already exists.
RECOMMENDED INSTEAD, as its own order: an `opportunity` axis on that
harness driven by DRIFT, not level -- recent AB/G (last 5) over season
AB/G. A deviation is not collinear the way the level is. Per-game atBats
is present in the statsapi log rows but MLB_LOG_FIELD never maps it, so it
is a small contained BUILD, which is why it gets its own order. Honest
caveat carried forward: it is still a proxy, not tonight's opportunity.
PROBE BUG RECORDED: the first run reported 0% for every feature including
l5_avg, on a pipeline that had just graded 365 props -- impossible, so the
probe was wrong. getFeatures takes camelCase and returns { features: {} };
I passed snake_case and read the top level. Fixed to call
computeFeaturesForProp. Same class as the earlier silent-false harness: a
measurement that makes working code look broken invites you to "fix"
something that was never broken.
Gates: 4,059 tests / 325 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
|
||
|
|
11b0139481 |
Verify the cap raise on prod: 7 -> 365 graded props
Induced, not projected. DEFAULT_LIMIT=500 produced 365 graded props in 114s (was 7 in 16s) -- 52x the board. All 365 carry a unique forecast_rank and ZERO leak p_win to anonymous callers, so the tier gating holds at 50x the volume. Anon payload 220KB in 0.44s. Stat mix went from three stats to ten. Health green. Measured cost curve via the ?limit= bisect hook: 1->42s, 25->58s, 60->42s, 120->66s, 500->114s. About 42s of that is FIXED overhead (odds fetch, roster logs, archetype classify, retention), paid whether we grade 1 prop or 500 -- grading is the cheap part. MY PRE-FLIGHT ESTIMATE WAS WRONG. I predicted ~72s from per-prop latency measured in isolation, which ignored the fixed cost. Real figure 114s. A FALSE ALARM RECORDED because acting on it would have meant reverting a fix that works: the first induced run 502'd at 13.4s and I hypothesised load -- memory or a proxy timeout under 20x the work. Wrong. A limit=25 run then 502'd in 2 seconds, which no amount of load explains, and both recovered on retry. The 502s were the deploy rolling, not the cap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
6c97f59546 |
WNBA truth correction + THE p_win FLIP (live, rollback armed)
PART A -- WNBA TRUTH CORRECTION (no behaviour change).
WNBA does not "abstain" and is not "anti-predictive". The -0.12 that
produced those words was NBA-template machinery run on WNBA data -- WNBA
has never had its own archetypes, variables, conditions or calibration,
which is precisely the "sport stubbed in on another sport's template"
CLAUDE.md forbids. That is an UNBUILT MODEL'S EXPECTED FAILURE, not a
verdict on the sport; reading it as a verdict would quietly retire a sport
we never actually attempted. Its own build is QUEUED, after MLB.
The guard CODE is unchanged -- FORECAST_RANKED_SPORTS = {'mlb'} and the
inheritance test are correct live safety either way. Only the meaning is
corrected, and generalised into the doctrine-as-a-gate: a sport ranks on
p_win ONLY once its OWN model is built and shown to predict (calibration
AND resolution on its own holdout). Others are held out as NOT-BUILT,
never as failed. Re-labelled across gradeRanking, snapshot route, tests,
MASTER-PLAN and the challenger report.
PART B -- THE FLIP, gated on a full-slate re-run.
The re-run found something better than a bigger sample. An induced
snapshot graded 7 props: gradeAndCacheSlate runs with DEFAULT_LIMIT = 25
and ~72% of those refuse for insufficient_data, while 546 props are
gradeable. So 8 props IS the board, structurally -- not a small sample of
it. Logged as its own finding; the cap is a separate order.
For a statistically meaningful delta I used 11 real historical boards
(n=328, board sizes 14-57): 79.9% of rows move, mean 5.16 places per
board, TOP READ CHANGES ON 9 OF 11 BOARDS. The re-ordering holds at real
board size. Query committed.
FLIPPED:
- rankGrades drops its edge key (safe for every sport: removes a
non-predictive tiebreak without putting p_win in front).
- selectTopGrades leads on forecast_rank, edge key removed.
- flattenToEdgeBoard sorts on forecastRank, not edge -- this board had
edge as its PRIMARY key, so the whole mobile board was ordered by a
quantity measured not to predict.
- forecast_rank threaded onto strip props.
Sports whose model is not built supply no forecast_rank, so their boards
fall through to the unchanged grade chain -- the fallback is the guard.
ROLLBACK ARMED: boards sort by forecast_rank WHEN PRESENT, so
FORECAST_RANK=0 reverts every surface on the next response -- no deploy,
no client release.
Edge is still computed, stored, carried and displayed as a labelled
diagnostic. Retired from ranking, not deleted.
Eight superseded tests updated to strictly stronger INVERSE properties --
they now fail if edge is ever re-introduced as a ranking key, which the
originals could not detect.
Gates: 4,045 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
|
||
|
|
ef4ac60b81 |
Per-sport rank guard + edge diagnostic-only display + delta report
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 |
||
|
|
7140e62b65 |
MLB re-run vs consensus ruler: premise dissolved, isotonic DECIDED
MEASURE-ONLY. No promotion, no flip, no tier spend. Live path
byte-identical: CURRENT_RULER_VERSION still v1_first_book, model still
consumes MODEL_BOOKS only.
MANDATE 1'S PREMISE DOES NOT HOLD. The p_win calibration is
RULER-INDEPENDENT, confirmed two ways: estimateProbability takes
{gameLogs, line, statType, features} and never sees a market price, and
the calibration fits p_win against OUTCOMES. Reliability and resolution
are both p_win-vs-outcome measures, so fair_prob cannot enter either.
There is nothing to re-fit -- the ruler changes edge, CLV and takeable,
not calibration.
I RETRACT MY OWN LABEL. I declared the MLB isotonic result PROVISIONAL
"because it was measured against the bent ruler". That over-applied the
ruler caveat to a measurement the ruler never touched. The result was
never contaminated; it moves PROVISIONAL -> DECIDED, not by re-running but
because the gate I attached does not apply.
RAN THE GENUINELY RULER-DEPENDENT QUESTION INSTEAD -- does a median
consensus rescue EDGE? Timing held constant (both rulers at close; a
lock-time reconstruction joins only 43 rows, and mixing lock-incumbent
with close-consensus would confound WHEN with WHAT).
n=200 MLB settled rows: mean |ruler gap| 0.0085. corr(edge_v1, outcome)
-0.0101; corr(edge_v2, outcome) -0.0220; corr(p_win, outcome) +0.2598.
THE HEADLINE: p_win predicts outcomes at +0.26 while p_win minus the
market predicts nothing under EITHER ruler. Subtracting the market price
destroys the signal -- a direct empirical vindication of the identity now
at the top of CLAUDE.md. Market edge is not merely a poor criterion here;
it is a strictly worse instrument than the raw forecast.
CALIBRATION REFRESH (ruler-independent, but n grew 119 -> 250):
time-forward holdout n=125, reliability 0.0846 (was 0.0939), resolution
0.190 (was 0.123). Both hold and both improved on a fresh later window
the earlier fit never saw. Independent replication.
THE LIMITATION THAT BLOCKS A FULL VERDICT: closing_captures holds only
MODEL books -- exchange quotes were never stored, because normalizeProps
discarded them until yesterday. Mean 1.97 books in the historical join. So
this tested a US-books-median ruler, not the exchange-inclusive consensus
whose live delta showed p90 +10 points. That ruler is UNTESTABLE on
existing data at any n. Per Mandate 4's third outcome: inconclusive, not
forced.
SEPARATE FINDING -- LIVE FEED REGRESSION: pinnacle MLB captures went 4,022
-> 0 on 2026-07-31 and have not returned, while every other book continued
(103,940 captures in the prior 10 days). This also corrects an Order Zero
claim of mine: "no sharp anchor exists in our feed" was accurate for the
day measured but wrong generally -- pinnacle was there until 07-30 with
17,090 two-sided captures. line_type='sharp' is a label in closingCapture
via SHARP_BOOKS, not a separate provider. We had a sharp anchor and lost
it two days ago; not caused by anything in this session.
Both queries committed: scripts/ruler-comparison.sql,
scripts/pwin-timeforward.sql.
Gates: 4,028 tests / 322 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
|
||
|
|
c79528abae |
Order Zero: tier-reality report + widening fingerprint
PHASE 1 resolved on our real keys, and a bad source was discarded on the way: a fetched rendering of PropLine's docs "tier matrix" claimed /odds/closing is 403 on free and that /odds returns prices nulled on free. Both are contradicted by direct observation (200-redacted, and 6,196 two-sided PRICED groups on MLB). Not cited. The report uses only the machine-readable OpenAPI contract and the verbatim detail bodies our keys received. Verdict: every one of the six endpoints behaves exactly as the Free tier's published contract says. error:"upgrade_required" with an explicit required_tier is unambiguous -- NOT a key-permission problem, NOT a plan problem. $9/mo Hobby buys /results + /odds/closing (the CLV instrument) + /movement (steam across 18 books); $19/mo Pro adds the 90-day settlement export. Priced and evidenced; not recommended here -- it is a decision. PHASE 2 fingerprint on the SERVED feed: 5 books -> 13, props rendered 546 -> 2,780 (5.1x), mean 4.22 books/prop. The unflattering half, stated up front: of 2,234 newly-visible props only 698 (31.2%) carry a real non-DFS market price; 1,536 (68.8%) are DFS-only pick'em rows. The honest headline is not "80% of the slate unlocked" -- the board is 5x fuller, about a third of the new depth is real market data, and the rest is pick'em inventory now shown but tagged. PHASE 3 verified: 546 gradeable props, unchanged. CURRENT_RULER_VERSION still v1_first_book. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
f0543b57a4 |
Product identity + widen books for DISPLAY, model input byte-identical
IDENTITY (CLAUDE.md top + MASTER-PLAN header). VYNDR is a PREDICTIVE MODEL: it projects what a player will DO and picks accurately. Market edge is a BYPRODUCT of a good prediction, never the success criterion. Success = the forecast is honest about its own confidence AND still ranks -- calibration and resolution, both. No edge/CLV term belongs in a pass/fail gate; they are diagnostics we report, not thresholds a model must clear. A model tuned to beat a closing line has been fitted to the market instead of to the game. Per-sport doctrine (Phillips 2022, classify by what players DO not by position): each sport is its own model -- own variables, archetypes, conditions, calibration, honest ceiling. Shared across sports: ONLY the Bayesian inference math. Truth Law: no fabricated data; honest-absent over invented; label limitations in-band; provisional stays provisional until re-run; documented is not verified. PHASE 2 -- AGGREGATOR WIDENING (live). normalizeProps now emits every DISPLAY book instead of 5 of 18. Before this we discarded 13 books of our own accord and 64.8% of the MLB slate was invisible to users. Every prop carries book_role (both/takeable/reference/dfs/offshore) so the display layer can say WHAT a price is -- a fixed-payout DFS number and a two-way sportsbook price are not interchangeable objects. Unknown books are still dropped. PHASE 3 -- MODEL GATE (the model does not move). bookRoles splits MODEL_BOOKS (the legacy allow-list, character for character) from DISPLAY_BOOKS. Both model paths re-filter before they pick a line: gradeSlateService.dedupeProps (before first-row-wins AND before the limit) and intradayRefreshService.indexOddsProps (which RE-GRADES at the current line -- without the gate, widening would have silently moved locked lines onto books the model has never been calibrated against). A test asserts the graded set is byte-identical through the widening. CURRENT_RULER_VERSION stays v1_first_book. The gate lifts only when the MLB calibration is re-run on the consensus ruler and v2 is promoted. HONEST FRAMING, recorded in the plan: this is an AGGREGATOR win and it does NOT fix the model. WNBA still abstains -- a model problem, not a coverage problem; it is better covered than MLB. MLB isotonic still provisional. The consensus is MARKET, not SHARP: pinnacle, matchbook and polymarket are 0% on both sports, so no sharp anchor exists in our feed. Two superseded tests updated to stronger properties rather than deleted: roleOf now names the KIND of book, and the normalizer test asserts the display set widens WHILE the model set does not. Gates: 4,027 tests / 322 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 |
||
|
|
1372e6bcf7 |
Order Zero Phases 1-3: keyed verification, ruler_version boundary, report
PHASE 1 (measured on the live prod feed with the real key): - WNBA is NOT thin at the feed -- 4.21 books/prop vs MLB's 3.61. It was allow-list-starved exactly as MLB was. This removes one candidate explanation for its anti-predictive result; it does not explain it, and WNBA stays abstaining. - We cannot see 64.8% of the MLB slate at all (zero admitted books). - Exchanges are real (smarkets 27%, novig 22%, kalshi 15% on MLB) but pinnacle, matchbook and polymarket measured 0% on BOTH sports. There is no sharp anchor for player props. The consensus is a MARKET consensus, not a SHARP one -- recorded as a permanent limitation, not a milestone. - DFS is the trap, quantified: prizepicks covers 82% of MLB props, the highest in the feed. Admitting it "for breadth" would have looked like the biggest available win. Permanently excluded. - Endpoints: /context WORKS and is FREE (umpire, roof, pitcher handedness, lineup confirmation -- richer than what we hand-built). /odds/closing and /movement are REDACTED (full structure, zero prices). /results and /exports/resolved-props are 403. - The $19/mo question is answered: soccer IS graded, ~15 competitions in 30 days (MLS 41k, Liga MX 15k, Brasileirao 12k, UCL/Europa/Conference). Our "soccer grades into a void" is a Pro-tier problem, not a data problem. NBA is absent because it is July -- seasonal, not inferable either way. PHASE 2 delta, corrected: MLB mean +1.50 pts, median 0, p90 +10.0, 17.0% of comparable props move >=5 pts, one-directional (the incumbent prices the over below the exchange-inclusive consensus). WNBA symmetric and tight. The median prop does not move -- the change is a right-skewed minority. That the rulers DIFFER is established; that the new one is BETTER is not, and that is the re-run. PHASE 2 item 6: ledger_entries.ruler_version applied to prod, 1,384 existing rows backfilled to v1_first_book (a statement of fact -- every row to date was produced by the first-book rule). ledgerService stamps CURRENT_RULER_VERSION on new rows. Never pool edge or CLV across it. Repo migration numbering lags prod; 025_ledger_ruler_version.sql records the DDL for review. PHASE 3: MLB isotonic p_win remains PROVISIONAL -- calibrated against v1_first_book, does not promote until re-run on the consensus ruler. NOT LIVE, deliberately: ALLOWED_BOOKS unchanged, served slate byte-identical, CURRENT_RULER_VERSION still v1_first_book, no live path calls consensusRuler. Gates: 4,022 tests passed / 322 suites; next build exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
293367917c |
Order Zero: book-breadth test + accrual clock correction (measure-only)
STEP 0 disproved the premise before any request was fired. PropLine's OpenAPI contract states verbatim that `bookmakers` omitted = ALL books, so proplineAdapter omitting it is correct and always was. Firing a guessed param would have RESTRICTED the response and produced exactly the false negative the order warned about. The real cause is ours: PropLine sends 18 books; oddsNormalizer ALLOWED_BOOKS intersects them at exactly 5 -- which is precisely the "5 MLB books" the 2.18 audit measured. Measured on real public data (no key, no quota): 4.41 books/prop from the feed, 1.50 after our filter, and 12 of 34 props go invisible entirely. Also corrected: "73% single-book" is the long tail of deep props sole-posted by DraftKings or Bovada. On the core props we grade, the market is 10-12 books wide. pinnacle appears on 0 of 40 MLB props -- the independent low-vig references present on 100% of core props are exchanges (novig/smarkets/kalshi). DFS pick'em also covers 100% but is not a market price and must never enter a consensus. Verdict is outcome (d) ALREADY OPEN, not (a)/(b)/(c) -- all three assumed the feed was the constraint. Ruler change scoped (not built): split one allow-list into takeable/reference/excluded, fair_prob_lock becomes a median consensus with n>=2 or a labelled fallback. Gated on exchange price validation + the WNBA measurement, which needs the PropLine key (prod-only, absent locally). MLB isotonic p_win declared PROVISIONAL until re-run on the real ruler. Side finding: we use 1 of 29 endpoints. /odds/closing, /movement, /odds/history, /best-line, /ev, /results, /exports/resolved-props, /context (free) map directly onto documented gaps -- and resolution across 33 sports suggests "no free settled feed for NBA/soccer" may be a $19/mo problem, not a data problem. Documented, not verified. Plan edits: §10.1 rewritten, §10.2/§10.5 corrected, and §11 adds the sequential post-completion accrual clock -- pre-completion data does not count, no pooling across the completion boundary, two clocks stated separately, per-sport clocks, verification gate before any accrual, users onboarded to a complete product only. §9.1's "6-10 weeks out" corrected: that is accrual duration, not distance to the answer. The ruler change independently forces the same no-pooling boundary by arithmetic. No API key was used, printed, or committed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
c98338ef23 |
plan: add §10 — aggregator + paid-model gaps, and the one root cause behind both
Answers "what makes this the top product, not just a finished one."
THE REFRAME: the aggregator gap and the model gap are the SAME gap in two places.
Our "market" is often ONE book — MLB props are 73% single-book, and
proplineAdapter sends only {apiKey, markets} with NO regions/bookmakers param
(:152), so we take PropLine's default response. That single fact causes four
problems we had been treating as unrelated: no line shopping (the category's #1
free hook), a fair_prob_lock that is a de-vigged single soft book rather than a
consensus (the bent ruler the model is judged against), weak CLV (cannot measure
beat-the-close against one book), and no steam/disagreement detection (needs >=2
books to exist).
So the highest-leverage unblocked action in the whole plan is a cheap API test:
does PropLine return more books with a regions/bookmakers param on our tier? One
request, and if it works it upgrades the free product, the model's denominator and
the CLV instrument simultaneously.
Aggregator gaps catalogued: book breadth, true consensus, historical odds archive
(started — closing_captures 844k rows, lock_lines new, but in-grade history capped
at 24 points, so no full open->close series), market breadth (11 live vs the
category's 50+), ingested alt-line ladders, injury/lineup wire, player news.
Paid-model gaps catalogued: distribution instead of a point (distribution.js
already computes survival probabilities and rungs but is proj-v1.1, ledger-only
and lost to the champion); opportunity/playing-time projected FIRST with its own
uncertainty (the single biggest available modelling gain); per-stat models instead
of one additive index; matchup granularity that actually reaches the grade;
applied calibration; a backtest harness (blocked by the archive gap — you cannot
backtest a price you never stored); CLV as north star.
THE PATTERN: almost every model capability is ALREADY BUILT AND DISCONNECTED.
VYNDR does not have a building problem, it has a connection-and-proof problem plus
one genuine ingestion gap that starves both halves. The expensive part is largely
done, but no new feature fixes it.
Ordering principle recorded: get MLB genuinely good BEFORE replicating across six
sports — a copied-six-times thin model is six times the maintenance for the same
absent edge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
|
||
|
|
37ee952e26 |
plan: add §9 — what is actually missing for the product to work, not just be built
The phases counted unbuilt code. This section names what is missing for VYNDR to do what it claims, including the parts that are not builds. THE CENTRAL GAP: there is no demonstrated edge yet. Every measurement this session returned null, negative or unproven — served grade r~0.005 and inverted; all three p_win-vs-fair_prob formulations negative on both sports and both splits; p_win alone on MLB holdout p~0.07; WNBA negative; CLV null by guard; ROI-by-grade likely an artifact. The product's core claim is not currently supported by our own data, and building all 23 orders without closing this leaves a well-built product that does not do the thing it sells. What closes it is sample and honest iteration, not code — roughly 6-10 weeks at the current accrual, a clock engineering cannot shorten and that must not be faked. Also named: the projection is thin (l5/l20 + opponent rank + rest + usage, with similarity/archetypes/conditions/Bayesian all built and disconnected, so connecting them is a hypothesis not a guarantee); it is a one-sport product today (NBA and soccer do not even settle); there are 3 users and 0 paid so nothing is validated by usage; there is NO distribution path at all, which appears in no phase and belongs on the board as its own track; the last mile is unclosed (push-to-book is a teaser, no affiliate live); and operational fragility remains (single box, two-sport settlement, three credentials flagged including a Stripe live key that transited a transcript, no staging). The honest summary: the truth infrastructure is genuinely well built and this codebase does not lie about what it knows. What is not yet true is that the model beats the market — not disproven, unmeasured at adequate n. The finish line is 23 orders PLUS a verdict from accrued data we cannot rush, and the discipline to report that verdict honestly if it says the edge is not there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |
||
|
|
e3ca1650d9 |
plan: specs/MASTER-PLAN.md — single source of truth, 7 phases, ~23 orders, defined END
Consolidation only. Nothing built, wired or promoted. NOTHING WAS RE-VERIFIED and no query was run — all 22 artifacts produced this session plus the completion matrix were taken as KNOWN, per the order's own clause. The verification ledger at the top of the plan lists exactly what was taken as known and which four items remain genuinely open (sport order, board-reasoning gating, the CLV flag, team colours) — each open because it needs a decision or a build, not a query. The plan captures all six tracks in one document: per-sport models (MLB's 8-layer stack with each layer marked BUILT/PARTIAL/NOT-WIRED, plus the sport order), design implementation (61 catalogued items), surfaces, the resolution tail, the sport boundary, and the Chrome audit. The through-line it makes visible: MLB's layers 2, 3, 5 and 6 are BUILT AND NOT CONNECTED, while layer 8 (the grade ladder) is connected and meaningless (r~0.005, inverted). MLB's fix is connection, not construction. Phasing is by dependency: MLB model truth -> resolution tail -> surfaces/design (parallel lane) -> sport boundary -> sport rollout (one order per sport) -> monetization finish -> Chrome audit and hardening. ~23 orders total, ~11 unblocked today, so "how many sessions left" now has a real answer. DEFINITION OF DONE is explicit and countable: MLB layers 1-8 connected with a monotone held-out-proven ladder; every listed sport finished on the same template or explicitly abstaining with its reason recorded; all 61 design items built; every surface reachable and honest; the resolution pipeline firing end-to-end; the sport boundary a registry; the Chrome audit passed; and the record publishable on its own terms with no claim outrunning its evidence. STATE.md now points at the plan and is demoted to history. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc |