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
+144
View File
@@ -0,0 +1,144 @@
'use strict';
/**
* calibrationService.loadSettledRows — the Fix A1 regression.
*
* This reader measured 24.8% corrupt on production (617 of 2,490 rows returned
* twice, an equal number never returned) via two independent defects: an
* unordered page walk, and a query error swallowed as end-of-data. Both are
* locked here against a fake `sb`, so the regression cannot return silently.
*/
const calSvc = require('../../src/services/model/calibrationService');
/**
* A fake PostgREST client for `ledger_entries`. Every filter in the reader's
* chain is accepted and RECORDED, so a dropped filter fails the test rather than
* quietly changing the row set.
*/
function fakeSb(rows, opts = {}) {
const state = { rows, filters: [], selected: null, rangeCalls: 0, ordered: [] };
const q = {
select(cols) { state.selected = cols; return q; },
eq(c, v) { state.filters.push(['eq', c, v]); return q; },
is(c, v) { state.filters.push(['is', c, v]); return q; },
in(c, v) { state.filters.push(['in', c, v]); return q; },
not(c, o, v) { state.filters.push(['not', c, o, v]); return q; },
lt(c, v) { state.filters.push(['lt', c, v]); return q; },
order(c, o) { state.ordered.push([c, o && o.ascending !== false]); return q; },
async range(from, to) {
state.rangeCalls += 1;
if (opts.errorOnRange && opts.errorOnRange(from)) {
return { data: null, error: { message: 'connection reset by peer' } };
}
if (opts.beforeRange) opts.beforeRange(from, state);
const key = state.ordered.length ? state.ordered[state.ordered.length - 1][0] : null;
const out = key
? [...state.rows].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0))
: [...state.rows];
return { data: out.slice(from, to + 1), error: null };
},
};
return { state, from() { return q; } };
}
const led = (id, p, won, date) => ({
id, p_win: p, outcome: won ? 'hit' : 'miss', game_date: date, quarantine_reason: null,
});
const many = (n) => Array.from({ length: n }, (_, i) => led(i + 1, 0.5 + (i % 40) / 100, i % 2 === 0, '2026-08-01'));
describe('loadSettledRows — orders on a UNIQUE key, every page', () => {
it('returns every row exactly once across a multi-page walk', async () => {
const sb = fakeSb(many(2490));
const rows = await calSvc.loadSettledRows(sb, { sport: 'mlb', stat: 'hits', before: '2026-08-09' });
expect(rows).toHaveLength(2490);
expect(new Set(rows.map((r) => r.id)).size).toBe(2490);
expect(sb.state.rangeCalls).toBe(3);
});
it('orders by id on EVERY page, not just the first', async () => {
const sb = fakeSb(many(2490));
await calSvc.loadSettledRows(sb, { before: '2026-08-09' });
expect(sb.state.ordered).toHaveLength(sb.state.rangeCalls);
for (const [col, asc] of sb.state.ordered) {
expect(col).toBe('id'); // a UNIQUE key — an order on a non-unique
expect(asc).toBe(true); // column still lets ties scramble
}
});
it('selects id — without it the ordering key is not in the payload', async () => {
const sb = fakeSb(many(10));
await calSvc.loadSettledRows(sb, { before: '2026-08-09' });
expect(sb.state.selected).toMatch(/\bid\b/);
});
it('keeps the point-in-time cut and every identity filter', async () => {
const sb = fakeSb(many(10));
await calSvc.loadSettledRows(sb, { sport: 'mlb', stat: 'hits', before: '2026-08-09' });
expect(sb.state.filters).toEqual(expect.arrayContaining([
['eq', 'sport', 'mlb'],
['is', 'user_id', null],
['eq', 'stat', 'hits'],
['in', 'outcome', ['hit', 'miss']],
['not', 'p_win', 'is', null],
['lt', 'game_date', '2026-08-09'], // STRICTLY before — never the answer
]));
});
});
describe('loadSettledRows — a failed read THROWS, never a short fit', () => {
it('throws when a later page errors instead of returning the earlier pages', async () => {
// The :117 defect returned 1,000 rows here and `build()` fitted a map on
// them, indistinguishable from a complete fit.
const sb = fakeSb(many(2490), { errorOnRange: (from) => from === 1000 });
await expect(calSvc.loadSettledRows(sb, { before: '2026-08-09' }))
.rejects.toThrow(/read failed at range 1000-1999/);
});
it('propagates through fromLedger — a read failure is not "thin history"', async () => {
// null means "not enough settled rows to fit"; a throw means "the read
// broke". Collapsing them is what let a fragment look like a fit.
const sb = fakeSb(many(2490), { errorOnRange: (from) => from === 1000 });
await expect(calSvc.fromLedger(sb, { sport: 'mlb', stat: 'hits', before: '2026-08-09' }))
.rejects.toThrow(/read failed/);
});
it('still returns null (not a throw) when there is genuinely no client', async () => {
await expect(calSvc.fromLedger(null, {})).resolves.toBeNull();
});
});
describe('loadSettledRows — concurrent append (the production condition)', () => {
it('returns every pre-existing row exactly once while the cron appends mid-walk', async () => {
// ledger_entries is written at 14/19/22/1/3 UTC. With a stable ascending
// unique key, an appended row lands after the cursor: nothing already
// returned can repeat, nothing pending can be skipped.
const original = many(2500);
let nextId = 90000;
const sb = fakeSb([...original], {
beforeRange: (from, state) => {
if (from > 0) state.rows.push(led(nextId++, 0.6, true, '2026-08-08'));
},
});
const rows = await calSvc.loadSettledRows(sb, { before: '2026-08-09' });
const seen = rows.map((r) => r.id);
expect(new Set(seen).size).toBe(seen.length); // zero duplicates
for (const o of original) expect(seen).toContain(o.id); // zero dropped
});
it('THROWS rather than returning a corrupted set if the walk ever repeats a row', async () => {
// Belt and braces: if the ordering guarantee were ever lost, the reader
// refuses instead of handing a duplicated sample to the fit.
const sb = fakeSb([led(1, 0.5, true, '2026-08-01'), led(1, 0.5, true, '2026-08-01')]);
await expect(calSvc.loadSettledRows(sb, { before: '2026-08-09' }))
.rejects.toThrow(/duplicate key \(id\)=1/);
});
});
describe('Fix A1 does not change what is SERVED', () => {
it('the deploy set is still empty — no stat serves a calibrated number', () => {
const snapshotService = require('../../src/services/snapshotService');
expect(snapshotService.CALIBRATION_DEPLOYED).toEqual([]);
});
});
+167
View File
@@ -0,0 +1,167 @@
'use strict';
/**
* factorFreeze — Fix A5.
*
* Freezes the RAW inputs a factor read, so a re-audit reads evidence instead of
* reconstructing context. The multiplier is deliberately NOT stored: a stored
* multiplier can only be compared to itself.
*/
const ff = require('../../src/services/model/factorFreeze');
const hf = require('../../src/services/model/hitsFactors');
const SPRAY = {
as_of_date: '2026-08-07', player_key: 'aaron 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 },
};
const FULL_CTX = {
spray: SPRAY, positionOaa: OAA, bats: 'R', throws: 'L',
pitcherHardHit: 0.42, platoonSplits: SPLITS, archetype: 'BOMBER', as_of: '2026-08-07',
};
const PROP = {
player: 'Aaron Judge', stat_type: 'hits', team: 'NYY',
opponent: 'Red Sox', opposing_pitcher: 'Chris Sale',
game_id: 'mlb:2026-08-07:BOS@NYY', game_date: '2026-08-07',
};
describe('INPUTS, NEVER OUTPUTS', () => {
it('stores the raw inputs', () => {
const f = ff.freeze(FULL_CTX, PROP);
expect(f.spray.pull_gb).toBe(0.30);
expect(f.position_oaa).toEqual(OAA);
expect(f.bats).toBe('R');
expect(f.throws).toBe('L');
expect(f.pitcher_hard_hit).toBe(0.42);
expect(f.platoon.vl).toEqual({ pa: 120, ab: 110, hits: 33 });
});
it('does NOT store the multiplier — it must stay recomputable, not asserted', () => {
const f = ff.freeze(FULL_CTX, PROP);
const flat = JSON.stringify(f);
expect(f.multiplier).toBeUndefined();
expect(f.factors_fired).toBeUndefined();
expect(flat).not.toMatch(/multiplier/);
});
it('carries the join keys AS THE RESOLVER SAW THEM', () => {
const f = ff.freeze(FULL_CTX, PROP);
expect(f.join).toEqual({ opponent: 'Red Sox', opposing_pitcher: 'Chris Sale' });
});
it('records the join keys as NULL when the resolver had none — the finding, not an omission', () => {
// This is the live case: nothing sets prop.opponent / prop.opposing_pitcher.
const f = ff.freeze({ spray: SPRAY, positionOaa: null, bats: 'R', throws: null, pitcherHardHit: null, platoonSplits: SPLITS }, { player: 'x', team: 'NYY', game_id: 'g1' });
expect(f.join).toEqual({ opponent: null, opposing_pitcher: null });
expect(f.position_oaa).toBeNull();
expect(f.throws).toBeNull();
expect(f.pitcher_hard_hit).toBeNull();
// and what COULD have been joined is recorded separately, never fed to a factor
expect(f.available).toEqual({ team: 'NYY', game_id: 'g1', game_date: null });
});
it('is versioned, so a shape change is distinguishable from a null', () => {
expect(ff.freeze(FULL_CTX, PROP).v).toBe(ff.FREEZE_VERSION);
});
it('returns null for no context rather than an empty husk', () => {
expect(ff.freeze(null, PROP)).toBeNull();
});
it('carries the archetype through (A4), never invents it', () => {
expect(ff.freeze(FULL_CTX, PROP).archetype).toBe('BOMBER');
expect(ff.freeze({ ...FULL_CTX, archetype: null }, PROP).archetype).toBeNull();
});
});
describe('RECOMPUTE — the reason inputs beat an output', () => {
it('a frozen vector reproduces the live multiplier EXACTLY', () => {
const live = hf.hitsFactorMultiplier(FULL_CTX);
const fromFrozen = ff.recompute(ff.freeze(FULL_CTX, PROP));
expect(fromFrozen.multiplier).toBe(live.multiplier);
expect(fromFrozen.factors_fired).toBe(live.factors_fired);
expect(fromFrozen.applied.map((a) => a.factor).sort())
.toEqual(live.applied.map((a) => a.factor).sort());
});
it('reproduces the live multiplier on the DEGRADED live shape too', () => {
const degraded = { spray: SPRAY, positionOaa: null, bats: 'R', throws: null, pitcherHardHit: null, platoonSplits: SPLITS };
const live = hf.hitsFactorMultiplier(degraded);
const fromFrozen = ff.recompute(ff.freeze(degraded, {}));
expect(fromFrozen.multiplier).toBe(live.multiplier);
expect(live.factors_fired).toBe(0); // the measured live state
expect(fromFrozen.factors_fired).toBe(0);
});
it('a frozen vector that fires would be CAUGHT if the factor code changed', () => {
// The audit property: recompute uses the CURRENT factor code against the
// FROZEN inputs, so a code change shows up as a difference.
const frozen = ff.freeze(FULL_CTX, PROP);
const fake = { hitsFactorMultiplier: () => ({ multiplier: 1.99, applied: [], skipped: [], factors_fired: 0 }) };
expect(ff.recompute(frozen, { hitsFactors: fake }).multiplier).toBe(1.99);
expect(ff.recompute(frozen).multiplier).not.toBe(1.99);
});
it('recompute(null) is null', () => {
expect(ff.recompute(null)).toBeNull();
});
});
describe('RECORDING IS NOT COMPUTING — the served grade cannot move', () => {
it('freezing does not mutate the context the factor uses', () => {
const ctx = JSON.parse(JSON.stringify(FULL_CTX));
const before = JSON.stringify(ctx);
ff.freeze(ctx, PROP);
expect(JSON.stringify(ctx)).toBe(before);
});
it('the grade path writes factor_inputs but leaves pOver alone', () => {
const src = require('fs').readFileSync(
require('path').join(__dirname, '..', '..', 'src/services/intelligence/analyzeViaEngine1.js'), 'utf8');
// Window from the A5 marker to the end of the factor block. A fixed byte
// count was brittle — A6 inserted the shadow resolve inside it and pushed
// the assignment past the window, failing a test whose subject had not moved.
const start = src.indexOf('FIX A5 — FREEZE THE INPUTS');
const block = src.slice(start, src.indexOf('const pWin = dir ===', start));
expect(block).toMatch(/legacy\.factor_inputs = /);
// The recording path must never assign the served probability.
expect(block).not.toMatch(/pOver\s*=[^=]/);
});
it('retention persists it as inputs, not as a multiplier', () => {
const src = require('fs').readFileSync(
require('path').join(__dirname, '..', '..', 'src/services/retentionService.js'), 'utf8');
expect(src).toMatch(/factor_inputs: s\.factor_inputs \|\| null/);
expect(src).not.toMatch(/factor_multiplier:/);
});
});
describe('retentionService carries the frozen vector onto the row', () => {
const ret = require('../../src/services/retentionService');
const ctx = { snapshotId: 's1', capturedAt: '2026-08-10T14:00:00Z', sport: 'mlb', gameDate: '2026-08-10' };
it('a graded hits side lands with its factor_inputs', () => {
const frozen = ff.freeze(FULL_CTX, PROP);
const rows = ret.__testing
? []
: ret.rowsFromSides({ player: 'Aaron Judge', stat_type: 'hits', line: 0.5, game_time: '2026-08-10T23:00:00Z' },
[{ direction: 'over', grade: 'B', p_win: 0.55, factor_inputs: frozen }], ctx);
expect(rows[0].factor_inputs).toEqual(frozen);
expect(rows[0].factor_inputs.multiplier).toBeUndefined();
});
it('is null when the grader had no factor context', () => {
const rows = ret.rowsFromSides({ player: 'A', stat_type: 'rbi', line: 0.5, game_time: '2026-08-10T23:00:00Z' },
[{ direction: 'over', grade: 'C', p_win: 0.5 }], ctx);
expect(rows[0].factor_inputs).toBeNull();
});
});
+191
View File
@@ -0,0 +1,191 @@
'use strict';
/**
* hitsFactorContext as-of — Fix A4.
*
* The contamination this locks out: without a cutoff, `latestBy` returns TODAY'S
* dated row, so auditing a 2026-08-07 grade would join a profile built from
* games played after it. A clean read of the wrong date is still contamination.
*
* Fake client throughout; nothing here touches a network.
*/
const ctxSvc = require('../../src/services/model/hitsFactorContext');
const hf = require('../../src/services/model/hitsFactors');
const { nameKey } = require('../../src/utils/playerName');
// Derived, never hardcoded: nameKey resolves nicknames ('Chris' -> 'christopher'),
// so a literal key would silently miss and look like an honest absence.
const BATTER = nameKey('Aaron Judge');
const PITCHER = nameKey('Chris Sale');
/** Records every filter so an unapplied as-of bound fails the test. */
function fakeSb(tables) {
const calls = [];
return {
calls,
from(table) {
const state = { table, lte: null, eq: {} };
calls.push(state);
const q = {
select() { return q; },
eq(c, v) { state.eq[c] = v; return q; },
lte(c, v) { if (c === 'as_of_date') state.lte = v; return q; },
order() { return q; },
async range(from, to) {
let rows = tables[table] || [];
if (state.lte) rows = rows.filter((r) => String(r.as_of_date) <= state.lte);
return { data: rows.slice(from, to + 1), error: null };
},
};
return q;
},
};
}
const spray = (d, key, id) => ({
as_of_date: d, sport: 'mlb', season: 2026, source_id: id, player_key: key,
pull_gb: 0.3, straight_gb: 0.2, oppo_gb: 0.1, pull_air: 0.2, straight_air: 0.1, oppo_air: 0.1,
});
// positionOaa is { position: { oaa, fielders } } — not a bare number.
const defense = (d, team) => ({
as_of_date: d, sport: 'mlb', season: 2026, team,
position_oaa: {
'3B': { oaa: 2 }, SS: { oaa: 1 }, LF: { oaa: 0 }, '1B': { oaa: 0 },
'2B': { oaa: 1 }, RF: { oaa: -1 }, CF: { oaa: 0 },
},
});
const platoon = (d, key) => ({
as_of_date: d, sport: 'mlb', season: 2026, player_key: key,
vl_pa: 120, vl_ab: 110, vl_hits: 33, vr_pa: 300, vr_ab: 280, vr_hits: 70,
});
const sc = (d, key, id, role, extra = {}) => ({
as_of_date: d, sport: 'mlb', season: 2026, source_id: id, role, player_key: key,
bats: 'R', throws: 'L', hard_hit_pct: 40, ...extra,
});
const TABLES = {
batter_spray: [spray('2026-08-05', BATTER, 1), spray('2026-08-09', BATTER, 1)],
team_defense: [defense('2026-08-05', 'Red Sox'), defense('2026-08-09', 'Red Sox')],
platoon_splits: [platoon('2026-08-05', BATTER), platoon('2026-08-09', BATTER)],
statcast_aggregates: [sc(null, BATTER, 1, 'batter'), sc(null, PITCHER, 2, 'pitcher', { hard_hit_pct: 30 })],
statcast_history: [
sc('2026-08-05', BATTER, 1, 'batter'), sc('2026-08-09', BATTER, 1, 'batter'),
sc('2026-08-05', PITCHER, 2, 'pitcher', { hard_hit_pct: 30 }),
sc('2026-08-09', PITCHER, 2, 'pitcher', { hard_hit_pct: 99 }), // today's value
],
};
const PROP = { player: 'Aaron Judge', opponent: 'Red Sox', opposing_pitcher: 'Chris Sale' };
describe('as-of bound is APPLIED, and only when asked', () => {
it('LIVE (no asOf) applies no as_of filter and reads statcast_aggregates', async () => {
const sb = fakeSb(TABLES);
const r = await ctxSvc.build(sb);
expect(r.asOf).toBeNull();
expect(r.__stats.mode).toBe('live');
expect(r.__stats.statcast_source).toBe('statcast_aggregates');
expect(sb.calls.every((c) => c.lte === null)).toBe(true);
expect(sb.calls.map((c) => c.table)).toContain('statcast_aggregates');
expect(sb.calls.map((c) => c.table)).not.toContain('statcast_history');
});
it('AS-OF applies lte on EVERY dated table and reads statcast_history', async () => {
const sb = fakeSb(TABLES);
const r = await ctxSvc.build(sb, { asOf: '2026-08-07' });
expect(r.__stats.mode).toBe('as-of (audit)');
expect(r.__stats.statcast_source).toBe('statcast_history');
for (const t of ['batter_spray', 'team_defense', 'platoon_splits', 'statcast_history']) {
const c = sb.calls.find((x) => x.table === t);
expect(c.lte).toBe('2026-08-07');
}
});
});
describe('THE CONTAMINATION, LOCKED OUT', () => {
it('never returns a row dated after asOf', async () => {
const r = await ctxSvc.build(fakeSb(TABLES), { asOf: '2026-08-07' });
const c = r(PROP);
expect(c.spray.as_of_date).toBe('2026-08-05'); // NOT the 08-09 row
expect(c.as_of).toBe('2026-08-07');
});
it("picks up TODAY's value on the live path — proving the test can tell them apart", async () => {
const live = await ctxSvc.build(fakeSb(TABLES));
const past = await ctxSvc.build(fakeSb(TABLES), { asOf: '2026-08-07' });
// The pitcher's hard-hit rate moved 30 -> 99 on 08-09.
expect(live(PROP).pitcherHardHit).toBeCloseTo(0.30, 5);
expect(past(PROP).pitcherHardHit).toBeCloseTo(0.30, 5);
const today = await ctxSvc.build(fakeSb(TABLES), { asOf: '2026-08-09' });
expect(today(PROP).pitcherHardHit).toBeCloseTo(0.99, 5);
});
it('takes the LATEST row within the bound, not the earliest', async () => {
const tables = { ...TABLES, batter_spray: [spray('2026-08-01', BATTER, 1), spray('2026-08-05', BATTER, 1), spray('2026-08-09', BATTER, 1)] };
const r = await ctxSvc.build(fakeSb(tables), { asOf: '2026-08-07' });
expect(r(PROP).spray.as_of_date).toBe('2026-08-05');
});
});
describe('REFUSAL OVER RECONSTRUCTION', () => {
it('an entity with NO row at-or-before asOf yields null — never the nearest row', async () => {
// Every dated row is AFTER the cutoff. The honest answer is "no context".
const r = await ctxSvc.build(fakeSb(TABLES), { asOf: '2026-07-01' });
expect(r).toBeNull(); // nothing loaded at all → serve unadjusted
});
it('refuses ONE entity while still serving another', async () => {
const tables = {
...TABLES,
batter_spray: [spray('2026-08-09', BATTER, 1)], // only AFTER the cutoff
platoon_splits: [platoon('2026-08-05', BATTER)], // present at the cutoff
};
const r = await ctxSvc.build(fakeSb(tables), { asOf: '2026-08-07' });
const c = r(PROP);
expect(c.spray).toBeNull(); // refused
expect(c.platoonSplits).not.toBeNull();
});
it('a refused input leaves the BASE RATE UNTOUCHED (factor does not apply)', () => {
// The end-to-end consequence: no spray → no defense_by_direction multiplier.
const withSpray = hf.hitsFactorMultiplier({
spray: TABLES.batter_spray[0], positionOaa: TABLES.team_defense[0].position_oaa, bats: 'R',
});
const refused = hf.hitsFactorMultiplier({
spray: null, positionOaa: TABLES.team_defense[0].position_oaa, bats: 'R',
});
expect(refused.applied.map((a) => a.factor)).not.toContain('defense_by_direction');
expect(refused.skipped.map((s) => s.factor)).toContain('defense_by_direction');
// and with nothing else readable the multiplier is exactly 1 — a no-op.
expect(refused.multiplier).toBe(1);
expect(withSpray.factors_fired).toBeGreaterThan(refused.factors_fired);
});
it('adjustProbability is unchanged when every input is refused', () => {
const out = hf.adjustProbability(0.42, { spray: null, positionOaa: null, bats: null });
expect(out.p_adjusted).toBe(0.42);
});
});
describe('archetype is CARRIED, never invented', () => {
it('passes the graded row archetype through', async () => {
const r = await ctxSvc.build(fakeSb(TABLES), { asOf: '2026-08-07' });
expect(r({ ...PROP, archetype: 'BOMBER' }).archetype).toBe('BOMBER');
});
it('is null when the caller supplies none — no context table holds it', async () => {
const r = await ctxSvc.build(fakeSb(TABLES), { asOf: '2026-08-07' });
expect(r(PROP).archetype).toBeNull();
});
});
describe('the context read stays on safePaginate with the real composite key', () => {
it('orders by the full unique tuple of each table', () => {
const src = require('fs').readFileSync(
require('path').join(__dirname, '..', '..', 'src/services/model/hitsFactorContext.js'), 'utf8');
expect(src).toMatch(/uniqueKeyFor\(table\)/);
expect(src).toMatch(/require\('\.\.\/\.\.\/utils\/safePaginate'\)/);
// No hand-rolled walk left behind.
expect(src).not.toMatch(/for \(let from = 0/);
});
});
+172
View File
@@ -0,0 +1,172 @@
'use strict';
/**
* A6 — the join keys, resolved into a SHADOW resolve.
*
* A5 measured all three hits factors skipped on 596/596 because nothing sets
* `prop.opponent` / `prop.opposing_pitcher`. A6 resolves them and freezes what
* the factors WOULD do. The served grade must not move.
*/
const mk = require('../../src/services/model/matchupKeys');
const ctxSvc = require('../../src/services/model/hitsFactorContext');
const hf = require('../../src/services/model/hitsFactors');
const ff = require('../../src/services/model/factorFreeze');
const { nameKey } = require('../../src/utils/playerName');
const JUDGE = nameKey('Aaron Judge');
const fakeSb = (rows) => ({
from() {
const st = { lte: null };
const q = {
select() { return q; }, eq() { return q; },
lte(c, v) { if (c === 'as_of_date') st.lte = v; return q; },
order() { return q; },
async range(f, t2) {
const r = st.lte ? rows.filter((x) => String(x.as_of_date) <= st.lte) : rows;
return { data: r.slice(f, t2 + 1), error: null };
},
};
return q;
},
});
const LINEUP = [{
as_of_date: '2026-08-09', game_date: '2026-08-09', sport: 'mlb',
game_pk: 1, team: 'New York Yankees', side: 'home', player_key: JUDGE,
}];
const SCHED = [{
home: { team: 'New York Yankees', probablePitcher: { id: 1, name: 'Gerrit Cole' } },
away: { team: 'Boston Red Sox', probablePitcher: { id: 2, name: 'Chris Sale' } },
}];
describe('key resolution', () => {
it('resolves opponent + opposing pitcher from lineup + schedule', async () => {
const r = await mk.build({ sb: fakeSb(LINEUP), getSchedule: async () => SCHED, gameDate: '2026-08-09' });
const k = r({ player: 'Aaron Judge' });
expect(k.opponent).toBe('Boston Red Sox');
expect(k.opposing_pitcher).toBe('Chris Sale');
expect(k.source).toBe('lineup_context+schedule');
expect(k.refused).toBeNull();
});
it('REFUSES a player with no posted lineup row — never guesses from the prop teams', async () => {
const r = await mk.build({ sb: fakeSb(LINEUP), getSchedule: async () => SCHED, gameDate: '2026-08-09' });
// The prop knows both teams; the resolver still refuses, because which side
// the hitter bats for is not knowable from that.
const k = r({ player: 'Nobody Atall', home_team: 'NYY', away_team: 'BOS' });
expect(k.opponent).toBeNull();
expect(k.opposing_pitcher).toBeNull();
expect(k.refused).toBe('no_lineup_row');
});
it('refuses the pitcher when the game has no probable, keeping the opponent', async () => {
const noSP = [{ home: { team: 'New York Yankees' }, away: { team: 'Boston Red Sox' } }];
const r = await mk.build({ sb: fakeSb(LINEUP), getSchedule: async () => noSP, gameDate: '2026-08-09' });
const k = r({ player: 'Aaron Judge' });
expect(k.opponent).toBe('Boston Red Sox');
expect(k.opposing_pitcher).toBeNull();
expect(k.refused).toBe('no_probable_pitcher');
});
it('bounds the lineup read by as-of (A4 discipline)', async () => {
const future = [{ ...LINEUP[0], as_of_date: '2026-08-20' }];
const r = await mk.build({ sb: fakeSb(future), getSchedule: async () => SCHED, gameDate: '2026-08-09', asOf: '2026-08-09' });
expect(r({ player: 'Aaron Judge' }).refused).toBe('no_lineup_row');
});
it('returns null (not a stub) when nothing loads', async () => {
expect(await mk.build({})).toBeNull();
});
});
describe('the resolver: LIVE unchanged, SHADOW fires', () => {
const TABLES = {
batter_spray: [{ as_of_date: '2026-08-09', sport: 'mlb', season: 2026, source_id: 1, player_key: JUDGE, pull_gb: 0.3, straight_gb: 0.2, oppo_gb: 0.1, pull_air: 0.2, straight_air: 0.1, oppo_air: 0.1 }],
team_defense: [{ as_of_date: '2026-08-09', sport: 'mlb', season: 2026, team: 'Boston Red Sox', position_oaa: { '3B': { oaa: 2 }, SS: { oaa: 1 }, LF: { oaa: 0 }, '1B': { oaa: 0 }, '2B': { oaa: 1 }, RF: { oaa: -1 }, CF: { oaa: 0 } } }],
platoon_splits: [{ as_of_date: '2026-08-09', sport: 'mlb', season: 2026, player_key: JUDGE, vl_pa: 120, vl_ab: 110, vl_hits: 33, vr_pa: 300, vr_ab: 280, vr_hits: 70 }],
statcast_aggregates: [
{ sport: 'mlb', season: 2026, source_id: 1, role: 'batter', player_key: JUDGE, bats: 'R', hard_hit_pct: 40 },
{ sport: 'mlb', season: 2026, source_id: 2, role: 'pitcher', player_key: nameKey('Chris Sale'), throws: 'L', hard_hit_pct: 45 },
],
};
const sbCtx = () => ({
from(table) {
const q = { select() { return q; }, eq() { return q; }, lte() { return q; }, order() { return q; },
async range(f, t2) { return { data: (TABLES[table] || []).slice(f, t2 + 1), error: null }; } };
return q;
},
});
const PROP = { player: 'Aaron Judge', stat_type: 'hits' };
const KEYS = { opponent: 'Boston Red Sox', opposing_pitcher: 'Chris Sale', source: 'lineup_context+schedule', refused: null };
it('WITHOUT keys nothing fires — the measured live state', async () => {
const ctx = await ctxSvc.build(sbCtx());
const c = ctx(PROP, 'mlb');
const m = hf.hitsFactorMultiplier(c);
expect(c.positionOaa).toBeNull();
expect(c.throws).toBeNull();
expect(m.factors_fired).toBe(0);
expect(m.multiplier).toBe(1);
});
it('WITH keys the factors fire — same indexes, different join', async () => {
const ctx = await ctxSvc.build(sbCtx());
const c = ctx(PROP, 'mlb', KEYS);
const m = hf.hitsFactorMultiplier(c);
expect(c.positionOaa).not.toBeNull();
expect(c.throws).toBe('L');
expect(m.factors_fired).toBeGreaterThan(0);
expect(m.multiplier).not.toBe(1);
});
it('the shadow block records the would-fire result and stays recomputable', async () => {
const ctx = await ctxSvc.build(sbCtx());
const live = ctx(PROP, 'mlb');
const shadowCtx = ctx(PROP, 'mlb', KEYS);
const shM = hf.hitsFactorMultiplier(shadowCtx);
const frozen = ff.freeze(live, PROP, ff.shadowBlock(shadowCtx, shM, KEYS));
expect(frozen.would_fire.multiplier).toBe(shM.multiplier);
expect(frozen.would_fire.factors_fired).toBe(shM.factors_fired);
expect(frozen.would_fire.keys.opponent).toBe('Boston Red Sox');
// Recomputable from what was stored beside it.
const re = hf.hitsFactorMultiplier({
spray: live.spray, bats: live.bats, platoonSplits: live.platoonSplits,
positionOaa: frozen.would_fire.shadow_inputs.position_oaa,
throws: frozen.would_fire.shadow_inputs.throws,
pitcherHardHit: frozen.would_fire.shadow_inputs.pitcher_hard_hit,
});
expect(re.multiplier).toBe(frozen.would_fire.multiplier);
});
it('the LIVE frozen inputs still show the refusal — shadow does not overwrite them', async () => {
const ctx = await ctxSvc.build(sbCtx());
const live = ctx(PROP, 'mlb');
const shadowCtx = ctx(PROP, 'mlb', KEYS);
const frozen = ff.freeze(live, PROP, ff.shadowBlock(shadowCtx, hf.hitsFactorMultiplier(shadowCtx), KEYS));
expect(frozen.position_oaa).toBeNull(); // what the SERVED grade saw
expect(frozen.throws).toBeNull();
expect(frozen.would_fire.shadow_inputs.throws).toBe('L'); // what it WOULD have seen
});
it('no shadow block when the keys were refused', async () => {
const ctx = await ctxSvc.build(sbCtx());
const live = ctx(PROP, 'mlb');
expect(ff.freeze(live, PROP, null).would_fire).toBeNull();
});
});
describe('the served grade cannot read the shadow', () => {
it('the engine assigns pOver only from the LIVE adjust, never from would_fire', () => {
const src = require('fs').readFileSync(
require('path').join(__dirname, '..', '..', 'src/services/intelligence/analyzeViaEngine1.js'), 'utf8');
const i = src.indexOf('FIX A6 — SHADOW RESOLVE');
const block = src.slice(i, i + 900);
expect(block).toMatch(/shadow = ffz\.shadowBlock/);
expect(block).not.toMatch(/pOver\s*=/);
// and the only pOver assignment in the factor area is gated on the LIVE adj
expect(src).toMatch(/if \(adj\.p_adjusted != null && adj\.factors_fired > 0\) \{\s*\n\s*pOver = adj\.p_adjusted;/);
});
});
+203
View File
@@ -0,0 +1,203 @@
'use strict';
/**
* Read-integrity harness — spec: specs/read-integrity-harness.md §5.
* Every case uses a FAKE page fetcher; nothing here touches a network.
*/
const ri = require('../../src/utils/readIntegrity');
/** A fetcher that serves a fixed list of pages, then empties. */
const pagesOf = (pages) => async (from, to) => {
const size = to - from + 1;
const idx = Math.floor(from / size);
return pages[idx] || [];
};
const row = (id) => ({ id });
const ids = (a, b) => Array.from({ length: b - a + 1 }, (_, i) => row(a + i));
describe('walk', () => {
it('stops on a short page', async () => {
const rows = await ri.walk(pagesOf([ids(1, 10), ids(11, 15)]), 10);
expect(rows).toHaveLength(15);
});
it('stops on an empty page after a full one', async () => {
const rows = await ri.walk(pagesOf([ids(1, 10), []]), 10);
expect(rows).toHaveLength(10);
});
it('keeps duplicates — collapsing them here would erase the measurement', async () => {
const rows = await ri.walk(pagesOf([ids(1, 10), [row(3), row(4)]]), 10);
expect(rows).toHaveLength(12);
});
it('PROPAGATES a fetch error instead of treating it as end-of-data', async () => {
// The calibrationService.fromLedger:117 defect: `if (error || !data) break`
// makes a partial read indistinguishable from a complete one.
const boom = async () => { throw new Error('fetch failed'); };
await expect(ri.walk(boom, 10)).rejects.toThrow('fetch failed');
});
it('refuses to report a runaway walk as complete', async () => {
const never = async () => ids(1, 10); // always a full page
await expect(ri.walk(never, 10, 3)).rejects.toThrow(/maxPages/);
});
});
describe('analyze — the verdict is measured, never inferred', () => {
it('PASSes a walk that is set-identical to the control', () => {
const r = ri.analyze({ exactCount: 15, unordered: ids(1, 15), ordered: ids(1, 15) });
expect(r.verdict).toBe(ri.VERDICT.PASS);
expect(r.unordered.duplicates).toBe(0);
expect(r.corruption_pct).toBe(0);
});
it('FAILs the measured production shape: last page re-emits earlier rows', () => {
// 2,490-row read reproduced in miniature: the tail page repeats rows already
// returned, so an equal number of real rows are never seen.
const unordered = [...ids(1, 10), ...ids(11, 20), ...[row(3), row(4), row(5)]];
const ordered = ids(1, 23);
const r = ri.analyze({ exactCount: 23, unordered, ordered });
expect(r.verdict).toBe(ri.VERDICT.FAIL);
expect(r.unordered.rows).toBe(23); // the count looks perfect
expect(r.unordered.distinct).toBe(20); // the rows are not
expect(r.unordered.duplicates).toBe(3);
expect(r.unordered.missing_vs_control).toBe(3);
expect(r.corruption_pct).toBeCloseTo(13.0, 1);
});
it('META-SCAR GUARD: an ORDERED walk that still duplicates is FAIL, not PASS', () => {
// The clause is not the result. A harness that credited the presence of an
// ORDER BY would be verifying its own intention.
const orderedButBroken = [...ids(1, 20), row(20)];
const r = ri.analyze({ exactCount: 21, unordered: orderedButBroken, ordered: ids(1, 21) });
expect(r.verdict).toBe(ri.VERDICT.FAIL);
expect(r.unordered.duplicates).toBe(1);
});
it('reports CONTROL_INVALID when the control is short of the server count', () => {
const r = ri.analyze({ exactCount: 30, unordered: ids(1, 25), ordered: ids(1, 25) });
expect(r.verdict).toBe(ri.VERDICT.CONTROL_INVALID);
expect(r.control_valid).toBe(false);
});
it('never issues PASS without a server count to validate against', () => {
const r = ri.analyze({ exactCount: null, unordered: ids(1, 5), ordered: ids(1, 5) });
expect(r.verdict).not.toBe(ri.VERDICT.PASS);
expect(r.verdict).toBe(ri.VERDICT.CONTROL_INVALID);
});
it('reports KEY_NOT_UNIQUE rather than blaming the walk for duplicate data', () => {
// batter_spray keyed on player_key|as_of_date has genuine duplicate keys.
const dup = [{ k: 'a' }, { k: 'a' }, { k: 'b' }];
const r = ri.analyze({ exactCount: 3, unordered: dup, ordered: dup, keyCols: ['k'] });
expect(r.verdict).toBe(ri.VERDICT.KEY_NOT_UNIQUE);
expect(r.key_unique).toBe(false);
});
it('counts a composite key across columns', () => {
const rows = [{ a: 1, b: 'x' }, { a: 1, b: 'y' }];
const r = ri.analyze({ exactCount: 2, unordered: rows, ordered: rows, keyCols: ['a', 'b'] });
expect(r.verdict).toBe(ri.VERDICT.PASS);
});
it('distinguishes an EXTRA row from a missing one', () => {
const r = ri.analyze({ exactCount: 5, unordered: [...ids(1, 5), row(99)], ordered: ids(1, 5) });
expect(r.unordered.extra_vs_control).toBe(1);
expect(r.unordered.missing_vs_control).toBe(0);
expect(r.verdict).toBe(ri.VERDICT.FAIL);
});
});
describe('applyFilters', () => {
const fake = () => {
const calls = [];
const q = {
calls,
eq: (...a) => { calls.push(['eq', ...a]); return q; },
is: (...a) => { calls.push(['is', ...a]); return q; },
in: (...a) => { calls.push(['in', ...a]); return q; },
not: (...a) => { calls.push(['not', ...a]); return q; },
lt: (...a) => { calls.push(['lt', ...a]); return q; },
gte: (...a) => { calls.push(['gte', ...a]); return q; },
};
return q;
};
it('chains ops in order', () => {
const q = ri.applyFilters(fake(), [
['eq', 'sport', 'mlb'],
['is', 'user_id', null],
['in', 'outcome', ['hit', 'miss']],
['not', 'p_win', 'is', null],
['lt', 'game_date', '2026-08-09'],
]);
expect(q.calls).toEqual([
['eq', 'sport', 'mlb'],
['is', 'user_id', null],
['in', 'outcome', ['hit', 'miss']],
['not', 'p_win', 'is', null],
['lt', 'game_date', '2026-08-09'],
]);
});
it('THROWS on an unknown op rather than skipping it — a dropped filter changes the row set', () => {
expect(() => ri.applyFilters(fake(), [['contains', 'x', 'y']])).toThrow(/unsupported filter op/);
});
it('handles an empty filter list', () => {
expect(ri.applyFilters(fake(), []).calls).toEqual([]);
});
});
describe('measure', () => {
const spec = { id: 'r1', source: 'f.js:1', table: 't', key: ['id'] };
it('assembles a full result for a clean reader', async () => {
const r = await ri.measure(spec, {
pageSize: 10,
exactCount: async () => 15,
fetchUnordered: pagesOf([ids(1, 10), ids(11, 15)]),
fetchOrdered: pagesOf([ids(1, 10), ids(11, 15)]),
});
expect(r.verdict).toBe(ri.VERDICT.PASS);
expect(r.pages).toBe(2);
expect(r.id).toBe('r1');
});
it('reports ERROR (not PASS, not empty) when a page fetch throws', async () => {
const r = await ri.measure(spec, {
pageSize: 10,
exactCount: async () => 15,
fetchUnordered: async () => { throw new Error('boom'); },
fetchOrdered: pagesOf([ids(1, 15)]),
});
expect(r.verdict).toBe(ri.VERDICT.ERROR);
expect(r.reason).toMatch(/boom/);
});
it('reports ERROR when the count itself fails', async () => {
const r = await ri.measure(spec, {
pageSize: 10,
exactCount: async () => { throw new Error('count failed'); },
fetchUnordered: pagesOf([ids(1, 5)]),
fetchOrdered: pagesOf([ids(1, 5)]),
});
expect(r.verdict).toBe(ri.VERDICT.ERROR);
});
});
describe('the harness writes nothing', () => {
it('has no insert/update/upsert/delete anywhere in its source', () => {
const fs = require('fs');
const path = require('path');
for (const f of ['../../src/utils/readIntegrity.js', '../../scripts/read-integrity.js']) {
const p = path.join(__dirname, f);
if (!fs.existsSync(p)) continue;
const src = fs.readFileSync(p, 'utf8');
expect(src).not.toMatch(/\.insert\(|\.update\(|\.upsert\(|\.delete\(|\.rpc\(/);
}
});
});
+139
View File
@@ -0,0 +1,139 @@
'use strict';
/**
* Fix A2 — the learning readers do not regress to the unordered walk.
*
* WHAT THIS TEST IS AND IS NOT. It does NOT assert that a reader is correct —
* correctness is measured by `scripts/read-integrity.js` against an ordered
* control on live data, and a source grep can never stand in for that (the
* meta-scar rule, specs/read-integrity-harness.md §2).
*
* What it DOES assert is structural: the two tables measured corrupt
* (`ledger_entries`, `model_snapshots`) are never read through the legacy
* `page()` helper again, the scripts stay importable without executing, and the
* named READS the harness measures still exist. Those are the properties that,
* if they silently regressed, would make the next harness run measure the wrong
* thing.
*/
const fs = require('fs');
const path = require('path');
const SCRIPTS = [
'prove-hit-factors', 'prove-tb-factors', 'cluster-prove',
'tb-solo-and-interactions', 'build-grade-bands', 'proven-status',
'champion-ablation',
// A2b — the context readers and the two write-scripts
'prove-runs-rbi', 'pitcher-prove-k', 'prove-park-weather', 'skill-v1-stagea',
'stagea-gate-run', 'backfill-context', 'reconstruct-game-environment',
'calibrate-hits',
];
/** Every table these scripts page through must have a recorded unique key. */
const KEYED_TABLES = ['ledger_entries', 'model_snapshots', 'statcast_aggregates',
'batter_spray', 'team_defense', 'platoon_splits', 'park_dimensions',
'hitter_opportunity', 'lineup_context', 'game_context'];
const CORRUPTED_TABLES = ['ledger_entries', 'model_snapshots'];
/**
* READS DELIBERATELY LEFT ON THE LEGACY WALK — named, with the reason.
*
* The A2 order scoped the fix to the eleven readers measured FAIL and said
* explicitly to leave the siblings that measured 0%. Those siblings are listed
* here rather than excluded by a loosened regex, so the gap is VISIBLE and
* tracked instead of invisible.
*
* They are not safe by construction — they are clean by plan. The same query on
* `ledger_entries` measured 16.5% one minute and 24.8% the next, so a 0% reading
* is a fact about today, not a property. These should move in a follow-up.
*/
const KNOWN_LEGACY_READS = {
// A2b closed the last exceptions: the three reads A2 left on the legacy walk
// are converted, and the unordered helper is DELETED from every script rather
// than parked beside the safe one — a dead broken helper is an invitation.
};
const read = (name) => fs.readFileSync(path.join(__dirname, '..', '..', 'scripts', `${name}.js`), 'utf8');
describe.each(SCRIPTS)('scripts/%s.js', (name) => {
const src = read(name);
const allowed = KNOWN_LEGACY_READS[name] || [];
it('never reads a keyed table through an unordered walk', () => {
for (const table of KEYED_TABLES) {
// `page(sb, 'ledger_entries'` — the unordered walk. `pageSafe(...)` is fine.
expect(src).not.toMatch(new RegExp(`[^e]page\\(\\s*sb,\\s*'${table}'`));
}
expect(allowed).toHaveLength(0);
});
it('carries NO unordered page() helper at all — the landmine is deleted', () => {
const code = src.replace(/\/\/.*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
const defs = code.match(/async function page\(/g) || [];
for (const _ of defs) {
// A surviving helper must itself order; an unordered one must not exist.
expect(code).toMatch(/paginate\(/);
}
});
it('looks its key up from the schema rather than assuming id', () => {
expect(src).toMatch(/uniqueKeyFor/);
});
it('routes its fixed reads through safePaginate, not a hand-rolled walk', () => {
expect(src).toMatch(/require\('\.\.\/src\/utils\/safePaginate'\)/);
expect(src).toMatch(/async function pageSafe\(/);
// pageSafe must delegate — a second walk implementation is the thing the
// A2 order explicitly forbade.
expect(src).toMatch(/return paginate\(/);
});
it('exports the named READS the harness measures', () => {
const mod = require(path.join('..', '..', 'scripts', name)); // must not execute
expect(mod.READS).toBeDefined();
for (const fn of Object.values(mod.READS)) expect(typeof fn).toBe('function');
});
it('does NOT run main() on require — importing it must not hit the network or write', () => {
// prove-hit-factors and friends write to mc_test_ledger via
// tl.recordAndCount, which is the Bonferroni denominator. An unguarded
// entrypoint would let the harness inflate the correction just by measuring.
expect(src).toMatch(/if \(require\.main === module\)/);
expect(src).not.toMatch(/^main\(\)\.catch/m);
});
});
describe('the calibration services load rows through safePaginate', () => {
it.each([
['calibrationService', '../../src/services/model/calibrationService'],
['lowParamService', '../../src/services/model/lowParamService'],
])('%s exports loadSettledRows', (label, modPath) => {
const mod = require(modPath);
expect(typeof mod.loadSettledRows).toBe('function');
});
it.each([
['calibrationService', 'src/services/model/calibrationService.js'],
['lowParamService', 'src/services/model/lowParamService.js'],
])('%s no longer swallows a query error as end-of-data', (label, rel) => {
const src = fs.readFileSync(path.join(__dirname, '..', '..', rel), 'utf8');
// The :117 defect, in CODE. Anchored to line-start so the comment that
// documents the defect does not count as the defect.
expect(src).not.toMatch(/^\s*if \(error \|\| !data/m);
expect(src).toMatch(/paginate\(/);
});
});
describe('A2 did not open anything it was not meant to', () => {
it('CALIBRATION_DEPLOYED is still empty — no stat serves a calibrated number', () => {
const snapshotService = require('../../src/services/snapshotService');
expect(snapshotService.CALIBRATION_DEPLOYED).toEqual([]);
});
it('both hits verdicts remain withdrawn', () => {
const wv = require('../../src/services/model/withdrawnVerdicts');
expect(wv.isWithdrawn('mlb', 'hits', 'defense_by_direction')).toBe(true);
expect(wv.isWithdrawn('mlb', 'hits', 'pitcher_contact_profile')).toBe(true);
});
});
+294
View File
@@ -0,0 +1,294 @@
'use strict';
/**
* safePaginate — spec: specs/read-integrity-harness.md §7.
* Fake query builders throughout; nothing here touches a network.
*/
const { paginate } = require('../../src/utils/safePaginate');
/**
* A fake PostgREST table. `rows` may be mutated between pages to simulate
* concurrent writes. Honours order+range the way Postgres does WITH an ORDER BY.
*/
function fakeTable(rows, opts = {}) {
const state = { rows, orderCalls: 0, rangeCalls: 0 };
state.make = () => {
let orderKey = null; let asc = true;
const q = {
order(k, o) { orderKey = k; asc = !o || o.ascending !== false; state.orderCalls += 1; return q; },
async range(from, to) {
state.rangeCalls += 1;
if (opts.errorOnRange && opts.errorOnRange(from)) return { data: null, error: { message: 'connection reset' } };
if (opts.beforeRange) opts.beforeRange(from, state);
// Unordered simulation: return physical order (the production defect).
let out = [...state.rows];
if (orderKey) {
out.sort((a, b) => (a[orderKey] < b[orderKey] ? -1 : a[orderKey] > b[orderKey] ? 1 : 0));
if (!asc) out.reverse();
}
return { data: out.slice(from, to + 1), error: null };
},
};
return q;
};
return state;
}
const row = (id) => ({ id, v: `r${id}` });
const ids = (a, b) => Array.from({ length: b - a + 1 }, (_, i) => row(a + i));
describe('safePaginate — (a) returns the full set, in order', () => {
it('walks every page and returns each row exactly once, ascending by key', async () => {
const t = fakeTable(ids(1, 2490));
const out = await paginate(t.make, { pageSize: 1000 });
expect(out).toHaveLength(2490);
expect(new Set(out.map((r) => r.id)).size).toBe(2490);
expect(out.map((r) => r.id)).toEqual(ids(1, 2490).map((r) => r.id)); // in order
expect(t.rangeCalls).toBe(3);
});
it('always applies the order — every page is ordered, not just the first', async () => {
const t = fakeTable(ids(1, 2490));
await paginate(t.make, { pageSize: 1000 });
expect(t.orderCalls).toBe(t.rangeCalls);
});
it('orders by the configured key, and can descend', async () => {
const t = fakeTable(ids(1, 5));
const out = await paginate(t.make, { pageSize: 10, ascending: false });
expect(out.map((r) => r.id)).toEqual([5, 4, 3, 2, 1]);
});
it('returns [] for an empty table without throwing', async () => {
const out = await paginate(fakeTable([]).make, { pageSize: 10 });
expect(out).toEqual([]);
});
});
describe('safePaginate — (b) a mid-walk error THROWS, never a silent end', () => {
it('throws when page 2 fails instead of returning page 1 as the whole set', async () => {
// THE :117 DEFECT: `if (error || !data) break` would return 1,000 rows here
// and the caller would fit a map on them believing it had 2,490.
const t = fakeTable(ids(1, 2490), { errorOnRange: (from) => from === 1000 });
await expect(paginate(t.make, { pageSize: 1000, label: 'cal' }))
.rejects.toThrow(/read failed at range 1000-1999 — connection reset/);
});
it('tags the error so a caller can tell a read failure from thin history', async () => {
const t = fakeTable(ids(1, 10), { errorOnRange: (from) => from === 0 });
await expect(paginate(t.make, { pageSize: 5 })).rejects.toMatchObject({ code: 'READ_FAILED' });
});
it('names itself in the message', async () => {
const t = fakeTable(ids(1, 10), { errorOnRange: () => true });
await expect(paginate(t.make, { pageSize: 5, label: 'fromLedger(hits)' }))
.rejects.toThrow(/^fromLedger\(hits\):/);
});
it('refuses a runaway walk rather than reporting a truncated read as complete', async () => {
// Fresh unique ids every page, so the runaway guard is what fires — not the
// duplicate guard.
let n = 0;
const t = {
make: () => ({
order() { return this; },
async range() { const page = ids(n * 10 + 1, n * 10 + 10); n += 1; return { data: page, error: null }; },
}),
};
await expect(paginate(t.make, { pageSize: 10, maxPages: 3 })).rejects.toThrow(/maxPages/);
});
});
describe('safePaginate — (c) set-identity vs an ordered full-table control', () => {
const control = (rows) => [...rows].sort((a, b) => a.id - b.id).map((r) => r.id);
it('is set-identical to the control on a static table', async () => {
const rows = ids(1, 4321);
const out = await paginate(fakeTable(rows).make, { pageSize: 1000 });
expect(out.map((r) => r.id)).toEqual(control(rows));
});
it('is set-identical when the physical order is shuffled under it', async () => {
// The planner returning rows in an arbitrary physical order is exactly the
// production condition. A stable ORDER BY makes it irrelevant.
const rows = ids(1, 3000);
const t = fakeTable([...rows].reverse());
const out = await paginate(t.make, { pageSize: 1000 });
expect(out.map((r) => r.id)).toEqual(control(rows));
});
it('THROWS on a duplicate key rather than returning a corrupted set', async () => {
// A non-unique ordering key: ties may come back in any order, so pages
// overlap. An .order() clause on such a column is not a fix.
const dup = [{ id: 1 }, { id: 2 }, { id: 2 }, { id: 3 }];
await expect(paginate(fakeTable(dup).make, { pageSize: 2 }))
.rejects.toThrow(/duplicate key \(id\)=2/);
});
});
describe('safePaginate — concurrent append (the regression this fix exists for)', () => {
it('returns every pre-existing row exactly once while rows are APPENDED mid-walk', async () => {
// The snapshot cron appends to ledger_entries at 14/19/22/1/3 UTC. With a
// stable ascending key, appended rows land after the cursor: nothing already
// returned can be returned again, and nothing pending can be skipped.
const original = ids(1, 2500);
let next = 10000;
const t = fakeTable([...original], {
beforeRange: (from, state) => {
if (from > 0) state.rows.push(row(next++)); // a write lands mid-walk
},
});
const out = await paginate(t.make, { pageSize: 1000 });
const seen = out.map((r) => r.id);
expect(new Set(seen).size).toBe(seen.length); // no duplicates
for (const r of original) expect(seen).toContain(r.id); // nothing dropped
});
it('the OLD unordered walk fails that same simulation — the test has teeth', async () => {
// Same table, paged over PHYSICAL order with no ORDER BY. An UPDATE in
// Postgres writes a new tuple version at the end of the heap, so a row that
// was already returned moves AFTER the cursor (returned twice) and every row
// behind it shifts back by one (one falls across the page boundary unseen).
// This is the production shape, and it is what the fix removes.
const original = ids(1, 2500);
const rows = [...original];
const unorderedWalk = async () => {
const out = [];
for (let from = 0; ; from += 1000) {
if (from > 0) rows.push(rows.splice(0, 1)[0]); // a row is re-written mid-walk
const page = rows.slice(from, from + 1000);
if (page.length === 0) break;
out.push(...page);
if (page.length < 1000) break;
}
return out;
};
const seen = (await unorderedWalk()).map((r) => r.id);
expect(new Set(seen).size).toBeLessThan(seen.length); // duplicates appear
const missing = original.filter((r) => !seen.includes(r.id));
expect(missing.length).toBeGreaterThan(0); // and rows are lost
});
});
// ── FIX A2b — COMPOSITE KEYS ────────────────────────────────────────────────
const { normalizeKey } = require('../../src/utils/safePaginate');
const { uniqueKeyFor } = require('../../src/utils/tableKeys');
/** A fake table keyed by a composite tuple, ordered the way Postgres would. */
function fakeComposite(rows, opts = {}) {
const state = { rows, orderCols: [], rangeCalls: 0 };
state.make = () => {
const cols = [];
const q = {
order(c) { cols.push(c); return q; },
async range(from, to) {
state.rangeCalls += 1;
state.orderCols = [...cols];
if (opts.errorOnRange && opts.errorOnRange(from)) return { data: null, error: { message: 'reset' } };
const out = [...state.rows].sort((a, b) => {
for (const c of cols) {
if (a[c] < b[c]) return -1;
if (a[c] > b[c]) return 1;
}
return 0;
});
return { data: out.slice(from, to + 1), error: null };
},
};
return q;
};
return state;
}
const KEY = ['as_of_date', 'sport', 'season', 'player_key'];
const composite = (n) => Array.from({ length: n }, (_, i) => ({
as_of_date: `2026-08-${String((i % 9) + 1).padStart(2, '0')}`,
sport: 'mlb', season: 2026, player_key: `p${i}`, v: i,
}));
describe('safePaginate — composite keys (A2b)', () => {
it('normalizeKey treats a string and a one-column list identically', () => {
expect(normalizeKey('id')).toEqual(['id']);
expect(normalizeKey(['id'])).toEqual(['id']);
expect(normalizeKey(undefined)).toEqual(['id']);
expect(normalizeKey(KEY)).toEqual(KEY);
});
it('rejects an empty key rather than silently defaulting to id', () => {
expect(() => normalizeKey([])).toThrow(/must be a column name/);
expect(() => normalizeKey([null])).toThrow(/must be a column name/);
});
it('returns the full set exactly once, ordered by the whole tuple', async () => {
const t = fakeComposite(composite(2500));
const rows = await paginate(t.make, { key: KEY, pageSize: 1000 });
expect(rows).toHaveLength(2500);
expect(new Set(rows.map((r) => r.player_key)).size).toBe(2500);
// sorted by as_of_date first
for (let i = 1; i < rows.length; i += 1) {
expect(rows[i].as_of_date >= rows[i - 1].as_of_date).toBe(true);
}
});
it('orders EVERY column of the key, in the declared order', async () => {
const t = fakeComposite(composite(10));
await paginate(t.make, { key: KEY, pageSize: 100 });
expect(t.orderCols).toEqual(KEY);
});
it('a mid-walk error still THROWS with a composite key', async () => {
const t = fakeComposite(composite(2500), { errorOnRange: (f) => f === 1000 });
await expect(paginate(t.make, { key: KEY, pageSize: 1000, label: 'ctx' }))
.rejects.toThrow(/ctx: read failed at range 1000-1999/);
});
it('THROWS on a duplicate TUPLE, naming every column', async () => {
const dup = [
{ as_of_date: '2026-08-01', sport: 'mlb', season: 2026, player_key: 'a' },
{ as_of_date: '2026-08-01', sport: 'mlb', season: 2026, player_key: 'a' },
];
await expect(paginate(fakeComposite(dup).make, { key: KEY, pageSize: 10 }))
.rejects.toThrow(/duplicate key \(as_of_date,sport,season,player_key\)/);
});
it('does NOT throw when only a PREFIX of the tuple repeats', async () => {
// Same day, same sport, different player — a legitimate row, not a duplicate.
const rows = [
{ as_of_date: '2026-08-01', sport: 'mlb', season: 2026, player_key: 'a' },
{ as_of_date: '2026-08-01', sport: 'mlb', season: 2026, player_key: 'b' },
];
await expect(paginate(fakeComposite(rows).make, { key: KEY, pageSize: 10 }))
.resolves.toHaveLength(2);
});
it('the single-key path is unchanged — backward compatible', async () => {
const t = fakeTable(ids(1, 2490));
const out = await paginate(t.make, { pageSize: 1000 }); // no key => 'id'
expect(out).toHaveLength(2490);
expect(new Set(out.map((r) => r.id)).size).toBe(2490);
});
});
describe('tableKeys — the constraints come from the schema, not a guess', () => {
it('gives the real composite key for each context table', () => {
expect(uniqueKeyFor('batter_spray')).toEqual(['as_of_date', 'sport', 'season', 'source_id']);
expect(uniqueKeyFor('team_defense')).toEqual(['as_of_date', 'sport', 'season', 'team']);
expect(uniqueKeyFor('statcast_aggregates')).toEqual(['sport', 'season', 'source_id', 'role']);
expect(uniqueKeyFor('platoon_splits')).toEqual(['as_of_date', 'sport', 'season', 'player_key']);
expect(uniqueKeyFor('park_dimensions')).toEqual(['as_of_date', 'sport', 'venue_id']);
expect(uniqueKeyFor('hitter_opportunity')).toEqual(['as_of_date', 'sport', 'season', 'player_key']);
expect(uniqueKeyFor('lineup_context')).toEqual(['as_of_date', 'sport', 'game_pk', 'player_key']);
});
it('keeps the single-column keys single', () => {
expect(uniqueKeyFor('ledger_entries')).toEqual(['id']);
expect(uniqueKeyFor('model_snapshots')).toEqual(['id']);
expect(uniqueKeyFor('game_context')).toEqual(['game_id']);
});
it('THROWS for an unknown table rather than defaulting to id', () => {
expect(() => uniqueKeyFor('some_new_table')).toThrow(/no unique key recorded/);
});
});
+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/);
});
});
@@ -0,0 +1,263 @@
'use strict';
/**
* snapshotSettlementService — Fix A3.
* Every dependency injected; nothing here touches a network or a database.
*/
const svc = require('../../src/services/snapshotSettlementService');
const line = (over) => ({ hits: over, totalBases: over, rbi: over, runs: over });
describe('decide — side-aligned outcomes', () => {
const base = { id: 1, game_date: '2026-08-05', captured_at: '2026-08-05T14:00:00Z', stat: 'hits', player_key: 'a' };
const lines = { '2026-08-05|a': line(2) };
it('an OVER that cleared the line is a hit', () => {
const { updates } = svc.decide([{ ...base, line: 0.5, side: 'over' }], lines);
expect(updates).toEqual([{ id: 1, outcome: 'hit', actual_value: 2 }]);
});
it('an UNDER on the SAME row is the mirror — a miss, not a hit', () => {
// A raw `realized > line` indicator is the OVER perspective and would invert
// the target on every under row.
const { updates } = svc.decide([{ ...base, line: 0.5, side: 'under' }], lines);
expect(updates).toEqual([{ id: 1, outcome: 'miss', actual_value: 2 }]);
});
it('an UNDER that held is a hit', () => {
const { updates } = svc.decide([{ ...base, line: 3.5, side: 'under' }], lines);
expect(updates[0].outcome).toBe('hit');
});
it('actual_value is the RAW realized stat, unopinionated by side', () => {
const o = svc.decide([{ ...base, line: 0.5, side: 'over' }], lines).updates[0];
const u = svc.decide([{ ...base, line: 0.5, side: 'under' }], lines).updates[0];
expect(o.actual_value).toBe(2);
expect(u.actual_value).toBe(2);
});
it('reads the right box-score field per stat', () => {
const l = { '2026-08-05|a': { hits: 1, totalBases: 4, rbi: 3, runs: 0 } };
const at = (stat) => svc.decide([{ ...base, stat, line: 1.5, side: 'over' }], l).updates[0];
expect(at('total_bases').actual_value).toBe(4);
expect(at('rbi').actual_value).toBe(3);
// hits=1 does not clear 1.5 — that is a settled MISS, not an absence.
expect(at('hits')).toEqual({ id: 1, outcome: 'miss', actual_value: 1 });
});
});
describe('decide — a row logged after first pitch is not a prediction', () => {
const lines = { '2026-08-05|a': line(2) };
const row = (capturedAt) => ({ id: 1, game_date: '2026-08-05', captured_at: capturedAt, stat: 'hits', player_key: 'a', line: 0.5, side: 'over' });
it('accepts a capture from the morning of the game', () => {
// 14:00 UTC = 10:00 ET, well before first pitch.
expect(svc.decide([row('2026-08-05T14:00:00Z')], lines).counts.settled).toBe(1);
});
it('accepts a capture from the day before', () => {
expect(svc.decide([row('2026-08-04T22:00:00Z')], lines).counts.settled).toBe(1);
});
it('REFUSES a 01:00-UTC capture, which is 21:00 ET the same evening — in-game', () => {
// The trap: a UTC date compare would keep this row.
const r = svc.decide([{ ...row('2026-08-06T01:00:00Z'), game_date: '2026-08-05' }], lines);
expect(r.counts.settled).toBe(0);
expect(r.counts.post_hoc_logged).toBe(1);
});
it('REFUSES a capture from the following day', () => {
const r = svc.decide([row('2026-08-06T14:00:00Z')], lines);
expect(r.counts.post_hoc_logged).toBe(1);
});
it('isPreGame is absent-safe', () => {
expect(svc.isPreGame(null, '2026-08-05')).toBe(false);
expect(svc.isPreGame('2026-08-05T14:00:00Z', null)).toBe(false);
expect(svc.isPreGame('not-a-date', '2026-08-05')).toBe(false);
});
});
describe('decide — integrity', () => {
it('every candidate lands in exactly one bucket (conservation)', () => {
const lines = { '2026-08-05|a': line(2) };
const rows = [
{ id: 1, game_date: '2026-08-05', captured_at: '2026-08-05T14:00:00Z', stat: 'hits', player_key: 'a', line: 0.5, side: 'over' },
{ id: 2, game_date: '2026-08-05', captured_at: '2026-08-05T14:00:00Z', stat: 'hits', player_key: 'ghost', line: 0.5, side: 'over' },
{ id: 3, game_date: '2026-08-05', captured_at: '2026-08-06T01:00:00Z', stat: 'hits', player_key: 'a', line: 0.5, side: 'over' },
{ id: 4, game_date: '2026-08-05', captured_at: '2026-08-05T14:00:00Z', stat: 'hits', player_key: 'a', line: null, side: 'over' },
];
const { counts } = svc.decide(rows, lines);
expect(counts.settled).toBe(1);
expect(counts.orphaned).toBe(1);
expect(counts.unresolvable).toBe(2);
expect(counts.settled + counts.orphaned + counts.unresolvable).toBe(counts.candidates);
});
it('THROWS on a duplicate row id — a doubled prediction poisons every measure', () => {
const r = { id: 7, game_date: '2026-08-05', captured_at: '2026-08-05T14:00:00Z', stat: 'hits', player_key: 'a', line: 0.5, side: 'over' };
expect(() => svc.decide([r, r], { '2026-08-05|a': line(2) })).toThrow(/duplicate snapshot id 7/);
});
it('a player with no box-score line is ORPHANED, never settled at zero', () => {
const r = svc.decide([{ id: 1, game_date: '2026-08-05', captured_at: '2026-08-05T14:00:00Z', stat: 'hits', player_key: 'nobody', line: 0.5, side: 'over' }], {});
expect(r.counts.orphaned).toBe(1);
expect(r.updates).toHaveLength(0);
});
});
describe('battingLines — doubleheaders sum, non-batters are absent', () => {
const sched = (pks) => ({ dates: [{ games: pks.map((pk) => ({ gamePk: pk, officialDate: '2026-08-05', status: { detailedState: 'Final' } })) }] });
const box = (rows) => ({
teams: {
home: { batters: rows.map((_, i) => i + 1), players: Object.fromEntries(rows.map((r, i) => [`ID${i + 1}`, { person: { fullName: r.name }, stats: { batting: r.b } }])) },
away: { batters: [], players: {} },
},
});
it('sums both games of a doubleheader — the prop covers the day', async () => {
const getJson = async (url) => {
if (url.includes('schedule')) return sched([1, 2]);
return box([{ name: 'Aaron Judge', b: { atBats: 4, hits: 1, totalBases: 2, rbi: 1, runs: 0 } }]);
};
const lines = await svc.battingLines(['2026-08-05'], { getJson });
const k = Object.keys(lines)[0];
expect(lines[k].hits).toBe(2); // 1 + 1
expect(lines[k].totalBases).toBe(4);
});
it('omits a player who did not bat (atBats null) rather than recording zero', async () => {
const getJson = async (url) => (url.includes('schedule') ? sched([1])
: box([{ name: 'Benchy McBench', b: { atBats: null } }]));
const lines = await svc.battingLines(['2026-08-05'], { getJson });
expect(Object.keys(lines)).toHaveLength(0);
});
it('ignores a game that is not Final', async () => {
const getJson = async (url) => (url.includes('schedule')
? { dates: [{ games: [{ gamePk: 1, status: { detailedState: 'In Progress' } }] }] } : box([]));
expect(Object.keys(await svc.battingLines(['2026-08-05'], { getJson }))).toHaveLength(0);
});
it('a failed date is absent, not fatal', async () => {
const getJson = async () => { throw new Error('502'); };
await expect(svc.battingLines(['2026-08-05'], { getJson })).resolves.toEqual({});
});
});
describe('settleSnapshots — orchestration', () => {
const fakeSb = (rows, captured = []) => ({
from() {
const q = {
select() { return q; }, eq() { return q; }, in() { return q; },
is() { return q; }, lt() { return q; }, order() { return q; },
async range(from, to) { return { data: rows.slice(from, to + 1), error: null }; },
update(patch) { return { eq: (_c, id) => ({ is: async () => { captured.push({ id, patch }); return { error: null }; } }) }; },
};
return q;
},
});
it('no client → skipped, never a silent zero', async () => {
expect(await svc.settleSnapshots({})).toMatchObject({ skipped: 'supabase not configured' });
});
it('settles and reports repaired-champion rows separately', async () => {
const rows = [
{ id: 1, game_date: '2026-08-05', captured_at: '2026-08-05T14:00:00Z', stat: 'hits', player_key: 'a', line: 0.5, side: 'over', model_version: 'engine1@2026-08-07-fullwindow' },
{ id: 2, game_date: '2026-08-05', captured_at: '2026-08-05T14:00:00Z', stat: 'hits', player_key: 'a', line: 0.5, side: 'over', model_version: 'engine1@2026-07-20' },
];
const captured = [];
const res = await svc.settleSnapshots({
sb: fakeSb(rows, captured),
now: () => new Date('2026-08-09T12:00:00Z'),
getJson: async (url) => (url.includes('schedule')
? { dates: [{ games: [{ gamePk: 1, officialDate: '2026-08-05', status: { detailedState: 'Final' } }] }] }
: { teams: { home: { batters: [1], players: { ID1: { person: { fullName: 'A' }, stats: { batting: { atBats: 4, hits: 2, totalBases: 2, rbi: 0, runs: 0 } } } } }, away: { batters: [], players: {} } } }),
});
expect(res.written).toBe(2);
expect(res.repaired_champion_settled).toBe(1);
// Real outcomes, not nulls.
for (const c of captured) {
expect(c.patch.outcome).toBe('hit');
expect(c.patch.actual_value).toBe(2);
expect(c.patch.settlement_source).toBe('statsapi_boxscore');
}
});
it('bounds the dates per run and drains NEWEST first (oldest-first deadlocks)', async () => {
const rows = Array.from({ length: 12 }, (_, i) => ({
id: i + 1, game_date: `2026-07-${String(i + 10).padStart(2, '0')}`,
captured_at: `2026-07-${String(i + 10).padStart(2, '0')}T14:00:00Z`,
stat: 'hits', player_key: 'a', line: 0.5, side: 'over',
}));
const res = await svc.settleSnapshots({
sb: fakeSb(rows), now: () => new Date('2026-08-09T12:00:00Z'),
maxDates: 3, maxWindows: 1, getJson: async () => ({ dates: [] }),
});
expect(res.dates).toHaveLength(3);
expect(res.dates_remaining).toBe(9);
// Newest first: an old date whose remaining rows are all post-hoc or
// orphaned would otherwise re-process forever and block the window.
expect(res.dates[0]).toBe('2026-07-21');
});
it('ADVANCES past a drained window so older settleable dates are reached', async () => {
// The newest dates keep rows that can never settle (no box-score line), so a
// fixed window would sit on them forever. Only the OLDEST date here has a
// matching box score; the run must walk to it.
const rows = Array.from({ length: 4 }, (_, i) => ({
id: i + 1, game_date: `2026-08-0${i + 1}`, captured_at: `2026-08-0${i + 1}T14:00:00Z`,
stat: 'hits', player_key: i === 0 ? 'a' : `ghost${i}`, line: 0.5, side: 'over',
}));
const res = await svc.settleSnapshots({
sb: fakeSb(rows), now: () => new Date('2026-08-09T12:00:00Z'),
maxDates: 1, maxWindows: 4,
getJson: async (url) => (url.includes('schedule')
? { dates: [{ games: [{ gamePk: 1, officialDate: '2026-08-01', status: { detailedState: 'Final' } }] }] }
: { teams: { home: { batters: [1], players: { ID1: { person: { fullName: 'A' }, stats: { batting: { atBats: 4, hits: 2, totalBases: 2, rbi: 0, runs: 0 } } } } }, away: { batters: [], players: {} } } }),
});
expect(res.windows_scanned).toBe(4); // walked past three drained windows
expect(res.written).toBe(1);
expect(res.dates).toEqual(['2026-08-01']);
});
it('counts permanently-unsettleable rows rather than hiding them', async () => {
const rows = [
// logged at 01:00 UTC = 21:00 ET the same evening — in-game, never a prediction
{ id: 1, game_date: '2026-08-05', captured_at: '2026-08-06T01:00:00Z', stat: 'hits', player_key: 'a', line: 0.5, side: 'over' },
{ id: 2, game_date: '2026-08-05', captured_at: '2026-08-05T14:00:00Z', stat: 'hits', player_key: 'a', line: 0.5, side: 'over' },
];
const res = await svc.settleSnapshots({
sb: fakeSb(rows), now: () => new Date('2026-08-09T12:00:00Z'),
getJson: async () => ({ dates: [] }),
});
expect(res.permanently_unsettleable).toBe(1);
});
it('dry run writes nothing', async () => {
const captured = [];
const rows = [{ id: 1, game_date: '2026-08-05', captured_at: '2026-08-05T14:00:00Z', stat: 'hits', player_key: 'a', line: 0.5, side: 'over' }];
const res = await svc.settleSnapshots({
sb: fakeSb(rows, captured), write: false, now: () => new Date('2026-08-09T12:00:00Z'),
getJson: async () => ({ dates: [] }),
});
expect(res.mode).toBe('dry-run');
expect(captured).toHaveLength(0);
});
});
describe('settlement does NOT reconstruct context', () => {
it('touches no context table and no context column', () => {
const src = require('fs').readFileSync(
require('path').join(__dirname, '..', '..', 'src/services/snapshotSettlementService.js'), 'utf8');
for (const t of ['batter_spray', 'team_defense', 'platoon_splits', 'park_dimensions',
'game_context', 'hitter_opportunity', 'statcast_aggregates', 'lineup_context']) {
expect(src).not.toContain(`'${t}'`);
}
// It writes outcome columns only.
expect(src).toMatch(/outcome: u\.outcome/);
expect(src).not.toMatch(/features:/);
});
});
+67
View File
@@ -0,0 +1,67 @@
'use strict';
/** The withdrawal record — spec: specs/read-integrity-harness.md §6. */
const wv = require('../../src/services/model/withdrawnVerdicts');
describe('withdrawnVerdicts', () => {
it('records BOTH hits PROVES as withdrawn', () => {
expect(wv.isWithdrawn('mlb', 'hits', 'defense_by_direction')).toBe(true);
expect(wv.isWithdrawn('mlb', 'hits', 'pitcher_contact_profile')).toBe(true);
});
it('does not withdraw a factor that was never proven', () => {
// platoon_severity never returned PROVES (n=452 < 500). Withdrawing it would
// imply it once stood, which is its own falsehood.
expect(wv.isWithdrawn('mlb', 'hits', 'platoon_severity')).toBe(false);
expect(wv.isWithdrawn('mlb', 'hits', 'park_hits')).toBe(false);
});
it('is scoped per stat — a hits withdrawal says nothing about total_bases', () => {
expect(wv.isWithdrawn('mlb', 'total_bases', 'defense_by_direction')).toBe(false);
});
it('APPENDS rather than replaces: the original verdict and evidence survive', () => {
for (const w of wv.WITHDRAWALS) {
expect(w.original_verdict).toBe('PROVES');
expect(w.original_evidence.n).toBeGreaterThan(0);
expect(Array.isArray(w.original_evidence.ci)).toBe(true);
}
});
it('every withdrawal carries a measured cause and implicated readers', () => {
for (const w of wv.WITHDRAWALS) {
expect(w.cause).toMatch(/unordered-pagination/);
expect(w.cause).toMatch(/2026-08-09/);
expect(w.readers_implicated.length).toBeGreaterThan(0);
expect(w.status).toBe(wv.STATUS.WITHDRAWN_PENDING_REAUDIT);
}
});
it('names the eligibility arithmetic for defense_by_direction AS arithmetic', () => {
const d = wv.WITHDRAWALS.find((w) => w.factor === 'defense_by_direction');
expect(d.eligibility_note).toMatch(/422/);
expect(d.eligibility_note).toMatch(/not a re-measurement/);
});
it('records the honesty gap: both are STILL SERVED despite withdrawal', () => {
const served = wv.servedButWithdrawn();
expect(served).toHaveLength(2);
for (const w of served) expect(w.still_served_note).toMatch(/hitsFactors/);
});
it('states a reinstatement path that requires a measured clean read', () => {
for (const w of wv.WITHDRAWALS) expect(w.reinstatement).toMatch(/readIntegrity/);
});
it('is frozen — a withdrawal must not be mutated in place', () => {
expect(Object.isFrozen(wv.WITHDRAWALS)).toBe(true);
for (const w of wv.WITHDRAWALS) expect(Object.isFrozen(w)).toBe(true);
});
it('withdrawalsFor filters by sport and stat', () => {
expect(wv.withdrawalsFor('mlb', 'hits')).toHaveLength(2);
expect(wv.withdrawalsFor('mlb', 'runs')).toHaveLength(0);
expect(wv.withdrawalsFor('mlb')).toHaveLength(2);
});
});