Read integrity, as-of context, and the shadow matchup resolve (A1-A7)

Seven orders of measurement-first repair. The served grade does not move.

A0/A1 — the unordered page walk returned the right COUNT and the wrong ROWS:
410-617 of 2,490 duplicated with an equal number never returned, while
rows.length matched the server exactly. safePaginate orders on a real unique
key, verifies the tuple at runtime, and THROWS on a query error instead of
treating it as end-of-data. Both hits PROVES are withdrawn: they were drawn
through that reader, and defense_by_direction's distinct-n was likely below
the gate floor all along.

A2/A2b — rolled across every reader: 11 FAIL -> 0. Composite keys pulled from
pg_index (the context tables are dated-composite and had no single unique
column). The unordered helper is deleted, not parked.

A3 — ledgerService and retentionService defaulted the SAME env var to
DIFFERENT versions, so no ledger row ever carried the marker eligibility
requires. One source now. model_snapshots settlement moved onto the cron:
15,484 -> 28,894 settled, repaired-champion 0 -> 7,556.

A4 — hitsFactorContext takes an as-of cutoff. Refusal over reconstruction: no
row at-or-before the date means the factor does not apply, never the nearest
row. Live path unchanged, proven 400/400 on real rows.

A5 — factor_inputs freezes what the factor READ, never the multiplier, so an
audit can recompute and check. It also recorded the finding: the three hits
factors have NEVER fired. prop.opponent and prop.opposing_pitcher are read by
the resolver and written by nothing.

A6/A7 — matchupKeys resolves those keys from the posted lineup plus the
schedule's probable pitchers, and fires the factors into a SHADOW freeze:
248 fires on 308 props, 245 of which would move the grade. The served
forecast is untouched. specs/a8-shadow-factor-gate.md pre-registers the test
that decides whether they ever go live.

Nothing is turned on. CALIBRATION_DEPLOYED stays []. Both verdicts stay
withdrawn. 4,772 tests / 371 suites green, web build exit 0, read-integrity
harness 34/34.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-08-11 22:49:56 -04:00
parent 387ae4d54e
commit f61ec6b391
49 changed files with 4874 additions and 308 deletions
+37 -16
View File
@@ -21,6 +21,7 @@
const lp = require('./lowParamCalibrator');
const cal = require('./calibration');
const { paginate } = require('../../utils/safePaginate');
const { knownNumber } = require('../../utils/known');
const MIN_FIT = 200;
@@ -74,24 +75,44 @@ function build(rows, opts = {}) {
};
}
function todayEt() {
return new Intl.DateTimeFormat('en-CA', {
timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit',
}).format(new Date());
}
/**
* The ROW LOAD — exported so the read-integrity harness measures THE REAL
* FUNCTION rather than a restatement of its query.
*
* ── FIX A2 (2026-08-09) — THIS READ WAS 24.8% CORRUPT ────────────────────
* This is the PRIMARY calibrator (calibrationService is only its shadow), and it
* carried the byte-identical defect A1 fixed there: an unordered `.range()` walk,
* plus `if (error || !data) break` swallowing a failed read as end-of-data.
* Measured on production: 617 of 2,490 rows returned twice, an equal number never
* returned, with `rows.length` matching the server count exactly.
*
* Both are now `safePaginate` on the unique `id`. A throw means the read failed;
* `null` from `fromLedger` still means "not enough settled history to fit". Those
* are different states and collapsing them is what hid the defect.
*/
async function loadSettledRows(sb, { sport = 'mlb', stat = 'hits', before = null } = {}) {
const cutoff = before || todayEt();
return paginate(
() => sb.from('ledger_entries')
.select('id, p_win, outcome, game_date, quarantine_reason')
.eq('sport', sport).is('user_id', null).eq('stat', stat)
.in('outcome', ['hit', 'miss']).not('p_win', 'is', null)
.lt('game_date', cutoff),
{ key: 'id', pageSize: 1000, label: `lowParamService.fromLedger(${sport}/${stat})` },
);
}
/** Load settled history and build, POINT-IN-TIME (strictly before today). */
async function fromLedger(sb, { sport = 'mlb', stat = 'hits', before = null, ...opts } = {}) {
if (!sb) return null;
const cutoff = before || new Intl.DateTimeFormat('en-CA', {
timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit',
}).format(new Date());
const rows = [];
for (let from = 0; ; from += 1000) {
const { data, error } = await sb.from('ledger_entries')
.select('p_win, outcome, game_date, quarantine_reason')
.eq('sport', sport).is('user_id', null).eq('stat', stat)
.in('outcome', ['hit', 'miss']).not('p_win', 'is', null)
.lt('game_date', cutoff)
.range(from, from + 999);
if (error || !data || data.length === 0) break;
rows.push(...data);
if (data.length < 1000) break;
}
const cutoff = before || todayEt();
const rows = await loadSettledRows(sb, { sport, stat, before: cutoff });
const clean = rows
.filter((r) => !(r.quarantine_reason || '').startsWith('nontakeable_book'))
.map((r) => ({ p: Number(r.p_win), won: r.outcome === 'hit' ? 1 : 0, date: String(r.game_date) }));
@@ -99,4 +120,4 @@ async function fromLedger(sb, { sport = 'mlb', stat = 'hits', before = null, ...
return built ? { ...built, cutoff } : null;
}
module.exports = { build, fromLedger, MIN_FIT, HOLDOUT_FRACTION };
module.exports = { build, fromLedger, loadSettledRows, MIN_FIT, HOLDOUT_FRACTION };