The mapping that ran now has a name, and the row carries it
Runtime probes say the fleet is on a8de676, one process generation. But
"isotonic" was a label, not a claim: probabilityContractService refits per
snapshot against `game_date < todayEt()`, so the mapping changes as outcomes
settle, and nothing on a row could say WHICH mapping produced its number.
The artifact now has an identity:
estimator_type / estimator_version / certification_version / model_version
fit_as_of the exact lt(game_date) bound 2026-09-02
training_cutoff last date INSIDE the fit 2026-08-21
fit_n / knot_count 6,084 / 28
knot_digest d9d571d728ba76de
served_curve the COMPLETE served function over [0.50,0.80)
served_curve_digest
The served curve is not a sample. p_win is quantised to three decimals at the
source, so a step table at 0.001 granularity is the mapping itself for every
input that can occur — six steps, ~200 bytes. Storing it makes a Read
reconstructable WITHOUT re-deriving a training set that may since have been
re-settled, and a claim you can only verify when the inputs happen not to have
moved is not a reconstructable claim.
Proven, not asserted: the production construction path run twice gives an
identical digest, and an INDEPENDENT reconstruction — re-walk 9,361 settled
ledger rows at the declared bound, refit from scratch — reproduces
d9d571d728ba76de exactly, 28 knots for 28.
A teeth injection found a real defect behind a coverage hole. `resolve` checked
the CONTRACT's model era and never the ARTIFACT's, so a mapping fitted for a
different era could be recorded beside a served number with every test green.
Both the era and the estimator type are now checked, and a mismatch serves
nothing rather than serving quietly.
OBSERVED AND NOT CHANGED: calibrationService splits 65/35 to certify its own
bands, a step this contract does not consume because support comes from the
frozen artifact. So the served map is fitted through 2026-08-21 while 3,277
more recent settled rows sit unused, and that lag grows with history. Changing
it would change the fitted function, which this tranche froze.
Shadow still defaults OFF. CALIBRATION_DEPLOYED still []. served_probability is
referenced by nothing outside the contract layer — asserted by a tooth.
Suite 401/401, 5,593 passed, 4 skipped. Teeth 23/23 (prior) + 7/7 (new).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
This commit is contained in:
@@ -14,9 +14,45 @@
|
||||
* own support is how an estimator certifies itself.
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const cal = require('./calibration');
|
||||
const pc = require('./probabilityContract');
|
||||
|
||||
/** Short, stable content digest. Full sha256 truncated — collision risk here is
|
||||
* irrelevant and 16 hex chars keeps the per-row payload small. */
|
||||
const digest = (obj) => crypto.createHash('sha256')
|
||||
.update(JSON.stringify(obj)).digest('hex').slice(0, 16);
|
||||
|
||||
/**
|
||||
* THE SERVED CURVE — the complete served function inside certified support.
|
||||
*
|
||||
* `p_win` is quantised to three decimals at the source
|
||||
* (`analyzeViaEngine1`: Math.round(pWin * 1000) / 1000), so a step table at
|
||||
* 0.001 granularity is not a sample of the mapping — it IS the mapping, for
|
||||
* every input that can actually occur. Six steps, ~200 bytes.
|
||||
*
|
||||
* Storing it on the row makes a Read reconstructable WITHOUT re-deriving the
|
||||
* training set. That matters because settled rows can be re-settled
|
||||
* (`re_settled_at`), so a later refit at the same cutoff is not guaranteed to
|
||||
* reproduce the same map — and a claim you can only verify when the inputs
|
||||
* happen not to have moved is not a reconstructable claim.
|
||||
*/
|
||||
function servedCurve(map, bands) {
|
||||
const steps = [];
|
||||
let prev = null;
|
||||
for (const [lo, hi] of bands) {
|
||||
for (let x = lo; x < hi - 1e-9; x += 0.001) {
|
||||
const raw = Math.round(x * 1000) / 1000;
|
||||
const v = cal.applyIsotonic(map, raw);
|
||||
if (v === null) continue;
|
||||
const rounded = Math.round(v * 1e6) / 1e6;
|
||||
if (rounded !== prev) { steps.push([raw, rounded]); prev = rounded; }
|
||||
}
|
||||
prev = null; // bands are independent segments
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {null|{estimate, fit_n, fitted_through, contract}} null when there
|
||||
* is not enough settled history — and null means NOTHING is served, never
|
||||
@@ -30,8 +66,31 @@ async function build(sb, { sport = 'mlb', stat = 'hits', before = null, ...opts
|
||||
const fitted = await svc.fromLedger(sb, { sport, stat, before, ...opts });
|
||||
if (!fitted || !fitted.map) return null;
|
||||
|
||||
// ── ARTIFACT IDENTITY ──────────────────────────────────────────────────
|
||||
// The runtime REFITS PER SNAPSHOT against `game_date < todayEt()`, so the
|
||||
// mapping changes as outcomes settle. That is point-in-time correct going
|
||||
// forward — a Read can only ever have seen settlements strictly before its
|
||||
// own day — but without an identity a served number could not be tied to the
|
||||
// function that produced it, and "isotonic" would be a label rather than a
|
||||
// claim. This is the identity.
|
||||
const curve = servedCurve(fitted.map, contract.certified_bands);
|
||||
const artifact = Object.freeze({
|
||||
estimator_type: contract.estimator_type,
|
||||
estimator_version: contract.estimator_version,
|
||||
certification_version: contract.certification_version,
|
||||
model_version: contract.model_version,
|
||||
fit_as_of: fitted.cutoff || null, // the exact lt(game_date) bound
|
||||
training_cutoff: fitted.fitted_through || null, // last date INSIDE the fit
|
||||
fit_n: fitted.fit_n ?? null,
|
||||
knot_count: Array.isArray(fitted.map) ? fitted.map.length : null,
|
||||
knot_digest: digest(fitted.map),
|
||||
served_curve: curve, // complete over certified support
|
||||
served_curve_digest: digest(curve),
|
||||
});
|
||||
|
||||
return {
|
||||
contract,
|
||||
artifact,
|
||||
fit_n: fitted.fit_n,
|
||||
fitted_through: fitted.fitted_through,
|
||||
cutoff: fitted.cutoff,
|
||||
@@ -39,7 +98,8 @@ async function build(sb, { sport = 'mlb', stat = 'hits', before = null, ...opts
|
||||
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) });
|
||||
return pc.resolve({ ...read, sport, stat },
|
||||
{ estimate: (p) => cal.applyIsotonic(fitted.map, p), artifact });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user