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:
Kev
2026-09-02 21:44:52 -04:00
parent a8de676756
commit 22cf51c4b0
6 changed files with 391 additions and 7 deletions
@@ -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 });
},
};
}