MLB canonical event identity, impossible-binding refusal, event-aware dedupe, publication commit
Release-isolated slice built from 41ba38e. Ships ONLY the event-integrity +
publication + lineage-canary closure; the 90-path development tree stays
undeployed.
- canonical MLB event identity from statsapi gamePk (mlb:gamepk:<pk>), with
event_identity_source/method/version recorded. The id is canonical; the
binding is derived and says so.
- IMPOSSIBLE-BINDING REFUSAL. Verified in prod 2026-08-26: Joe Mack (Marlins)
was bound to Dodgers@Braves and Yandy Diaz (Rays) to Rangers@WhiteSox, both
from one book in the 01:00/03:01 UTC cycles after their own games began. Root
cause is source market data, not the binder. A prop whose player's team is not
an event participant now refuses; unknown team preserves uncertainty.
- event-aware dedupe: books still collapse, events no longer do. An unresolved
MLB event fractures rather than falling back to the collision-prone
date+teams key.
- publication commit moved AFTER the authoritative Redis slate write, with
exact parity-gap identity when the product publishes and the record does not.
- lineage dual-write behind LINEAGE_CANARY_SPORTS, DISABLED for this deploy.
Excluded deliberately: WNBA feed/chain, market ontology, PerformanceDistribution,
calibration certification, truth diagnostics, applyRevision Phase-1, and the
analyzeViaEngine1 confidence-rounding change (a served field).
Suite 380/5,040/0 from this worktree; web tsc exit 0; champion output identical
to production.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
This commit is contained in:
@@ -88,6 +88,10 @@ function boolOrNull(v) {
|
||||
*/
|
||||
function rowsFromSides(base, sides, ctx = {}) {
|
||||
const rows = [];
|
||||
// `published` is declared on every row and defaults FALSE. A capture is not a
|
||||
// publication until the winner signal says so; defaulting true would assert
|
||||
// that 65% of these rows were shown to a user.
|
||||
|
||||
const list = Array.isArray(sides) ? sides : [];
|
||||
for (const s of list) {
|
||||
if (!s) continue;
|
||||
@@ -143,6 +147,17 @@ function rowsFromSides(base, sides, ctx = {}) {
|
||||
archetype: s.archetype || null,
|
||||
|
||||
refused,
|
||||
// Set TRUE only by the collector's publication signal (see createCollector).
|
||||
published: false,
|
||||
// CANONICAL EVENT IDENTITY. Declared on every row; null when the sport
|
||||
// has no resolver or the event could not be resolved. `game_id` below
|
||||
// stays as the LEGACY DERIVED label — useful for diagnostics and
|
||||
// compatibility, never authoritative for identity.
|
||||
canonical_event_id: (s.canonical_event_id ?? base.canonical_event_id) ?? null,
|
||||
event_identity_source: (s.event_identity_source ?? base.event_identity_source) ?? null,
|
||||
event_identity_method: (s.event_identity_method ?? base.event_identity_method) ?? null,
|
||||
event_identity_version: (s.event_identity_version ?? base.event_identity_version) ?? null,
|
||||
event_occurrence: (s.event_occurrence ?? base.event_occurrence) ?? null,
|
||||
refusal_reason: refused
|
||||
? (s.suppressed_reason || (s.insufficient_data ? 'insufficient_data' : 'no_grade'))
|
||||
: null,
|
||||
@@ -171,16 +186,397 @@ function rowsFromSides(base, sides, ctx = {}) {
|
||||
/** A collector to hand to gradeAndCacheSlate's `onGraded` hook. */
|
||||
function createCollector(ctx) {
|
||||
const rows = [];
|
||||
// Index by (player|stat|line|side) so the publication signal can find the row
|
||||
// it already collected without re-deriving the winner rule.
|
||||
const index = new Map();
|
||||
const keyOf = (r) => [r.player_key, r.stat, r.line, r.side].join('|');
|
||||
return {
|
||||
rows,
|
||||
onGraded(base, sides) {
|
||||
try {
|
||||
rows.push(...rowsFromSides(base, sides, ctx));
|
||||
const made = rowsFromSides(base, sides, ctx);
|
||||
for (const r of made) index.set(keyOf(r), r);
|
||||
rows.push(...made);
|
||||
} catch { /* collection never affects grading */ }
|
||||
},
|
||||
/**
|
||||
* PUBLICATION SIGNAL — fired only for the side that actually reaches the
|
||||
* slate.
|
||||
*
|
||||
* ── WHY THIS EXISTS ──────────────────────────────────────────────────
|
||||
* `onGraded` fires with BOTH sides, graded AND refused, BEFORE any
|
||||
* filtering. Only the higher-confidence graded side becomes the served
|
||||
* Read. MEASURED on mlb 2026-08-26: of 13,012 collected rows, 3,859 were
|
||||
* refusals and only 4,569 matched a prop that reached the ledger — 8,443
|
||||
* (64.9%) describe a state the user was never shown.
|
||||
*
|
||||
* Nothing on the row said which was which, so treating every capture as a
|
||||
* published claim would have built "publication history" for model activity
|
||||
* that was never published. This marks the difference at the one place that
|
||||
* knows it: the moment the winner is chosen.
|
||||
*/
|
||||
onPublished(base, winner) {
|
||||
try {
|
||||
if (!winner) return;
|
||||
const made = rowsFromSides(base, [winner], ctx);
|
||||
for (const r of made) {
|
||||
const hit = index.get(keyOf(r));
|
||||
if (hit) { hit.published = true; hit.published_side = true; }
|
||||
}
|
||||
} catch { /* signalling never affects grading */ }
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* DUAL-WRITE: attach append-only Read lineage to rows about to be persisted.
|
||||
*
|
||||
* ── WHY THIS IS BEST-EFFORT AND WHY IT MUST DECLARE EVERY KEY ────────────
|
||||
* Retention is already best-effort relative to the ledger; lineage sits one
|
||||
* layer further out, so a lineage failure must never cost a retention row and
|
||||
* never reach the grade. Every failure path below leaves the row persistable
|
||||
* with NULL lineage, which reads as "unknown" — the honest value.
|
||||
*
|
||||
* Lineage keys are declared on EVERY row even when unresolved. PostgREST builds
|
||||
* a bulk insert from the FIRST row's shape, so a key present on only some rows
|
||||
* is dropped for the whole batch — the same defect that silently killed
|
||||
* retention for three days when `chain_shadow` was missing from the schema.
|
||||
*
|
||||
* NOT AUTHORITATIVE. Nothing reads these columns to serve a product surface;
|
||||
* this records truth in parallel so it can be compared against the live path
|
||||
* before any authority changes.
|
||||
*/
|
||||
const LINEAGE_KEYS = Object.freeze([
|
||||
'read_id', 'read_natural_key', 'claim_digest', 'revision_ordinal',
|
||||
'supersedes_id', 'recaptures_id', 'lineage_action', 'lineage_state',
|
||||
'lineage_version', 'change_type', 'claim_schema_version',
|
||||
'digest_algorithm_version',
|
||||
]);
|
||||
|
||||
/** Claim fields the resolver needs from EXISTING rows to classify a change. */
|
||||
const LINEAGE_FETCH_CLAIM = Object.freeze([
|
||||
'canonical_event_id',
|
||||
'line', 'side', 'book', 'over_odds', 'under_odds',
|
||||
'p_win', 'grade', 'confidence', 'projection', 'edge_pct', 'ev_pct',
|
||||
'takeable', 'value', 'fair_odds', 'fair_prob', 'confidence_basis',
|
||||
'refused', 'refusal_reason',
|
||||
]);
|
||||
|
||||
function blankLineage() {
|
||||
const o = {};
|
||||
for (const k of LINEAGE_KEYS) o[k] = null;
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* ── DUAL-WRITE FAILURE CONTRACT ──────────────────────────────────────────
|
||||
* Lineage lives on the SAME ROW as the retention record and is attached before
|
||||
* the single upsert that writes it. That is not a convenience; it is what makes
|
||||
* two of the four failure quadrants structurally impossible:
|
||||
*
|
||||
* legacy OK + lineage OK one insert carries both.
|
||||
* legacy OK + lineage FAILS the row still persists with NULL lineage. The
|
||||
* claim is retained; only its position in the
|
||||
* chronology is unknown, and `out.lineage.error`
|
||||
* plus the not_published/refused counters make
|
||||
* the gap measurable rather than silent.
|
||||
* legacy FAILS + lineage OK IMPOSSIBLE. There is no separate lineage
|
||||
* write, so a failed retention insert cannot
|
||||
* leave behind a lineage record claiming a Read
|
||||
* was published. This is the quadrant that would
|
||||
* manufacture false publication history, and the
|
||||
* write ordering removes it rather than guarding
|
||||
* against it.
|
||||
* legacy FAILS + lineage n/a nothing is written at all.
|
||||
*
|
||||
* Ordering matters and is deliberate: resolve first, then one write. A separate
|
||||
* lineage table written after the fact would reintroduce the impossible
|
||||
* quadrant as a real one.
|
||||
*/
|
||||
async function attachLineage(rows, deps = {}) {
|
||||
const out = {
|
||||
attempted: Array.isArray(rows) ? rows.length : 0,
|
||||
origins: 0, revisions: 0, recaptures: 0, forks: 0, refused: 0, unresolved: 0,
|
||||
// A capture that was never the served Read is NOT a lineage failure. It is
|
||||
// counted separately so the parity gap stays legible: lumping it into
|
||||
// `refused` would make a healthy slate look broken.
|
||||
not_published: 0,
|
||||
change_types: {},
|
||||
error: null,
|
||||
};
|
||||
const list = Array.isArray(rows) ? rows : [];
|
||||
// Declare the keys first, unconditionally. Even a total failure leaves a
|
||||
// persistable batch shape.
|
||||
for (const r of list) Object.assign(r, blankLineage());
|
||||
if (list.length === 0) return out;
|
||||
|
||||
try {
|
||||
const lineage = deps.lineage || require('./read/readLineage');
|
||||
const mintReadId = deps.mintReadId || (() => require('crypto').randomUUID());
|
||||
|
||||
const keyed = [];
|
||||
for (const r of list) {
|
||||
const key = lineage.readNaturalKey(r);
|
||||
if (!key) { out.unresolved += 1; continue; }
|
||||
r.read_natural_key = key;
|
||||
keyed.push(r);
|
||||
}
|
||||
if (keyed.length === 0) return out;
|
||||
|
||||
// ONE bounded read of the existing families for these natural keys.
|
||||
const keys = [...new Set(keyed.map((r) => r.read_natural_key))];
|
||||
const fetchExisting = deps.fetchExisting || (async (naturalKeys) => {
|
||||
const getClient = deps.getClient || require('../utils/supabase').getSupabaseServiceClient;
|
||||
const supabase = getClient();
|
||||
if (!supabase) return null;
|
||||
const found = [];
|
||||
const CH = 100; // never send an unbounded id list — it becomes a URL
|
||||
for (let i = 0; i < naturalKeys.length; i += CH) {
|
||||
const { data, error } = await supabase
|
||||
.from('model_snapshots')
|
||||
.select(['id', 'read_id', 'read_natural_key', 'game_id', 'claim_digest',
|
||||
'revision_ordinal', 'lineage_action', 'supersedes_id', 'captured_at',
|
||||
...LINEAGE_FETCH_CLAIM].join(', '))
|
||||
.in('read_natural_key', naturalKeys.slice(i, i + CH));
|
||||
if (error) throw new Error(error.message);
|
||||
if (Array.isArray(data)) found.push(...data);
|
||||
}
|
||||
return found;
|
||||
});
|
||||
|
||||
const existing = await fetchExisting(keys);
|
||||
if (existing === null) return out; // no database configured — leave NULL
|
||||
|
||||
const byKey = new Map();
|
||||
for (const e of existing) {
|
||||
// The prior claim rides alongside so `classifyChange` can say WHY the
|
||||
// published state advanced, not merely that the digest differs.
|
||||
const claim = {};
|
||||
for (const f of LINEAGE_FETCH_CLAIM) claim[f] = e[f];
|
||||
const entry = { ...e, claim };
|
||||
if (!byKey.has(e.read_natural_key)) byKey.set(e.read_natural_key, []);
|
||||
byKey.get(e.read_natural_key).push(entry);
|
||||
}
|
||||
|
||||
for (const r of keyed) {
|
||||
const res = lineage.resolveLineage({
|
||||
candidate: r,
|
||||
existing: byKey.get(r.read_natural_key) || [],
|
||||
mintReadId,
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.refused === 'not_published') out.not_published += 1;
|
||||
else if (res.action === lineage.LINEAGE_ACTION.FORK_DETECTED) out.forks += 1;
|
||||
else out.refused += 1;
|
||||
// A refusal leaves NULL lineage on a row that is still persisted. The
|
||||
// claim is retained; only its position in the chronology is unknown.
|
||||
continue;
|
||||
}
|
||||
r.read_id = res.read_id;
|
||||
r.claim_digest = res.claim_digest;
|
||||
r.revision_ordinal = res.revision_ordinal;
|
||||
r.supersedes_id = res.supersedes_id ?? null;
|
||||
r.recaptures_id = res.recaptures_id ?? null;
|
||||
r.lineage_action = res.action;
|
||||
r.lineage_state = res.lineage_state;
|
||||
r.lineage_version = res.lineage_version;
|
||||
r.change_type = res.change_type || null;
|
||||
r.claim_schema_version = res.claim_schema_version || null;
|
||||
r.digest_algorithm_version = res.digest_algorithm_version || null;
|
||||
if (res.change_type) {
|
||||
out.change_types[res.change_type] = (out.change_types[res.change_type] || 0) + 1;
|
||||
}
|
||||
|
||||
if (res.action === lineage.LINEAGE_ACTION.ORIGIN) out.origins += 1;
|
||||
else if (res.action === lineage.LINEAGE_ACTION.REVISION) out.revisions += 1;
|
||||
else if (res.action === lineage.LINEAGE_ACTION.RECAPTURE) out.recaptures += 1;
|
||||
|
||||
// Newly minted read_ids must be visible to later rows in the SAME batch,
|
||||
// or two rows of one Read would each mint an id and fork it immediately.
|
||||
const fam = byKey.get(r.read_natural_key) || [];
|
||||
const claim = {};
|
||||
for (const f of LINEAGE_FETCH_CLAIM) claim[f] = r[f];
|
||||
fam.push({
|
||||
id: null, read_id: r.read_id, read_natural_key: r.read_natural_key,
|
||||
game_id: r.game_id, claim_digest: r.claim_digest,
|
||||
revision_ordinal: r.revision_ordinal, lineage_action: r.lineage_action,
|
||||
supersedes_id: r.supersedes_id, captured_at: r.captured_at, claim,
|
||||
});
|
||||
byKey.set(r.read_natural_key, fam);
|
||||
}
|
||||
} catch (e) {
|
||||
out.error = e && e.message ? e.message : String(e);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The unique index that prevents a forked chain, named so the guard is legible. */
|
||||
const SUPERSEDES_CONSTRAINT = 'model_snapshots_supersedes_unique';
|
||||
const FORK_RETRY_LIMIT = 2;
|
||||
|
||||
function isSupersedesConflict(error) {
|
||||
if (!error) return false;
|
||||
const m = `${error.message || ''} ${error.details || ''}`;
|
||||
return m.includes(SUPERSEDES_CONSTRAINT) || (error && error.code === '23505' && m.includes('supersedes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover a batch that lost a supersedes race.
|
||||
*
|
||||
* Bounded on purpose: two attempts, then an explicit failure. An unbounded loop
|
||||
* against a writer that keeps winning would spin forever, and the correct answer
|
||||
* after a couple of misses is to report the gap rather than keep trying.
|
||||
*
|
||||
* Sequence per attempt: re-resolve the batch against the CURRENT head, then
|
||||
* write. Re-resolution is what makes this safe -- if the rival already published
|
||||
* an identical claim, `attachLineage` returns RECAPTURE and the row lands
|
||||
* idempotently; if our claim is still materially different it appends after the
|
||||
* new head instead of trying to supersede a parent that is no longer the head.
|
||||
*/
|
||||
async function recoverFromFork(chunk, deps = {}) {
|
||||
const out = { recovered: false, written: 0, attempts: 0, error: null };
|
||||
const supabase = deps.supabase;
|
||||
for (let attempt = 1; attempt <= FORK_RETRY_LIMIT; attempt += 1) {
|
||||
out.attempts = attempt;
|
||||
try {
|
||||
// Clear stale lineage so re-resolution sees the row as a fresh candidate.
|
||||
for (const r of chunk) Object.assign(r, blankLineage());
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await attachLineage(chunk, deps);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { error } = await supabase
|
||||
.from('model_snapshots')
|
||||
.upsert(chunk, { onConflict: 'snapshot_id,player_key,stat,line,side' });
|
||||
if (!error) { out.recovered = true; out.written = chunk.length; return out; }
|
||||
if (!isSupersedesConflict(error)) { out.error = error.message; return out; }
|
||||
out.error = error.message;
|
||||
} catch (e) {
|
||||
out.error = e && e.message ? e.message : String(e);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* COMMIT PUBLICATION - the only place a claim becomes "published".
|
||||
*
|
||||
* -- WHY THIS IS SEPARATE FROM THE CAPTURE WRITE -------------------------
|
||||
* The authoritative served slate is ONE atomic Redis write,
|
||||
* `cacheSet('snapshot:{sport}:latest', snapshot)`. Retention persists BEFORE
|
||||
* that write (measured: persistRetention at snapshotService:1137, the slate
|
||||
* write at :1152), and the winner is selected earlier still. So neither capture
|
||||
* nor winner-selection can mean "published":
|
||||
*
|
||||
* winner selected -> intended for the slate
|
||||
* retention persisted -> the capture is on the record
|
||||
* REDIS WRITE SUCCEEDS -> the claim was actually made available <-- HERE
|
||||
*
|
||||
* Setting the marker any earlier lets lineage assert a publication that a failed
|
||||
* Redis write never made. That is the one thing lineage must never do.
|
||||
*
|
||||
* -- PUBLICATION IS SLATE-LEVEL, BECAUSE THE WRITE IS --------------------
|
||||
* The whole envelope commits or none of it does, so every served row shares one
|
||||
* `publication_id` and one `published_at`. Modelling per-row publication would
|
||||
* claim a granularity the transport does not have.
|
||||
*
|
||||
* -- FAILURE DIRECTION IS DELIBERATE ------------------------------------
|
||||
* If this update fails after a successful Redis write, rows stay
|
||||
* `published: false` with NULL lineage. The record then UNDERSTATES what was
|
||||
* served, which is recoverable and observable. The opposite error - claiming a
|
||||
* publication that did not happen - is not.
|
||||
*/
|
||||
async function commitPublication(spec, deps = {}) {
|
||||
const s = spec || {};
|
||||
const out = {
|
||||
publication_id: s.publicationId || null,
|
||||
published_at: s.publishedAt || null,
|
||||
candidates: 0, published: 0, skipped: false, error: null, lineage: null,
|
||||
};
|
||||
const served = Array.isArray(s.rows) ? s.rows.filter((r) => r && r.published === true) : [];
|
||||
out.candidates = served.length;
|
||||
if (served.length === 0) return out;
|
||||
|
||||
try {
|
||||
const getClient = deps.getClient || require('../utils/supabase').getSupabaseServiceClient;
|
||||
const supabase = getClient();
|
||||
if (!supabase) { out.skipped = true; return out; }
|
||||
|
||||
for (const r of served) {
|
||||
r.published_at = out.published_at;
|
||||
r.publication_id = out.publication_id;
|
||||
}
|
||||
// Lineage resolves ONLY over confirmed-published rows.
|
||||
if (deps.lineage !== false) {
|
||||
out.lineage = await attachLineage(served, { ...deps, getClient });
|
||||
}
|
||||
|
||||
const CHUNK = 250;
|
||||
for (let i = 0; i < served.length; i += CHUNK) {
|
||||
const chunk = served.slice(i, i + CHUNK);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { error } = await supabase
|
||||
.from('model_snapshots')
|
||||
.upsert(chunk, { onConflict: 'snapshot_id,player_key,stat,line,side' });
|
||||
if (!error) { out.published += chunk.length; continue; }
|
||||
|
||||
// ── CONCURRENCY RECOVERY ────────────────────────────────────────────
|
||||
// `model_snapshots_supersedes_unique` rejects a second row superseding the
|
||||
// same parent. That constraint is the authority against a forked history,
|
||||
// and hitting it means another writer advanced the chain between our
|
||||
// resolve and our write. The loser must not silently drop a materially new
|
||||
// published state, and must not loop.
|
||||
if (!isSupersedesConflict(error)) { out.error = error.message; break; }
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const rec = await recoverFromFork(chunk, { ...deps, getClient, supabase });
|
||||
out.conflicts = (out.conflicts || 0) + 1;
|
||||
if (rec.recovered) {
|
||||
out.recovered = (out.recovered || 0) + 1;
|
||||
out.published += rec.written;
|
||||
} else {
|
||||
out.recovery_failed = (out.recovery_failed || 0) + 1;
|
||||
out.error = rec.error || 'fork recovery exhausted';
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
out.error = e && e.message ? e.message : String(e);
|
||||
}
|
||||
|
||||
// ── EXACT PARITY-GAP IDENTITY ──────────────────────────────────────────
|
||||
// "lineage failed" is not an answer. When the product published successfully
|
||||
// and the record did not, the system must be able to say WHICH EXACT SERVED
|
||||
// CLAIM is missing, without re-deriving it from player/stat/line/date.
|
||||
//
|
||||
// Everything needed to recover it deterministically is captured here from the
|
||||
// in-memory rows that were actually served.
|
||||
if (out.error || (out.lineage && out.lineage.error) || out.published < out.candidates) {
|
||||
out.parity_gap = Object.freeze({
|
||||
sport: served[0] ? served[0].sport : null,
|
||||
publication_id: out.publication_id,
|
||||
published_at: out.published_at,
|
||||
attempted_at: (deps.now || (() => new Date().toISOString()))(),
|
||||
reason: out.error || (out.lineage && out.lineage.error) || 'partial_commit',
|
||||
missing_count: out.candidates - out.published,
|
||||
// The exact rows, by the identity the retention table itself keys on.
|
||||
missing: Object.freeze(served.slice(out.published).map((r) => Object.freeze({
|
||||
snapshot_id: r.snapshot_id || null,
|
||||
player_key: r.player_key || null,
|
||||
stat: r.stat || null,
|
||||
line: r.line === undefined ? null : r.line,
|
||||
side: r.side || null,
|
||||
canonical_event_id: r.canonical_event_id || null,
|
||||
read_id: r.read_id || null,
|
||||
read_natural_key: r.read_natural_key || null,
|
||||
claim_digest: r.claim_digest || null,
|
||||
captured_at: r.captured_at || null,
|
||||
}))),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist rows. Chunked, upsert-on-conflict-ignore so a retried cycle can never
|
||||
* duplicate. No-ops (successfully) without Supabase env, so tests and local dev
|
||||
@@ -193,6 +589,15 @@ async function persist(rows, deps = {}) {
|
||||
const getClient = deps.getClient || require('../utils/supabase').getSupabaseServiceClient;
|
||||
const supabase = getClient();
|
||||
if (!supabase) { out.skipped = true; return out; }
|
||||
// NOTE: lineage is NOT resolved here any more.
|
||||
//
|
||||
// Capture happens BEFORE the authoritative Redis slate write
|
||||
// (`snapshot:{sport}:latest`), so resolving lineage at this point would
|
||||
// record a published claim for a slate that may never be served. Publication
|
||||
// is committed by `commitPublication()` after that write succeeds.
|
||||
//
|
||||
// Rows are persisted with `published: false` and NULL lineage, which is the
|
||||
// truthful state of a capture that has not yet been served.
|
||||
const CHUNK = 250;
|
||||
for (let i = 0; i < rows.length; i += CHUNK) {
|
||||
const chunk = rows.slice(i, i + CHUNK);
|
||||
@@ -253,6 +658,29 @@ 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.
|
||||
*/
|
||||
/**
|
||||
* Attach the SHADOW CHAIN read to collected rows — the (chain_p, counter_p,
|
||||
* outcome) triple's first two thirds.
|
||||
@@ -303,6 +731,13 @@ module.exports = {
|
||||
mergeEnrichment,
|
||||
mergeChainShadow,
|
||||
persist,
|
||||
attachLineage,
|
||||
commitPublication,
|
||||
recoverFromFork,
|
||||
isSupersedesConflict,
|
||||
FORK_RETRY_LIMIT,
|
||||
LINEAGE_KEYS,
|
||||
LINEAGE_FETCH_CLAIM,
|
||||
newSnapshotId,
|
||||
__internals: { numOrNull, intOrNull, boolOrNull },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user