diff --git a/src/services/model/probabilityContract.js b/src/services/model/probabilityContract.js index 166466a..10da3b5 100644 --- a/src/services/model/probabilityContract.js +++ b/src/services/model/probabilityContract.js @@ -305,6 +305,31 @@ const SHADOW_ENV = 'PROBABILITY_CONTRACT_SHADOW'; function shadowState(env = process.env) { const raw = env[SHADOW_ENV]; const on = String(raw || '') === '1'; + // WHICH FROZEN ARTIFACT THIS PROCESS WOULD USE — resolvable with the shadow + // OFF, because "is the right curve deployed?" must be answerable before + // anything is switched on. Identity only: the curve itself is committed and + // addressable by `artifact_id`, and repeating it here would put a second copy + // of the truth on a status page. + let artifact = null; + try { + const a = require('./artifactRegistry').load(MLB_HITS.sport, MLB_HITS.stat); + if (a) { + artifact = Object.freeze({ + artifact_id: a.artifact_id, procedure_version: a.procedure_version, + model_version: a.model_version, fit_as_of: a.fit_as_of, + training_cutoff: a.training_cutoff, fit_n: a.fit_n, + withheld_from_fit: a.withheld_from_fit, + source_digest: a.source_digest, knot_digest: a.knot_digest, + served_curve_digest: a.served_curve_digest, knot_count: a.knot_count, + certified_bands: a.certified_bands, stage: a.stage, + approved_for_shadow: a.approved_for_shadow, approved_for_live: a.approved_for_live, + servable: a.servable, fit_policy_valid: a.fit_policy_valid, + fit_policy_violations: a.fit_policy_violations, + wrong_era_rows: a.era_audit ? a.era_audit.wrong_era_rows : null, + }); + } + } catch { artifact = null; } + return Object.freeze({ shadow: on ? 'ON' : 'OFF', env_var: SHADOW_ENV, @@ -315,6 +340,9 @@ function shadowState(env = process.env) { certified_support: MLB_HITS.certified_bands, estimator_version: MLB_HITS.estimator_version, model_version: MLB_HITS.model_version, + // null means NOTHING is promoted — which is also the honest answer when the + // file is missing or fails its policy at load. + artifact, }); } diff --git a/tests/unit/artifactGovernance.test.js b/tests/unit/artifactGovernance.test.js index 8163fd8..40f75b6 100644 --- a/tests/unit/artifactGovernance.test.js +++ b/tests/unit/artifactGovernance.test.js @@ -172,3 +172,29 @@ describe('model-era transition law', () => { expect(check.servable).toBe(false); }); }); + +describe('the runtime can name its artifact with the shadow OFF', () => { + it('reports the full identity without enabling anything', () => { + const st = pc.shadowState({}); + expect(st.shadow).toBe('OFF'); + expect(st.live_serving).toBe('OFF'); + const a = st.artifact; + for (const k of ['artifact_id', 'procedure_version', 'model_version', 'fit_as_of', + 'training_cutoff', 'fit_n', 'source_digest', 'knot_digest', 'served_curve_digest', + 'certified_bands', 'stage']) expect(a[k]).toBeTruthy(); + expect(a.servable).toBe(true); + expect(a.approved_for_shadow).toBe(true); + expect(a.approved_for_live).toBe(false); + expect(a.wrong_era_rows).toBe(0); + expect(a.withheld_from_fit).toBe(0); + }); + + it('does not put a second copy of the curve on a status surface', () => { + expect(pc.shadowState({}).artifact.served_curve).toBeUndefined(); + }); + + it('two resolutions return the same artifact — no refit between calls', () => { + expect(JSON.stringify(pc.shadowState({}).artifact)) + .toBe(JSON.stringify(pc.shadowState({}).artifact)); + }); +});