checkpoint: chain shadow, WNBA possession feed, baseball chain

Backup commit of uncommitted working-tree state found during Legion
recon (Tony resurrection, STEP 0). This work existed only on the
laptop disk.

- chain shadow accrual + probe script (038_chain_shadow.sql)
- WNBA possession feed: ESPN adapter, usage service, verify script
  (039_wnba_player_game.sql)
- baseball chain
- retention/snapshot service updates, tableKeys, matchupKeys
- specs: chain-v1, wnba-possession-feed, wnba-source-survey
- unit tests for the above

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QnvJAkC3h5QGmb6dipoiWn
This commit is contained in:
Kev
2026-08-14 16:53:37 -04:00
parent 0657b71d18
commit 6c34af3414
22 changed files with 4958 additions and 32 deletions
+45
View File
@@ -156,6 +156,13 @@ function rowsFromSides(base, sides, ctx = {}) {
// the multiplier is recomputable from them and is deliberately not stored.
// Null on every non-hits row and on any row with no factor context.
factor_inputs: s.factor_inputs || null,
// The SHADOW CHAIN read. Declared here (always present, usually null) so
// every row in a batch carries the same keys — PostgREST builds a bulk
// insert from the FIRST row's shape, so a column that appears only on some
// rows is silently dropped for the whole batch. Filled by
// `mergeChainShadow` after enrichment; never read by anything served.
chain_shadow: null,
});
}
return rows;
@@ -246,6 +253,43 @@ function mergeEnrichment(rows, enrichedGrades) {
});
}
/**
* Attach the SHADOW CHAIN read to collected rows — the (chain_p, counter_p,
* outcome) triple's first two thirds.
*
* Like `mergeEnrichment` this fills ONE field and touches nothing else. It runs
* later than grade time for the same structural reason the archetype does: the
* chain needs the statcast rows and park factors the enrichment pass loaded, and
* pulling that forward into the grader would put per-prop I/O on the serving
* path.
*
* THE SIDE ALIGNMENT IS THE LOAD-BEARING PART. The chain computes P(over the
* line); `p_win` on the row is expressed for the graded SIDE. Storing the raw
* over-probability against an under row's `p_win` would invert every comparison
* made from it afterwards, silently — so `alignToSide` is given the row's own
* side and the row's own counter probability, and both halves of the triple end
* up pointing the same way. `outcome` is written by the ordinary settle pass and
* is already side-aligned, which completes it.
*
* A row the chain could not read is left NULL. It is not a zero probability and
* not an average hitter — the chain refusing to read someone is a fact worth
* keeping, and a fabricated third of a triple would poison the adjudication this
* column exists to enable.
*/
function mergeChainShadow(rows, shadow) {
if (!Array.isArray(rows) || !rows.length) return rows || [];
const byKey = shadow && shadow.byKey;
if (!byKey || typeof byKey.get !== 'function') return rows;
const cs = require('./model/chainShadow');
return rows.map((r) => {
const block = byKey.get(cs.shadowKey(r.player_key, r.stat, r.line));
if (!block) return r;
const aligned = cs.alignToSide(block, r.side, r.p_win);
return aligned ? { ...r, chain_shadow: aligned } : r;
});
}
function newSnapshotId() {
return crypto.randomUUID();
}
@@ -257,6 +301,7 @@ module.exports = {
rowsFromSides,
createCollector,
mergeEnrichment,
mergeChainShadow,
persist,
newSnapshotId,
__internals: { numOrNull, intOrNull, boolOrNull },