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:
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user