'use strict'; /** * read-integrity — measure whether each learning reader returns the rows it thinks. * * Spec: specs/read-integrity-harness.md * * READ-ONLY. It issues SELECTs and a head-count. It writes nothing, anywhere — * a test asserts this file contains no insert/update/upsert/delete/rpc. * * USAGE * node scripts/read-integrity.js # every registered reader * node scripts/read-integrity.js --id prove-hit-factors:136 * node scripts/read-integrity.js --acceptance # AC5 only: known-corrupt + known-clean * * WHAT IT IS FOR: the 2026-08-09 probe proved exposure cannot be reasoned from * source — `challenger-scoreboard` walks 12 pages of a live table and is clean, * `calibrationService.fromLedger` walks 3 pages of the same table and is a * quarter corrupt. So every reader fix must be verified by MEASUREMENT, on the * day, against an ordered control. Never by the presence of an ORDER BY clause. */ const path = require('path'); require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); const { createClient } = require('@supabase/supabase-js'); const ri = require('../src/utils/readIntegrity'); const { uniqueKeyFor } = require('../src/utils/tableKeys'); // The local .env carries a transposed project ref (CLAUDE.md, S76). Allow an // explicit override so a local run reaches the real project. const SB_URL = process.env.READ_INTEGRITY_SUPABASE_URL || process.env.SUPABASE_URL; const SB_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_KEY; const MLB = ['eq', 'sport', 'mlb']; const PUBLIC = ['is', 'user_id', null]; const SETTLED = ['in', 'outcome', ['hit', 'miss']]; const HAS_PWIN = ['not', 'p_win', 'is', null]; const HAS_ARCH = ['not', 'archetype', 'is', null]; const LEDGER_KEY = ['id']; // model_snapshots rows are one per prop PER CYCLE, so identity needs the cycle. const SNAP_KEY = ['id']; /** * THE REGISTRY — every reader that walks pages without a stable order, as it * stands today. Declarative so it is auditable against the cited source line. */ const READERS = [ // ── the hits gate ────────────────────────────────────────────────────── { id: 'prove-hit-factors:136', source: 'scripts/prove-hit-factors.js:136', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, readerRows: (sb) => require('../scripts/prove-hit-factors').READS.ledger(sb), filters: [MLB, PUBLIC, ['eq', 'stat', 'hits'], SETTLED, HAS_PWIN] }, { id: 'prove-hit-factors:131', source: 'scripts/prove-hit-factors.js:131', table: 'model_snapshots', select: 'id', key: SNAP_KEY, readerRows: (sb) => require('../scripts/prove-hit-factors').READS.snaps(sb), filters: [MLB, ['eq', 'stat', 'hits'], HAS_ARCH] }, // ── the other stat gates ─────────────────────────────────────────────── { id: 'prove-tb-factors:154', source: 'scripts/prove-tb-factors.js:154', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, readerRows: (sb) => require('../scripts/prove-tb-factors').READS.ledger(sb), filters: [MLB, PUBLIC, ['eq', 'stat', 'total_bases'], SETTLED, HAS_PWIN] }, { id: 'prove-tb-factors:149', source: 'scripts/prove-tb-factors.js:149', table: 'model_snapshots', select: 'id', key: SNAP_KEY, readerRows: (sb) => require('../scripts/prove-tb-factors').READS.snaps(sb), filters: [MLB, ['eq', 'stat', 'total_bases'], HAS_ARCH] }, { id: 'prove-runs-rbi:200(rbi)', source: 'scripts/prove-runs-rbi.js:200', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, filters: [MLB, PUBLIC, ['eq', 'stat', 'rbi'], SETTLED] }, { id: 'prove-runs-rbi:200(runs)', source: 'scripts/prove-runs-rbi.js:200', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, filters: [MLB, PUBLIC, ['eq', 'stat', 'runs'], SETTLED] }, { id: 'prove-park-weather:54', source: 'scripts/prove-park-weather.js:54', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, readerRows: (sb) => require('../scripts/prove-park-weather').READS.ledger(sb), filters: [MLB, PUBLIC, ['eq', 'stat', 'total_bases'], SETTLED] }, { id: 'pitcher-prove-k:137', source: 'scripts/pitcher-prove-k.js:137', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, readerRows: (sb) => require('../scripts/pitcher-prove-k').READS.ledger(sb), filters: [MLB, PUBLIC, ['eq', 'stat', 'strikeouts'], SETTLED, HAS_PWIN] }, { id: 'cluster-prove:337', source: 'scripts/cluster-prove.js:337', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, readerRows: (sb) => require('../scripts/cluster-prove').READS.ledger(sb), filters: [MLB, PUBLIC, ['eq', 'stat', 'total_bases'], SETTLED, HAS_PWIN] }, { id: 'tb-solo-and-interactions:260', source: 'scripts/tb-solo-and-interactions.js:260', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, readerRows: (sb) => require('../scripts/tb-solo-and-interactions').READS.ledger(sb), filters: [MLB, PUBLIC, ['eq', 'stat', 'total_bases'], SETTLED, HAS_PWIN] }, { id: 'stagea-gate-run:142', source: 'scripts/stagea-gate-run.js:142', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, readerRows: (sb) => require('../scripts/stagea-gate-run').READS.ledger(sb), filters: [MLB, PUBLIC] }, { id: 'skill-v1-stagea:185', source: 'scripts/skill-v1-stagea.js:185', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, readerRows: (sb) => require('../scripts/skill-v1-stagea').READS.ledger(sb), filters: [MLB, PUBLIC, ['eq', 'stat', 'hits'], SETTLED] }, // ── the status/ablation/board readers ────────────────────────────────── { id: 'proven-status:76', source: 'scripts/proven-status.js:76', table: 'model_snapshots', select: 'id', key: SNAP_KEY, readerRows: (sb) => require('../scripts/proven-status').READS.snaps(sb), filters: [MLB, HAS_ARCH] }, { id: 'champion-ablation:173', source: 'scripts/champion-ablation.js:173', table: 'model_snapshots', select: 'id', key: SNAP_KEY, readerRows: (sb) => require('../scripts/champion-ablation').READS.snaps(sb), filters: [MLB, HAS_PWIN, ['not', 'features', 'is', null]] }, { id: 'build-grade-bands:54', source: 'scripts/build-grade-bands.js:54', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, readerRows: (sb) => require('../scripts/build-grade-bands').READS.ledger(sb), filters: [MLB, PUBLIC, ['eq', 'stat', 'hits'], SETTLED, HAS_PWIN] }, { id: 'build-grade-bands:49', source: 'scripts/build-grade-bands.js:49', table: 'model_snapshots', select: 'id', key: SNAP_KEY, readerRows: (sb) => require('../scripts/build-grade-bands').READS.snaps(sb), filters: [MLB, ['eq', 'stat', 'hits'], HAS_ARCH] }, { id: 'challenger-scoreboard:112', source: 'scripts/challenger-scoreboard.js:112', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, filters: [MLB, PUBLIC, SETTLED, HAS_PWIN] }, // ── calibrationService: FIXED in A1, and measured through the REAL function ── // `readerRows` calls the production `loadSettledRows`, so a PASS here means the // shipped code returned a set identical to the control — not that its query was // re-declared correctly in this registry. { id: 'calibrationService.fromLedger:110', source: 'src/services/model/calibrationService.js:110', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, live: true, readerRows: (sb) => require('../src/services/model/calibrationService') .loadSettledRows(sb, { sport: 'mlb', stat: 'hits' }), filters: [MLB, PUBLIC, ['eq', 'stat', 'hits'], SETTLED, HAS_PWIN, ['lt', 'game_date', todayEt()]] }, // The PRIMARY calibrator (calibrationService is only its shadow). FIXED in A2. { id: 'lowParamService.fromLedger:78', source: 'src/services/model/lowParamService.js:78', table: 'ledger_entries', select: 'id', key: LEDGER_KEY, live: true, readerRows: (sb) => require('../src/services/model/lowParamService') .loadSettledRows(sb, { sport: 'mlb', stat: 'hits' }), filters: [MLB, PUBLIC, ['eq', 'stat', 'hits'], SETTLED, HAS_PWIN, ['lt', 'game_date', todayEt()]] }, // ── context tables — A2b: composite keys from the live schema ────────── { id: 'context:statcast_aggregates', source: 'scripts/prove-hit-factors.js:104', table: 'statcast_aggregates', select: '*', key: uniqueKeyFor('statcast_aggregates'), readerRows: (sb) => require('../scripts/prove-hit-factors').READS.statcast(sb), filters: [MLB] }, { id: 'context:batter_spray', source: 'scripts/prove-hit-factors.js:111', table: 'batter_spray', select: '*', key: uniqueKeyFor('batter_spray'), readerRows: (sb) => require('../scripts/prove-hit-factors').READS.spray(sb), filters: [MLB] }, { id: 'context:platoon_splits', source: 'scripts/prove-hit-factors.js:119', table: 'platoon_splits', select: '*', key: uniqueKeyFor('platoon_splits'), readerRows: (sb) => require('../scripts/prove-hit-factors').READS.platoon(sb), filters: [MLB] }, { id: 'context:team_defense', source: 'scripts/prove-hit-factors.js:127', table: 'team_defense', select: '*', key: uniqueKeyFor('team_defense'), readerRows: (sb) => require('../scripts/prove-hit-factors').READS.defense(sb), filters: [MLB] }, { id: 'context:park_dimensions', source: 'scripts/prove-tb-factors.js:138', table: 'park_dimensions', select: '*', key: uniqueKeyFor('park_dimensions'), readerRows: (sb) => require('../scripts/prove-tb-factors').READS.parks(sb), filters: [MLB] }, { id: 'context:game_context', source: 'scripts/prove-tb-factors.js:142', table: 'game_context', select: 'game_id, venue_id, wx_temp_f, wx_wind_speed_mph, wx_wind_direction_deg', key: uniqueKeyFor('game_context'), readerRows: (sb) => require('../scripts/prove-tb-factors').READS.gameCtx(sb), filters: [] }, { id: 'context:hitter_opportunity', source: 'scripts/prove-runs-rbi.js:183', table: 'hitter_opportunity', select: '*', key: uniqueKeyFor('hitter_opportunity'), readerRows: (sb) => require('../scripts/prove-runs-rbi').READS.opportunity(sb), filters: [MLB] }, // lineup_context has no paginated READER today (lineupContextService writes it); // measured as-written so its exposure is on the record rather than assumed. { id: 'context:lineup_context', source: '(no paginated reader — write-only today)', table: 'lineup_context', select: 'as_of_date, sport, game_pk, player_key', key: uniqueKeyFor('lineup_context'), filters: [MLB] }, // ── A2b: the three legacy reads, now converted ──────────────────────── { id: 'proven-status:66', source: 'scripts/proven-status.js:66', table: 'ledger_entries', select: 'id', key: uniqueKeyFor('ledger_entries'), readerRows: (sb) => require('../scripts/proven-status').READS_LEDGER.ledgerAll(sb), filters: [MLB, PUBLIC] }, { id: 'proven-status:85', source: 'scripts/proven-status.js:85', table: 'ledger_entries', select: 'id', key: uniqueKeyFor('ledger_entries'), readerRows: (sb) => require('../scripts/proven-status').READS_LEDGER.ledgerSettled(sb), filters: [MLB, PUBLIC, SETTLED] }, { id: 'champion-ablation:176', source: 'scripts/champion-ablation.js:176', table: 'ledger_entries', select: 'id', key: uniqueKeyFor('ledger_entries'), readerRows: (sb) => require('../scripts/champion-ablation').READS_LEDGER.ledger(sb), filters: [MLB, PUBLIC, SETTLED] }, // ── A2b: found during the sweep — the last unordered ledger walker ──── // Measured 24.7% before conversion; converted in the same pass because it is // the identical defect and the fix is the identical one-liner. { id: 'calibrate-hits:40', source: 'scripts/calibrate-hits.js:40', table: 'ledger_entries', select: 'id', key: uniqueKeyFor('ledger_entries'), readerRows: (sb) => require('../scripts/calibrate-hits').READS.ledger(sb), filters: [MLB, PUBLIC, ['eq', 'stat', 'hits'], SETTLED, HAS_PWIN] }, // ── A2b: the two WRITE-scripts, previously unmeasured ───────────────── { id: 'backfill-context:55', source: 'scripts/backfill-context.js:55', table: 'ledger_entries', select: 'id', key: uniqueKeyFor('ledger_entries'), writes: 'platoon_splits', readerRows: (sb) => require('../scripts/backfill-context').READS.ledger(sb), filters: [MLB, PUBLIC, ['in', 'stat', ['hits', 'total_bases']], SETTLED] }, { id: 'backfill-context:64', source: 'scripts/backfill-context.js:64', table: 'platoon_splits', select: 'as_of_date, sport, season, player_key', key: uniqueKeyFor('platoon_splits'), writes: 'platoon_splits', readerRows: (sb) => require('../scripts/backfill-context').READS.platoonHeld(sb), filters: [MLB] }, { id: 'reconstruct-game-environment:61', source: 'scripts/reconstruct-game-environment.js:61', table: 'ledger_entries', select: 'id', key: uniqueKeyFor('ledger_entries'), writes: 'game_context', readerRows: (sb) => require('../scripts/reconstruct-game-environment').READS.ledger(sb), filters: [MLB, PUBLIC, ['in', 'stat', ['hits', 'total_bases']], SETTLED] }, ]; function todayEt() { return new Intl.DateTimeFormat('en-CA', { timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit', }).format(new Date()); } /** Build the three queries a spec needs. Read-only by construction. */ function bindings(sb, spec) { const base = () => ri.applyFilters(sb.from(spec.table).select(spec.select), spec.filters); // A2b — the CONTROL orders by the FULL unique tuple. Ordering it on a prefix // would leave the control itself tie-scrambled, and a control that cannot be // trusted must never issue a PASS. const orderCols = spec.key || ['id']; return { exactCount: async () => { const { count, error } = await ri.applyFilters( sb.from(spec.table).select(orderCols[0], { count: 'exact', head: true }), spec.filters); if (error) throw new Error(`count: ${error.message}`); return count; }, // ARM (a): exactly as the reader stands — no order. fetchUnordered: async (from, to) => { const { data, error } = await base().range(from, to); if (error) throw new Error(`unordered: ${error.message}`); return data; }, // ARM (b): identical + a stable order. The CONTROL, which must itself // reconcile to the server count before any PASS is issued. fetchOrdered: async (from, to) => { let q = base(); for (const c of orderCols) q = q.order(c, { ascending: true }); const { data, error } = await q.range(from, to); if (error) throw new Error(`ordered: ${error.message}`); return data; }, // Once a reader is fixed, arm (a) becomes the REAL function's output. ...(spec.readerRows ? { readerRows: () => spec.readerRows(sb) } : {}), }; } async function main() { if (!SB_URL || !SB_KEY) throw new Error('SUPABASE_URL / service key required'); const sb = createClient(SB_URL, SB_KEY, { auth: { persistSession: false } }); const argv = process.argv.slice(2); const only = argv.includes('--id') ? argv[argv.indexOf('--id') + 1] : null; const acceptance = argv.includes('--acceptance'); let specs = READERS; if (only) specs = READERS.filter((s) => s.id === only); if (acceptance) { specs = READERS.filter((s) => ['prove-hit-factors:136', 'challenger-scoreboard:112'].includes(s.id)); } const results = []; for (const spec of specs) { const r = await ri.measure(spec, bindings(sb, spec)); results.push(r); process.stderr.write(` ${r.verdict.padEnd(15)} ${r.id} — ${r.corruption_pct ?? '?'}%\n`); } const summary = { run_at: new Date().toISOString(), readers_measured: results.length, pass: results.filter((r) => r.verdict === ri.VERDICT.PASS).length, fail: results.filter((r) => r.verdict === ri.VERDICT.FAIL).length, other: results.filter((r) => ![ri.VERDICT.PASS, ri.VERDICT.FAIL].includes(r.verdict)).length, worst_corruption_pct: results.reduce((m, r) => Math.max(m, r.corruption_pct || 0), 0), }; if (acceptance) { // AC5 — the harness must reproduce the probe or it is not trustworthy. const corrupt = results.find((r) => r.id === 'prove-hit-factors:136'); const clean = results.find((r) => r.id === 'challenger-scoreboard:112'); summary.acceptance = { known_corrupt_reader: corrupt && corrupt.id, known_corrupt_pct: corrupt && corrupt.corruption_pct, known_corrupt_in_16_to_25_band: !!corrupt && corrupt.corruption_pct >= 16 && corrupt.corruption_pct <= 25, known_clean_reader: clean && clean.id, known_clean_pct: clean && clean.corruption_pct, known_clean_is_zero: !!clean && clean.corruption_pct === 0, }; summary.acceptance.AC5_PASSES = summary.acceptance.known_corrupt_in_16_to_25_band && summary.acceptance.known_clean_is_zero; } console.log(JSON.stringify({ summary, results }, null, 2)); } if (require.main === module) { main().then(() => process.exit(0)).catch((e) => { console.error('read-integrity FAILED:', e.message); process.exit(1); }); } module.exports = { READERS, bindings };