@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
@@ -0,0 +1,912 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { applyTargetAcceptance } from '../acceptance.js';
3
+ import { composeRuleset } from '../compose.js';
4
+ import { decisionInvocationFingerprint } from '../receipts.js';
5
+ import { artifactDigest, artifactPin, assertArtifactPin, validateAgainstSchema, validateBinding, validateRuleset } from '../validate.js';
6
+ import { sensitivityDigest, validateSensitivityPlan, validateSensitivityReport, SensitivityContractError } from './contract.js';
7
+ export async function analyzeDecisionSensitivity(request) {
8
+ let plan;
9
+ const runNonce = randomUUID();
10
+ const warnings = ['not-causal-explanation', 'not-correctness-proof', 'not-action-authority'];
11
+ try {
12
+ plan = validateSensitivityPlan(request.plan);
13
+ validateInputs(plan, request, request.now?.() ?? Date.now());
14
+ validateChangeTargets(plan, request);
15
+ }
16
+ catch (error) {
17
+ if (error instanceof SensitivityContractError)
18
+ return rejectedReport(request, error.details.length ? [...error.details] : [error.message]);
19
+ throw error;
20
+ }
21
+ if (plan.mode === 'disabled')
22
+ return rejectedReport(request, ['sensitivity plan is disabled']);
23
+ // Every pre-inference rejection happens before the probe budget is charged: a rejected plan
24
+ // performs no inference and discloses no boundary, so it does not consume the budget.
25
+ if (plan.analysisKind === 'input-reevaluation' && !request.reevaluate && requiresEvaluator(plan, request)) {
26
+ return rejectedReport(request, ['input reevaluation requires a host-supplied evaluator']);
27
+ }
28
+ try {
29
+ enforceProbeLimits(plan, request.probeState ?? defaultProbeState, probeRoot(plan, request), request.now?.() ?? Date.now(), request.probeIdentity, probeSubjectDigest(request));
30
+ }
31
+ catch (error) {
32
+ if (error instanceof SensitivityContractError)
33
+ return rejectedReport(request, error.details.length ? [...error.details] : [error.message]);
34
+ throw error;
35
+ }
36
+ const rows = [];
37
+ const budget = { maxVariants: plan.budgets.maxVariants, processedVariants: 0, backendCalls: 0, tokens: 0, costMicros: 0, exhausted: false };
38
+ const deadline = (request.now?.() ?? Date.now()) + plan.budgets.deadlineMs;
39
+ const usedInvocations = new Set([request.sourceResult.spec.invocationId]);
40
+ if (plan.analysisKind === 'input-reevaluation' && plan.baselineStability.enabled) {
41
+ if (!request.reevaluate)
42
+ return rejectedReport(request, ['input reevaluation requires a host-supplied evaluator']);
43
+ for (let repeat = 0; repeat < plan.baselineStability.repeats; repeat += 1) {
44
+ if (!canSpendCall(plan, request.sourceBinding, request.sourceResult, budget, deadline, request.now?.() ?? Date.now())) {
45
+ budget.exhausted = true;
46
+ break;
47
+ }
48
+ const variantId = `baseline-${repeat + 1}`;
49
+ const input = structuredClone(request.sourceInput);
50
+ const invocationId = freshInvocationId(plan, variantId, runNonce);
51
+ const receiptFingerprint = expectedReceiptFingerprint(request, invocationId, input);
52
+ try {
53
+ ensureFreshBeforeDispatch(invocationId, receiptFingerprint, usedInvocations);
54
+ }
55
+ catch (error) {
56
+ if (error instanceof SensitivityContractError)
57
+ return failedPartialReport(request, plan, rows, budget, warnings, error.message);
58
+ throw error;
59
+ }
60
+ let result;
61
+ try {
62
+ result = await request.reevaluate({ variantId, invocationId, receiptFingerprint, input, changes: [] });
63
+ }
64
+ catch (error) {
65
+ if (!spendFailure(budget, error))
66
+ spendUnknownReserve(budget, warnings, request.sourceBinding, request.sourceResult);
67
+ return failedPartialReport(request, plan, rows, budget, warnings, `baseline ${variantId} failed: ${sanitizeReason(error)}`);
68
+ }
69
+ if (!isRulesetResultShape(result)) {
70
+ chargeUnusableResult(budget, warnings, request, result);
71
+ return failedPartialReport(request, plan, rows, budget, warnings, `baseline ${variantId} returned a malformed result`);
72
+ }
73
+ try {
74
+ const failure = validateReevaluationResult(request, result, variantId, invocationId, receiptFingerprint, input);
75
+ if (failure) {
76
+ spendResult(budget, result);
77
+ return failedPartialReport(request, plan, rows, budget, warnings, failure);
78
+ }
79
+ rows.push(rowForResult({ plan, source: request.sourceResult, result, variantId: `baseline-${repeat + 1}`,
80
+ kind: 'baseline-stability', inference: 'new-invocation', changes: [], invocationId }));
81
+ }
82
+ catch {
83
+ chargeUnusableResult(budget, warnings, request, result);
84
+ return failedPartialReport(request, plan, rows, budget, warnings, `baseline ${variantId} returned an unusable result`);
85
+ }
86
+ spendResult(budget, result);
87
+ }
88
+ }
89
+ for (const variant of plan.variants) {
90
+ if (rows.filter(row => row.kind === 'variant' || row.kind === 'unchanged-control').length >= plan.budgets.maxVariants) {
91
+ budget.exhausted = true;
92
+ break;
93
+ }
94
+ if ((request.now?.() ?? Date.now()) >= deadline) {
95
+ budget.exhausted = true;
96
+ break;
97
+ }
98
+ const noChange = isNoChange(plan.analysisKind === 'policy-replay'
99
+ ? { ruleset: request.sourceRuleset, binding: request.sourceBinding } : { input: request.sourceInput }, variant.changes);
100
+ if (noChange) {
101
+ rows.push(rowForResult({ plan, source: request.sourceResult, result: request.sourceResult, variantId: variant.id,
102
+ kind: 'unchanged-control', inference: 'deduplicated-control', changes: variant.changes, invocationId: null }));
103
+ budget.processedVariants += 1;
104
+ continue;
105
+ }
106
+ if (plan.analysisKind === 'policy-replay') {
107
+ const { ruleset, binding } = replayArtifacts(request.sourceRuleset, request.sourceBinding, variant.changes);
108
+ const replay = replayPolicy(request.sourceResult, request.sourceBinding, ruleset, binding, request.sourceDefinitions ?? {}, request.sourceInput, variant.id);
109
+ if (replay.unreplayable || dependsOnUnobservedOutcome(variant.changes, request.sourceBinding, request.sourceResult)) {
110
+ // The row would depend on a target outcome that was never observed; it makes no sensitivity claim.
111
+ rows.push(rowForResult({ plan, source: request.sourceResult, result: request.sourceResult, variantId: variant.id,
112
+ kind: 'variant', inference: 'unreplayable', changes: variant.changes, invocationId: null,
113
+ extraWarnings: ['unreplayable-unobserved-outcome'] }));
114
+ if (!warnings.includes('unreplayable-rows-present'))
115
+ warnings.push('unreplayable-rows-present');
116
+ }
117
+ else {
118
+ rows.push(rowForResult({ plan, source: request.sourceResult, result: replay.result, variantId: variant.id,
119
+ kind: 'variant', inference: 'reused-stored-evidence', changes: variant.changes, invocationId: null }));
120
+ }
121
+ budget.processedVariants += 1;
122
+ continue;
123
+ }
124
+ if (!request.reevaluate)
125
+ return rejectedReport(request, ['input reevaluation requires a host-supplied evaluator']);
126
+ if (!canSpendCall(plan, request.sourceBinding, request.sourceResult, budget, deadline, request.now?.() ?? Date.now())) {
127
+ budget.exhausted = true;
128
+ break;
129
+ }
130
+ const input = applyChanges({ input: structuredClone(request.sourceInput) }, variant.changes).input;
131
+ const invocationId = freshInvocationId(plan, variant.id, runNonce);
132
+ const receiptFingerprint = expectedReceiptFingerprint(request, invocationId, input);
133
+ try {
134
+ ensureFreshBeforeDispatch(invocationId, receiptFingerprint, usedInvocations);
135
+ }
136
+ catch (error) {
137
+ if (error instanceof SensitivityContractError)
138
+ return failedPartialReport(request, plan, rows, budget, warnings, error.message);
139
+ throw error;
140
+ }
141
+ let result;
142
+ try {
143
+ result = await request.reevaluate({ variantId: variant.id, invocationId, receiptFingerprint, input, changes: structuredClone(variant.changes) });
144
+ }
145
+ catch (error) {
146
+ if (!spendFailure(budget, error))
147
+ spendUnknownReserve(budget, warnings, request.sourceBinding, request.sourceResult);
148
+ return failedPartialReport(request, plan, rows, budget, warnings, `variant ${variant.id} failed: ${sanitizeReason(error)}`);
149
+ }
150
+ if (!isRulesetResultShape(result)) {
151
+ chargeUnusableResult(budget, warnings, request, result);
152
+ return failedPartialReport(request, plan, rows, budget, warnings, `variant ${variant.id} returned a malformed result`);
153
+ }
154
+ try {
155
+ const failure = validateReevaluationResult(request, result, variant.id, invocationId, receiptFingerprint, input);
156
+ if (failure) {
157
+ spendResult(budget, result);
158
+ return failedPartialReport(request, plan, rows, budget, warnings, failure);
159
+ }
160
+ rows.push(rowForResult({ plan, source: request.sourceResult, result, variantId: variant.id,
161
+ kind: noChange ? 'unchanged-control' : 'variant', inference: 'new-invocation', changes: variant.changes, invocationId }));
162
+ }
163
+ catch {
164
+ // For example a cyclic or non-serializable result: fail the run, never throw into the caller.
165
+ chargeUnusableResult(budget, warnings, request, result);
166
+ return failedPartialReport(request, plan, rows, budget, warnings, `variant ${variant.id} returned an unusable result`);
167
+ }
168
+ spendResult(budget, result);
169
+ budget.processedVariants += 1;
170
+ }
171
+ const report = buildReport(request, plan, rows, budget, warnings);
172
+ validateSensitivityReport(report);
173
+ return report;
174
+ }
175
+ // The implicit state is a process-local safety net, not durable storage. When it is full it fails
176
+ // closed instead of evicting live limit state; only counters from windows before the current one
177
+ // are evicted. Production hosts must supply durable, non-resettable probe state.
178
+ const DEFAULT_PROBE_STATE_MAX_ENTRIES = 512;
179
+ const DEFAULT_PROBE_STATE_MAX_ENTRIES_PER_PRINCIPAL = 64;
180
+ const DEFAULT_PROBE_WINDOW_MS = 3_600_000;
181
+ const defaultProbeState = {
182
+ reportsByWindow: new Map(), pathCounts: new Map(),
183
+ };
184
+ function probeRoot(plan, request) {
185
+ return plan.analysisKind === 'policy-replay'
186
+ ? { ruleset: request.sourceRuleset, binding: request.sourceBinding } : { input: request.sourceInput };
187
+ }
188
+ function validateInputs(plan, request, now) {
189
+ assertArtifactPin(request.sourceRuleset, plan.source.ruleset, 'sensitivity source ruleset');
190
+ assertArtifactPin(request.sourceBinding, plan.source.binding, 'sensitivity source binding');
191
+ assertArtifactPin(request.sourceResult, plan.source.result, 'sensitivity source result');
192
+ validateRuleset(request.sourceRuleset);
193
+ validateBinding(request.sourceBinding, request.sourceRuleset);
194
+ if (request.sourceResult.spec.invocationId.length === 0)
195
+ throw new SensitivityContractError('source result must have an invocation ID');
196
+ if (Date.parse(plan.authorization.expiresAt) <= now) {
197
+ throw new SensitivityContractError('sensitivity authorization is expired', 'semantic', ['authorization expired']);
198
+ }
199
+ try {
200
+ validateAgainstSchema(request.sourceRuleset.spec.inputSchema, request.sourceInput, 'sensitivity source input');
201
+ }
202
+ catch {
203
+ throw new SensitivityContractError('sensitivity source input does not match the ruleset input schema', 'semantic', ['source input does not match the ruleset input schema']);
204
+ }
205
+ const identity = request.probeIdentity;
206
+ const fields = ['tenantId', 'workspaceId', 'projectId', 'principalId'];
207
+ if (!identity || typeof identity !== 'object' || fields.some(field => typeof identity[field] !== 'string' || identity[field].length === 0)) {
208
+ throw new SensitivityContractError('sensitivity probe identity is missing', 'semantic', ['probe identity is missing; the host must supply it']);
209
+ }
210
+ if (identity.tenantId !== plan.tenantId || identity.workspaceId !== plan.workspaceId || identity.projectId !== plan.projectId
211
+ || identity.principalId !== plan.actor.principalId) {
212
+ throw new SensitivityContractError('sensitivity probe identity does not match the plan', 'semantic', ['probe identity does not match the plan']);
213
+ }
214
+ }
215
+ // The probed subject is the decision input under the canonical ruleset and binding content.
216
+ // Caller-chosen metadata (id, version, description) is not part of the key: renaming an
217
+ // otherwise identical artifact must not mint a new budget for re-evaluating the same input.
218
+ // Any change to the evaluated spec does mint one, and the binding's ruleset pin is normalized
219
+ // to the canonical ruleset digest so a re-pinned rename cannot mint a budget either.
220
+ // The caller-supplied result artifact is not part of the key: re-pinning a cosmetically
221
+ // different result must not mint a new budget for re-evaluating the same input.
222
+ function probeSubjectDigest(request) {
223
+ const ruleset = subjectArtifact(request.sourceRuleset);
224
+ const binding = {
225
+ apiVersion: request.sourceBinding.apiVersion,
226
+ kind: request.sourceBinding.kind,
227
+ spec: { ...request.sourceBinding.spec, ruleset: { digest: sensitivityDigest(ruleset) } },
228
+ };
229
+ return sensitivityDigest({
230
+ input: request.sourceInput,
231
+ ruleset,
232
+ binding,
233
+ });
234
+ }
235
+ // Canonical policy content for subject identity: apiVersion, kind and spec only.
236
+ // Decision-definition renames that alter the ruleset spec still mint distinct subjects.
237
+ function subjectArtifact(artifact) {
238
+ return { apiVersion: artifact.apiVersion, kind: artifact.kind, spec: artifact.spec };
239
+ }
240
+ function requiresEvaluator(plan, request) {
241
+ return plan.baselineStability.enabled || plan.variants.some(variant => !isNoChange({ input: request.sourceInput }, variant.changes));
242
+ }
243
+ function validateChangeTargets(plan, request) {
244
+ const root = plan.analysisKind === 'policy-replay'
245
+ ? { ruleset: request.sourceRuleset, binding: request.sourceBinding } : { input: request.sourceInput };
246
+ for (const variant of plan.variants)
247
+ for (const change of variant.changes)
248
+ assertPointerTarget(root, change.path);
249
+ }
250
+ function enforceProbeLimits(plan, state, root, now, identity, subjectDigest) {
251
+ const reportsByWindow = state?.reportsByWindow ?? new Map();
252
+ const pathCounts = state?.pathCounts ?? new Map();
253
+ if (state && !state.reportsByWindow)
254
+ state.reportsByWindow = reportsByWindow;
255
+ if (state && !state.pathCounts)
256
+ state.pathCounts = pathCounts;
257
+ // The window comes from the host clock and host-owned duration. plan.probeControl.windowId is a
258
+ // descriptive label only: the plan author cannot pick a fresh window to reset the budget.
259
+ const windowMs = state?.windowMs ?? DEFAULT_PROBE_WINDOW_MS;
260
+ if (!Number.isSafeInteger(windowMs) || windowMs <= 0 || !Number.isFinite(now)) {
261
+ throw new SensitivityContractError('sensitivity probe window is invalid', 'semantic', ['probe window is invalid']);
262
+ }
263
+ const window = Math.floor(now / windowMs);
264
+ // Counters are keyed on the host-authenticated identity and the pinned source result, never on
265
+ // plan-authored tenant, principal or subjectRef fields, so rotating those cannot mint a budget.
266
+ const principal = [identity.tenantId, identity.workspaceId, identity.projectId, identity.principalId];
267
+ const reportKey = JSON.stringify([window, ...principal, subjectDigest]);
268
+ const reports = reportsByWindow.get(reportKey) ?? 0;
269
+ if (reports >= plan.probeControl.maxReportsPerWindow)
270
+ throw new SensitivityContractError('sensitivity probe report limit exceeded', 'semantic', ['probe report limit exceeded']);
271
+ const charges = changedProbePaths(plan).map(path => ({
272
+ path,
273
+ key: JSON.stringify([window, ...principal, subjectDigest, path]),
274
+ increments: plan.variants.reduce((sum, variant) => sum + (variant.changes.some(change => change.path === path && changeIsEffective(root, change)) ? 1 : 0), 0),
275
+ })).filter(charge => charge.increments > 0);
276
+ for (const charge of charges) {
277
+ if ((pathCounts.get(charge.key) ?? 0) + charge.increments > plan.probeControl.maxPerPrincipalSubjectPath) {
278
+ throw new SensitivityContractError('sensitivity path probe limit exceeded', 'semantic', [`probe path limit exceeded: ${charge.path}`]);
279
+ }
280
+ }
281
+ const perPrincipal = state?.maxEntriesPerPrincipal ?? DEFAULT_PROBE_STATE_MAX_ENTRIES_PER_PRINCIPAL;
282
+ if (!Number.isSafeInteger(perPrincipal) || perPrincipal <= 0) {
283
+ throw new SensitivityContractError('sensitivity probe principal quota is invalid', 'semantic', ['probe principal quota is invalid']);
284
+ }
285
+ if (state === defaultProbeState) {
286
+ evictExpiredWindows(reportsByWindow, window);
287
+ evictExpiredWindows(pathCounts, window);
288
+ }
289
+ const newReportKeys = reportsByWindow.has(reportKey) ? 0 : 1;
290
+ const newPathKeys = charges.filter(charge => !pathCounts.has(charge.key)).length;
291
+ // Applies to host-supplied state too: one principal cannot mint unbounded subjects in a window.
292
+ if (principalEntries(reportsByWindow, principal, window) + newReportKeys > perPrincipal
293
+ || principalEntries(pathCounts, principal, window) + newPathKeys > perPrincipal) {
294
+ throw new SensitivityContractError('sensitivity probe principal quota exhausted', 'semantic', ['probe principal quota exhausted for this window']);
295
+ }
296
+ if (state === defaultProbeState) {
297
+ if (reportsByWindow.size + newReportKeys > DEFAULT_PROBE_STATE_MAX_ENTRIES
298
+ || pathCounts.size + newPathKeys > DEFAULT_PROBE_STATE_MAX_ENTRIES) {
299
+ throw new SensitivityContractError('sensitivity probe state capacity exhausted', 'semantic', ['probe state capacity exhausted; supply durable probe state']);
300
+ }
301
+ }
302
+ // Charge only after every check has passed.
303
+ reportsByWindow.set(reportKey, reports + 1);
304
+ for (const charge of charges)
305
+ pathCounts.set(charge.key, (pathCounts.get(charge.key) ?? 0) + charge.increments);
306
+ }
307
+ function principalEntries(map, principal, window) {
308
+ let count = 0;
309
+ for (const key of map.keys()) {
310
+ let parts;
311
+ try {
312
+ parts = JSON.parse(key);
313
+ }
314
+ catch {
315
+ continue;
316
+ }
317
+ if (!Array.isArray(parts) || parts[0] !== window)
318
+ continue;
319
+ if (principal.every((value, index) => parts[index + 1] === value))
320
+ count += 1;
321
+ }
322
+ return count;
323
+ }
324
+ function evictExpiredWindows(map, currentWindow) {
325
+ for (const key of [...map.keys()]) {
326
+ let window;
327
+ try {
328
+ window = JSON.parse(key)[0];
329
+ }
330
+ catch {
331
+ continue;
332
+ }
333
+ if (typeof window === 'number' && window < currentWindow)
334
+ map.delete(key);
335
+ }
336
+ }
337
+ function changedProbePaths(plan) {
338
+ return [...new Set(plan.variants.flatMap(variant => variant.changes.map(change => change.path)))];
339
+ }
340
+ function replayArtifacts(sourceRuleset, sourceBinding, changes) {
341
+ const artifacts = { ruleset: structuredClone(sourceRuleset), binding: structuredClone(sourceBinding) };
342
+ applyChanges(artifacts, changes);
343
+ validateRuleset(artifacts.ruleset);
344
+ if (artifactDigest(artifacts.ruleset) !== artifacts.binding.spec.ruleset.digest) {
345
+ artifacts.binding.spec.ruleset = artifactPin(artifacts.ruleset);
346
+ }
347
+ validateBinding(artifacts.binding, artifacts.ruleset);
348
+ return artifacts;
349
+ }
350
+ function replayPolicy(source, sourceBinding, ruleset, binding, definitions, input, variantId) {
351
+ let unreplayable = false;
352
+ const evaluations = Object.fromEntries(Object.entries(source.spec.evaluations).map(([alias, result]) => {
353
+ const targets = binding.spec.evaluations[alias]?.targets ?? [];
354
+ const index = replayTargetIndex(result, sourceBinding.spec.evaluations[alias]?.targets ?? []);
355
+ const sourceTarget = index === null ? undefined : sourceBinding.spec.evaluations[alias]?.targets[index];
356
+ const target = index === null ? undefined : targets[index];
357
+ const definition = definitions[result.spec.decision.id];
358
+ const replayed = sourceTarget && target ? replayAcceptance(result, sourceTarget, target, definition) : structuredClone(result);
359
+ // Stored evidence says nothing about a different executor: any change of target identity is unreplayable.
360
+ if (!sameTargetIdentities(sourceBinding.spec.evaluations[alias]?.targets ?? [], targets))
361
+ unreplayable = true;
362
+ // A newly failed acceptance that the binding would route to a later target depends on that
363
+ // target's unobserved outcome.
364
+ if (index !== null && result.spec.status === 'success' && replayed.spec.status !== 'success'
365
+ && index < targets.length - 1 && binding.spec.evaluations[alias].fallbackOn.includes(replayed.spec.reason))
366
+ unreplayable = true;
367
+ return [alias, replayed];
368
+ }));
369
+ const composition = composeRuleset(ruleset, input, evaluations);
370
+ const rulesetPin = artifactPin(ruleset);
371
+ const bindingPin = artifactPin(binding);
372
+ return { unreplayable, result: {
373
+ ...source,
374
+ metadata: { id: `${source.metadata.id}-${variantId}`, version: source.metadata.version, description: source.metadata.description },
375
+ spec: {
376
+ ...source.spec,
377
+ ruleset: rulesetPin,
378
+ binding: bindingPin,
379
+ status: composition.status,
380
+ reason: composition.reason,
381
+ ...(composition.outcome !== undefined ? { outcome: composition.outcome } : {}),
382
+ matchedRules: composition.matchedRules,
383
+ evaluations,
384
+ },
385
+ } };
386
+ }
387
+ function sameTargetIdentities(left, right) {
388
+ const identity = (target) => sensitivityDigest({
389
+ adapter: target.adapter, adapterVersion: target.adapterVersion, model: target.model,
390
+ subagent: target.subagent ?? null, credentialRef: target.credentialRef ?? null,
391
+ });
392
+ return left.length === right.length && left.every((target, index) => identity(target) === identity(right[index]));
393
+ }
394
+ // Changing a target other than the one that produced the stored result, when that target was
395
+ // attempted and abstained, or changing fallback routing, depends on an outcome never observed.
396
+ function dependsOnUnobservedOutcome(changes, sourceBinding, source) {
397
+ for (const change of changes) {
398
+ const parts = change.path.split('/').slice(1).map(part => part.replace(/~1/g, '/').replace(/~0/g, '~'));
399
+ if (parts[0] !== 'binding' || parts[1] !== 'spec' || parts[2] !== 'evaluations' || parts[3] === undefined)
400
+ continue;
401
+ const result = source.spec.evaluations[parts[3]];
402
+ const targets = sourceBinding.spec.evaluations[parts[3]]?.targets ?? [];
403
+ if (!result)
404
+ continue;
405
+ if (parts[4] === 'fallbackOn')
406
+ return true;
407
+ if (parts[4] !== 'targets' || parts[5] === undefined)
408
+ continue;
409
+ const replayIndex = replayTargetIndex(result, targets);
410
+ if (replayIndex === null)
411
+ return true;
412
+ const changed = Number(parts[5]);
413
+ if (changed === replayIndex)
414
+ continue;
415
+ const target = targets[changed];
416
+ if (target && result.spec.attempts.some(attempt => attempt.status === 'abstained' && attempt.adapter === target.adapter
417
+ && attempt.adapterVersion === target.adapterVersion && attempt.requestedModel === target.model))
418
+ return true;
419
+ }
420
+ return false;
421
+ }
422
+ // The stored result came from the final attempt (the final successful one when it was accepted), so
423
+ // its adapter, adapter version and requested model select the target whose acceptance is replayed.
424
+ // An ambiguous or unmatched attempt keeps the stored result rather than guessing a target.
425
+ function replayTargetIndex(result, targets) {
426
+ const attempts = result.spec.attempts;
427
+ const attempt = result.spec.status === 'success'
428
+ ? [...attempts].reverse().find(item => item.status === 'success') : attempts.at(-1);
429
+ if (!attempt)
430
+ return null;
431
+ const matches = targets.flatMap((target, index) => target.adapter === attempt.adapter
432
+ && target.adapterVersion === attempt.adapterVersion && target.model === attempt.requestedModel ? [index] : []);
433
+ return matches.length === 1 ? matches[0] : null;
434
+ }
435
+ function replayAcceptance(result, sourceTarget, target, definition) {
436
+ if (!canReplayAcceptance(result, sourceTarget, definition))
437
+ return structuredClone(result);
438
+ const replayed = acceptanceObservation(result, target, definition);
439
+ const spec = {
440
+ ...result.spec,
441
+ status: replayed.status,
442
+ reason: replayed.reason,
443
+ uncertainty: replayed.uncertainty,
444
+ ...(replayed.acceptance ? { acceptance: replayed.acceptance } : {}),
445
+ };
446
+ if (replayed.status === 'success' && replayed.value !== undefined)
447
+ spec.value = replayed.value;
448
+ else
449
+ delete spec.value;
450
+ return {
451
+ ...result,
452
+ spec,
453
+ };
454
+ }
455
+ function acceptanceObservation(result, target, definition) {
456
+ const replayDefinition = definition ?? replayDefinitionFromEvidence(result, target);
457
+ const value = replayValue(result, replayDefinition);
458
+ const observation = {
459
+ status: value === undefined ? result.spec.status : 'success',
460
+ reason: value === undefined ? result.spec.reason : 'none',
461
+ ...(value !== undefined ? { value } : {}),
462
+ uncertainty: structuredClone(result.spec.uncertainty),
463
+ actualModel: result.spec.attempts.at(-1)?.actualModel ?? null,
464
+ usage: { inputTokens: null, outputTokens: null, costUsd: null },
465
+ requestId: null,
466
+ };
467
+ return applyTargetAcceptance(replayDefinition, target, observation);
468
+ }
469
+ // Only an abstention that acceptance produced may be replayed. Primitive-policy acceptance records
470
+ // DecisionAcceptanceEvidence, so its absence means the adapter abstained. Confidence-threshold
471
+ // acceptance records no evidence, so the abstention must be reproduced by the source target's own
472
+ // threshold; one the source threshold would have accepted came from the adapter. An adapter that
473
+ // abstains with a confidence below the source threshold is indistinguishable and is replayed.
474
+ function canReplayAcceptance(result, sourceTarget, definition) {
475
+ if (result.spec.status === 'success')
476
+ return true;
477
+ if (sourceTarget.acceptance.mode === 'primitive-policy')
478
+ return result.spec.acceptance !== undefined;
479
+ if (sourceTarget.acceptance.mode !== 'confidence-threshold')
480
+ return false;
481
+ if (result.spec.reason !== 'low-confidence' && result.spec.reason !== 'missing-confidence'
482
+ && result.spec.reason !== 'confidence-profile-mismatch')
483
+ return false;
484
+ const reproduced = acceptanceObservation(result, sourceTarget, definition);
485
+ return reproduced.status === result.spec.status && reproduced.reason === result.spec.reason;
486
+ }
487
+ function replayDefinitionFromEvidence(result, target) {
488
+ let primitive;
489
+ if (target.acceptance.mode === 'primitive-policy') {
490
+ const primitives = [...new Set(target.acceptance.rules.map(rule => rule.primitive))];
491
+ primitive = primitives.length === 1 ? primitives[0] : target.acceptance.requiredOptions ? 'choice' : undefined;
492
+ }
493
+ else {
494
+ primitive = typeof result.spec.value === 'string' ? 'choice'
495
+ : typeof result.spec.value === 'number' ? 'truth-probability' : undefined;
496
+ }
497
+ if (primitive === 'choice') {
498
+ const keys = [...new Set([...(result.spec.uncertainty?.distribution ? Object.keys(result.spec.uncertainty.distribution) : []),
499
+ ...(typeof result.spec.value === 'string' ? [result.spec.value] : []),
500
+ ...(target.acceptance.mode === 'primitive-policy' ? target.acceptance.requiredOptions ?? [] : [])])];
501
+ return replayDefinitionWithAnswer(result, { kind: 'choice', options: (keys.length ? keys : ['unknown']).map(id => ({ id, description: id })) });
502
+ }
503
+ if (primitive === 'ordinal-score') {
504
+ const levels = Object.keys(result.spec.uncertainty?.distribution ?? {}).sort((a, b) => Number(a) - Number(b));
505
+ return replayDefinitionWithAnswer(result, { kind: 'ordinal-score', levels: levels.length >= 2 ? levels : ['0', '1'] });
506
+ }
507
+ return replayDefinitionWithAnswer(result, { kind: 'truth-probability', trueDescription: 'true', falseDescription: 'false' });
508
+ }
509
+ function replayDefinitionWithAnswer(result, answer) {
510
+ return {
511
+ apiVersion: result.apiVersion,
512
+ kind: 'DecisionDefinition',
513
+ metadata: { id: result.spec.decision.id, version: result.spec.decision.version, description: result.spec.alias },
514
+ spec: { purpose: 'sensitivity replay', inputSchema: {}, question: 'sensitivity replay', answer, requiredCapabilities: [] },
515
+ };
516
+ }
517
+ function replayValue(result, definition) {
518
+ if (result.spec.value !== undefined)
519
+ return result.spec.value;
520
+ if (definition.spec.answer.kind === 'choice') {
521
+ const distribution = result.spec.uncertainty?.distribution;
522
+ if (!distribution)
523
+ return undefined;
524
+ return Object.entries(distribution).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))[0]?.[0];
525
+ }
526
+ if (definition.spec.answer.kind === 'truth-probability') {
527
+ const yes = result.spec.acceptance?.values['yes-probability'];
528
+ if (yes?.provenance === 'provider-value')
529
+ return yes.value;
530
+ return undefined;
531
+ }
532
+ return result.spec.acceptance?.values['expected-score']?.value;
533
+ }
534
+ function applyChanges(target, changes) {
535
+ for (const change of changes)
536
+ setPointer(target, change.path, structuredClone(change.value));
537
+ return target;
538
+ }
539
+ function setPointer(root, pointer, value) {
540
+ const parts = pointer.split('/').slice(1).map(part => part.replace(/~1/g, '/').replace(/~0/g, '~'));
541
+ if (!parts.length)
542
+ throw new SensitivityContractError('root replacement is not supported');
543
+ let current = root;
544
+ for (const part of parts.slice(0, -1)) {
545
+ assertSafePointerSegment(part);
546
+ if (!current || typeof current !== 'object' || !Object.hasOwn(current, part)) {
547
+ throw new SensitivityContractError(`sensitivity path ${pointer} is not structurally present`);
548
+ }
549
+ current = current[part];
550
+ }
551
+ assertSafePointerSegment(parts[parts.length - 1]);
552
+ if (!current || typeof current !== 'object')
553
+ throw new SensitivityContractError(`sensitivity path ${pointer} is not structurally present`);
554
+ current[parts[parts.length - 1]] = value;
555
+ }
556
+ function assertPointerTarget(root, pointer) {
557
+ const parts = pointer.split('/').slice(1).map(part => part.replace(/~1/g, '/').replace(/~0/g, '~'));
558
+ if (!parts.length)
559
+ throw new SensitivityContractError('root replacement is not supported');
560
+ let current = root;
561
+ for (const part of parts.slice(0, -1)) {
562
+ assertSafePointerSegment(part);
563
+ if (!current || typeof current !== 'object' || !Object.hasOwn(current, part)) {
564
+ throw new SensitivityContractError(`sensitivity path ${pointer} is not structurally present`);
565
+ }
566
+ current = current[part];
567
+ }
568
+ assertSafePointerSegment(parts[parts.length - 1]);
569
+ if (!current || typeof current !== 'object')
570
+ throw new SensitivityContractError(`sensitivity path ${pointer} is not structurally present`);
571
+ }
572
+ function assertSafePointerSegment(segment) {
573
+ if (segment === '__proto__' || segment === 'constructor' || segment === 'prototype') {
574
+ throw new SensitivityContractError(`sensitivity path segment ${segment} is prohibited`);
575
+ }
576
+ }
577
+ function isNoChange(root, changes) {
578
+ const before = sensitivityDigest(root);
579
+ const copy = structuredClone(root);
580
+ const normalized = changes.map(change => ({ path: change.path, value: change.value }));
581
+ applyChanges(copy, normalized);
582
+ return sensitivityDigest(copy) === before;
583
+ }
584
+ function changeIsEffective(root, change) {
585
+ const current = getPointer(root, change.path);
586
+ return current === POINTER_MISSING || sensitivityDigest(current) !== sensitivityDigest(change.value);
587
+ }
588
+ const POINTER_MISSING = Symbol('pointer-missing');
589
+ function getPointer(root, pointer) {
590
+ const parts = pointer.split('/').slice(1).map(part => part.replace(/~1/g, '/').replace(/~0/g, '~'));
591
+ let current = root;
592
+ for (const part of parts) {
593
+ if (!current || typeof current !== 'object' || !Object.hasOwn(current, part))
594
+ return POINTER_MISSING;
595
+ current = current[part];
596
+ }
597
+ return current;
598
+ }
599
+ function rowForResult(input) {
600
+ const sourceReceipt = artifactPin(input.source);
601
+ const resultDigest = sensitivityDigest(input.result);
602
+ return {
603
+ variantId: input.variantId,
604
+ kind: input.kind,
605
+ changes: input.changes.map(change => redactChange(input.plan, change)),
606
+ inference: input.inference,
607
+ sourceReceipt,
608
+ counterfactualReceipt: input.inference === 'new-invocation' ? artifactPin(input.result) : null,
609
+ resultDigest,
610
+ freshInvocationId: input.invocationId,
611
+ deltas: deltas(input.source, input.result),
612
+ resourceUse: usage(input.source, input.result, input.inference),
613
+ warnings: [...warningForResult(input.source, input.result), ...(input.extraWarnings ?? [])],
614
+ };
615
+ }
616
+ function redactChange(plan, change) {
617
+ return {
618
+ path: change.path,
619
+ valueDigest: sensitivityDigest(change.value),
620
+ sensitivity: plan.pathDomains.find(domain => domain.path === change.path)?.sensitivity ?? 'restricted',
621
+ };
622
+ }
623
+ function deltas(source, result) {
624
+ const sourceRules = source.spec.matchedRules;
625
+ const resultRules = result.spec.matchedRules;
626
+ return {
627
+ outcomeChanged: sensitivityDigest(source.spec.outcome ?? null) !== sensitivityDigest(result.spec.outcome ?? null),
628
+ acceptanceChanged: acceptanceDigest(source) !== acceptanceDigest(result),
629
+ ruleChanged: sensitivityDigest(sourceRules) !== sensitivityDigest(resultRules),
630
+ firstChangedRule: firstChanged(sourceRules, resultRules),
631
+ distributionDistanceBps: distributionDistanceBps(source, result),
632
+ status: result.spec.status,
633
+ reason: result.spec.reason,
634
+ };
635
+ }
636
+ function acceptanceDigest(result) {
637
+ return sensitivityDigest(Object.fromEntries(Object.entries(result.spec.evaluations)
638
+ .map(([alias, evaluation]) => [alias, {
639
+ status: evaluation.spec.status,
640
+ reason: evaluation.spec.reason,
641
+ acceptance: evaluation.spec.acceptance ?? null,
642
+ }])));
643
+ }
644
+ function firstChanged(left, right) {
645
+ const length = Math.max(left.length, right.length);
646
+ for (let index = 0; index < length; index += 1)
647
+ if (left[index] !== right[index])
648
+ return right[index] ?? left[index] ?? null;
649
+ return null;
650
+ }
651
+ function distributionDistanceBps(source, result) {
652
+ const distances = Object.entries(source.spec.evaluations).map(([alias, left]) => {
653
+ const right = result.spec.evaluations[alias];
654
+ const a = left.spec.uncertainty?.distribution;
655
+ const b = right?.spec.uncertainty?.distribution;
656
+ if (!a || !b)
657
+ return null;
658
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
659
+ const total = [...keys].reduce((sum, key) => sum + Math.abs((a[key] ?? 0) - (b[key] ?? 0)), 0) / 2;
660
+ return Math.round(total * 10_000);
661
+ }).filter((value) => value !== null);
662
+ return distances.length ? Math.max(...distances) : null;
663
+ }
664
+ function usage(source, result, inference) {
665
+ if (inference !== 'new-invocation')
666
+ return { backendCalls: 0, tokens: 0, costMicros: 0 };
667
+ const attempts = Object.values(result.spec.evaluations).flatMap(evaluation => evaluation.spec.attempts);
668
+ const sourceAttempts = new Set(Object.values(source.spec.evaluations).flatMap(evaluation => evaluation.spec.attempts)
669
+ .map(attempt => `${attempt.ordinal}:${attempt.adapter}:${attempt.requestedModel}`));
670
+ const newAttempts = attempts.filter(attempt => !sourceAttempts.has(`${attempt.ordinal}:${attempt.adapter}:${attempt.requestedModel}`));
671
+ return {
672
+ backendCalls: newAttempts.length,
673
+ tokens: newAttempts.reduce((sum, attempt) => sum + usageNumber(attempt.usage.inputTokens) + usageNumber(attempt.usage.outputTokens), 0),
674
+ costMicros: Math.round(newAttempts.reduce((sum, attempt) => sum + usageNumber(attempt.usage.costUsd), 0) * 1_000_000),
675
+ };
676
+ }
677
+ function spendResult(budget, result) {
678
+ const attempts = Object.values(result.spec.evaluations).flatMap(evaluation => evaluation.spec.attempts);
679
+ budget.backendCalls += attempts.length;
680
+ budget.tokens += attempts.reduce((sum, attempt) => sum + usageNumber(attempt.usage.inputTokens) + usageNumber(attempt.usage.outputTokens), 0);
681
+ budget.costMicros += Math.round(attempts.reduce((sum, attempt) => sum + usageNumber(attempt.usage.costUsd), 0) * 1_000_000);
682
+ }
683
+ /** Spend can be read from a value only when every evaluation carries attempts with usage objects. */
684
+ function hasAttemptLineage(value) {
685
+ const spec = isRecord(value) ? value.spec : undefined;
686
+ if (!isRecord(spec) || !isRecord(spec.evaluations))
687
+ return false;
688
+ return Object.values(spec.evaluations).every(evaluation => isRecord(evaluation) && isRecord(evaluation.spec)
689
+ && Array.isArray(evaluation.spec.attempts)
690
+ && evaluation.spec.attempts.every(attempt => isRecord(attempt) && isRecord(attempt.usage)
691
+ && validUsage(attempt.usage.inputTokens) && validUsage(attempt.usage.outputTokens) && validUsage(attempt.usage.costUsd)));
692
+ }
693
+ function validUsage(value) {
694
+ return value === null || (typeof value === 'number' && Number.isFinite(value) && value >= 0);
695
+ }
696
+ /** Non-negative finite usage; anything else contributes nothing (validated results never reach this). */
697
+ function usageNumber(value) {
698
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
699
+ }
700
+ function chargeUnusableResult(budget, warnings, request, value) {
701
+ if (hasAttemptLineage(value))
702
+ spendResult(budget, value);
703
+ else
704
+ spendUnknownReserve(budget, warnings, request.sourceBinding, request.sourceResult);
705
+ }
706
+ /** A returned reevaluation must have the fields the validator and row builder read. */
707
+ function isRulesetResultShape(value) {
708
+ if (!hasAttemptLineage(value))
709
+ return false;
710
+ const metadata = value.metadata;
711
+ if (!isRecord(metadata) || typeof metadata.id !== 'string' || typeof metadata.version !== 'string')
712
+ return false;
713
+ const spec = value.spec;
714
+ return typeof spec.invocationId === 'string' && typeof spec.status === 'string' && typeof spec.reason === 'string'
715
+ && Array.isArray(spec.matchedRules) && spec.matchedRules.every(rule => typeof rule === 'string')
716
+ && Object.values(value.spec.evaluations).every(evaluation => typeof evaluation.spec.invocationId === 'string');
717
+ }
718
+ function isRecord(value) {
719
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
720
+ }
721
+ /** Charges host-supplied spend evidence from a failed reevaluation; returns false when there is none. */
722
+ function spendFailure(budget, error) {
723
+ const record = error && typeof error === 'object' ? error : {};
724
+ const result = record.result;
725
+ if (hasAttemptLineage(result)) {
726
+ spendResult(budget, result);
727
+ return true;
728
+ }
729
+ const resourceUse = record.resourceUse ?? record.sensitivitySpend;
730
+ if (!resourceUse || typeof resourceUse !== 'object')
731
+ return false;
732
+ const usage = resourceUse;
733
+ budget.backendCalls += integerUsage(usage.backendCalls);
734
+ budget.tokens += integerUsage(usage.tokens);
735
+ budget.costMicros += integerUsage(usage.costMicros);
736
+ return true;
737
+ }
738
+ // A throw without spend evidence may still have reached the backend. Unknown spend is not zero:
739
+ // charge the pre-dispatch reservation and flag the figures as a conservative reservation.
740
+ function spendUnknownReserve(budget, warnings, binding, source) {
741
+ const reserve = reevaluationReserve(binding, source);
742
+ budget.backendCalls += reserve.backendCalls;
743
+ budget.tokens += reserve.tokens;
744
+ budget.costMicros += reserve.costMicros;
745
+ if (!warnings.includes('spend-unknown-reserved'))
746
+ warnings.push('spend-unknown-reserved');
747
+ }
748
+ function integerUsage(value) {
749
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : 0;
750
+ }
751
+ function canSpendCall(plan, binding, source, budget, deadline, now) {
752
+ const reserve = reevaluationReserve(binding, source);
753
+ return now < deadline && budget.backendCalls + reserve.backendCalls <= plan.budgets.maxBackendCalls
754
+ && budget.tokens + reserve.tokens <= plan.budgets.maxTokens && budget.costMicros + reserve.costMicros <= plan.budgets.maxCostMicros
755
+ && budget.backendCalls < plan.budgets.maxBackendCalls && budget.tokens < plan.budgets.maxTokens
756
+ && budget.costMicros < plan.budgets.maxCostMicros;
757
+ }
758
+ function reevaluationReserve(binding, source) {
759
+ const evaluationCount = Math.max(1, Object.keys(source.spec.evaluations).length, Object.keys(binding.spec.evaluations).length);
760
+ const attempts = Math.max(1, binding.spec.maxAttempts) * evaluationCount;
761
+ const sourceAttempts = Object.values(source.spec.evaluations).flatMap(evaluation => evaluation.spec.attempts);
762
+ const perAttemptTokens = Math.max(0, ...sourceAttempts.map(attempt => usageNumber(attempt.usage.inputTokens) + usageNumber(attempt.usage.outputTokens)));
763
+ const perAttemptCostMicros = Math.max(0, ...sourceAttempts.map(attempt => Math.round(usageNumber(attempt.usage.costUsd) * 1_000_000)));
764
+ return { backendCalls: attempts, tokens: attempts * perAttemptTokens, costMicros: attempts * perAttemptCostMicros };
765
+ }
766
+ function warningForResult(source, result) {
767
+ const warnings = [];
768
+ if (result.spec.status !== source.spec.status)
769
+ warnings.push('status-changed');
770
+ if (result.spec.reason !== source.spec.reason)
771
+ warnings.push('reason-changed');
772
+ return warnings;
773
+ }
774
+ function buildReport(request, plan, rows, budget, warnings) {
775
+ const minimum = minimumThresholdChange(plan);
776
+ const status = budget.exhausted ? rows.length ? 'partial' : 'budget-exhausted' : 'completed';
777
+ const withoutDigest = {
778
+ schemaVersion: 'decision-sensitivity-report/v1',
779
+ id: `${plan.id}-report`,
780
+ plan: { id: plan.id, version: plan.version, digest: sensitivityDigest(plan) },
781
+ tenantId: plan.tenantId,
782
+ workspaceId: plan.workspaceId,
783
+ projectId: plan.projectId,
784
+ sourceSubject: structuredClone(plan.sourceSubject),
785
+ analysisKind: plan.analysisKind,
786
+ semantics: 'associative-sensitivity-not-causal',
787
+ actionAuthorization: 'not-authorized',
788
+ status,
789
+ generatedAt: request.generatedAt,
790
+ privacy: { redaction: plan.privacy.redaction, summaryPrecisionBps: plan.privacy.summaryPrecisionBps },
791
+ retention: structuredClone(plan.retention),
792
+ budget,
793
+ baseline: {
794
+ resultRef: artifactPin(request.sourceResult),
795
+ outcomeDigest: sensitivityDigest(request.sourceResult.spec.outcome ?? null),
796
+ status: request.sourceResult.spec.status,
797
+ reason: request.sourceResult.spec.reason,
798
+ },
799
+ rows,
800
+ summary: {
801
+ totalRows: rows.length,
802
+ changedOutcomes: rows.filter(row => row.deltas.outcomeChanged).length,
803
+ changedAcceptance: rows.filter(row => row.deltas.acceptanceChanged).length,
804
+ changedRules: rows.filter(row => row.deltas.ruleChanged).length,
805
+ minimumThresholdChangeBps: minimum,
806
+ },
807
+ warnings,
808
+ };
809
+ return { ...withoutDigest, digest: sensitivityDigest(withoutDigest) };
810
+ }
811
+ function failedPartialReport(request, plan, rows, budget, warnings, reason) {
812
+ budget.exhausted = true;
813
+ const report = buildReport(request, plan, rows, budget, [...warnings, 'partial-failure', reason]);
814
+ const { digest: _digest, ...payload } = report;
815
+ const status = rows.length ? 'partial' : 'failed';
816
+ return { ...payload, status, digest: sensitivityDigest({ ...payload, status }) };
817
+ }
818
+ function minimumThresholdChange(plan) {
819
+ const values = plan.variants.flatMap(variant => variant.changes.map(change => change.value))
820
+ .filter((value) => typeof value === 'number' && Number.isFinite(value));
821
+ if (values.length < 2 || plan.privacy.differencing === 'deny')
822
+ return null;
823
+ const sorted = [...new Set(values)].sort((a, b) => a - b);
824
+ const distances = sorted.slice(1).map((value, index) => value - sorted[index]);
825
+ const raw = Math.min(...distances);
826
+ return Math.ceil(raw / plan.privacy.summaryPrecisionBps) * plan.privacy.summaryPrecisionBps;
827
+ }
828
+ function freshInvocationId(plan, variantId, runNonce) {
829
+ return `sens-${plan.id}-${runNonce}-${variantId}`.slice(0, 160);
830
+ }
831
+ function expectedReceiptFingerprint(request, invocationId, input) {
832
+ return decisionInvocationFingerprint({
833
+ invocationId,
834
+ value: input,
835
+ definitions: request.sourceRuleset.spec.evaluations.map(evaluation => evaluation.decision),
836
+ ruleset: artifactPin(request.sourceRuleset),
837
+ binding: artifactPin(request.sourceBinding),
838
+ });
839
+ }
840
+ function ensureFreshBeforeDispatch(invocationId, receiptFingerprint, usedInvocations) {
841
+ if (usedInvocations.has(invocationId))
842
+ throw new SensitivityContractError(`invocation ${invocationId} was already used`);
843
+ usedInvocations.add(invocationId);
844
+ if (!/^sha256:[a-f0-9]{64}$/.test(receiptFingerprint))
845
+ throw new SensitivityContractError('invalid reevaluation receipt fingerprint');
846
+ }
847
+ function validateReevaluationResult(request, result, variantId, invocationId, receiptFingerprint, input) {
848
+ if (result.spec.invocationId !== invocationId)
849
+ return `variant ${variantId} returned invocation ID ${result.spec.invocationId} instead of ${invocationId}`;
850
+ if (result.spec.invocationId === request.sourceResult.spec.invocationId)
851
+ return `variant ${variantId} reused the source invocation ID`;
852
+ const actualFingerprint = decisionInvocationFingerprint({
853
+ invocationId: result.spec.invocationId,
854
+ value: input,
855
+ definitions: request.sourceRuleset.spec.evaluations.map(evaluation => evaluation.decision),
856
+ ruleset: artifactPin(request.sourceRuleset),
857
+ binding: artifactPin(request.sourceBinding),
858
+ });
859
+ if (actualFingerprint !== receiptFingerprint)
860
+ return `variant ${variantId} receipt fingerprint mismatch`;
861
+ if (Object.values(result.spec.evaluations).some(evaluation => evaluation.spec.invocationId !== invocationId)) {
862
+ return `variant ${variantId} returned an evaluation with the wrong invocation ID`;
863
+ }
864
+ if (Object.values(result.spec.evaluations).some(evaluation => evaluation.spec.attempts.length === 0)) {
865
+ return `variant ${variantId} did not return complete attempt lineage`;
866
+ }
867
+ return null;
868
+ }
869
+ function sanitizeReason(error) {
870
+ const message = error instanceof Error ? error.message : String(error);
871
+ return message.replace(/[^\w .:/-]/g, '').slice(0, 120) || 'reevaluation failed';
872
+ }
873
+ function rejectedReport(request, reasons) {
874
+ const plan = request.plan;
875
+ const budget = { maxVariants: plan.budgets.maxVariants, processedVariants: 0, backendCalls: 0, tokens: 0, costMicros: 0, exhausted: false };
876
+ const withoutDigest = {
877
+ schemaVersion: 'decision-sensitivity-report/v1',
878
+ id: `${plan.id}-report`,
879
+ plan: { id: plan.id, version: plan.version, digest: sensitivityDigest(plan) },
880
+ tenantId: plan.tenantId,
881
+ workspaceId: plan.workspaceId,
882
+ projectId: plan.projectId,
883
+ sourceSubject: structuredClone(plan.sourceSubject),
884
+ analysisKind: plan.analysisKind,
885
+ semantics: 'associative-sensitivity-not-causal',
886
+ actionAuthorization: 'not-authorized',
887
+ status: 'rejected',
888
+ generatedAt: request.generatedAt,
889
+ privacy: { redaction: plan.privacy.redaction, summaryPrecisionBps: plan.privacy.summaryPrecisionBps },
890
+ retention: structuredClone(plan.retention),
891
+ budget,
892
+ baseline: {
893
+ resultRef: safePin(request.sourceResult),
894
+ outcomeDigest: sensitivityDigest(request.sourceResult?.spec?.outcome ?? null),
895
+ status: request.sourceResult?.spec?.status ?? 'error',
896
+ reason: request.sourceResult?.spec?.reason ?? 'invalid-definition',
897
+ },
898
+ rows: [],
899
+ summary: { totalRows: 0, changedOutcomes: 0, changedAcceptance: 0, changedRules: 0, minimumThresholdChangeBps: null },
900
+ warnings: ['rejected-before-inference', ...reasons.map(reason => reason.replace(/[^\w .:/-]/g, ''))],
901
+ };
902
+ return { ...withoutDigest, digest: sensitivityDigest(withoutDigest) };
903
+ }
904
+ function safePin(value) {
905
+ try {
906
+ return artifactPin(value);
907
+ }
908
+ catch {
909
+ return { id: 'invalid', version: '0.0.0', digest: `sha256:${'0'.repeat(64)}` };
910
+ }
911
+ }
912
+ //# sourceMappingURL=runtime.js.map