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
+172
View File
@@ -0,0 +1,172 @@
'use strict';
/**
* A7 — does the shadow EVIDENCE actually accrue, on the scheduled path?
*
* A6 measured the shadow firing in a hand-run script. That proves the resolver
* works; it does not prove a scheduled snapshot writes anything. This drives the
* REAL `gradeAndCacheSlate` -> onGraded -> rowsFromSides chain with injected
* deps and asserts a `would_fire` block lands on the retention row.
*
* The distinction matters: A5 found factors that were built, correct, and never
* invoked. Evidence that is generated but never persisted is the same failure
* one layer along.
*/
const gradeSlate = require('../../src/services/gradeSlateService');
const retention = require('../../src/services/retentionService');
const ff = require('../../src/services/model/factorFreeze');
const hf = require('../../src/services/model/hitsFactors');
const { nameKey } = require('../../src/utils/playerName');
const JUDGE = nameKey('Aaron Judge');
const SPRAY = {
as_of_date: '2026-08-10', player_key: JUDGE,
pull_gb: 0.30, straight_gb: 0.20, oppo_gb: 0.10,
pull_air: 0.20, straight_air: 0.10, oppo_air: 0.10,
};
const OAA = {
'3B': { oaa: 2 }, SS: { oaa: 1 }, LF: { oaa: 0 }, '1B': { oaa: 0 },
'2B': { oaa: 1 }, RF: { oaa: -1 }, CF: { oaa: 0 },
};
const SPLITS = { vl: { pa: 120, atBats: 110, hits: 33 }, vr: { pa: 300, atBats: 280, hits: 70 } };
/** The LIVE context: no opponent, no pitcher — the measured production state. */
const liveCtx = () => ({
spray: SPRAY, positionOaa: null, bats: 'R', throws: null,
pitcherHardHit: null, platoonSplits: SPLITS, archetype: 'BOMBER', as_of: null,
});
/** The SHADOW context: same indexes, joined through the resolved keys. */
const shadowCtx = () => ({ ...liveCtx(), positionOaa: OAA, throws: 'L', pitcherHardHit: 0.45 });
const KEYS = {
opponent: 'Boston Red Sox', opposing_pitcher: 'Chris Sale',
source: 'lineup_context+schedule', refused: null,
};
/** A resolver with A6's signature: keys omitted = live, keys supplied = shadow. */
const resolver = (prop, sport, keys) => (keys ? shadowCtx() : liveCtx());
const PROP = {
player: 'Aaron Judge', stat_type: 'hits', line: 0.5, book: 'draftkings',
over_odds: -120, under_odds: 100, game_time: '2026-08-10T23:05:00Z',
};
/** A grader that runs the REAL engine freeze/shadow logic in miniature. */
function fakeGrade(base) {
const legacy = {
player: base.player, stat_type: base.stat_type, line: base.line,
direction: base.direction, grade: 'B', confidence: 70, p_win: 0.55,
};
// This mirrors analyzeViaEngine1's A5/A6 block exactly.
if (String(base.stat_type).toLowerCase() === 'hits' && base.factor_context) {
const live = hf.adjustProbability(0.55, base.factor_context);
if (live.p_adjusted != null && live.factors_fired > 0) legacy.p_win = live.p_adjusted;
let shadow = null;
const keys = base.matchup_keys || null;
if (keys && (keys.opponent || keys.opposing_pitcher) && typeof base.factor_context_resolver === 'function') {
const sc = base.factor_context_resolver(base, base.sport, keys);
if (sc) shadow = ff.shadowBlock(sc, hf.hitsFactorMultiplier(sc), keys);
}
legacy.factor_inputs = ff.freeze(base.factor_context, base, shadow);
}
return legacy;
}
async function runSlate(opts = {}) {
const collected = [];
await gradeSlate.gradeAndCacheSlate('mlb', [PROP], {
grade: fakeGrade,
factorContext: resolver,
matchupKeys: opts.matchupKeys !== undefined ? opts.matchupKeys : (() => KEYS),
cacheSet: async () => {},
onGraded: (base, sides) => {
collected.push(...retention.rowsFromSides(base, sides, {
snapshotId: 's1', capturedAt: '2026-08-10T14:00:00Z', sport: 'mlb', gameDate: '2026-08-10',
}));
},
...opts.slate,
});
return collected;
}
describe('the shadow accrues through the REAL slate path', () => {
it('a graded hits row lands with would_fire on it', async () => {
const rows = await runSlate();
expect(rows.length).toBeGreaterThan(0);
const row = rows.find((r) => r.factor_inputs);
expect(row).toBeDefined();
expect(row.factor_inputs.would_fire).not.toBeNull();
expect(row.factor_inputs.would_fire.factors_fired).toBeGreaterThan(0);
expect(row.factor_inputs.would_fire.keys.opponent).toBe('Boston Red Sox');
});
it('the row also carries the LIVE refusal — both states, side by side', async () => {
const rows = await runSlate();
const fi = rows.find((r) => r.factor_inputs).factor_inputs;
expect(fi.position_oaa).toBeNull(); // what the grade saw
expect(fi.throws).toBeNull();
expect(fi.would_fire.shadow_inputs.throws).toBe('L'); // what it would have
});
it('would_fire stays RE-DERIVABLE from what is stored beside it', async () => {
const rows = await runSlate();
const fi = rows.find((r) => r.factor_inputs).factor_inputs;
const re = hf.hitsFactorMultiplier({
spray: fi.spray, bats: fi.bats,
platoonSplits: fi.platoon
? { vl: { pa: fi.platoon.vl.pa, atBats: fi.platoon.vl.ab, hits: fi.platoon.vl.hits },
vr: { pa: fi.platoon.vr.pa, atBats: fi.platoon.vr.ab, hits: fi.platoon.vr.hits } }
: null,
positionOaa: fi.would_fire.shadow_inputs.position_oaa,
throws: fi.would_fire.shadow_inputs.throws,
pitcherHardHit: fi.would_fire.shadow_inputs.pitcher_hard_hit,
});
expect(re.multiplier).toBe(fi.would_fire.multiplier);
});
it('THE SERVED GRADE IS UNTOUCHED — p_win is the unadjusted forecast', async () => {
const rows = await runSlate();
for (const r of rows) expect(Number(r.p_win)).toBe(0.55);
});
it('no matchupKeys resolver => no would_fire, and the grade is the same', async () => {
const rows = await runSlate({ matchupKeys: null });
const fi = rows.find((r) => r.factor_inputs).factor_inputs;
expect(fi.would_fire).toBeNull();
for (const r of rows) expect(Number(r.p_win)).toBe(0.55);
});
it('refused keys => no shadow fire, never a guess', async () => {
const refused = { opponent: null, opposing_pitcher: null, source: null, refused: 'no_lineup_row' };
const rows = await runSlate({ matchupKeys: () => refused });
expect(rows.find((r) => r.factor_inputs).factor_inputs.would_fire).toBeNull();
});
});
describe('the scheduled path carries the shadow (not just a hand-run script)', () => {
const src = require('fs').readFileSync(
require('path').join(__dirname, '..', '..', 'src/services/snapshotService.js'), 'utf8');
it('runSnapshot builds the key index and passes it to the grader', () => {
expect(src).toMatch(/matchupKeys/);
expect(src).toMatch(/gradeAndCacheSlate\(sp, props, \{\s*\n\s*factorContext,\s*\n\s*matchupKeys,/);
});
it('the index is built BEFORE the grade call, not after', () => {
expect(src.indexOf('FIX A6 — THE JOIN KEYS')).toBeLessThan(src.indexOf('await deps.gradeAndCacheSlate('));
});
it('runAllSnapshots (the cron entrypoint) routes through runSnapshot', () => {
expect(src).toMatch(/async function runAllSnapshots/);
expect(src).toMatch(/runSnapshot\(/);
});
it('a key-resolve failure degrades to no-shadow, never breaking the slate', () => {
const i = src.indexOf('FIX A6 — THE JOIN KEYS');
const block = src.slice(i, i + 1600);
expect(block).toMatch(/catch \(e\)/);
expect(block).toMatch(/matchupKeys = null|no key index/);
});
});