@aiwg/cli 2026.9.24 → 2026.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agentic/code/providers/capability-matrix.yaml +2 -1
- package/dist/src/artifacts/discovery-shadow-evaluator.js +177 -0
- package/dist/src/artifacts/discovery-shadow.js +195 -0
- package/dist/src/artifacts/index-builder.js +47 -9
- package/dist/src/artifacts/query-engine.js +94 -1
- package/dist/src/artifacts/types.js +1 -0
- package/dist/src/cli/handlers/decision.js +88 -0
- package/dist/src/cli/handlers/gates.js +154 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/sessions.js +10 -7
- package/dist/src/cli/handlers/utilities.js +20 -0
- package/dist/src/config/aiwg-config.js +15 -2
- package/dist/src/decision/acceptance.js +25 -0
- package/dist/src/decision/adapters/jev.js +24 -2
- package/dist/src/decision/calibration/registry.js +9 -0
- package/dist/src/decision/compose.js +74 -15
- package/dist/src/decision/context-live-promotion.js +344 -0
- package/dist/src/decision/context-live-qualification.js +591 -0
- package/dist/src/decision/context-pruning.js +847 -0
- package/dist/src/decision/driver.js +430 -0
- package/dist/src/decision/egress-live-qualification.js +874 -0
- package/dist/src/decision/ensemble/contract.js +64 -1
- package/dist/src/decision/ensemble/index.js +1 -0
- package/dist/src/decision/ensemble/report.js +8 -42
- package/dist/src/decision/ensemble/runtime.js +571 -0
- package/dist/src/decision/ensemble-study/artifacts.js +85 -0
- package/dist/src/decision/ensemble-study/calibration.js +443 -0
- package/dist/src/decision/ensemble-study/corpus.js +229 -0
- package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
- package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
- package/dist/src/decision/ensemble-study/multifact.js +241 -0
- package/dist/src/decision/ensemble-study/protocol.js +77 -0
- package/dist/src/decision/ensemble-study/score.js +462 -0
- package/dist/src/decision/ensemble-study/staged.js +87 -0
- package/dist/src/decision/ensemble-study/statistics.js +84 -0
- package/dist/src/decision/evaluate.js +227 -14
- package/dist/src/decision/export.js +4 -0
- package/dist/src/decision/feature-export.js +1074 -0
- package/dist/src/decision/graph-index.js +16 -0
- package/dist/src/decision/graph-live-qualification.js +1041 -0
- package/dist/src/decision/graph-live-workload.js +245 -0
- package/dist/src/decision/graph-skill-bridge.js +21 -3
- package/dist/src/decision/heldout/calibration.js +140 -0
- package/dist/src/decision/heldout/collector.js +430 -0
- package/dist/src/decision/heldout/contract.js +309 -0
- package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
- package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
- package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
- package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
- package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
- package/dist/src/decision/heldout/d29-pools.js +288 -0
- package/dist/src/decision/heldout/d29-v6.js +169 -0
- package/dist/src/decision/heldout/d29-v7.js +202 -0
- package/dist/src/decision/heldout/d29-v8.js +586 -0
- package/dist/src/decision/heldout/generator-registry.js +67 -0
- package/dist/src/decision/heldout/generators.js +696 -0
- package/dist/src/decision/heldout/index.js +8 -0
- package/dist/src/decision/heldout/journal.js +346 -0
- package/dist/src/decision/heldout/study-generators.js +64 -0
- package/dist/src/decision/heldout/types.js +2 -0
- package/dist/src/decision/host-policy.js +61 -0
- package/dist/src/decision/index.js +11 -0
- package/dist/src/decision/interop-profile.v1.json +112 -0
- package/dist/src/decision/interop.js +653 -0
- package/dist/src/decision/issue-triage/index.js +3 -0
- package/dist/src/decision/issue-triage/runtime.js +1069 -0
- package/dist/src/decision/issue-triage/types.js +2 -0
- package/dist/src/decision/patterns/catalog.js +28 -8
- package/dist/src/decision/patterns/playground.js +28 -11
- package/dist/src/decision/patterns/specs.js +19 -10
- package/dist/src/decision/preprocessed-evidence.js +724 -0
- package/dist/src/decision/qualification/capture.js +8 -2
- package/dist/src/decision/qualification/gate-evidence.js +19 -4
- package/dist/src/decision/qualification/quality.js +57 -49
- package/dist/src/decision/qualification/release.js +76 -3
- package/dist/src/decision/qualification/runner.js +5 -2
- package/dist/src/decision/routing/contract.js +175 -0
- package/dist/src/decision/routing/index.js +5 -0
- package/dist/src/decision/routing/report.js +216 -0
- package/dist/src/decision/routing/runtime.js +685 -0
- package/dist/src/decision/routing/types.js +2 -0
- package/dist/src/decision/sdlc-screening.js +1157 -0
- package/dist/src/decision/sensitivity/comparative.js +154 -0
- package/dist/src/decision/sensitivity/contract.js +218 -0
- package/dist/src/decision/sensitivity/index.js +5 -0
- package/dist/src/decision/sensitivity/runtime.js +912 -0
- package/dist/src/decision/sensitivity/types.js +2 -0
- package/dist/src/decision/validate.js +26 -0
- package/dist/src/extensions/commands/definitions.js +38 -0
- package/dist/src/extensions/manifest.js +44 -0
- package/dist/src/gates/discovery.js +315 -0
- package/dist/src/gates/driver.js +388 -0
- package/dist/src/gates/evaluate.js +719 -0
- package/dist/src/gates/floors.js +255 -0
- package/dist/src/gates/index.js +17 -0
- package/dist/src/gates/providers/evidence.js +38 -0
- package/dist/src/gates/providers/index.js +21 -0
- package/dist/src/gates/providers/isolated-child-source.js +446 -0
- package/dist/src/gates/providers/loader.js +1069 -0
- package/dist/src/gates/providers/paired.js +52 -0
- package/dist/src/gates/providers/proportion.js +60 -0
- package/dist/src/gates/providers/registry.js +40 -0
- package/dist/src/gates/providers/scalar.js +41 -0
- package/dist/src/gates/providers/screening.js +53 -0
- package/dist/src/gates/providers/types.js +25 -0
- package/dist/src/gates/registry.js +1003 -0
- package/dist/src/gates/report.js +102 -0
- package/dist/src/gates/schema.js +56 -0
- package/dist/src/gates/stats/binomial.js +26 -0
- package/dist/src/gates/stats/bootstrap.js +62 -0
- package/dist/src/gates/stats/clopper-pearson.js +95 -0
- package/dist/src/gates/stats/digest.js +26 -0
- package/dist/src/gates/stats/error.js +5 -0
- package/dist/src/gates/stats/index.js +9 -0
- package/dist/src/gates/stats/normal.js +35 -0
- package/dist/src/gates/stats/paired.js +120 -0
- package/dist/src/gates/stats/splits.js +30 -0
- package/dist/src/gates/types.js +8 -0
- package/dist/src/governance/redaction.js +14 -4
- package/dist/src/mcp/muse-native-config.mjs +123 -0
- package/dist/src/mcp/tools/decision.mjs +188 -0
- package/dist/src/mcp/tools/subsystems.mjs +2 -0
- package/dist/src/providers/capability-matrix.yaml +2 -1
- package/dist/src/providers/provider-definitions.js +2 -1
- package/dist/src/sessions/adapters/muse.js +336 -39
- package/dist/src/sessions/batch-import.js +7 -0
- package/dist/src/sessions/workspace-discovery.js +13 -2
- package/package.json +1 -1
- package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
- package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
- package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
- package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
- package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
- package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
- package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
- package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
- package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
- package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
- package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
- package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
- package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
- package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
- package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
- package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
- package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
- package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
- package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
- package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
- package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
- package/schemas/decision/D29Study.v1.schema.json +1903 -0
- package/schemas/decision/D29Study.v2.schema.json +1344 -0
- package/schemas/decision/D29Study.v3.schema.json +609 -0
- package/schemas/decision/D29Study.v4.schema.json +1133 -0
- package/schemas/decision/D29Study.v6.schema.json +1205 -0
- package/schemas/decision/D29Study.v7.schema.json +1471 -0
- package/schemas/decision/D29Study.v8.schema.json +671 -0
- package/schemas/decision/D29Study.v9.schema.json +778 -0
- package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
- package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
- package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
- package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
- package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
- package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
- package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
- package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
- package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
- package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
- package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
- package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
- package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
- package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
- package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
- package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
- package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
- package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
- package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
- package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
- package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
- package/schemas/decision/DecisionRuleset.schema.json +8 -2
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
- package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
- package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
- package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
- package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
- package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
- package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
- package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
- package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
- package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
- package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
- package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
- package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
- package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
- package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
- package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
- package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
- package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
- package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
- package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
- package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
- package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
- package/tools/agents/deploy-agents.mjs +1 -1
- package/tools/agents/providers/base.mjs +26 -7
- package/tools/agents/providers/factory.mjs +10 -2
- package/tools/agents/providers/muse-hooks.mjs +1 -29
- package/tools/agents/providers/muse.mjs +6 -3
|
@@ -21,7 +21,7 @@ function errorText(error) {
|
|
|
21
21
|
* captured too; run the qualification alone. Writes from child processes are
|
|
22
22
|
* not captured and must be collected by the caller.
|
|
23
23
|
*/
|
|
24
|
-
export async function captureQualificationLifetime(operation) {
|
|
24
|
+
export async function captureQualificationLifetime(operation, options = {}) {
|
|
25
25
|
const stdout = [];
|
|
26
26
|
const stderr = [];
|
|
27
27
|
const originalStdout = process.stdout.write;
|
|
@@ -29,6 +29,11 @@ export async function captureQualificationLifetime(operation) {
|
|
|
29
29
|
const originalConsole = Object.fromEntries(CONSOLE_METHODS.map(name => [name, console[name]]));
|
|
30
30
|
const tap = (sink, original) => function (chunk, ...rest) {
|
|
31
31
|
sink.push(typeof chunk === 'string' ? chunk : chunk instanceof Uint8Array ? new TextDecoder().decode(chunk) : String(chunk));
|
|
32
|
+
if (options.suppressOutput) {
|
|
33
|
+
const callback = rest.find(value => typeof value === 'function');
|
|
34
|
+
callback?.();
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
32
37
|
return original.call(this, chunk, ...rest);
|
|
33
38
|
};
|
|
34
39
|
process.stdout.write = tap(stdout, originalStdout);
|
|
@@ -36,7 +41,8 @@ export async function captureQualificationLifetime(operation) {
|
|
|
36
41
|
for (const name of CONSOLE_METHODS) {
|
|
37
42
|
console[name] = (...args) => {
|
|
38
43
|
(STDERR_METHODS.has(name) ? stderr : stdout).push(`${format(...args)}\n`);
|
|
39
|
-
|
|
44
|
+
if (!options.suppressOutput)
|
|
45
|
+
originalConsole[name].apply(console, args);
|
|
40
46
|
};
|
|
41
47
|
}
|
|
42
48
|
let result;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
3
|
+
import { matchEvidenceDigest } from '../../gates/stats/index.js';
|
|
3
4
|
import { CalibrationRegistry, calibrationArtifactDigest } from '../calibration/registry.js';
|
|
4
5
|
import { validateCaseInventory } from './manifest.js';
|
|
5
6
|
import { verifyQualificationSplits } from './quality.js';
|
|
@@ -29,7 +30,7 @@ export const DECISION_GATE_SUITES = {
|
|
|
29
30
|
};
|
|
30
31
|
/** Flags backed by a checked, digest-pinned artifact copied into the run directory. */
|
|
31
32
|
export const GATE_ARTIFACT_SCHEMAS = {
|
|
32
|
-
'immutable-splits': 'decision-binary-benchmark-plan/
|
|
33
|
+
'immutable-splits': 'decision-binary-benchmark-plan/v2',
|
|
33
34
|
'calibration-qualified': 'decision-calibration-artifact/v1',
|
|
34
35
|
'load-manifest-qualified': 'decision-load-result/v1',
|
|
35
36
|
'review-decision-recorded': 'decision-qualification-review/v1',
|
|
@@ -76,8 +77,14 @@ export function suiteSatisfied(manifest, suite) {
|
|
|
76
77
|
}
|
|
77
78
|
return (suite.evidenceIds ?? []).every(id => proven.has(id));
|
|
78
79
|
}
|
|
80
|
+
/** Split-plan versions with retained evidence: v1 is the pre-migration lineage (legacy allowlisted), v2 is canonical-only. */
|
|
81
|
+
export const IMMUTABLE_SPLIT_VERSIONS = [
|
|
82
|
+
'decision-binary-benchmark-plan/v1',
|
|
83
|
+
'decision-binary-benchmark-plan/v2',
|
|
84
|
+
];
|
|
79
85
|
function validSplitPlan(value) {
|
|
80
|
-
if (!record(value) || value.schemaVersion !==
|
|
86
|
+
if (!record(value) || (value.schemaVersion !== 'decision-binary-benchmark-plan/v1'
|
|
87
|
+
&& value.schemaVersion !== 'decision-binary-benchmark-plan/v2'))
|
|
81
88
|
return false;
|
|
82
89
|
const { digest, ...fields } = value;
|
|
83
90
|
try {
|
|
@@ -86,7 +93,12 @@ function validSplitPlan(value) {
|
|
|
86
93
|
catch {
|
|
87
94
|
return false;
|
|
88
95
|
}
|
|
89
|
-
|
|
96
|
+
// Versioned legacy allowlist: pre-migration plans hashed JSON.stringify output.
|
|
97
|
+
// The v1 schema version is the pre-migration lineage, so it alone allowlists
|
|
98
|
+
// legacy; v2 verifies canonical-only even under an explicit allowlist.
|
|
99
|
+
const modes = value.schemaVersion === 'decision-binary-benchmark-plan/v1'
|
|
100
|
+
? ['canonical', 'legacy'] : ['canonical'];
|
|
101
|
+
return typeof digest === 'string' && sha256Pattern.test(digest) && matchEvidenceDigest(fields, digest, [...modes]) !== null
|
|
90
102
|
&& sha256Pattern.test(String(fields.datasetDigest));
|
|
91
103
|
}
|
|
92
104
|
function validCalibration(value, context) {
|
|
@@ -164,7 +176,10 @@ export function deriveGateEvidence(manifest, proof = {}) {
|
|
|
164
176
|
flags[name] = suiteSatisfied(manifest, suite);
|
|
165
177
|
for (const flag of GATE_ARTIFACT_FLAGS) {
|
|
166
178
|
const ref = manifest.gateArtifacts?.[flag];
|
|
167
|
-
|
|
179
|
+
const versionOk = flag === 'immutable-splits'
|
|
180
|
+
? ref !== undefined && IMMUTABLE_SPLIT_VERSIONS.includes(ref.schemaVersion)
|
|
181
|
+
: ref !== undefined && ref.schemaVersion === GATE_ARTIFACT_SCHEMAS[flag];
|
|
182
|
+
flags[flag] = versionOk && ref !== undefined
|
|
168
183
|
&& typeof ref.artifact === 'string' && ref.artifact.length > 0 && sha256Pattern.test(ref.digest);
|
|
169
184
|
}
|
|
170
185
|
return flags;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { wilson95 } from '../../gates/stats/binomial.js';
|
|
2
|
+
import { canonicalSha256, matchEvidenceDigest, verifyQualificationSplits, } from '../../gates/stats/index.js';
|
|
3
|
+
export { PairedDifferenceError, clopperPearsonInterval, freezeQualificationSplit, normalQuantile, pairedBinaryDifferenceInterval, pairedMeanDifferenceBootstrap, pairedNonInferiority, verifyQualificationSplits, wilsonScoreInterval, } from '../../gates/stats/index.js';
|
|
3
4
|
/** Freeze thresholds and all three label sets before acquiring test predictions. */
|
|
4
5
|
export function freezeBinaryBenchmarkPlan(splits, labels, limits) {
|
|
6
|
+
const fields = buildBinaryBenchmarkPlanFields(splits, labels, limits);
|
|
7
|
+
return { ...fields, digest: canonicalSha256(fields) };
|
|
8
|
+
}
|
|
9
|
+
function buildBinaryBenchmarkPlanFields(splits, labels, limits) {
|
|
5
10
|
verifyQualificationSplits(splits);
|
|
6
11
|
const ids = splits.flatMap(split => split.ids).sort();
|
|
7
12
|
if (labels.length !== ids.length || new Set(labels.map(label => label.id)).size !== ids.length
|
|
@@ -12,24 +17,57 @@ export function freezeBinaryBenchmarkPlan(splits, labels, limits) {
|
|
|
12
17
|
.some(value => !Number.isFinite(value) || value < 0 || value > 1)) {
|
|
13
18
|
throw new Error('invalid preregistered benchmark plan');
|
|
14
19
|
}
|
|
15
|
-
|
|
16
|
-
schemaVersion: 'decision-binary-benchmark-plan/
|
|
20
|
+
return {
|
|
21
|
+
schemaVersion: 'decision-binary-benchmark-plan/v2',
|
|
17
22
|
splits: ['tuning', 'calibration', 'test'].map(name => splits.find(split => split.name === name)),
|
|
18
|
-
datasetDigest:
|
|
23
|
+
datasetDigest: canonicalSha256([...labels].sort((a, b) => a.id.localeCompare(b.id))),
|
|
19
24
|
...limits,
|
|
20
25
|
};
|
|
21
|
-
return { ...fields, digest: sha256(fields) };
|
|
22
26
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Verifies a frozen plan digest without scoring. Canonical-only by default; pass
|
|
29
|
+
* `{ digestModes: ['canonical', 'legacy'] }` to allowlist the versioned legacy
|
|
30
|
+
* (`JSON.stringify`) digest for pre-migration v1 evidence. Fresh v2 records are
|
|
31
|
+
* canonical-only: a legacy digest on v2 fields never verifies, even under an
|
|
32
|
+
* explicit allowlist.
|
|
33
|
+
*/
|
|
34
|
+
export function verifyBinaryBenchmarkPlanDigest(plan, labels, options) {
|
|
25
35
|
const { digest, ...fields } = plan;
|
|
26
|
-
if (
|
|
27
|
-
|
|
36
|
+
if (fields.schemaVersion !== 'decision-binary-benchmark-plan/v1'
|
|
37
|
+
&& fields.schemaVersion !== 'decision-binary-benchmark-plan/v2')
|
|
38
|
+
return null;
|
|
39
|
+
let expected;
|
|
40
|
+
try {
|
|
41
|
+
expected = buildBinaryBenchmarkPlanFields(plan.splits, labels, {
|
|
28
42
|
minimumOverallN: plan.minimumOverallN, minimumSliceN: plan.minimumSliceN,
|
|
29
43
|
maximumSelectiveRisk: plan.maximumSelectiveRisk, maximumReviewRate: plan.maximumReviewRate,
|
|
30
44
|
maximumBrier: plan.maximumBrier,
|
|
31
|
-
})
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
// Substantive fields must match; the version selects the digest lineage.
|
|
51
|
+
const { schemaVersion: _expectedVersion, ...expectedRest } = expected;
|
|
52
|
+
void _expectedVersion;
|
|
53
|
+
const { schemaVersion: _fieldsVersion, ...fieldsRest } = fields;
|
|
54
|
+
void _fieldsVersion;
|
|
55
|
+
if (canonicalSha256(expectedRest) !== canonicalSha256(fieldsRest))
|
|
56
|
+
return null;
|
|
57
|
+
// v1 respects the caller's allowlist (default canonical-only); v2 intersects
|
|
58
|
+
// it with canonical-only, so a legacy digest on v2 never verifies.
|
|
59
|
+
const modes = fields.schemaVersion === 'decision-binary-benchmark-plan/v1'
|
|
60
|
+
? options?.digestModes
|
|
61
|
+
: (options?.digestModes ?? ['canonical']).filter(mode => mode === 'canonical');
|
|
62
|
+
return matchEvidenceDigest(fields, digest, modes);
|
|
63
|
+
}
|
|
64
|
+
/** A separately anchored digest is required: a caller-created plan cannot attest its own preregistration. */
|
|
65
|
+
export function evaluatePreregisteredBinaryBenchmark(plan, trustedPlanDigest, labels, samples, options) {
|
|
66
|
+
const { digest } = plan;
|
|
67
|
+
if (digest !== trustedPlanDigest
|
|
68
|
+
|| verifyBinaryBenchmarkPlanDigest(plan, labels, options) === null) {
|
|
32
69
|
throw new Error('benchmark preregistration or dataset mismatch');
|
|
70
|
+
}
|
|
33
71
|
const testLabels = new Map(labels.filter(label => plan.splits.find(split => split.name === 'test').ids.includes(label.id))
|
|
34
72
|
.map(label => [label.id, label]));
|
|
35
73
|
if (samples.some(sample => sample.label !== testLabels.get(sample.id)?.label || sample.slice !== testLabels.get(sample.id)?.slice)) {
|
|
@@ -53,38 +91,17 @@ export function evaluatePreregisteredBinaryBenchmark(plan, trustedPlanDigest, la
|
|
|
53
91
|
return { decision: exceeded ? 'fail' : insufficient || metrics.overall.selectiveRisk === null ? 'insufficient-evidence'
|
|
54
92
|
: 'pass', reasons, metrics };
|
|
55
93
|
}
|
|
56
|
-
function digestIds(ids) {
|
|
57
|
-
return `sha256:${createHash('sha256').update(JSON.stringify([...ids].sort())).digest('hex')}`;
|
|
58
|
-
}
|
|
59
|
-
/** Hash the exact membership set; duplicate, empty and overlapping IDs fail closed. */
|
|
60
|
-
export function freezeQualificationSplit(name, ids) {
|
|
61
|
-
if (!ids.length || ids.some(id => !id.trim()) || new Set(ids).size !== ids.length) {
|
|
62
|
-
throw new Error('qualification split requires unique nonempty IDs');
|
|
63
|
-
}
|
|
64
|
-
return { name, ids: [...ids].sort(), digest: digestIds(ids) };
|
|
65
|
-
}
|
|
66
|
-
export function verifyQualificationSplits(splits) {
|
|
67
|
-
if (splits.length !== 3 || new Set(splits.map(split => split.name)).size !== 3
|
|
68
|
-
|| splits.some(split => !['tuning', 'calibration', 'test'].includes(split.name))) {
|
|
69
|
-
throw new Error('qualification requires tuning, calibration and test splits');
|
|
70
|
-
}
|
|
71
|
-
const all = new Set();
|
|
72
|
-
for (const split of splits) {
|
|
73
|
-
if (!split.ids.length || split.ids.some(id => typeof id !== 'string' || !id.trim())
|
|
74
|
-
|| new Set(split.ids).size !== split.ids.length || split.digest !== digestIds(split.ids)) {
|
|
75
|
-
throw new Error('qualification split digest or membership mismatch');
|
|
76
|
-
}
|
|
77
|
-
for (const id of split.ids) {
|
|
78
|
-
if (all.has(id))
|
|
79
|
-
throw new Error('qualification splits overlap');
|
|
80
|
-
all.add(id);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
94
|
/** Refuses to score unregistered or non-held-out samples. */
|
|
85
95
|
export function evaluateBinaryHeldout(splits, samples) {
|
|
96
|
+
return evaluateBinarySplit(splits, samples, 'test');
|
|
97
|
+
}
|
|
98
|
+
/** Descriptive fit diagnostics; calibration observations are never held-out test evidence. */
|
|
99
|
+
export function evaluateBinaryCalibration(splits, samples) {
|
|
100
|
+
return evaluateBinarySplit(splits, samples, 'calibration');
|
|
101
|
+
}
|
|
102
|
+
function evaluateBinarySplit(splits, samples, name) {
|
|
86
103
|
verifyQualificationSplits(splits);
|
|
87
|
-
const test = splits.find(split => split.name ===
|
|
104
|
+
const test = splits.find(split => split.name === name);
|
|
88
105
|
if (samples.length !== test.ids.length || new Set(samples.map(sample => sample.id)).size !== samples.length
|
|
89
106
|
|| samples.some(sample => !test.ids.includes(sample.id))) {
|
|
90
107
|
throw new Error('held-out sample membership mismatch');
|
|
@@ -170,15 +187,6 @@ export function measurePairedMovement(pairs) {
|
|
|
170
187
|
const [low, high] = wilson95(changedN, pairs.length);
|
|
171
188
|
return { sampleN: pairs.length, changedN, changedRate: changedN / pairs.length, changedWilson95: [low, high] };
|
|
172
189
|
}
|
|
173
|
-
function wilson95(errors, n) {
|
|
174
|
-
// 95% normal quantile; finite-sample Wilson interval for binomial events.
|
|
175
|
-
const z = 1.959963984540054;
|
|
176
|
-
const rate = errors / n;
|
|
177
|
-
const denominator = 1 + z * z / n;
|
|
178
|
-
const center = (rate + z * z / (2 * n)) / denominator;
|
|
179
|
-
const margin = z * Math.sqrt(rate * (1 - rate) / n + z * z / (4 * n * n)) / denominator;
|
|
180
|
-
return [Math.max(0, center - margin), Math.min(1, center + margin)];
|
|
181
|
-
}
|
|
182
190
|
function scoreBinary(samples) {
|
|
183
191
|
const n = samples.length;
|
|
184
192
|
const errors = samples.filter(s => (s.probability >= 0.5 ? 1 : 0) !== s.label).length;
|
|
@@ -1,4 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { canonicalSha256, matchEvidenceDigest } from '../../gates/stats/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* #2037/#2048 integrity modes whose verified state has a trusted score source.
|
|
4
|
+
* Mirrors tools/eval buildIntegrityMetadata: each strict mode maps to exactly one source.
|
|
5
|
+
*/
|
|
6
|
+
export const VERIFIED_QUALIFICATION_INTEGRITY_SOURCES = Object.freeze({
|
|
7
|
+
fresh: 'fresh-workspace',
|
|
8
|
+
locked: 'locked-artifact-snapshot',
|
|
9
|
+
'full-locked': 'full-locked-workspace',
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Allowlist check for promotable integrity metadata. Any malformed, unknown or
|
|
13
|
+
* unverified mode/state/score-source is a problem; an empty result is the only
|
|
14
|
+
* state that may PROMOTE. Callers still honour upstream HOLD/ROLLBACK separately.
|
|
15
|
+
*/
|
|
16
|
+
export function qualificationIntegrityAllowlistProblems(integrity) {
|
|
17
|
+
const value = integrity;
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
19
|
+
|| !Number.isSafeInteger(value.sample_n) || value.sample_n < 0
|
|
20
|
+
|| typeof value.integrity_mode !== 'string' || typeof value.integrity_state !== 'string'
|
|
21
|
+
|| typeof value.trusted_score_source !== 'string'
|
|
22
|
+
|| typeof value.fresh_workspace_required !== 'boolean' || typeof value.fresh_workspace_verified !== 'boolean'
|
|
23
|
+
|| !Array.isArray(value.compromise_labels) || value.compromise_labels.some(label => typeof label !== 'string')
|
|
24
|
+
|| !(value.weak_signal_reason === null || typeof value.weak_signal_reason === 'string')
|
|
25
|
+
|| !('uncertainty' in value) || !('paired_baseline' in value)
|
|
26
|
+
|| !value.release_gate || typeof value.release_gate !== 'object'
|
|
27
|
+
|| !['PROMOTE', 'HOLD', 'ROLLBACK'].includes(value.release_gate.decision)
|
|
28
|
+
|| !Array.isArray(value.release_gate.reasons)) {
|
|
29
|
+
return ['integrity-invalid'];
|
|
30
|
+
}
|
|
31
|
+
const problems = [];
|
|
32
|
+
const expectedSource = Object.hasOwn(VERIFIED_QUALIFICATION_INTEGRITY_SOURCES, value.integrity_mode)
|
|
33
|
+
? VERIFIED_QUALIFICATION_INTEGRITY_SOURCES[value.integrity_mode] : undefined;
|
|
34
|
+
if (value.integrity_state !== 'verified')
|
|
35
|
+
problems.push('integrity-not-verified');
|
|
36
|
+
if (expectedSource === undefined)
|
|
37
|
+
problems.push('integrity-mode-not-allowlisted');
|
|
38
|
+
else if (value.trusted_score_source !== expectedSource)
|
|
39
|
+
problems.push('untrusted-score-source');
|
|
40
|
+
if (value.fresh_workspace_required && !value.fresh_workspace_verified)
|
|
41
|
+
problems.push('fresh-workspace-unverified');
|
|
42
|
+
if (value.uncertainty === null || value.uncertainty === undefined)
|
|
43
|
+
problems.push('uncertainty-missing');
|
|
44
|
+
if (value.paired_baseline === null || value.paired_baseline === undefined)
|
|
45
|
+
problems.push('paired-baseline-missing');
|
|
46
|
+
if (value.weak_signal_reason !== null)
|
|
47
|
+
problems.push('weak-signal');
|
|
48
|
+
if (value.compromise_labels.length > 0)
|
|
49
|
+
problems.push('compromised');
|
|
50
|
+
if (value.sample_n <= 0)
|
|
51
|
+
problems.push('insufficient-samples');
|
|
52
|
+
if (value.release_gate.decision !== 'PROMOTE')
|
|
53
|
+
problems.push(`upstream-integrity-${value.release_gate.decision.toLowerCase()}`);
|
|
54
|
+
return problems;
|
|
55
|
+
}
|
|
2
56
|
/** D30 compile/prefix cache, D03 receipt replay and D15 result cache evidence, each a verified evidence manifest. */
|
|
3
57
|
export const QUALIFICATION_CACHE_LAYERS = ['compilePrefixCache', 'receiptReplay', 'resultCache'];
|
|
4
58
|
const REQUIRED_PINS = [
|
|
@@ -83,7 +137,7 @@ export function buildQualificationReleaseRecord(executed, input) {
|
|
|
83
137
|
|| report.decision === 'ROLLBACK' || input.integrity.integrity_state === 'compromised' || input.integrity.compromise_labels.length > 0 ? 'ROLLBACK'
|
|
84
138
|
: complete && releaseDecision === 'PROMOTE' ? 'PROMOTE' : 'HOLD';
|
|
85
139
|
const fields = {
|
|
86
|
-
schemaVersion: 'decision-qualification-release/
|
|
140
|
+
schemaVersion: 'decision-qualification-release/v2',
|
|
87
141
|
runId: manifest.runId, sourceCommit: manifest.sourceCommit, dirty: manifest.dirty,
|
|
88
142
|
environment: input.environment, commands: [...input.commands],
|
|
89
143
|
pins: Object.fromEntries(Object.entries(pinned).sort(([a], [b]) => a.localeCompare(b))),
|
|
@@ -98,7 +152,26 @@ export function buildQualificationReleaseRecord(executed, input) {
|
|
|
98
152
|
}])) : null,
|
|
99
153
|
decision,
|
|
100
154
|
};
|
|
101
|
-
return { ...fields, digest:
|
|
155
|
+
return { ...fields, digest: canonicalSha256(fields) };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Verifies a release record digest without rebuilding it. Canonical-only by default;
|
|
159
|
+
* pass `{ digestModes: ['canonical', 'legacy'] }` to allowlist the versioned legacy
|
|
160
|
+
* (`JSON.stringify`) digest for pre-migration v1 evidence. Fresh v2 records are
|
|
161
|
+
* canonical-only: a legacy digest on v2 fields never verifies, even under an
|
|
162
|
+
* explicit allowlist. Returns the matching mode, or null when neither matches.
|
|
163
|
+
*/
|
|
164
|
+
export function verifyQualificationReleaseDigest(record, options) {
|
|
165
|
+
if (!record || typeof record !== 'object')
|
|
166
|
+
return null;
|
|
167
|
+
if (record.schemaVersion !== 'decision-qualification-release/v1'
|
|
168
|
+
&& record.schemaVersion !== 'decision-qualification-release/v2')
|
|
169
|
+
return null;
|
|
170
|
+
const { digest, ...fields } = record;
|
|
171
|
+
const modes = record.schemaVersion === 'decision-qualification-release/v1'
|
|
172
|
+
? options?.digestModes
|
|
173
|
+
: (options?.digestModes ?? ['canonical']).filter(mode => mode === 'canonical');
|
|
174
|
+
return matchEvidenceDigest(fields, digest, modes);
|
|
102
175
|
}
|
|
103
176
|
/** Deliberately excludes private callback details, stdout/stderr and raw captures. */
|
|
104
177
|
export function qualificationReleaseSummary(record) {
|
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { lstat, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, resolve, sep } from 'node:path';
|
|
4
4
|
import { evaluateQualification } from './gates.js';
|
|
5
|
-
import { deriveGateEvidence, DERIVED_GATE_EVIDENCE, GATE_ARTIFACT_FLAGS, GATE_ARTIFACT_SCHEMAS, validateGateArtifact, } from './gate-evidence.js';
|
|
5
|
+
import { deriveGateEvidence, DERIVED_GATE_EVIDENCE, GATE_ARTIFACT_FLAGS, GATE_ARTIFACT_SCHEMAS, IMMUTABLE_SPLIT_VERSIONS, validateGateArtifact, } from './gate-evidence.js';
|
|
6
6
|
import { scanQualificationPrivacy } from './privacy.js';
|
|
7
7
|
/** Named master-test-plan suites plus the M01-M11 amendment IDs. */
|
|
8
8
|
export const QUALIFICATION_EVIDENCE_ID = /^(?:CON|POL|BCH|CTX|CNC|CAN|RTY|REC|SEC|PRV|CAL|DRF|CCP|M(?:0[1-9]|1[01]))-[A-Z0-9][A-Z0-9._-]*$/;
|
|
@@ -266,7 +266,10 @@ export async function verifyQualificationGateArtifacts(manifest, artifactRoot, m
|
|
|
266
266
|
continue;
|
|
267
267
|
}
|
|
268
268
|
const parsed = parseJson(bytes);
|
|
269
|
-
|
|
269
|
+
const versionOk = flag === 'immutable-splits'
|
|
270
|
+
? IMMUTABLE_SPLIT_VERSIONS.includes(ref.schemaVersion)
|
|
271
|
+
: ref.schemaVersion === GATE_ARTIFACT_SCHEMAS[flag];
|
|
272
|
+
if (!versionOk || !validateGateArtifact(flag, parsed, context)) {
|
|
270
273
|
results.push({ flag, verified: false, reason: 'invalid-artifact' });
|
|
271
274
|
continue;
|
|
272
275
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
6
|
+
import addFormats from 'ajv-formats';
|
|
7
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
8
|
+
import { admitEntry } from '../entry.js';
|
|
9
|
+
export class RoutingContractError extends Error {
|
|
10
|
+
layer;
|
|
11
|
+
details;
|
|
12
|
+
constructor(message, layer = 'semantic', details = []) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.layer = layer;
|
|
15
|
+
this.details = details;
|
|
16
|
+
this.name = 'RoutingContractError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export const ROUTING_SCHEMA_FILES = {
|
|
20
|
+
policy: 'DecisionRoutingPolicy.v1.schema.json',
|
|
21
|
+
task: 'DecisionRoutingTask.v1.schema.json',
|
|
22
|
+
receipt: 'DecisionRouteReceipt.v1.schema.json',
|
|
23
|
+
shadowReport: 'DecisionRoutingShadowReport.v1.schema.json',
|
|
24
|
+
};
|
|
25
|
+
/** Floors below which no preregistration is accepted (#2620 review: n=2 per arm is not evidence). */
|
|
26
|
+
export const ROUTING_MINIMUM_OVERALL_N = 30;
|
|
27
|
+
export const ROUTING_MINIMUM_SLICE_N = 10;
|
|
28
|
+
let validators = null;
|
|
29
|
+
function validator(kind) {
|
|
30
|
+
if (!validators) {
|
|
31
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const dir = [resolve(here, '../../../schemas/decision'), resolve(here, '../../../../schemas/decision')]
|
|
33
|
+
.find(path => existsSync(resolve(path, ROUTING_SCHEMA_FILES.policy)));
|
|
34
|
+
if (!dir)
|
|
35
|
+
throw new Error('Decision routing schema directory is unavailable');
|
|
36
|
+
const ajv = new Ajv2020({ strict: true, allErrors: true });
|
|
37
|
+
addFormats(ajv);
|
|
38
|
+
const compiled = new Map(Object.keys(ROUTING_SCHEMA_FILES).map(name => [
|
|
39
|
+
name,
|
|
40
|
+
ajv.compile(JSON.parse(readFileSync(resolve(dir, ROUTING_SCHEMA_FILES[name]), 'utf8'))),
|
|
41
|
+
]));
|
|
42
|
+
compiled.set('jevEvidence', ajv.getSchema('https://aiwg.io/schemas/decision/DecisionRouteReceipt.v1.schema.json#/$defs/jevEvidence'));
|
|
43
|
+
compiled.set('preregistration', ajv.getSchema('https://aiwg.io/schemas/decision/DecisionRoutingShadowReport.v1.schema.json#/$defs/preregistration'));
|
|
44
|
+
compiled.set('observation', ajv.getSchema('https://aiwg.io/schemas/decision/DecisionRoutingShadowReport.v1.schema.json#/$defs/observation'));
|
|
45
|
+
validators = compiled;
|
|
46
|
+
}
|
|
47
|
+
return validators.get(kind);
|
|
48
|
+
}
|
|
49
|
+
export function routingDigest(value) {
|
|
50
|
+
return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
|
|
51
|
+
}
|
|
52
|
+
export function compareRoutingKeys(a, b) { return a < b ? -1 : a > b ? 1 : 0; }
|
|
53
|
+
/** Deep-freezes a structured clone so no caller or callback can widen a trusted value in place. */
|
|
54
|
+
export function frozenRoutingClone(value) {
|
|
55
|
+
const freeze = (item) => {
|
|
56
|
+
if (item && typeof item === 'object' && !Object.isFrozen(item)) {
|
|
57
|
+
Object.freeze(item);
|
|
58
|
+
for (const child of Object.values(item))
|
|
59
|
+
freeze(child);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const copy = structuredClone(value);
|
|
63
|
+
freeze(copy);
|
|
64
|
+
return copy;
|
|
65
|
+
}
|
|
66
|
+
export function checkRoutingSchema(kind, value) {
|
|
67
|
+
try {
|
|
68
|
+
admitEntry(value);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new RoutingContractError(`${kind} admission denied`, 'admission');
|
|
72
|
+
}
|
|
73
|
+
const check = validator(kind);
|
|
74
|
+
if (!check(value)) {
|
|
75
|
+
throw new RoutingContractError(`${kind} does not match its v1 schema`, 'schema', (check.errors ?? []).map(error => `${error.instancePath || '/'} ${error.message ?? 'invalid'}`));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function semantic(problems, message) {
|
|
79
|
+
if (problems.length)
|
|
80
|
+
throw new RoutingContractError(`${message}: ${problems[0]}`, 'semantic', problems);
|
|
81
|
+
}
|
|
82
|
+
/** Returns a deep-frozen copy; the digest covers candidates in canonical ID order, so it does not depend on input order. */
|
|
83
|
+
export function validateRoutingPolicy(value) {
|
|
84
|
+
checkRoutingSchema('policy', value);
|
|
85
|
+
const input = value;
|
|
86
|
+
const policy = frozenRoutingClone({ ...input, candidates: [...input.candidates].sort((a, b) => compareRoutingKeys(a.id, b.id)) });
|
|
87
|
+
const problems = [];
|
|
88
|
+
const ids = new Set();
|
|
89
|
+
for (const candidate of policy.candidates) {
|
|
90
|
+
if (ids.has(candidate.id))
|
|
91
|
+
problems.push(`route ${candidate.id} is duplicated`);
|
|
92
|
+
ids.add(candidate.id);
|
|
93
|
+
if (candidate.operations.maxAttempts > policy.ceilings.maxAttempts)
|
|
94
|
+
problems.push(`route ${candidate.id} exceeds policy attempt ceiling`);
|
|
95
|
+
if (candidate.operations.deadlineMs > policy.ceilings.deadlineMs)
|
|
96
|
+
problems.push(`route ${candidate.id} exceeds policy deadline ceiling`);
|
|
97
|
+
if (candidate.operations.costMicrosPerAttempt !== null && candidate.operations.costMicrosPerAttempt > policy.ceilings.maxCostMicros) {
|
|
98
|
+
problems.push(`route ${candidate.id} exceeds policy cost ceiling`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const routeId of [policy.defaultRouteId, policy.deterministicFallbackRouteId].filter(Boolean)) {
|
|
102
|
+
if (!ids.has(routeId))
|
|
103
|
+
problems.push(`configured route ${routeId} is not declared`);
|
|
104
|
+
}
|
|
105
|
+
if (Object.values(policy.utility).some(value => !Number.isFinite(value) || value < 0))
|
|
106
|
+
problems.push('utility weights must be finite non-negative numbers');
|
|
107
|
+
if (policy.ceilings.maxFallbacks >= policy.ceilings.maxAttempts)
|
|
108
|
+
problems.push('maxFallbacks must be below maxAttempts');
|
|
109
|
+
semantic(problems, 'routing policy rejected');
|
|
110
|
+
return { policy, digest: routingDigest(policy) };
|
|
111
|
+
}
|
|
112
|
+
/** Closed task schema plus semantic checks; unknown, missing or non-finite requirements fail closed. */
|
|
113
|
+
export function validateRoutingTask(value) {
|
|
114
|
+
checkRoutingSchema('task', value);
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
/** Closed evidence schema plus finiteness; NaN and Infinity cannot pass a JSON schema number check reliably. */
|
|
118
|
+
export function validateJevRoutingEvidence(value) {
|
|
119
|
+
checkRoutingSchema('jevEvidence', value);
|
|
120
|
+
const evidence = value;
|
|
121
|
+
const numbers = [evidence.taskComplexity, evidence.ambiguity, ...evidence.distributions.flatMap(item => [item.taskFit, item.reasoningNeed])];
|
|
122
|
+
if (!numbers.every(item => Number.isFinite(item) && item >= 0 && item <= 1)) {
|
|
123
|
+
throw new RoutingContractError('Jev routing evidence must be finite within [0, 1]', 'semantic');
|
|
124
|
+
}
|
|
125
|
+
return evidence;
|
|
126
|
+
}
|
|
127
|
+
/** Freeze the paired task set, slices and every promotion threshold before any holdout access. */
|
|
128
|
+
export function freezeRoutingPreregistration(input) {
|
|
129
|
+
const fields = {
|
|
130
|
+
schemaVersion: 'decision-routing-preregistration/v1',
|
|
131
|
+
policy: input.policy,
|
|
132
|
+
baselineArm: input.baselineArm,
|
|
133
|
+
registeredAt: input.registeredAt,
|
|
134
|
+
tasks: [...(input.tasks ?? [])].map(item => ({ id: item.id, slice: item.slice })).sort((a, b) => compareRoutingKeys(a.id, b.id)),
|
|
135
|
+
thresholds: input.thresholds,
|
|
136
|
+
};
|
|
137
|
+
const value = { ...fields, digest: routingDigest(fields) };
|
|
138
|
+
checkPreregistration(value);
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
/** A separately anchored digest is required, as for evaluatePreregisteredBinaryBenchmark. */
|
|
142
|
+
export function verifyRoutingPreregistration(value, trustedDigest) {
|
|
143
|
+
checkPreregistration(value);
|
|
144
|
+
const preregistration = value;
|
|
145
|
+
const { digest, ...fields } = preregistration;
|
|
146
|
+
if (digest !== trustedDigest || digest !== routingDigest(fields)) {
|
|
147
|
+
throw new RoutingContractError('routing preregistration does not match its trusted digest', 'semantic');
|
|
148
|
+
}
|
|
149
|
+
return preregistration;
|
|
150
|
+
}
|
|
151
|
+
function checkPreregistration(value) {
|
|
152
|
+
try {
|
|
153
|
+
checkRoutingSchema('preregistration', value);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
throw new RoutingContractError('routing preregistration rejected: invalid schema', 'schema', error instanceof RoutingContractError ? error.details : []);
|
|
157
|
+
}
|
|
158
|
+
const preregistration = value;
|
|
159
|
+
const problems = [];
|
|
160
|
+
const ids = preregistration.tasks.map(item => item.id);
|
|
161
|
+
if (new Set(ids).size !== ids.length)
|
|
162
|
+
problems.push('task IDs must be unique');
|
|
163
|
+
if (ids.some((id, index) => index > 0 && compareRoutingKeys(ids[index - 1], id) >= 0))
|
|
164
|
+
problems.push('tasks must be sorted by id');
|
|
165
|
+
const { thresholds } = preregistration;
|
|
166
|
+
if (thresholds.minimumOverallN < ROUTING_MINIMUM_OVERALL_N || thresholds.minimumSliceN < ROUTING_MINIMUM_SLICE_N)
|
|
167
|
+
problems.push('sample floors are below the pilot minimum');
|
|
168
|
+
if (ids.length < thresholds.minimumOverallN)
|
|
169
|
+
problems.push('fewer registered tasks than minimumOverallN');
|
|
170
|
+
if (!Number.isFinite(Date.parse(preregistration.registeredAt)))
|
|
171
|
+
problems.push('registeredAt is not a timestamp');
|
|
172
|
+
if (problems.length)
|
|
173
|
+
throw new RoutingContractError(`routing preregistration rejected: ${problems[0]}`, 'semantic', problems);
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=contract.js.map
|