A probability is served because evidence supports it, not because nothing else answered
The band gate was blocked for its `else` branch. It read:
candidate = F(raw)
served = inCertifiedBand(candidate) ? candidate : RAW
and above raw 0.60 the model is measured overconfident — holdout raw 0.80-0.90
predicts 0.843 and realizes 0.639. So "the calibrator is not supported here" was
being answered with a number already proven wrong. Unsupported calibration does
not make raw true.
Four candidates were adjudicated on ONE split — fit on the earliest 60% of
train, decide support on the last 40%, evaluate on a holdout that saw neither:
A low-param 80.2% coverage 0.24374 REFUTED — its extra region
(raw 0.80-0.90) certified on cert (err +0.040, n=55) and
refuted on holdout (served 0.754 vs observed 0.639), and it
leaves a hole at 0.70-0.80 while serving the island above it
B isotonic 91.3% coverage 0.24337 CERTIFIED, contiguous raw [0.50,0.80)
C empirical band 91.3% coverage 0.24335 REFUTED — refitted point-in-time on
current-model hits the realized rates INVERT in grade order
(B+ 0.593 < B 0.614 < C+ 0.623), so the served function steps
down at raw 0.78. Its shipped constants come from 3,417 props
pooled across four batter stats and do not reproduce here
D raw identity 43.1% coverage 0.24866 certifies raw 0.50-0.60 and only there
Raw is candidate D, not a fallback. It earns exactly one region (holdout error
+0.010 on n=1,316), which is why the law is "raw must earn its region" rather
than "raw is never true". B already covers that region, so no hybrid is built.
Above raw 0.80 nothing is certified and nothing is served. That is the region
where raw is most wrong, isotonic over-corrects (cert err -0.093) and its LODO
mapping at 0.95 has spread 0.180. 8.7% of holdout rows land there.
The registry did not need changing. `serves(stat, p)` already tested certified
bands against the RAW p_win — support in the input domain, the correct question —
and returned {serve:false, reason}. It never said "serve raw". The output-space
gate and the raw fallback were both invented downstream in calibrationService.
ACTIVATION IS OFF. PROBABILITY_CONTRACT_SHADOW defaults to 0, CALIBRATION_DEPLOYED
stays frozen empty, and every served field is byte-identical. This releases the
support first, which is the required order. The shadow records raw belief, the
candidate served value, the state, the estimator identity, and what EV/Kelly/VALUE
would be under the actionability law — into its own column, read by nothing.
Migration 051 was applied to production BEFORE retentionService named the column.
PostgREST builds a bulk insert from the first row's shape, so a key whose column
does not exist 400s the whole batch silently — that is how migration 038 took
retention down for three days.
The user-facing contradiction is NOT fixed here. A B+ still says "realized about
66%" beside a confidence of 84. Fixing that is activation, and activation costs
32% of VALUE flags and 46% of Kelly recommendations on the holdout.
Suite 401/401, 5,580 passed, 4 skipped, deterministic across three runs.
Teeth 23/23, each independently injected and restored byte-identically.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* probabilityContract — WHAT NUMBER MAY BE SERVED, AND WHY.
|
||||
*
|
||||
* ── THE LAW THIS ENFORCES ────────────────────────────────────────────────
|
||||
* RAW MODEL BELIEF IS NOT THE AUTOMATIC FALLBACK FOR SERVED PROBABILITY.
|
||||
*
|
||||
* The blocked contract was:
|
||||
*
|
||||
* candidate = F(raw)
|
||||
* served = inCertifiedBand(candidate) ? candidate : RAW
|
||||
*
|
||||
* Its `else` branch is the defect. Above raw 0.60 the model is measured
|
||||
* overconfident (holdout raw 0.80-0.90: predicted 0.843, observed 0.639), so
|
||||
* "the calibrator is not supported here" was being answered with a number we
|
||||
* had already proven wrong. Unsupported calibration does not make RAW true.
|
||||
*
|
||||
* Here there is NO fallback. A region is served because evidence supports the
|
||||
* estimator that produced it, or it is not served at all.
|
||||
*
|
||||
* ── RAW IS A CANDIDATE, NOT A DEFAULT ────────────────────────────────────
|
||||
* Raw identity is CANDIDATE D and it certifies in exactly one region
|
||||
* (raw 0.50-0.60, holdout error +0.010 on n=1,316). That is a real result and
|
||||
* it is why the law is "raw must earn its region", not "raw is never true".
|
||||
*
|
||||
* ── WHY THE REGISTRY DID NOT NEED CHANGING ───────────────────────────────
|
||||
* `calibrationRegistry.serves(stat, p)` already tests certified bands against
|
||||
* the RAW p_win — support in the INPUT domain, which is the correct question:
|
||||
* what evidence makes THIS prediction eligible? And it returns
|
||||
* `{serve:false, reason}`; it never said "serve raw". The output-space gate and
|
||||
* the raw fallback were both invented downstream in calibrationService.
|
||||
*
|
||||
* ── THE PROBABILITY OBJECT ───────────────────────────────────────────────
|
||||
* P(selected side succeeds). Proven, not assumed: 493/493 two-sided stored
|
||||
* pairs satisfy p(over)+p(under)=1 within 3dp rounding. Side selection happens
|
||||
* upstream in gradeBestSide and this module never touches it.
|
||||
*/
|
||||
|
||||
const { knownNumber } = require('../../utils/known');
|
||||
|
||||
/** Step 17 states. NO_SERVED_PROBABILITY is the ABSENCE of a number, which is
|
||||
* expressed as `served_probability === null`, not as a seventh state name. */
|
||||
const STATE = Object.freeze({
|
||||
CERTIFIED_CALIBRATED: 'CERTIFIED_CALIBRATED',
|
||||
CERTIFIED_RAW: 'CERTIFIED_RAW',
|
||||
CERTIFIED_EMPIRICAL_BAND: 'CERTIFIED_EMPIRICAL_BAND',
|
||||
UNCERTIFIED: 'UNCERTIFIED',
|
||||
UNSUPPORTED: 'UNSUPPORTED',
|
||||
VERSION_MISMATCH: 'VERSION_MISMATCH',
|
||||
INVALID: 'INVALID',
|
||||
});
|
||||
|
||||
const CERTIFIED_STATES = Object.freeze([
|
||||
STATE.CERTIFIED_CALIBRATED, STATE.CERTIFIED_RAW, STATE.CERTIFIED_EMPIRICAL_BAND,
|
||||
]);
|
||||
|
||||
const ESTIMATOR = Object.freeze({
|
||||
ISOTONIC: 'isotonic',
|
||||
LOW_PARAM: 'low_param',
|
||||
EMPIRICAL_BAND: 'empirical_band',
|
||||
RAW_IDENTITY: 'raw_identity',
|
||||
});
|
||||
|
||||
/**
|
||||
* THE CERTIFIED ARTIFACT — MLB hits, engine1@2026-08-07-fullwindow.
|
||||
*
|
||||
* Adjudicated 2026-09-02 on 6,050 picked-side settled rows over 22 dates, one
|
||||
* split shared by all four candidates:
|
||||
* FIT earliest 60% of TRAIN (n=1,798, through 08-17) -> derive
|
||||
* CERT latest 40% of TRAIN (n=1,199, 08-17..08-21) -> decide support
|
||||
* HOLD >= 2026-08-22 (n=3,053, 11 dates) -> evaluate, untouched
|
||||
*
|
||||
* candidate coverage holdout brier vs raw on covered verdict
|
||||
* A low-param 80.2% 0.24374 -0.00273 [-.0039,-.0014] REFUTED
|
||||
* B isotonic 91.3% 0.24337 -0.00347 [-.0061,-.0010] CERTIFIED
|
||||
* C empirical band 91.3% 0.24335 -0.00348 [-.0061,-.0011] REFUTED
|
||||
* D raw identity 43.1% 0.24866 n/a (is raw) narrower
|
||||
*
|
||||
* A is REFUTED despite more coverage: its extra region (raw 0.80-0.90) was
|
||||
* certified on CERT (err +0.040, n=55) and refuted on HOLD (served 0.754 vs
|
||||
* observed 0.639, CI [0.566,0.705], err +0.115). It also leaves a hole at
|
||||
* 0.70-0.80 while serving 0.80-0.90 — a discontiguous contract whose upper
|
||||
* island is the wrong one.
|
||||
*
|
||||
* C is REFUTED as a scalar estimator: refitted point-in-time on current-model
|
||||
* hits-only rows, the realized rates INVERT in grade order (B+ 0.593 <
|
||||
* B 0.614 < C+ 0.623), so the served function moves DOWNWARD at raw 0.78.
|
||||
* Its shipped constants come from 3,417 props POOLED ACROSS FOUR BATTER STATS
|
||||
* and do not reproduce here. Two of its seven bands (D, F) have n=0.
|
||||
*
|
||||
* D certifies only raw 0.50-0.60 and B covers that region already, so no hybrid
|
||||
* is built (Step 14: prefer one estimator; do not patchwork for coverage).
|
||||
*
|
||||
* B's certified support is CONTIGUOUS raw [0.50, 0.80). Holdout band errors:
|
||||
* 0.50-0.60 served 0.530 observed 0.540 [0.513,0.567] -0.011 z -0.73
|
||||
* 0.60-0.70 served 0.578 observed 0.613 [0.582,0.643] -0.035 z -2.22
|
||||
* 0.70-0.80 served 0.616 observed 0.604 [0.561,0.645] +0.012 z +0.56
|
||||
* The middle band is the weakest: p 0.027 uncorrected, which does NOT clear the
|
||||
* programme's cumulative Bonferroni bar (alpha ~= 0.001). It is inside the
|
||||
* repository's own 0.05 tolerance and is recorded here rather than smoothed.
|
||||
*
|
||||
* ABOVE raw 0.80 NOTHING is certified — isotonic over-corrects there (CERT err
|
||||
* -0.093) and its LODO mapping at 0.95 has spread 0.180. That region is exactly
|
||||
* where raw is most wrong, so it receives NO SERVED PROBABILITY.
|
||||
*/
|
||||
const MLB_HITS = Object.freeze({
|
||||
sport: 'mlb',
|
||||
stat: 'hits',
|
||||
model_version: 'engine1@2026-08-07-fullwindow',
|
||||
estimator_type: ESTIMATOR.ISOTONIC,
|
||||
estimator_version: 'mlb-hits-isotonic@2026-09-02',
|
||||
certification_version: 'adjudication@2026-09-02',
|
||||
/** Support in the RAW INPUT domain — half-open [lo, hi). */
|
||||
certified_bands: Object.freeze([Object.freeze([0.50, 0.80])]),
|
||||
state_when_served: STATE.CERTIFIED_CALIBRATED,
|
||||
/** Recorded so a later reader can re-derive the decision, not just trust it. */
|
||||
evidence: Object.freeze({
|
||||
n: 6050, dates: 22, fit_n: 1798, cert_n: 1199, holdout_n: 3053, holdout_dates: 11,
|
||||
holdout_brier: 0.24337, holdout_brier_raw_same_rows: 0.24684,
|
||||
delta_vs_raw: -0.00347, ci95: Object.freeze([-0.00609, -0.00099]),
|
||||
coverage_pct: 0.913, tolerance: 0.05, min_bin: 40,
|
||||
}),
|
||||
});
|
||||
|
||||
const CONTRACTS = Object.freeze({ 'mlb:hits': MLB_HITS });
|
||||
const contractKey = (sport, stat) => `${String(sport || '').toLowerCase()}:${String(stat || '').toLowerCase()}`;
|
||||
const contractFor = (sport, stat) => CONTRACTS[contractKey(sport, stat)] || null;
|
||||
|
||||
const inCertifiedRawBand = (bands, p) =>
|
||||
Array.isArray(bands) && bands.some(([lo, hi]) => p >= lo && p < hi);
|
||||
|
||||
/**
|
||||
* Resolve the served probability for one Read.
|
||||
*
|
||||
* `raw_model_probability` is ALWAYS preserved — it is model evidence and is not
|
||||
* deleted because another estimator answered, or declined to.
|
||||
*
|
||||
* @param {object} read {sport, stat, model_version, p_win}
|
||||
* @param {object} deps {estimate(p) -> number|null} the fitted estimator
|
||||
*/
|
||||
function resolve(read = {}, deps = {}) {
|
||||
const raw = knownNumber(read.p_win);
|
||||
const contract = contractFor(read.sport, read.stat);
|
||||
|
||||
const base = {
|
||||
raw_model_probability: raw,
|
||||
served_probability: null,
|
||||
probability_state: null,
|
||||
estimator_type: contract ? contract.estimator_type : null,
|
||||
estimator_version: contract ? contract.estimator_version : null,
|
||||
certification_version: contract ? contract.certification_version : null,
|
||||
model_version: read.model_version ?? null,
|
||||
contract_model_version: contract ? contract.model_version : null,
|
||||
reason: null,
|
||||
};
|
||||
|
||||
if (!contract) {
|
||||
return { ...base, probability_state: STATE.UNSUPPORTED, reason: 'no certified contract for this sport/stat' };
|
||||
}
|
||||
if (read.model_version !== contract.model_version) {
|
||||
// A calibration artifact is only meaningful against the forecast it was
|
||||
// fitted to. A different model era is a different forecaster.
|
||||
return { ...base, probability_state: STATE.VERSION_MISMATCH, reason: 'model version differs from the certified era' };
|
||||
}
|
||||
if (raw === null || raw < 0 || raw > 1) {
|
||||
return { ...base, probability_state: STATE.INVALID, reason: 'raw probability absent or out of range' };
|
||||
}
|
||||
if (!inCertifiedRawBand(contract.certified_bands, raw)) {
|
||||
// NO RAW FALLBACK. This is the whole point of the module.
|
||||
return { ...base, probability_state: STATE.UNCERTIFIED, reason: 'raw value lies outside certified estimator support' };
|
||||
}
|
||||
|
||||
const estimate = typeof deps.estimate === 'function' ? knownNumber(deps.estimate(raw)) : null;
|
||||
if (estimate === null || estimate < 0 || estimate > 1) {
|
||||
// The estimator was supposed to answer here and did not. Refuse; never
|
||||
// substitute raw, which is what made the previous contract untruthful.
|
||||
return { ...base, probability_state: STATE.UNCERTIFIED, reason: 'estimator produced no value inside its own support' };
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
served_probability: Math.round(estimate * 1000) / 1000,
|
||||
probability_state: contract.state_when_served,
|
||||
reason: null,
|
||||
};
|
||||
}
|
||||
|
||||
const isCertified = (res) => !!res && CERTIFIED_STATES.includes(res.probability_state)
|
||||
&& knownNumber(res.served_probability) !== null;
|
||||
|
||||
/**
|
||||
* THE ACTIONABILITY LAW. Probability-derived claims require a certified served
|
||||
* probability. They are NOT computed from raw when the contract declines —
|
||||
* doing that in secret is the same lie as displaying raw, one layer down.
|
||||
*
|
||||
* Market data is untouched: odds are a fact about the book, not a model output.
|
||||
*/
|
||||
function derivedClaims(res, odds, deps = {}) {
|
||||
const unavailable = (reason) => ({
|
||||
available: false, ev_pct: null, kelly: null, value: null, reason,
|
||||
});
|
||||
if (!isCertified(res)) return unavailable(res ? res.probability_state : 'no resolution');
|
||||
|
||||
const evPct = deps.evPct || require('../../utils/devig').evPct;
|
||||
const isValue = deps.isValue || require('../../config/valueEngine').isValue;
|
||||
const quarterKelly = deps.quarterKelly || require('../../utils/kelly').quarterKelly;
|
||||
|
||||
const p = res.served_probability;
|
||||
const ev = evPct(p, odds);
|
||||
return {
|
||||
available: true,
|
||||
ev_pct: ev,
|
||||
kelly: quarterKelly(p, odds),
|
||||
value: isValue(odds, ev),
|
||||
reason: null,
|
||||
computed_from: 'served_probability',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* What the surface may say. An uncertified Read keeps its grade and its market
|
||||
* data; what it loses is the claim to an exact number.
|
||||
*/
|
||||
function confidenceDisplay(res) {
|
||||
if (isCertified(res)) {
|
||||
return {
|
||||
exact_probability: res.served_probability,
|
||||
exact_pct: Math.round(res.served_probability * 1000) / 10,
|
||||
state: res.probability_state,
|
||||
calibrated: res.probability_state === STATE.CERTIFIED_CALIBRATED,
|
||||
};
|
||||
}
|
||||
return {
|
||||
exact_probability: null,
|
||||
exact_pct: null,
|
||||
state: res ? res.probability_state : STATE.UNSUPPORTED,
|
||||
calibrated: false,
|
||||
label: 'Confidence not calibrated',
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
STATE, CERTIFIED_STATES, ESTIMATOR, CONTRACTS, MLB_HITS,
|
||||
contractFor, inCertifiedRawBand, resolve, isCertified, derivedClaims, confidenceDisplay,
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* probabilityContractService — fit the certified estimator, point in time.
|
||||
*
|
||||
* REUSES the existing fitter and its "fit on settled history strictly before
|
||||
* today" discipline. What it does NOT reuse is `calibrationService.calibrate()`,
|
||||
* whose gate is evaluated in CALIBRATED-OUTPUT space and whose else-branch
|
||||
* serves RAW. Both of those are the blocked contract; only the MAP is taken.
|
||||
*
|
||||
* SUPPORT COMES FROM THE CERTIFIED ARTIFACT, NOT FROM TONIGHT'S FIT. The bands
|
||||
* in `probabilityContract.MLB_HITS` were adjudicated on a three-way split and
|
||||
* are a fixed property of that adjudication. Letting a nightly refit widen its
|
||||
* own support is how an estimator certifies itself.
|
||||
*/
|
||||
|
||||
const cal = require('./calibration');
|
||||
const pc = require('./probabilityContract');
|
||||
|
||||
/**
|
||||
* @returns {null|{estimate, fit_n, fitted_through, contract}} null when there
|
||||
* is not enough settled history — and null means NOTHING is served, never
|
||||
* "pass raw through".
|
||||
*/
|
||||
async function build(sb, { sport = 'mlb', stat = 'hits', before = null, ...opts } = {}) {
|
||||
const contract = pc.contractFor(sport, stat);
|
||||
if (!contract || !sb) return null;
|
||||
|
||||
const svc = opts.calibrationService || require('./calibrationService');
|
||||
const fitted = await svc.fromLedger(sb, { sport, stat, before, ...opts });
|
||||
if (!fitted || !fitted.map) return null;
|
||||
|
||||
return {
|
||||
contract,
|
||||
fit_n: fitted.fit_n,
|
||||
fitted_through: fitted.fitted_through,
|
||||
cutoff: fitted.cutoff,
|
||||
/** The estimator, and only the estimator. No gate, no fallback. */
|
||||
estimate: (p) => cal.applyIsotonic(fitted.map, p),
|
||||
/** Resolve one grade through the full contract. */
|
||||
resolve(read) {
|
||||
return pc.resolve({ ...read, sport, stat }, { estimate: (p) => cal.applyIsotonic(fitted.map, p) });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { build };
|
||||
@@ -193,6 +193,12 @@ function rowsFromSides(base, sides, ctx = {}) {
|
||||
// rows is silently dropped for the whole batch. Filled by
|
||||
// `mergeChainShadow` after enrichment; never read by anything served.
|
||||
chain_shadow: null,
|
||||
|
||||
// The PROBABILITY CONTRACT shadow — what the certified serving contract
|
||||
// WOULD serve, beside what was actually served. Declared always, for the
|
||||
// same first-row-shape reason as chain_shadow. Filled by
|
||||
// `mergeProbabilityContract`; SHADOW ONLY, read by nothing served.
|
||||
probability_contract: null,
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
@@ -825,6 +831,61 @@ function mergeEnrichment(rows, enrichedGrades) {
|
||||
* keeping, and a fabricated third of a triple would poison the adjudication this
|
||||
* column exists to enable.
|
||||
*/
|
||||
/**
|
||||
* PROBABILITY CONTRACT SHADOW.
|
||||
*
|
||||
* Records, per row: the raw model belief, what the certified contract would
|
||||
* serve, the state, the estimator identity, and what EV/Kelly/VALUE would be
|
||||
* under the actionability law. It writes to its own column and mutates nothing
|
||||
* a user sees — `p_win`, `confidence`, `grade`, `ev_pct`, `value` and
|
||||
* `takeable` on the row are untouched.
|
||||
*
|
||||
* The raw probability is ALWAYS carried, including on refusals: it is model
|
||||
* evidence, and a row that records only the refusal cannot be re-adjudicated.
|
||||
*/
|
||||
function mergeProbabilityContract(rows, contract) {
|
||||
if (!Array.isArray(rows) || !rows.length) return rows || [];
|
||||
if (!contract || typeof contract.resolve !== 'function') return rows;
|
||||
const pc = require('./model/probabilityContract');
|
||||
|
||||
return rows.map((r) => {
|
||||
let res;
|
||||
try {
|
||||
res = contract.resolve({ model_version: r.model_version, p_win: numOrNull(r.p_win) });
|
||||
} catch { return r; }
|
||||
if (!res) return r;
|
||||
let derived;
|
||||
// The SIDE'S price. A retention row carries book_odds plus both sides'
|
||||
// prices; using the wrong side would price the opposite bet.
|
||||
const sideOdds = r.book_odds != null ? r.book_odds
|
||||
: (String(r.side) === 'under' ? r.under_odds : r.over_odds);
|
||||
try { derived = pc.derivedClaims(res, sideOdds); } catch { derived = null; }
|
||||
return {
|
||||
...r,
|
||||
probability_contract: {
|
||||
raw_model_probability: res.raw_model_probability,
|
||||
served_probability: res.served_probability,
|
||||
probability_state: res.probability_state,
|
||||
estimator_type: res.estimator_type,
|
||||
estimator_version: res.estimator_version,
|
||||
certification_version: res.certification_version,
|
||||
model_version: res.model_version,
|
||||
reason: res.reason,
|
||||
fitted_through: contract.fitted_through || null,
|
||||
fit_n: contract.fit_n || null,
|
||||
derived: derived ? {
|
||||
available: derived.available,
|
||||
ev_pct: derived.ev_pct,
|
||||
kelly_pct: derived.kelly ? derived.kelly.pct : null,
|
||||
value: derived.value,
|
||||
} : null,
|
||||
// SHADOW. Nothing here has been served to anyone.
|
||||
servable: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function mergeChainShadow(rows, shadow) {
|
||||
if (!Array.isArray(rows) || !rows.length) return rows || [];
|
||||
const byKey = shadow && shadow.byKey;
|
||||
@@ -1183,6 +1244,7 @@ module.exports = {
|
||||
createCollector,
|
||||
mergeEnrichment,
|
||||
mergeChainShadow,
|
||||
mergeProbabilityContract,
|
||||
persist,
|
||||
attachLineage,
|
||||
commitPublication,
|
||||
|
||||
@@ -852,6 +852,26 @@ async function runSnapshot(sport, opts = {}) {
|
||||
// read_revision_id is deliberately NOT resolved here: the upsert does not
|
||||
// return row ids, and issuing a second query in the hot path to obtain one
|
||||
// would be a real cost for a column nothing reads yet.
|
||||
// ── PROBABILITY CONTRACT SHADOW (default OFF) ──────────────────────────
|
||||
// Builds the certified estimator once per snapshot so the shadow costs one
|
||||
// ledger fit, not one per row. OFF unless PROBABILITY_CONTRACT_SHADOW=1:
|
||||
// this releases the support with activation off, which is the required order.
|
||||
// A failure here must never cost the snapshot — it is a measurement layer.
|
||||
let probContract = null;
|
||||
if (String(process.env.PROBABILITY_CONTRACT_SHADOW || '') === '1' && sp === 'mlb') {
|
||||
try {
|
||||
const pcs = deps.probabilityContractService || require('./model/probabilityContractService');
|
||||
const sbc = deps.supabase || require('../utils/supabase').getSupabaseServiceClient();
|
||||
probContract = sbc ? await pcs.build(sbc, { sport: 'mlb', stat: 'hits' }) : null;
|
||||
console.log(probContract
|
||||
? `[probability-contract] shadow armed — isotonic, fit n=${probContract.fit_n} through ${probContract.fitted_through}, certified raw [0.50,0.80)`
|
||||
: '[probability-contract] shadow armed but NO estimator (thin history) — nothing would be served');
|
||||
} catch (e) {
|
||||
probContract = null;
|
||||
console.warn('[probability-contract] shadow build failed (snapshot continues):', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
let lineageIndex = null;
|
||||
let persistedRows = null;
|
||||
const persistRetention = async (enrichedGrades, chainShadow = null) => {
|
||||
@@ -868,6 +888,13 @@ async function runSnapshot(sport, opts = {}) {
|
||||
console.warn('[chain-shadow] merge skipped (retention continues):', e.message);
|
||||
}
|
||||
}
|
||||
// PROBABILITY CONTRACT SHADOW — its own guard, for the same reason: the
|
||||
// retention write is the record and a measurement layer may not cost it.
|
||||
if (probContract && retention.mergeProbabilityContract) {
|
||||
try { rows = retention.mergeProbabilityContract(rows, probContract); } catch (e) {
|
||||
console.warn('[probability-contract] merge skipped (retention continues):', e.message);
|
||||
}
|
||||
}
|
||||
const r = await retention.persist(rows);
|
||||
retentionRows = r.written || 0;
|
||||
// Held for the publication commit, which happens only after the
|
||||
|
||||
Reference in New Issue
Block a user