Layer 3 Step 1: wire the measurement instrument

Step 0 found we have been flying without one. p_win lives only in
model_snapshots, which has 1,000 rows and ZERO settled outcomes; the closing
line lives only in closing_captures, which carries no link to a result; and
ledger_entries, the row that actually settles, carries no probability at all.
So "is the projection calibrated" and "does it beat the market" have never been
answerable — the entire measurable universe was 35 rows recovered by a lossy
in-memory join.

PHASE 0 — closing coverage verified BEFORE reuse, because an instrument built
on a partial close measures a biased subset. closing_captures holds 70,254 rows
of which 13,364 are usable, and the 56,890 refusals are candidates we never
graded plus one-sided prices — not refusals of our props. Coverage on graded
props since capture started is 83/83, 100%. Safe to reuse, with the honest
caveat that capture only began 2026-07-20.

THE FOUR-TUPLE NOW LANDS ON ONE ROW. ledger_entries gains p_win, fair_prob_lock,
archetype_vector and projection_locked_at at LOCK time, and closing_prob plus
closing_captured_at from the append-only capture store. The join is the whole
point: calibration is p_win against outcome, market-comparison is p_win against
the close, and both become plain SQL on one record instead of a join that
silently drops 90% of the rows.

p_win and the archetype vector are IMMUTABLE — written once at lock via the
existing ignoreDuplicates upsert, never re-derived at settle. A re-derivation
would measure a projection we never made.

The archetype is stored as the VECTOR, not the label. "Did archetype-awareness
help?" can only be answered against the axes that were live at grade time, and
a single text column cannot express a blend. A grade with no archetype stores
null rather than an empty object.

HONEST-ABSENT BOTH WAYS. A past game with no usable capture is marked
market_unavailable_reason and never given an imputed line; calibration still
scores on those rows, only market-comparison is absent. And a game that has not
started yet is NOT declared closeless — a close can still arrive, and premature
absence is as dishonest as imputation in the other direction.

One bug caught before it shipped: the scheduler hook iterated a SPORTS
identifier that does not exist in that scope. Inside its try/catch it would have
thrown ReferenceError every tick and silently never run — the instrument would
have looked wired and captured nothing. Now iterates cadence.ALL_SPORTS.

The baseline accrues FORWARD. Historical p_win and closes are gone, discarded
before this existed. Calibration and market-comparison stay honest-absent until
volume accrues.

Tests 3614 passed / 294 suites, web build exit 0. Migration 033 applied.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
This commit is contained in:
Kev
2026-07-20 22:58:30 -04:00
parent 063e9fb3f7
commit c5580f333e
4 changed files with 251 additions and 1 deletions
+15
View File
@@ -286,6 +286,21 @@ function startSnapshotScheduler(opts = {}) {
}
} catch (e) { console.warn('[harness] nightly run failed:', e.message); }
// Session 70 — THE MEASUREMENT INSTRUMENT. Attach the de-vigged CLOSING
// probability to locked rows from the append-only closing_captures, so
// p_win, the close, the archetype vector and the outcome all land on ONE
// joinable record. Runs beside the settle pass; write-once, and a past game
// with no usable capture is marked market-unavailable rather than imputed.
try {
const led = opts.ledgerService || require('./services/ledgerService');
for (const sp of cadence.ALL_SPORTS) {
const r = await led.attachClosingProb(sp, {});
if (r && (r.updated || r.absent)) {
console.log(`[instrument] ${sp} — closes attached ${r.updated}, market-unavailable ${r.absent}`);
}
}
} catch (e) { console.warn('[instrument] closing attach failed:', e.message); }
// Session 68 — LAYER 1 MECHANISM DATA. Nightly full re-pull of the
// ~1,350-row Statcast aggregate set at STATCAST_HOUR_UTC. Backfill and
// refresh are the SAME call, upserted on the natural key, so the job is