@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.
Files changed (207) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +2 -1
  2. package/dist/src/artifacts/discovery-shadow-evaluator.js +177 -0
  3. package/dist/src/artifacts/discovery-shadow.js +195 -0
  4. package/dist/src/artifacts/index-builder.js +47 -9
  5. package/dist/src/artifacts/query-engine.js +94 -1
  6. package/dist/src/artifacts/types.js +1 -0
  7. package/dist/src/cli/handlers/decision.js +88 -0
  8. package/dist/src/cli/handlers/gates.js +154 -0
  9. package/dist/src/cli/handlers/index.js +5 -1
  10. package/dist/src/cli/handlers/sessions.js +10 -7
  11. package/dist/src/cli/handlers/utilities.js +20 -0
  12. package/dist/src/config/aiwg-config.js +15 -2
  13. package/dist/src/decision/acceptance.js +25 -0
  14. package/dist/src/decision/adapters/jev.js +24 -2
  15. package/dist/src/decision/calibration/registry.js +9 -0
  16. package/dist/src/decision/compose.js +74 -15
  17. package/dist/src/decision/context-live-promotion.js +344 -0
  18. package/dist/src/decision/context-live-qualification.js +591 -0
  19. package/dist/src/decision/context-pruning.js +847 -0
  20. package/dist/src/decision/driver.js +430 -0
  21. package/dist/src/decision/egress-live-qualification.js +874 -0
  22. package/dist/src/decision/ensemble/contract.js +64 -1
  23. package/dist/src/decision/ensemble/index.js +1 -0
  24. package/dist/src/decision/ensemble/report.js +8 -42
  25. package/dist/src/decision/ensemble/runtime.js +571 -0
  26. package/dist/src/decision/ensemble-study/artifacts.js +85 -0
  27. package/dist/src/decision/ensemble-study/calibration.js +443 -0
  28. package/dist/src/decision/ensemble-study/corpus.js +229 -0
  29. package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
  30. package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
  31. package/dist/src/decision/ensemble-study/multifact.js +241 -0
  32. package/dist/src/decision/ensemble-study/protocol.js +77 -0
  33. package/dist/src/decision/ensemble-study/score.js +462 -0
  34. package/dist/src/decision/ensemble-study/staged.js +87 -0
  35. package/dist/src/decision/ensemble-study/statistics.js +84 -0
  36. package/dist/src/decision/evaluate.js +227 -14
  37. package/dist/src/decision/export.js +4 -0
  38. package/dist/src/decision/feature-export.js +1074 -0
  39. package/dist/src/decision/graph-index.js +16 -0
  40. package/dist/src/decision/graph-live-qualification.js +1041 -0
  41. package/dist/src/decision/graph-live-workload.js +245 -0
  42. package/dist/src/decision/graph-skill-bridge.js +21 -3
  43. package/dist/src/decision/heldout/calibration.js +140 -0
  44. package/dist/src/decision/heldout/collector.js +430 -0
  45. package/dist/src/decision/heldout/contract.js +309 -0
  46. package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
  47. package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
  48. package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
  49. package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
  50. package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
  51. package/dist/src/decision/heldout/d29-pools.js +288 -0
  52. package/dist/src/decision/heldout/d29-v6.js +169 -0
  53. package/dist/src/decision/heldout/d29-v7.js +202 -0
  54. package/dist/src/decision/heldout/d29-v8.js +586 -0
  55. package/dist/src/decision/heldout/generator-registry.js +67 -0
  56. package/dist/src/decision/heldout/generators.js +696 -0
  57. package/dist/src/decision/heldout/index.js +8 -0
  58. package/dist/src/decision/heldout/journal.js +346 -0
  59. package/dist/src/decision/heldout/study-generators.js +64 -0
  60. package/dist/src/decision/heldout/types.js +2 -0
  61. package/dist/src/decision/host-policy.js +61 -0
  62. package/dist/src/decision/index.js +11 -0
  63. package/dist/src/decision/interop-profile.v1.json +112 -0
  64. package/dist/src/decision/interop.js +653 -0
  65. package/dist/src/decision/issue-triage/index.js +3 -0
  66. package/dist/src/decision/issue-triage/runtime.js +1069 -0
  67. package/dist/src/decision/issue-triage/types.js +2 -0
  68. package/dist/src/decision/patterns/catalog.js +28 -8
  69. package/dist/src/decision/patterns/playground.js +28 -11
  70. package/dist/src/decision/patterns/specs.js +19 -10
  71. package/dist/src/decision/preprocessed-evidence.js +724 -0
  72. package/dist/src/decision/qualification/capture.js +8 -2
  73. package/dist/src/decision/qualification/gate-evidence.js +19 -4
  74. package/dist/src/decision/qualification/quality.js +57 -49
  75. package/dist/src/decision/qualification/release.js +76 -3
  76. package/dist/src/decision/qualification/runner.js +5 -2
  77. package/dist/src/decision/routing/contract.js +175 -0
  78. package/dist/src/decision/routing/index.js +5 -0
  79. package/dist/src/decision/routing/report.js +216 -0
  80. package/dist/src/decision/routing/runtime.js +685 -0
  81. package/dist/src/decision/routing/types.js +2 -0
  82. package/dist/src/decision/sdlc-screening.js +1157 -0
  83. package/dist/src/decision/sensitivity/comparative.js +154 -0
  84. package/dist/src/decision/sensitivity/contract.js +218 -0
  85. package/dist/src/decision/sensitivity/index.js +5 -0
  86. package/dist/src/decision/sensitivity/runtime.js +912 -0
  87. package/dist/src/decision/sensitivity/types.js +2 -0
  88. package/dist/src/decision/validate.js +26 -0
  89. package/dist/src/extensions/commands/definitions.js +38 -0
  90. package/dist/src/extensions/manifest.js +44 -0
  91. package/dist/src/gates/discovery.js +315 -0
  92. package/dist/src/gates/driver.js +388 -0
  93. package/dist/src/gates/evaluate.js +719 -0
  94. package/dist/src/gates/floors.js +255 -0
  95. package/dist/src/gates/index.js +17 -0
  96. package/dist/src/gates/providers/evidence.js +38 -0
  97. package/dist/src/gates/providers/index.js +21 -0
  98. package/dist/src/gates/providers/isolated-child-source.js +446 -0
  99. package/dist/src/gates/providers/loader.js +1069 -0
  100. package/dist/src/gates/providers/paired.js +52 -0
  101. package/dist/src/gates/providers/proportion.js +60 -0
  102. package/dist/src/gates/providers/registry.js +40 -0
  103. package/dist/src/gates/providers/scalar.js +41 -0
  104. package/dist/src/gates/providers/screening.js +53 -0
  105. package/dist/src/gates/providers/types.js +25 -0
  106. package/dist/src/gates/registry.js +1003 -0
  107. package/dist/src/gates/report.js +102 -0
  108. package/dist/src/gates/schema.js +56 -0
  109. package/dist/src/gates/stats/binomial.js +26 -0
  110. package/dist/src/gates/stats/bootstrap.js +62 -0
  111. package/dist/src/gates/stats/clopper-pearson.js +95 -0
  112. package/dist/src/gates/stats/digest.js +26 -0
  113. package/dist/src/gates/stats/error.js +5 -0
  114. package/dist/src/gates/stats/index.js +9 -0
  115. package/dist/src/gates/stats/normal.js +35 -0
  116. package/dist/src/gates/stats/paired.js +120 -0
  117. package/dist/src/gates/stats/splits.js +30 -0
  118. package/dist/src/gates/types.js +8 -0
  119. package/dist/src/governance/redaction.js +14 -4
  120. package/dist/src/mcp/muse-native-config.mjs +123 -0
  121. package/dist/src/mcp/tools/decision.mjs +188 -0
  122. package/dist/src/mcp/tools/subsystems.mjs +2 -0
  123. package/dist/src/providers/capability-matrix.yaml +2 -1
  124. package/dist/src/providers/provider-definitions.js +2 -1
  125. package/dist/src/sessions/adapters/muse.js +336 -39
  126. package/dist/src/sessions/batch-import.js +7 -0
  127. package/dist/src/sessions/workspace-discovery.js +13 -2
  128. package/package.json +1 -1
  129. package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
  130. package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
  131. package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
  132. package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
  133. package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
  134. package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
  135. package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
  136. package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
  137. package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
  138. package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
  139. package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
  140. package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
  141. package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
  142. package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
  143. package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
  144. package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
  145. package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
  146. package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
  147. package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
  148. package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
  149. package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
  150. package/schemas/decision/D29Study.v1.schema.json +1903 -0
  151. package/schemas/decision/D29Study.v2.schema.json +1344 -0
  152. package/schemas/decision/D29Study.v3.schema.json +609 -0
  153. package/schemas/decision/D29Study.v4.schema.json +1133 -0
  154. package/schemas/decision/D29Study.v6.schema.json +1205 -0
  155. package/schemas/decision/D29Study.v7.schema.json +1471 -0
  156. package/schemas/decision/D29Study.v8.schema.json +671 -0
  157. package/schemas/decision/D29Study.v9.schema.json +778 -0
  158. package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
  159. package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
  160. package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
  161. package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
  162. package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
  163. package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
  164. package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
  165. package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
  166. package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
  167. package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
  168. package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
  169. package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
  170. package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
  171. package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
  172. package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
  173. package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
  174. package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
  175. package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
  176. package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
  177. package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
  178. package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
  179. package/schemas/decision/DecisionRuleset.schema.json +8 -2
  180. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
  181. package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
  182. package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
  183. package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
  184. package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
  185. package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
  186. package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
  187. package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
  188. package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
  189. package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
  190. package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
  191. package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
  192. package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
  193. package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
  194. package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
  195. package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
  196. package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
  197. package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
  198. package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
  199. package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
  200. package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
  201. package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
  202. package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
  203. package/tools/agents/deploy-agents.mjs +1 -1
  204. package/tools/agents/providers/base.mjs +26 -7
  205. package/tools/agents/providers/factory.mjs +10 -2
  206. package/tools/agents/providers/muse-hooks.mjs +1 -29
  207. 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
- originalConsole[name].apply(console, args);
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/v1',
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 !== GATE_ARTIFACT_SCHEMAS['immutable-splits'])
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
- return typeof digest === 'string' && sha256Pattern.test(digest) && digest === sha256(JSON.stringify(fields))
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
- flags[flag] = ref !== undefined && ref.schemaVersion === GATE_ARTIFACT_SCHEMAS[flag]
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 { createHash } from 'node:crypto';
2
- const sha256 = (value) => `sha256:${createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
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
- const fields = {
16
- schemaVersion: 'decision-binary-benchmark-plan/v1',
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: sha256([...labels].sort((a, b) => a.id.localeCompare(b.id))),
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
- /** A separately anchored digest is required: a caller-created plan cannot attest its own preregistration. */
24
- export function evaluatePreregisteredBinaryBenchmark(plan, trustedPlanDigest, labels, samples) {
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 (digest !== trustedPlanDigest || digest !== sha256(fields)
27
- || freezeBinaryBenchmarkPlan(plan.splits, labels, {
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
- }).digest !== digest)
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 === 'test');
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 { createHash } from 'node:crypto';
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/v1',
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: `sha256:${createHash('sha256').update(JSON.stringify(fields)).digest('hex')}` };
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
- if (ref.schemaVersion !== GATE_ARTIFACT_SCHEMAS[flag] || !validateGateArtifact(flag, parsed, context)) {
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
@@ -0,0 +1,5 @@
1
+ export * from './types.js';
2
+ export * from './contract.js';
3
+ export * from './runtime.js';
4
+ export * from './report.js';
5
+ //# sourceMappingURL=index.js.map