8d741e3932
The acquisition recorder proved the previous diagnosis wrong: the 03:00 MLB
attempt acquired 6,365 props from a propline cache hit and CONTINUED. Zero
reached the first grading callback, and nothing durable says why.
WHY REPLAY WAS IMPOSSIBLE. The 03:00 input was the cache object written
02:20:24.889Z. ODDS_CACHE_TTL defaults to 3600s, so it expired ~03:20; it is
now gone. No historical copy exists: closing_captures holds no MLB rows past
01:40:52, model_snapshots none, and `bookprices:mlb` carries no timestamp the
book-comparison route exposes. Calling /api/odds/mlb was REFUSED — a cold cache
would fetch and fire recordDownstream -> gradeAndCacheSlate, writing product
state and manufacturing a false recovery. So the input is NOT RECOVERABLE and
no replay was attempted.
A SECOND CLAIM IS WITHDRAWN. The previous tranche concluded "all 6,365 props
were rejected at event admission", reasoning that dedupe cannot empty a
non-empty list. That is FALSE: `dedupeProps` also FILTERS — it drops props with
no player/stat_type/line and any prop whose book is not a MODEL book. A fully
admitted cohort can still dedupe to zero. Demonstrated in test. So the failing
branch was never established, only assumed — which is exactly what the order
forbade, and why DEDUPE_EMPTY is a first-class outcome here.
THE REGION IS SMALL AND EVERY BRANCH LOOKS IDENTICAL FROM OUTSIDE:
identity annotation (runSnapshot, mlb only, own catch)
-> admitForGrading (pure, can throw)
-> dedupeProps (pure, can throw, ALSO filters)
-> mapLimit(gradeBestSide) <- first onGraded-capable call
gradeAndCacheSlate swallows every throw in that region and returns the same
{written:false,count:0} it returns for an honest zero.
The recorder distinguishes them: READY_FOR_GRADING · ALL_REJECTED ·
IDENTITY_STAGE_ERROR · ADMISSION_STAGE_ERROR · DEDUPE_STAGE_ERROR ·
DEDUPE_EMPTY · NO_INPUT_PROPS · OTHER_PREGRADING_ERROR. An exception is never
folded into ALL_REJECTED, and with no admission evidence the classifier refuses
to classify at all — a test pins that.
EXCEPTION SEMANTICS UNCHANGED. Each stage is wrapped to record and then RETHROW
the identical error, so the enclosing best-effort catch still handles it exactly
as before: nothing caught that was not caught, nothing swallowed that was not
swallowed. Admission and dedupe rules, the impossible-binding invariant, event
identity, model books and the first-row-wins cap are untouched — the only
behavioural lines in the diff are `const gate/unique` becoming `let`.
Correlated to the SAME snapshot_attempt_id the acquisition recorder minted — not
a new run id — and stored under its own key `ops:pregrade:{sport}` so it can
never displace the acquisition record. Same atomic LPUSH/LTRIM pattern, bounded
per sport, scheduled-only, best-effort at the call site, auto-disabled under
test.
CAUGHT DURING BUILD: `savePgTrace` was defined and NEVER CALLED — the recorder
would have persisted nothing, the same "built, correct, never invoked" failure
this programme has hit before. A test now drives the real runSnapshot and
asserts a trace is persisted carrying the acquisition's attempt id.
Ten teeth, injections verified present, against a green baseline:
empty collector read as ALL_REJECTED (2) · admission throw reported as
admitted=0 (1) · dedupe throw reported as output=0 (1) · dedupe-to-zero
mislabeled as rejection (1) · identity exception hidden (1) · observer reorders
the candidate array (1) · trace failure changes product outcome (1) · intraday
overwrites scheduled (1) · different attempt id (2) · secret leak (1).
Restored byte-identically.
THREE OF THOSE LANDED AND PASSED FIRST TIME — coverage holes, not safe defects:
the dedupe-throw and identity-throw tests only exercised the recorder directly,
never the real path, and nothing asserted the CANDIDATE array is not reordered.
All three closed with real-path tests, then re-run failing.
Two stale source assertions updated with the reason recorded: both pinned the
exact `const gate = …` / opts-key order that the recording wrapper changed.
387 suites / 5,237 tests pass. web tsc exit 0. Lineage stays OFF.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
178 lines
7.5 KiB
JavaScript
178 lines
7.5 KiB
JavaScript
'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/);
|
|
// The opts object now leads with the diagnostic `pregrade` recorder, so
|
|
// assert both keys are passed rather than pinning their exact order.
|
|
const call = src.slice(src.indexOf('await deps.gradeAndCacheSlate(sp, props, {'));
|
|
const opts = call.slice(0, call.indexOf('});'));
|
|
expect(opts).toMatch(/factorContext,/);
|
|
expect(opts).toMatch(/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/);
|
|
});
|
|
});
|