One evaluator for the shadow, so the probe cannot report a mode the pipeline is not in

"Is the shadow effective?" was only answerable by waiting for a snapshot to
write a row. That leaves a blind spot with real cost: a variable SET IN COOLIFY
BUT NOT YET APPLIED to the running process is indistinguishable from an unset
one, and the runtime probe already proves the distinction matters — code_sha
22cf51c with started_at 01:45:44Z means anything set after that is not in this
process's environment.

probabilityContract.shadowState() is now the single evaluator. snapshotService
calls it and the protected status probe calls it, and a test asserts NEITHER
reads process.env directly — the same rule that keeps lineage_write_mode honest.
Reading the env in two places is how a status page and a gate come to disagree.

Strict by construction: only the exact string '1' enables it. 'true', 'yes',
'on', '01', ' 1 ' and '' are all OFF, because a loose parse turns a typo into an
activation. `configuration_source` separates an unset variable from one
explicitly set to '0', and `live_serving` is reported as its own switch so the
shadow can never be read as implying serving.

No behaviour changes. The shadow still defaults OFF, CALIBRATION_DEPLOYED is
still [], and served fields are untouched.

Frontend byte-identical to the last green build (git reports zero changes under
web/), so the build from 22cf51c stands.

Suite 401/401, 5,597 passed, 4 skipped.

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 22:53:21 -04:00
parent 22cf51c4b0
commit 556d186ff1
6 changed files with 240 additions and 4 deletions
+5
View File
@@ -276,6 +276,11 @@ router.get('/snapshot/status', async (req, res) => {
// it from a second parse is how a status surface and a gate come to
// disagree, so there is only one.
lineage_write_mode: require('../services/lineageWriteMode').state(),
// THE PROBABILITY-CONTRACT SHADOW, from the SAME evaluator the pipeline
// calls. Without it "is the shadow effective?" is only answerable by
// waiting for a snapshot to write, and a set-but-not-restarted variable
// is indistinguishable from an unset one.
probability_contract: require('../services/model/probabilityContract').shadowState(),
// INDEPENDENT COVERAGE. Derived from durable retained state, never from
// the writer's own counters — an observer that reads the failing writer's
// return value cannot see that writer fail.
+30 -1
View File
@@ -255,7 +255,36 @@ function confidenceDisplay(res) {
};
}
/**
* THE ONE EVALUATOR for the shadow's effective state.
*
* `snapshotService` and the status probe both call this, so the surface cannot
* report a mode the pipeline is not in — the same rule that keeps
* `lineage_write_mode` honest. Reading the env in two places is how a status
* page and a gate come to disagree.
*
* Strict: only the exact string '1' enables it. Anything else is OFF, including
* 'true', 'yes' and ' 1 ' — a loose parse turns a typo into an activation.
*/
const SHADOW_ENV = 'PROBABILITY_CONTRACT_SHADOW';
function shadowState(env = process.env) {
const raw = env[SHADOW_ENV];
const on = String(raw || '') === '1';
return Object.freeze({
shadow: on ? 'ON' : 'OFF',
env_var: SHADOW_ENV,
configuration_source: raw === undefined ? 'default' : 'environment',
scope: on ? Object.freeze({ sport: 'mlb', stat: 'hits' }) : null,
// Serving is a SEPARATE switch and is not implied by the shadow.
live_serving: 'OFF',
certified_support: MLB_HITS.certified_bands,
estimator_version: MLB_HITS.estimator_version,
model_version: MLB_HITS.model_version,
});
}
module.exports = {
STATE, CERTIFIED_STATES, ESTIMATOR, CONTRACTS, MLB_HITS,
STATE, CERTIFIED_STATES, ESTIMATOR, CONTRACTS, MLB_HITS, SHADOW_ENV,
contractFor, inCertifiedRawBand, resolve, isCertified, derivedClaims, confidenceDisplay,
shadowState,
};
+1 -1
View File
@@ -858,7 +858,7 @@ async function runSnapshot(sport, opts = {}) {
// 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') {
if (require('./model/probabilityContract').shadowState().shadow === 'ON' && sp === 'mlb') {
try {
const pcs = deps.probabilityContractService || require('./model/probabilityContractService');
const sbc = deps.supabase || require('../utils/supabase').getSupabaseServiceClient();