Files
vyndr/tests/unit/shadowAccrual.test.js
T
builtbykev 8d741e3932 Pre-grading stage trace: record which branch loses the cohort
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
2026-08-28 00:41:34 -04:00

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/);
});
});