@aiwg/cli 2026.9.24 → 2026.10.1

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 (209) 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 +177 -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 +29 -3
  15. package/dist/src/decision/ask.js +352 -0
  16. package/dist/src/decision/calibration/registry.js +9 -0
  17. package/dist/src/decision/compose.js +74 -15
  18. package/dist/src/decision/context-live-promotion.js +344 -0
  19. package/dist/src/decision/context-live-qualification.js +591 -0
  20. package/dist/src/decision/context-pruning.js +847 -0
  21. package/dist/src/decision/driver.js +432 -0
  22. package/dist/src/decision/egress-live-qualification.js +874 -0
  23. package/dist/src/decision/ensemble/contract.js +64 -1
  24. package/dist/src/decision/ensemble/index.js +1 -0
  25. package/dist/src/decision/ensemble/report.js +8 -42
  26. package/dist/src/decision/ensemble/runtime.js +571 -0
  27. package/dist/src/decision/ensemble-study/artifacts.js +85 -0
  28. package/dist/src/decision/ensemble-study/calibration.js +443 -0
  29. package/dist/src/decision/ensemble-study/corpus.js +229 -0
  30. package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
  31. package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
  32. package/dist/src/decision/ensemble-study/multifact.js +241 -0
  33. package/dist/src/decision/ensemble-study/protocol.js +77 -0
  34. package/dist/src/decision/ensemble-study/score.js +462 -0
  35. package/dist/src/decision/ensemble-study/staged.js +87 -0
  36. package/dist/src/decision/ensemble-study/statistics.js +84 -0
  37. package/dist/src/decision/evaluate.js +227 -14
  38. package/dist/src/decision/export.js +4 -0
  39. package/dist/src/decision/feature-export.js +1074 -0
  40. package/dist/src/decision/graph-index.js +16 -0
  41. package/dist/src/decision/graph-live-qualification.js +1041 -0
  42. package/dist/src/decision/graph-live-workload.js +245 -0
  43. package/dist/src/decision/graph-skill-bridge.js +21 -3
  44. package/dist/src/decision/heldout/calibration.js +140 -0
  45. package/dist/src/decision/heldout/collector.js +430 -0
  46. package/dist/src/decision/heldout/contract.js +309 -0
  47. package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
  48. package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
  49. package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
  50. package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
  51. package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
  52. package/dist/src/decision/heldout/d29-pools.js +288 -0
  53. package/dist/src/decision/heldout/d29-v6.js +169 -0
  54. package/dist/src/decision/heldout/d29-v7.js +202 -0
  55. package/dist/src/decision/heldout/d29-v8.js +586 -0
  56. package/dist/src/decision/heldout/generator-registry.js +67 -0
  57. package/dist/src/decision/heldout/generators.js +696 -0
  58. package/dist/src/decision/heldout/index.js +8 -0
  59. package/dist/src/decision/heldout/journal.js +346 -0
  60. package/dist/src/decision/heldout/study-generators.js +64 -0
  61. package/dist/src/decision/heldout/types.js +2 -0
  62. package/dist/src/decision/host-policy.js +61 -0
  63. package/dist/src/decision/index.js +13 -0
  64. package/dist/src/decision/interop-profile.v1.json +112 -0
  65. package/dist/src/decision/interop.js +653 -0
  66. package/dist/src/decision/issue-triage/index.js +3 -0
  67. package/dist/src/decision/issue-triage/runtime.js +1069 -0
  68. package/dist/src/decision/issue-triage/types.js +2 -0
  69. package/dist/src/decision/jev-credentials.js +121 -0
  70. package/dist/src/decision/patterns/catalog.js +28 -8
  71. package/dist/src/decision/patterns/playground.js +28 -11
  72. package/dist/src/decision/patterns/specs.js +19 -10
  73. package/dist/src/decision/preprocessed-evidence.js +724 -0
  74. package/dist/src/decision/qualification/capture.js +8 -2
  75. package/dist/src/decision/qualification/gate-evidence.js +19 -4
  76. package/dist/src/decision/qualification/quality.js +57 -49
  77. package/dist/src/decision/qualification/release.js +76 -3
  78. package/dist/src/decision/qualification/runner.js +5 -2
  79. package/dist/src/decision/routing/contract.js +175 -0
  80. package/dist/src/decision/routing/index.js +5 -0
  81. package/dist/src/decision/routing/report.js +216 -0
  82. package/dist/src/decision/routing/runtime.js +685 -0
  83. package/dist/src/decision/routing/types.js +2 -0
  84. package/dist/src/decision/sdlc-screening.js +1157 -0
  85. package/dist/src/decision/sensitivity/comparative.js +154 -0
  86. package/dist/src/decision/sensitivity/contract.js +218 -0
  87. package/dist/src/decision/sensitivity/index.js +5 -0
  88. package/dist/src/decision/sensitivity/runtime.js +912 -0
  89. package/dist/src/decision/sensitivity/types.js +2 -0
  90. package/dist/src/decision/validate.js +26 -0
  91. package/dist/src/extensions/commands/definitions.js +38 -0
  92. package/dist/src/extensions/manifest.js +44 -0
  93. package/dist/src/gates/discovery.js +315 -0
  94. package/dist/src/gates/driver.js +388 -0
  95. package/dist/src/gates/evaluate.js +719 -0
  96. package/dist/src/gates/floors.js +255 -0
  97. package/dist/src/gates/index.js +17 -0
  98. package/dist/src/gates/providers/evidence.js +38 -0
  99. package/dist/src/gates/providers/index.js +21 -0
  100. package/dist/src/gates/providers/isolated-child-source.js +446 -0
  101. package/dist/src/gates/providers/loader.js +1069 -0
  102. package/dist/src/gates/providers/paired.js +52 -0
  103. package/dist/src/gates/providers/proportion.js +60 -0
  104. package/dist/src/gates/providers/registry.js +40 -0
  105. package/dist/src/gates/providers/scalar.js +41 -0
  106. package/dist/src/gates/providers/screening.js +53 -0
  107. package/dist/src/gates/providers/types.js +25 -0
  108. package/dist/src/gates/registry.js +1003 -0
  109. package/dist/src/gates/report.js +102 -0
  110. package/dist/src/gates/schema.js +56 -0
  111. package/dist/src/gates/stats/binomial.js +26 -0
  112. package/dist/src/gates/stats/bootstrap.js +62 -0
  113. package/dist/src/gates/stats/clopper-pearson.js +95 -0
  114. package/dist/src/gates/stats/digest.js +26 -0
  115. package/dist/src/gates/stats/error.js +5 -0
  116. package/dist/src/gates/stats/index.js +9 -0
  117. package/dist/src/gates/stats/normal.js +35 -0
  118. package/dist/src/gates/stats/paired.js +120 -0
  119. package/dist/src/gates/stats/splits.js +30 -0
  120. package/dist/src/gates/types.js +8 -0
  121. package/dist/src/governance/redaction.js +14 -4
  122. package/dist/src/mcp/muse-native-config.mjs +123 -0
  123. package/dist/src/mcp/tools/decision.mjs +228 -0
  124. package/dist/src/mcp/tools/subsystems.mjs +2 -0
  125. package/dist/src/providers/capability-matrix.yaml +2 -1
  126. package/dist/src/providers/provider-definitions.js +2 -1
  127. package/dist/src/sessions/adapters/muse.js +336 -39
  128. package/dist/src/sessions/batch-import.js +7 -0
  129. package/dist/src/sessions/workspace-discovery.js +13 -2
  130. package/package.json +1 -1
  131. package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
  132. package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
  133. package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
  134. package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
  135. package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
  136. package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
  137. package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
  138. package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
  139. package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
  140. package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
  141. package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
  142. package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
  143. package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
  144. package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
  145. package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
  146. package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
  147. package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
  148. package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
  149. package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
  150. package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
  151. package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
  152. package/schemas/decision/D29Study.v1.schema.json +1903 -0
  153. package/schemas/decision/D29Study.v2.schema.json +1344 -0
  154. package/schemas/decision/D29Study.v3.schema.json +609 -0
  155. package/schemas/decision/D29Study.v4.schema.json +1133 -0
  156. package/schemas/decision/D29Study.v6.schema.json +1205 -0
  157. package/schemas/decision/D29Study.v7.schema.json +1471 -0
  158. package/schemas/decision/D29Study.v8.schema.json +671 -0
  159. package/schemas/decision/D29Study.v9.schema.json +778 -0
  160. package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
  161. package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
  162. package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
  163. package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
  164. package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
  165. package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
  166. package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
  167. package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
  168. package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
  169. package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
  170. package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
  171. package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
  172. package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
  173. package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
  174. package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
  175. package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
  176. package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
  177. package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
  178. package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
  179. package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
  180. package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
  181. package/schemas/decision/DecisionRuleset.schema.json +8 -2
  182. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
  183. package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
  184. package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
  185. package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
  186. package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
  187. package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
  188. package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
  189. package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
  190. package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
  191. package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
  192. package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
  193. package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
  194. package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
  195. package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
  196. package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
  197. package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
  198. package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
  199. package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
  200. package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
  201. package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
  202. package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
  203. package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
  204. package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
  205. package/tools/agents/deploy-agents.mjs +1 -1
  206. package/tools/agents/providers/base.mjs +40 -18
  207. package/tools/agents/providers/factory.mjs +10 -2
  208. package/tools/agents/providers/muse-hooks.mjs +1 -29
  209. package/tools/agents/providers/muse.mjs +6 -3
@@ -0,0 +1,847 @@
1
+ import { ContextBudgetManager } from '../metrics/context-budget.js';
2
+ import { sha256 } from './compile-cache/identity.js';
3
+ import { admitEntry } from './entry.js';
4
+ import { PairedDifferenceError, pairedBinaryDifferenceInterval, pairedMeanDifferenceBootstrap, pairedNonInferiority, } from './qualification/quality.js';
5
+ export const CONTEXT_PRUNING_PROTECTED_POLICY_VERSION = 'decision-context-protected/v1';
6
+ export const CONTEXT_PRUNING_CANDIDATE_SCHEMA = 'decision-context-candidate/v1';
7
+ export const CONTEXT_PRUNING_RECEIPT_SCHEMA = 'decision-context-pruning-receipt/v1';
8
+ export const CONTEXT_PRUNING_PREREGISTRATION_SCHEMA = 'decision-context-pruning-preregistration/v1';
9
+ export const CONTEXT_PRUNING_EVALUATION_REPORT_SCHEMA = 'decision-context-pruning-evaluation-report/v1';
10
+ /**
11
+ * Clock-skew allowance for `holdoutAccessedAt` attestations: the recorder that stamps holdout access and the
12
+ * evaluator that builds the report may disagree by up to five minutes. An attestation later than the
13
+ * evaluation clock plus this allowance is a future attestation and is rejected.
14
+ */
15
+ export const CONTEXT_PRUNING_HOLDOUT_CLOCK_SKEW_MS = 5 * 60 * 1_000;
16
+ export const CONTEXT_PRUNING_QUALITY_METRICS = [
17
+ 'downstream-task-success',
18
+ 'requirement-coverage',
19
+ 'factual-coverage',
20
+ 'citation-accuracy',
21
+ 'human-preference',
22
+ ];
23
+ export class ContextPruningError extends Error {
24
+ reason;
25
+ constructor(message, reason) {
26
+ super(message);
27
+ this.reason = reason;
28
+ this.name = 'ContextPruningError';
29
+ }
30
+ }
31
+ const SHA = /^sha256:[0-9a-f]{64}$/;
32
+ const SOURCE_TO_REASON = {
33
+ system: 'system-rule',
34
+ developer: 'developer-rule',
35
+ project: 'project-rule',
36
+ 'security-policy': 'security-policy',
37
+ user: 'current-user-requirement',
38
+ approval: 'action-approval',
39
+ blocker: 'unresolved-blocker',
40
+ provenance: 'provenance-required',
41
+ citation: 'citation-required',
42
+ artifact: 'artifact-pin',
43
+ 'test-evidence': 'test-gate-evidence',
44
+ 'open-decision': 'open-decision-evidence',
45
+ };
46
+ const VALID_SOURCE_KINDS = new Set([
47
+ 'system', 'developer', 'project', 'security-policy', 'user', 'approval', 'blocker',
48
+ 'provenance', 'citation', 'artifact', 'test-evidence', 'open-decision', 'retrieved',
49
+ 'memory', 'tool-output', 'ordinary',
50
+ ]);
51
+ const VALID_PROTECTED_REASONS = new Set([
52
+ 'system-rule', 'developer-rule', 'project-rule', 'security-policy', 'current-user-requirement',
53
+ 'explicit-mention', 'unresolved-blocker', 'action-approval', 'artifact-pin', 'version-pin',
54
+ 'digest-pin', 'provenance-required', 'citation-required', 'test-gate-evidence',
55
+ 'open-decision-evidence', 'protected-dependency', 'external-evaluation-denied', 'local-only',
56
+ 'restricted-data', 'legal-policy',
57
+ ]);
58
+ export function contextPruningDigest(value) {
59
+ return sha256(value);
60
+ }
61
+ /** Order-independent digest of the frozen evaluation pairs: sorted (pairId, slice) tuples. */
62
+ export function contextPruningPairSetDigest(pairs) {
63
+ const tuples = pairs.map(pair => [pair.pairId, pair.slice])
64
+ .sort(([a, sa], [b, sb]) => a < b ? -1 : a > b ? 1 : sa < sb ? -1 : sa > sb ? 1 : 0);
65
+ return contextPruningDigest({ schemaVersion: 'decision-context-pruning-pair-set/v1', pairs: tuples });
66
+ }
67
+ /** Identity of one pilot run over an ordered candidate set, optionally tied to an evaluation pair. */
68
+ export function contextPruningRunId(pairId, candidates) {
69
+ return contextPruningDigest({
70
+ schemaVersion: 'decision-context-pruning-run-id/v1',
71
+ pairId,
72
+ candidates: candidates.map(candidate => contextPruningDigest(candidate)),
73
+ });
74
+ }
75
+ /** Order-independent digest of a receipt set, for anchoring the evaluated receipts separately from the report. */
76
+ export function contextPruningReceiptSetDigest(receipts) {
77
+ return contextPruningDigest({
78
+ schemaVersion: 'decision-context-pruning-receipt-set/v1',
79
+ receipts: receipts.map(receipt => receipt.receiptDigest).sort(),
80
+ });
81
+ }
82
+ export function classifyContextPruningCandidate(candidate) {
83
+ validateContextPruningCandidate(candidate);
84
+ const reasons = new Set();
85
+ const mapped = SOURCE_TO_REASON[candidate.source.kind];
86
+ if (mapped)
87
+ reasons.add(mapped);
88
+ for (const hint of candidate.protectedHints)
89
+ reasons.add(hint);
90
+ if (candidate.dataPolicy.externalEvaluation === 'denied')
91
+ reasons.add('external-evaluation-denied');
92
+ if (candidate.dataPolicy.localOnly)
93
+ reasons.add('local-only');
94
+ if (candidate.dataPolicy.legalAction === 'requires-review')
95
+ reasons.add('legal-policy');
96
+ if (candidate.sensitivity === 'restricted')
97
+ reasons.add('restricted-data');
98
+ const sorted = [...reasons].sort();
99
+ const policy = {
100
+ version: CONTEXT_PRUNING_PROTECTED_POLICY_VERSION,
101
+ sourceRules: SOURCE_TO_REASON,
102
+ dataPolicy: ['externalEvaluation', 'localOnly', 'legalAction'],
103
+ sensitivity: ['restricted'],
104
+ protectedHints: [...VALID_PROTECTED_REASONS].sort(),
105
+ };
106
+ return {
107
+ schemaVersion: CONTEXT_PRUNING_PROTECTED_POLICY_VERSION,
108
+ itemId: candidate.itemId,
109
+ protected: sorted.length > 0,
110
+ reasons: sorted,
111
+ policyDigest: contextPruningDigest(policy),
112
+ };
113
+ }
114
+ export function classifyContextPruningCandidates(candidates) {
115
+ const byId = uniqueCandidates(candidates);
116
+ const classifications = new Map();
117
+ for (const candidate of candidates)
118
+ classifications.set(candidate.itemId, classifyContextPruningCandidate(candidate));
119
+ const protectedQueue = candidates.filter(candidate => classifications.get(candidate.itemId).protected)
120
+ .map(candidate => candidate.itemId);
121
+ for (let index = 0; index < protectedQueue.length; index++) {
122
+ const item = byId.get(protectedQueue[index]);
123
+ for (const dependencyId of item.dependencies) {
124
+ const dependency = byId.get(dependencyId);
125
+ if (!dependency)
126
+ throw new ContextPruningError(`unknown context dependency '${dependencyId}'`, 'semantic');
127
+ const current = classifications.get(dependencyId);
128
+ if (current.reasons.includes('protected-dependency'))
129
+ continue;
130
+ classifications.set(dependencyId, {
131
+ ...current,
132
+ protected: true,
133
+ reasons: [...new Set([...current.reasons, 'protected-dependency'])].sort(),
134
+ });
135
+ protectedQueue.push(dependencyId);
136
+ }
137
+ }
138
+ return classifications;
139
+ }
140
+ export function planContextPruningEvaluations(candidates, questions) {
141
+ return planContextPruningEvaluationRun(candidates, questions).jobs;
142
+ }
143
+ export function planContextPruningEvaluationRun(candidates, questions) {
144
+ const byId = uniqueCandidates(candidates);
145
+ const classifications = classifyContextPruningCandidates(candidates);
146
+ const exclusions = [...classifications.values()]
147
+ .filter(classification => classification.protected)
148
+ .map(classification => ({ itemId: classification.itemId, reasons: [...classification.reasons].sort() }));
149
+ const excluded = new Set(exclusions.map(item => item.itemId));
150
+ const groups = new Map();
151
+ for (const question of questions) {
152
+ if (!question.id || !question.itemId || !question.subject || !['relevance', 'disposition', 'omission-risk'].includes(question.kind)) {
153
+ throw new ContextPruningError('invalid context pruning question', 'schema');
154
+ }
155
+ const candidate = byId.get(question.itemId);
156
+ if (!candidate)
157
+ throw new ContextPruningError(`unknown context item '${question.itemId}'`, 'semantic');
158
+ if (excluded.has(candidate.itemId))
159
+ continue;
160
+ const subject = pruningSubject(candidate);
161
+ if (question.subject !== subject)
162
+ throw new ContextPruningError('question subject must be the context item subject', 'semantic');
163
+ const key = `${question.itemId}\0${question.batchKey ?? 'default'}`;
164
+ const group = groups.get(key) ?? [];
165
+ group.push(question);
166
+ groups.set(key, group);
167
+ }
168
+ const jobs = [...groups.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, group]) => {
169
+ const candidate = byId.get(group[0].itemId);
170
+ const subject = pruningSubject(candidate);
171
+ if (group.some(question => question.itemId !== candidate.itemId || question.subject !== subject)) {
172
+ throw new ContextPruningError('unrelated context chunks cannot share a native batch', 'semantic');
173
+ }
174
+ return {
175
+ subject,
176
+ itemId: candidate.itemId,
177
+ state: { candidateDigest: contextPruningDigest(candidate), itemId: candidate.itemId, locator: candidate.locator },
178
+ questions: [...group].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
179
+ contextQuestion: {
180
+ id: `context-pruning:${candidate.itemId}:${group[0].batchKey ?? 'default'}`,
181
+ subject,
182
+ compatibilityKey: `context-pruning:${candidate.itemId}:${group[0].batchKey ?? 'default'}`,
183
+ entry: { candidateDigest: contextPruningDigest(candidate), questions: group.map(question => question.kind).sort() },
184
+ },
185
+ };
186
+ });
187
+ return { jobs, exclusions };
188
+ }
189
+ export function applyContextPruningPilot(input) {
190
+ validatePolicy(input.policy);
191
+ if (input.pairId !== undefined && (typeof input.pairId !== 'string' || !input.pairId)) {
192
+ throw new ContextPruningError('pairId must be a non-empty string', 'invalid-input');
193
+ }
194
+ const pairId = input.pairId ?? null;
195
+ const now = input.now ?? (() => new Date().toISOString());
196
+ const baseline = input.candidates.map(candidate => candidate.itemId);
197
+ const evidence = new Map((input.evidence ?? []).map(item => [item.itemId, item]));
198
+ const disabledReason = input.monitoringRegression ? 'drift-or-monitoring-regression'
199
+ : input.policy.mode === 'disabled' ? 'configured-disabled' : null;
200
+ const mode = disabledReason ? 'deterministic-fallback' : input.policy.mode;
201
+ const classifications = classifyContextPruningCandidates(input.candidates);
202
+ const runId = contextPruningRunId(pairId, input.candidates);
203
+ const deterministicBaseline = input.budget === undefined ? null
204
+ : computeContextBudgetManagerBaseline(input.candidates, input.budget);
205
+ const deterministicDrops = new Set(deterministicBaseline?.droppedItemIds ?? []);
206
+ const receipts = input.candidates.map(candidate => {
207
+ validateContextPruningCandidate(candidate);
208
+ const classification = classifications.get(candidate.itemId);
209
+ const itemEvidence = evidence.get(candidate.itemId);
210
+ const fallbackAction = deterministicDrops.has(candidate.itemId) ? 'drop' : 'keep';
211
+ const decision = decideContextAction(candidate, classification, input.policy, itemEvidence, disabledReason, fallbackAction);
212
+ return buildContextPruningReceipt(candidate, classification, decision, now(), runId, pairId);
213
+ });
214
+ return {
215
+ schemaVersion: 'decision-context-pruning-run/v1',
216
+ runId,
217
+ pairId,
218
+ mode,
219
+ baselineItemIds: baseline,
220
+ // A distinct array: callers that mutate one list cannot silently alter the other.
221
+ downstreamItemIds: [...baseline],
222
+ receipts,
223
+ disabledReason,
224
+ deterministicBaseline,
225
+ };
226
+ }
227
+ /**
228
+ * Validates a receipt's shape and self-digest. When the run's candidate envelopes are supplied, it also
229
+ * re-derives the run identity and the protected-item classification and refuses any mismatch, so a
230
+ * relabelled receipt with a recomputed digest, or a receipt from another run, is rejected.
231
+ */
232
+ export function validateContextPruningReceipt(receipt, candidates) {
233
+ requirePlain(receipt, 'receipt');
234
+ if (receipt.schemaVersion !== CONTEXT_PRUNING_RECEIPT_SCHEMA || !receipt.itemId || !receipt.locator
235
+ || typeof receipt.runId !== 'string' || !SHA.test(receipt.runId)
236
+ || !(receipt.pairId === null || (typeof receipt.pairId === 'string' && receipt.pairId))
237
+ || !SHA.test(receipt.originalDigest) || !Number.isSafeInteger(receipt.originalTokenEstimate) || receipt.originalTokenEstimate < 0
238
+ || !['keep', 'drop', 'truncate', 'summarize'].includes(receipt.proposedAction)
239
+ || !['keep', 'drop', 'truncate', 'summarize'].includes(receipt.appliedAction)
240
+ || !(receipt.decisionReceiptDigest === null || SHA.test(receipt.decisionReceiptDigest))
241
+ || !receipt.reversibleReference || receipt.reversibleReference.originalDigest !== receipt.originalDigest
242
+ || !receipt.reversibleReference.locator || !receipt.reversibleReference.restorationPath
243
+ || !SHA.test(receipt.receiptDigest)) {
244
+ throw new ContextPruningError('invalid pruning receipt', 'schema');
245
+ }
246
+ if ((receipt.proposedAction === 'truncate' || receipt.proposedAction === 'summarize')
247
+ && receipt.reason === 'accepted-evidence' && receipt.reversibleReference.transformation === null) {
248
+ throw new ContextPruningError('transformed receipts require a reversible artifact reference', 'semantic');
249
+ }
250
+ if (receipt.reversibleReference.transformation !== null
251
+ && receipt.reversibleReference.transformation.sourceDigest !== receipt.originalDigest) {
252
+ throw new ContextPruningError('transformation source digest must match the original context item', 'semantic');
253
+ }
254
+ const { receiptDigest, ...payload } = receipt;
255
+ if (receiptDigest !== contextPruningDigest(payload))
256
+ throw new ContextPruningError('pruning receipt digest mismatch', 'semantic');
257
+ if (candidates !== undefined) {
258
+ const candidate = candidates.find(item => item.itemId === receipt.itemId);
259
+ if (!candidate || receipt.runId !== contextPruningRunId(receipt.pairId, candidates)
260
+ || receipt.originalDigest !== candidate.contentDigest || receipt.locator !== candidate.locator
261
+ || receipt.originalTokenEstimate !== candidate.tokenEstimate) {
262
+ throw new ContextPruningError('pruning receipt is not bound to the evaluated run', 'semantic');
263
+ }
264
+ const derived = classifyContextPruningCandidates(candidates).get(candidate.itemId);
265
+ if (contextPruningDigest(derived) !== contextPruningDigest(receipt.classification)) {
266
+ throw new ContextPruningError('pruning receipt classification does not match the candidate envelope', 'semantic');
267
+ }
268
+ // A protected item is never proposed for, or subjected to, a destructive action by this pilot.
269
+ if (derived.protected && (receipt.proposedAction !== 'keep' || receipt.appliedAction !== 'keep')) {
270
+ throw new ContextPruningError('pruning receipt classification does not match its protected action', 'semantic');
271
+ }
272
+ }
273
+ return receipt;
274
+ }
275
+ export function createContextPruningPreregistration(input) {
276
+ const payload = { schemaVersion: CONTEXT_PRUNING_PREREGISTRATION_SCHEMA, ...input };
277
+ return validateContextPruningPreregistration({ ...payload, digest: contextPruningDigest(payload) });
278
+ }
279
+ export function validateContextPruningPreregistration(value) {
280
+ requirePlain(value, 'preregistration');
281
+ const t = value.thresholds;
282
+ const ci = t?.confidenceInterval;
283
+ const metrics = Array.isArray(t?.qualityMetrics) ? t.qualityMetrics : [];
284
+ const slices = Array.isArray(t?.slices) ? t.slices : [];
285
+ const bootstrapTail = ci && Number.isSafeInteger(ci.bootstrapResamples) && Number.isSafeInteger(ci.levelBps)
286
+ ? Math.floor(ci.bootstrapResamples * (10_000 - ci.levelBps) / 20_000) : 0;
287
+ if (value.schemaVersion !== CONTEXT_PRUNING_PREREGISTRATION_SCHEMA || !value.id
288
+ || !Number.isFinite(Date.parse(value.registeredAt))
289
+ || typeof value.pairSetDigest !== 'string' || !SHA.test(value.pairSetDigest)
290
+ || !t || t.protectedRetentionBps !== 10_000 || !ci
291
+ // pairedBinaryDifferenceInterval accepts only levels strictly between 5000 and 9999.
292
+ || !Number.isSafeInteger(ci.levelBps) || ci.levelBps <= 5_000 || ci.levelBps >= 9_999
293
+ || !['newcombe-10', 'tango'].includes(ci.binaryMethod) || ci.boundedMethod !== 'percentile-bootstrap'
294
+ || !Number.isSafeInteger(ci.bootstrapSeed) || ci.bootstrapSeed < 0 || ci.bootstrapSeed > 0xffff_ffff
295
+ || !Number.isSafeInteger(ci.bootstrapResamples) || ci.bootstrapResamples > 1_000_000 || bootstrapTail < 5
296
+ || metrics.length !== CONTEXT_PRUNING_QUALITY_METRICS.length
297
+ || new Set(metrics.map(item => item?.metric)).size !== metrics.length
298
+ || metrics.some(item => !CONTEXT_PRUNING_QUALITY_METRICS.includes(item?.metric) || !['binary', 'bounded'].includes(item.scale))
299
+ || slices.length === 0 || new Set(slices).size !== slices.length
300
+ || slices.some(slice => typeof slice !== 'string' || !slice.trim())
301
+ || !Number.isSafeInteger(t.minimumOverallN) || t.minimumOverallN < 2
302
+ || !Number.isSafeInteger(t.minimumSliceN) || t.minimumSliceN < 1
303
+ || !Number.isSafeInteger(t.minimumZeroVarianceN) || t.minimumZeroVarianceN < 2
304
+ || !Number.isSafeInteger(t.minimumProtectedN) || t.minimumProtectedN < 2
305
+ || (t.powerRule !== null && (typeof t.powerRule !== 'string' || !t.powerRule.trim()))
306
+ || !Number.isSafeInteger(t.qualityNonInferiorityMarginBps) || t.qualityNonInferiorityMarginBps > 0
307
+ || t.qualityNonInferiorityMarginBps < -10_000
308
+ || !Number.isSafeInteger(t.positiveTotalTokenTarget) || t.positiveTotalTokenTarget <= 0
309
+ || !Number.isFinite(t.positiveTotalCostTargetUsd) || t.positiveTotalCostTargetUsd <= 0) {
310
+ throw new ContextPruningError('invalid context pruning preregistration', 'schema');
311
+ }
312
+ const { digest, ...payload } = value;
313
+ if (digest !== contextPruningDigest(payload))
314
+ throw new ContextPruningError('preregistration digest mismatch', 'semantic');
315
+ return value;
316
+ }
317
+ /**
318
+ * Builds the paired evaluation report. `trustedPreregistrationDigest` must come from a separately anchored
319
+ * record made before holdout access (as evaluatePreregisteredBinaryBenchmark requires): a caller-created
320
+ * preregistration cannot attest itself. A report without a recorded holdout access time cannot PROMOTE.
321
+ * A holdout access attested later than the evaluation clock plus CONTEXT_PRUNING_HOLDOUT_CLOCK_SKEW_MS
322
+ * is rejected as a future attestation.
323
+ */
324
+ export function buildContextPruningEvaluationReport(input) {
325
+ const preregistration = validateContextPruningPreregistration(input.preregistration);
326
+ if (typeof input.trustedPreregistrationDigest !== 'string' || !SHA.test(input.trustedPreregistrationDigest)
327
+ || preregistration.digest !== input.trustedPreregistrationDigest) {
328
+ throw new ContextPruningError('preregistration is not anchored to the trusted digest', 'semantic');
329
+ }
330
+ const holdoutAccessedAt = input.holdoutAccessedAt;
331
+ const holdoutAtMs = holdoutAccessedAt === null ? NaN : Date.parse(holdoutAccessedAt);
332
+ if (holdoutAccessedAt !== null && (typeof holdoutAccessedAt !== 'string' || !Number.isFinite(holdoutAtMs)
333
+ || holdoutAtMs <= Date.parse(preregistration.registeredAt))) {
334
+ throw new ContextPruningError('holdout access must be recorded after preregistration', 'semantic');
335
+ }
336
+ if (Number.isFinite(holdoutAtMs)) {
337
+ const evaluationNowMs = (input.evaluationNow ?? Date.now)();
338
+ // Fail closed: an unknown evaluation time cannot vouch for the attestation.
339
+ if (!Number.isFinite(evaluationNowMs) || holdoutAtMs - evaluationNowMs > CONTEXT_PRUNING_HOLDOUT_CLOCK_SKEW_MS) {
340
+ throw new ContextPruningError('future holdout access attestation is rejected', 'semantic');
341
+ }
342
+ }
343
+ validateIntegrity(input.integrity);
344
+ const thresholds = preregistration.thresholds;
345
+ validateMetrics(input.metrics, preregistration);
346
+ const metrics = input.metrics;
347
+ if (contextPruningPairSetDigest(metrics.pairs) !== preregistration.pairSetDigest) {
348
+ throw new ContextPruningError('evaluated pair set does not match the preregistered pair set', 'semantic');
349
+ }
350
+ const findings = new Set();
351
+ const missing = new Set(input.missingInputs ?? []);
352
+ if (holdoutAccessedAt === null)
353
+ findings.add('holdout-access-unrecorded');
354
+ const sliceSupport = Object.fromEntries(thresholds.slices.map(slice => [slice, 0]));
355
+ for (const pair of metrics.pairs)
356
+ sliceSupport[pair.slice] += 1;
357
+ if (metrics.pairs.length < thresholds.minimumOverallN)
358
+ findings.add('insufficient-overall-sample');
359
+ for (const slice of thresholds.slices) {
360
+ if (sliceSupport[slice] < thresholds.minimumSliceN)
361
+ findings.add(`insufficient-slice:${slice}`);
362
+ }
363
+ const protectedRetention = deriveProtectedRetention(input.pruningRuns, metrics.pairs, input.trustedReceiptSetDigest);
364
+ if (protectedRetention.bps === null)
365
+ findings.add('insufficient-protected-receipts');
366
+ else if (protectedRetention.bps !== metrics.protectedRetentionBps) {
367
+ throw new ContextPruningError('protected retention does not reconcile with the pruning receipts', 'semantic');
368
+ }
369
+ if (metrics.protectedRetentionBps < 10_000 || (protectedRetention.bps !== null && protectedRetention.bps < 10_000)) {
370
+ findings.add('protected-retention-breach');
371
+ }
372
+ // A full retention rate on too few protected items is not evidence: the count comes from validated receipts.
373
+ if (protectedRetention.protectedItems < thresholds.minimumProtectedN)
374
+ findings.add('insufficient-protected-sample');
375
+ const quality = thresholds.qualityMetrics.map(({ metric, scale }) => evaluateQualityMetric(metric, scale, metrics.quality.find(item => item.metric === metric), metrics.pairs, preregistration));
376
+ for (const result of quality) {
377
+ for (const slice of thresholds.slices) {
378
+ if (result.sliceSupport[slice] < thresholds.minimumSliceN)
379
+ findings.add(`insufficient-quality-slice:${result.metric}:${slice}`);
380
+ }
381
+ if (result.missingPairs > 0)
382
+ findings.add(`quality-outcomes-incomplete:${result.metric}`);
383
+ if (result.decision === 'not-non-inferior') {
384
+ findings.add('quality-non-inferiority-failed');
385
+ findings.add(`quality-non-inferiority-failed:${result.metric}`);
386
+ if (result.harm) {
387
+ findings.add('quality-harm');
388
+ findings.add(`quality-harm:${result.metric}`);
389
+ }
390
+ else {
391
+ findings.add(`quality-non-inferiority-inconclusive:${result.metric}`);
392
+ }
393
+ }
394
+ else if (result.decision === 'insufficient' && result.n === 0) {
395
+ findings.add(`quality-metric-missing:${result.metric}`);
396
+ }
397
+ else if (result.decision === 'insufficient' && result.n < thresholds.minimumOverallN) {
398
+ findings.add(`insufficient-quality-sample:${result.metric}`);
399
+ }
400
+ else if (result.decision === 'insufficient' && result.missingPairs === 0 && result.scale === 'bounded'
401
+ && result.interval !== null && result.interval.lowerBps === result.interval.upperBps
402
+ && result.n < thresholds.minimumZeroVarianceN) {
403
+ // A conclusive-sized read withheld for lack of variability evidence: a zero-width bounded
404
+ // interval below the preregistered zero-variance support.
405
+ findings.add(`insufficient-quality-support:${result.metric}`);
406
+ }
407
+ else if (result.decision === 'insufficient') {
408
+ // Interval construction failed, the verdict was unreadable, or a passing
409
+ // verdict was withheld for omitted pairs: adequate n is not evidence.
410
+ findings.add(`insufficient-quality-evidence:${result.metric}`);
411
+ }
412
+ }
413
+ const providerSavings = providerUsageSavings(metrics.providerUsage);
414
+ const estimatorSavings = { totalTokens: totalSavings(metrics.estimatorUsage, 'total') };
415
+ if (providerSavings.totalTokens < 0 || providerSavings.uncachedTokens < 0
416
+ || (providerSavings.costUsd !== null && providerSavings.costUsd < 0)) {
417
+ findings.add('negative-net-economics');
418
+ }
419
+ if (providerSavings.totalTokens < thresholds.positiveTotalTokenTarget
420
+ || providerSavings.uncachedTokens < thresholds.positiveTotalTokenTarget)
421
+ findings.add('provider-token-target-not-met');
422
+ if (estimatorSavings.totalTokens < thresholds.positiveTotalTokenTarget)
423
+ findings.add('estimator-token-target-not-met');
424
+ if (providerSavings.costUsd === null)
425
+ findings.add('provider-cost-unknown');
426
+ else if (providerSavings.costUsd < thresholds.positiveTotalCostTargetUsd)
427
+ findings.add('provider-cost-target-not-met');
428
+ if (input.integrity.integrity_state !== 'verified')
429
+ findings.add('integrity-not-verified');
430
+ const upstreamDecision = input.integrity.release_gate.decision;
431
+ if (upstreamDecision === 'ROLLBACK')
432
+ findings.add('upstream-rollback');
433
+ if (upstreamDecision === 'HOLD')
434
+ findings.add('upstream-hold');
435
+ for (const name of missing)
436
+ findings.add(`missing-input:${name}`);
437
+ // Automatic rollback triggers from the D26 rollout plan: protected miss, demonstrated quality harm, negative
438
+ // economics. An inconclusive non-inferiority result (no evidence of harm) holds instead.
439
+ const rollback = upstreamDecision === 'ROLLBACK' || findings.has('protected-retention-breach')
440
+ || findings.has('quality-harm') || findings.has('negative-net-economics');
441
+ const decision = rollback ? 'ROLLBACK' : upstreamDecision === 'HOLD' || findings.size > 0 ? 'HOLD' : 'PROMOTE';
442
+ const advisory = [...findings].some(item => item.startsWith('insufficient-') || item.startsWith('quality-metric-missing:')
443
+ || item.startsWith('missing-input:') || item.startsWith('quality-outcomes-incomplete:')
444
+ || item.startsWith('quality-non-inferiority-inconclusive:')
445
+ || item === 'holdout-access-unrecorded' || item === 'provider-cost-unknown')
446
+ ? 'INSUFFICIENT EVIDENCE' : null;
447
+ const payload = {
448
+ schemaVersion: CONTEXT_PRUNING_EVALUATION_REPORT_SCHEMA,
449
+ preregistration,
450
+ holdoutAccessedAt,
451
+ integrity: input.integrity,
452
+ metrics,
453
+ derived: { sampleN: metrics.pairs.length, sliceSupport, quality, providerSavings, estimatorSavings, protectedRetention },
454
+ missingInputs: [...missing].sort(),
455
+ findings: [...findings].sort(),
456
+ upstreamDecision,
457
+ decision,
458
+ advisory,
459
+ };
460
+ return { ...payload, digest: contextPruningDigest(payload) };
461
+ }
462
+ function evaluateQualityMetric(metric, scale, outcomes, recordedPairs, preregistration) {
463
+ const { confidenceInterval: ci, minimumOverallN, minimumZeroVarianceN, qualityNonInferiorityMarginBps, slices } = preregistration.thresholds;
464
+ const pairs = outcomes?.pairs ?? [];
465
+ const sliceOf = new Map(recordedPairs.map(pair => [pair.pairId, pair.slice]));
466
+ const sliceSupport = Object.fromEntries(slices.map(slice => [slice, 0]));
467
+ for (const pair of pairs)
468
+ sliceSupport[sliceOf.get(pair.pairId)] += 1;
469
+ // validateMetrics guarantees outcome pair IDs are a unique subset of the recorded pairs.
470
+ const missingPairs = recordedPairs.length - pairs.length;
471
+ const result = (interval, decision) => ({
472
+ metric, scale, n: pairs.length, missingPairs,
473
+ harm: decision === 'not-non-inferior' && interval !== null
474
+ && (interval.upperBps < 0 || interval.estimateBps < qualityNonInferiorityMarginBps),
475
+ sliceSupport, interval, decision,
476
+ });
477
+ if (pairs.length < minimumOverallN)
478
+ return result(null, 'insufficient');
479
+ let interval;
480
+ try {
481
+ if (scale === 'binary') {
482
+ const counts = { both: 0, candidateOnly: 0, baselineOnly: 0, neither: 0 };
483
+ for (const pair of pairs) {
484
+ if (pair.candidate === 1 && pair.baseline === 1)
485
+ counts.both++;
486
+ else if (pair.candidate === 1)
487
+ counts.candidateOnly++;
488
+ else if (pair.baseline === 1)
489
+ counts.baselineOnly++;
490
+ else
491
+ counts.neither++;
492
+ }
493
+ interval = pairedBinaryDifferenceInterval({ counts, levelBps: ci.levelBps, method: ci.binaryMethod });
494
+ }
495
+ else {
496
+ interval = pairedMeanDifferenceBootstrap({
497
+ differences: pairs.map(pair => pair.candidate - pair.baseline), levelBps: ci.levelBps,
498
+ seed: ci.bootstrapSeed, resamples: ci.bootstrapResamples, bounds: [-1, 1],
499
+ });
500
+ }
501
+ }
502
+ catch (error) {
503
+ if (error instanceof PairedDifferenceError)
504
+ return result(null, 'insufficient');
505
+ throw error;
506
+ }
507
+ const verdict = pairedNonInferiority({ interval, marginBps: qualityNonInferiorityMarginBps });
508
+ // A regression visible in the reported outcomes still fails; omitted pairs can only withhold a pass.
509
+ if (verdict.decision === 'non-inferior' && missingPairs > 0)
510
+ return result(interval, 'insufficient');
511
+ // A zero-width bounded interval carries no variability evidence: identical pairs at a small n are
512
+ // inconclusive, so a would-be pass needs the preregistered zero-variance support first. Only passes
513
+ // are withheld here; demonstrated harm still rolls back through the verdict below.
514
+ if (verdict.decision === 'non-inferior' && scale === 'bounded'
515
+ && interval.lowerBps === interval.upperBps && pairs.length < minimumZeroVarianceN) {
516
+ return result(interval, 'insufficient');
517
+ }
518
+ return result(interval, verdict.decision);
519
+ }
520
+ function deriveProtectedRetention(runs, pairs, trustedReceiptSetDigest) {
521
+ const pairIds = new Set(pairs.map(pair => pair.pairId));
522
+ if (!Array.isArray(runs) || runs.length !== pairIds.size || new Set(runs.map(run => run?.pairId)).size !== runs.length
523
+ || runs.some(run => !run || !pairIds.has(run.pairId) || !Array.isArray(run.candidates) || !Array.isArray(run.receipts))) {
524
+ throw new ContextPruningError('pruning runs must cover every evaluated pair exactly once', 'invalid-input');
525
+ }
526
+ const allReceipts = runs.flatMap(run => run.receipts);
527
+ if (typeof trustedReceiptSetDigest !== 'string' || !SHA.test(trustedReceiptSetDigest)
528
+ || contextPruningReceiptSetDigest(allReceipts) !== trustedReceiptSetDigest) {
529
+ throw new ContextPruningError('evaluated receipt set does not match the trusted receipt-set digest', 'semantic');
530
+ }
531
+ let protectedItems = 0;
532
+ let retained = 0;
533
+ for (const run of runs) {
534
+ uniqueCandidates(run.candidates);
535
+ const itemIds = run.receipts.map((receipt) => receipt?.itemId);
536
+ if (new Set(itemIds).size !== itemIds.length || itemIds.length !== run.candidates.length) {
537
+ throw new ContextPruningError('pruning receipts are not bound to the evaluated run candidates', 'semantic');
538
+ }
539
+ for (const receipt of run.receipts) {
540
+ if (receipt?.pairId !== run.pairId)
541
+ throw new ContextPruningError('pruning receipt is not bound to the evaluated run', 'semantic');
542
+ validateContextPruningReceipt(receipt, run.candidates);
543
+ if (!receipt.classification.protected)
544
+ continue;
545
+ protectedItems++;
546
+ if (receipt.proposedAction === 'keep' && receipt.appliedAction === 'keep')
547
+ retained++;
548
+ }
549
+ }
550
+ return { protectedItems, retained, bps: protectedItems === 0 ? null : Math.floor(retained * 10_000 / protectedItems) };
551
+ }
552
+ function pruningSubject(candidate) {
553
+ return `context-item:${candidate.itemId}`;
554
+ }
555
+ function decideContextAction(candidate, classification, policy, evidence, disabledReason, fallbackAction) {
556
+ if (classification.protected)
557
+ return { proposedAction: 'keep', appliedAction: 'keep',
558
+ mode: disabledReason ? 'deterministic-fallback' : policy.mode,
559
+ reason: 'protected-item', decisionReceiptDigest: null, transformation: null };
560
+ const fallback = (reason) => deterministicFallback(fallbackAction, reason);
561
+ if (disabledReason)
562
+ return fallback(disabledReason === 'configured-disabled' ? 'disabled' : 'drift-or-monitoring-regression');
563
+ if (policy.mode === 'disabled')
564
+ return fallback('disabled');
565
+ if (!evidence || evidence.itemId !== candidate.itemId || evidence.subject !== pruningSubject(candidate))
566
+ return fallback('invalid-evidence');
567
+ if (evidence.status === 'cancelled')
568
+ return fallback('cancelled');
569
+ if (evidence.status === 'incomplete')
570
+ return fallback('incomplete-state');
571
+ if (evidence.status === 'uncalibrated')
572
+ return fallback('uncalibrated');
573
+ if (evidence.status === 'drifted')
574
+ return fallback('drift-or-monitoring-regression');
575
+ if (evidence.status === 'failed')
576
+ return fallback('evaluation-failed');
577
+ if (policy.modelIdentityDigest === null || policy.calibrationDigest === null
578
+ || evidence.modelIdentityDigest === undefined || evidence.calibrationDigest === undefined
579
+ || evidence.modelIdentityDigest !== policy.modelIdentityDigest || evidence.calibrationDigest !== policy.calibrationDigest) {
580
+ return fallback('drift-or-monitoring-regression');
581
+ }
582
+ const confidenceBps = evidence.confidenceBps;
583
+ const marginBps = evidence.marginBps;
584
+ if (evidence.status !== 'success' || evidence.disagreement || evidence.proposedAction === undefined
585
+ || typeof confidenceBps !== 'number' || typeof marginBps !== 'number'
586
+ || !Number.isSafeInteger(confidenceBps) || !Number.isSafeInteger(marginBps)
587
+ || confidenceBps < policy.minimumConfidenceBps || marginBps < policy.minimumMarginBps
588
+ || evidence.calibrated !== true || !evidence.decisionReceiptDigest || !SHA.test(evidence.decisionReceiptDigest)) {
589
+ return fallback(evidence.status === 'uncertain' ? 'uncertain-evidence' : 'invalid-evidence');
590
+ }
591
+ const proposed = evidence.proposedAction;
592
+ if (!policy.allowedDestructiveActions.includes(proposed) && proposed !== 'keep')
593
+ return fallback('policy-denied-action');
594
+ if ((proposed === 'truncate' || proposed === 'summarize') && !validTransformation(evidence.transformation, proposed, candidate.contentDigest)) {
595
+ return fallback('unsupported-transformation');
596
+ }
597
+ return {
598
+ proposedAction: proposed,
599
+ appliedAction: 'keep',
600
+ mode: policy.mode,
601
+ reason: policy.mode === 'shadow' && proposed !== 'keep' ? 'shadow-only' : 'accepted-evidence',
602
+ decisionReceiptDigest: evidence.decisionReceiptDigest,
603
+ transformation: evidence.transformation ?? null,
604
+ };
605
+ }
606
+ function deterministicFallback(action, reason) {
607
+ return {
608
+ proposedAction: action,
609
+ appliedAction: 'keep',
610
+ mode: 'deterministic-fallback',
611
+ reason: reason === 'disabled' ? 'disabled'
612
+ : ['invalid-evidence', 'uncertain-evidence', 'uncalibrated', 'incomplete-state', 'cancelled', 'evaluation-failed',
613
+ 'drift-or-monitoring-regression', 'unsupported-transformation', 'policy-denied-action'].includes(reason)
614
+ ? reason : 'prior-deterministic-fallback',
615
+ decisionReceiptDigest: null,
616
+ transformation: null,
617
+ };
618
+ }
619
+ function buildContextPruningReceipt(candidate, classification, decision, createdAt, runId, pairId) {
620
+ const transformation = decision.transformation ?? null;
621
+ const payload = {
622
+ schemaVersion: CONTEXT_PRUNING_RECEIPT_SCHEMA,
623
+ runId,
624
+ pairId,
625
+ itemId: candidate.itemId,
626
+ locator: candidate.locator,
627
+ originalDigest: candidate.contentDigest,
628
+ originalTokenEstimate: candidate.tokenEstimate,
629
+ classification,
630
+ proposedAction: decision.proposedAction,
631
+ appliedAction: decision.appliedAction,
632
+ mode: decision.mode,
633
+ reason: decision.reason,
634
+ decisionReceiptDigest: decision.decisionReceiptDigest,
635
+ reversibleReference: {
636
+ locator: candidate.locator,
637
+ originalDigest: candidate.contentDigest,
638
+ restorationPath: candidate.locator,
639
+ transformation,
640
+ },
641
+ createdAt,
642
+ };
643
+ return validateContextPruningReceipt({ ...payload, receiptDigest: contextPruningDigest(payload) });
644
+ }
645
+ function validTransformation(value, action, sourceDigest) {
646
+ return Boolean(value && value.action === action && value.qualityCheck === 'passed'
647
+ && value.sourceDigest === sourceDigest && value.locator && SHA.test(value.digest));
648
+ }
649
+ export function computeContextBudgetManagerBaseline(candidates, config) {
650
+ const manager = new ContextBudgetManager(process.cwd(), config);
651
+ for (const candidate of candidates) {
652
+ validateContextPruningCandidate(candidate);
653
+ // ContextBudgetManager sizes item text itself; sizing the digest string would understate real content.
654
+ if (candidate.content === undefined) {
655
+ throw new ContextPruningError(`ContextBudgetManager baseline requires host-owned content for '${candidate.itemId}'`, 'invalid-input');
656
+ }
657
+ // The envelope priority is passed as ContextBudgetManager similarity; the manager derives its own priority.
658
+ manager.addItem(candidate.itemId, candidate.content, sourceTypeForBudget(candidate), candidate.priority);
659
+ }
660
+ const result = manager.degrade();
661
+ // ContextBudgetManager spares only source type 'system'; protected (and dependency-protected) items are
662
+ // retained here so the drop list can be applied as the fallback without removing protected context.
663
+ const classifications = classifyContextPruningCandidates(candidates);
664
+ const isProtected = (id) => classifications.get(id).protected;
665
+ const byId = new Map([...result.kept, ...result.dropped].map(item => [item.id, item]));
666
+ const droppedIds = new Set(result.dropped.filter(item => !isProtected(item.id)).map(item => item.id));
667
+ // Re-adding protected items can exceed the manager's target, so continue its own rule (ascending priority,
668
+ // never system items) over the remaining unprotected items until the target is met or none remain.
669
+ const target = Math.floor(manager.contextBudget * manager.getConfig().warningThreshold);
670
+ let current = candidates.reduce((sum, candidate) => sum + (droppedIds.has(candidate.itemId) ? 0 : byId.get(candidate.itemId).tokens), 0);
671
+ const remaining = candidates.map(candidate => byId.get(candidate.itemId))
672
+ .filter(item => !droppedIds.has(item.id) && !isProtected(item.id) && item.source.type !== 'system')
673
+ .sort((a, b) => a.priority - b.priority);
674
+ for (const item of remaining) {
675
+ if (current <= target)
676
+ break;
677
+ droppedIds.add(item.id);
678
+ current -= item.tokens;
679
+ }
680
+ const kept = candidates.map(candidate => byId.get(candidate.itemId)).filter(item => !droppedIds.has(item.id));
681
+ const dropped = candidates.map(candidate => byId.get(candidate.itemId)).filter(item => droppedIds.has(item.id));
682
+ return {
683
+ source: 'ContextBudgetManager',
684
+ usage: {
685
+ inputTokens: kept.reduce((sum, item) => sum + item.tokens, 0),
686
+ cachedInputTokens: null,
687
+ outputTokens: 0,
688
+ costUsd: null,
689
+ },
690
+ keptItemIds: kept.map(item => item.id).sort(),
691
+ droppedItemIds: dropped.map(item => item.id).sort(),
692
+ protectedRetainedItemIds: result.dropped.filter(item => isProtected(item.id)).map(item => item.id).sort(),
693
+ tokensFreed: dropped.reduce((sum, item) => sum + item.tokens, 0),
694
+ withinBudget: current <= target,
695
+ };
696
+ }
697
+ function validateContextPruningCandidate(candidate) {
698
+ requirePlain(candidate, 'candidate');
699
+ if (candidate.schemaVersion !== CONTEXT_PRUNING_CANDIDATE_SCHEMA || !candidate.itemId || !candidate.locator
700
+ || !SHA.test(candidate.contentDigest) || !candidate.source || !VALID_SOURCE_KINDS.has(candidate.source.kind) || !candidate.source.addedAt
701
+ || !Number.isFinite(Date.parse(candidate.source.addedAt))
702
+ || (candidate.content !== undefined && contextPruningDigest(candidate.content) !== candidate.contentDigest)
703
+ || !Number.isSafeInteger(candidate.tokenEstimate) || candidate.tokenEstimate < 0
704
+ || !Number.isFinite(candidate.priority) || candidate.priority < 0 || candidate.priority > 1
705
+ || !['verified', 'untrusted'].includes(candidate.trust)
706
+ || !['public', 'internal', 'confidential', 'restricted'].includes(candidate.sensitivity)
707
+ || !Array.isArray(candidate.dependencies) || new Set(candidate.dependencies).size !== candidate.dependencies.length
708
+ || candidate.dependencies.some(value => typeof value !== 'string' || !value)
709
+ || !Array.isArray(candidate.protectedHints) || new Set(candidate.protectedHints).size !== candidate.protectedHints.length
710
+ || candidate.protectedHints.some(reason => !VALID_PROTECTED_REASONS.has(reason))
711
+ || !candidate.taskSubject || !candidate.dataPolicy
712
+ || !['allowed', 'denied'].includes(candidate.dataPolicy.externalEvaluation)
713
+ || typeof candidate.dataPolicy.localOnly !== 'boolean'
714
+ || !['none', 'requires-review'].includes(candidate.dataPolicy.legalAction)) {
715
+ throw new ContextPruningError('invalid context candidate', 'schema');
716
+ }
717
+ try {
718
+ admitEntry(candidate);
719
+ }
720
+ catch {
721
+ throw new ContextPruningError('context candidate failed admission', 'schema');
722
+ }
723
+ }
724
+ function validatePolicy(policy) {
725
+ if (!['disabled', 'shadow', 'advisory'].includes(policy.mode)
726
+ || !Number.isSafeInteger(policy.minimumConfidenceBps) || policy.minimumConfidenceBps < 0 || policy.minimumConfidenceBps > 10_000
727
+ || !Number.isSafeInteger(policy.minimumMarginBps) || policy.minimumMarginBps < 0 || policy.minimumMarginBps > 10_000
728
+ || !Array.isArray(policy.allowedDestructiveActions)
729
+ || policy.allowedDestructiveActions.some(action => !['keep', 'drop', 'truncate', 'summarize'].includes(action))
730
+ || (policy.calibrationDigest !== null && !SHA.test(policy.calibrationDigest))
731
+ || (policy.modelIdentityDigest !== null && !SHA.test(policy.modelIdentityDigest))) {
732
+ throw new ContextPruningError('invalid context pruning policy', 'schema');
733
+ }
734
+ }
735
+ function validateIntegrity(integrity) {
736
+ if (!integrity || typeof integrity !== 'object' || !integrity.release_gate
737
+ || !['PROMOTE', 'HOLD', 'ROLLBACK'].includes(integrity.release_gate.decision)
738
+ || typeof integrity.integrity_state !== 'string') {
739
+ throw new ContextPruningError('invalid eval-integrity metadata', 'schema');
740
+ }
741
+ }
742
+ const isCount = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
743
+ function validateMetrics(metrics, preregistration) {
744
+ requirePlain(metrics, 'metrics');
745
+ const slices = new Set(preregistration.thresholds.slices);
746
+ const pairs = Array.isArray(metrics.pairs) ? metrics.pairs : null;
747
+ const pairIds = new Set(pairs?.map(pair => pair?.pairId));
748
+ if (!pairs || pairIds.size !== pairs.length
749
+ || pairs.some(pair => !pair || typeof pair.pairId !== 'string' || !pair.pairId || !slices.has(pair.slice))) {
750
+ throw new ContextPruningError('pair records must have unique IDs and preregistered slices', 'schema');
751
+ }
752
+ const scales = new Map(preregistration.thresholds.qualityMetrics.map(item => [item.metric, item.scale]));
753
+ const quality = Array.isArray(metrics.quality) ? metrics.quality : null;
754
+ if (!quality || new Set(quality.map(item => item?.metric)).size !== quality.length
755
+ || quality.some(item => !item || !scales.has(item.metric) || !Array.isArray(item.pairs))) {
756
+ throw new ContextPruningError('quality outcomes must name each preregistered metric at most once', 'schema');
757
+ }
758
+ for (const item of quality) {
759
+ const scale = scales.get(item.metric);
760
+ const seen = new Set();
761
+ for (const pair of item.pairs) {
762
+ const inRange = (value) => typeof value === 'number' && Number.isFinite(value)
763
+ && (scale === 'binary' ? value === 0 || value === 1 : value >= 0 && value <= 1);
764
+ if (!pair || !pairIds.has(pair.pairId) || seen.has(pair.pairId) || !inRange(pair.baseline) || !inRange(pair.candidate)) {
765
+ throw new ContextPruningError(`quality outcome for '${item.metric}' is out of range or not reconciled with the pair records`, 'schema');
766
+ }
767
+ seen.add(pair.pairId);
768
+ }
769
+ }
770
+ if (!isCount(metrics.protectedRetentionBps) || metrics.protectedRetentionBps > 10_000
771
+ || metrics.sharedStateAccounting !== 'not-applicable'
772
+ || !usageAccounting(metrics.providerUsage, 'provider') || !usageAccounting(metrics.estimatorUsage, 'estimator')
773
+ || !isCount(metrics.totalCalls) || !metrics.latencyMs
774
+ || [metrics.latencyMs.p50, metrics.latencyMs.p95, metrics.latencyMs.p99]
775
+ .some(value => value !== null && (!Number.isFinite(value) || value < 0))) {
776
+ throw new ContextPruningError('invalid context pruning metrics', 'schema');
777
+ }
778
+ const cache = metrics.promptCache;
779
+ for (const arm of ['baseline', 'prunedDownstream']) {
780
+ const reported = cache?.[arm];
781
+ const usage = metrics.providerUsage[arm];
782
+ // Cache credit is never a caller assertion: it must match provider-reported usage for the same arm.
783
+ if (!reported || !isCount(reported.hits) || !isCount(reported.misses) || !isCount(reported.cachedInputTokens)
784
+ || reported.cachedInputTokens !== usage.cachedInputTokens
785
+ || (reported.cachedInputTokens > 0 && reported.hits === 0)) {
786
+ throw new ContextPruningError(`prompt-cache usage for '${arm}' does not reconcile with provider usage`, 'semantic');
787
+ }
788
+ }
789
+ }
790
+ const USAGE_ARMS = ['baseline', 'prunedDownstream', 'decisionCalls', 'fallbackCalls', 'transformationCalls'];
791
+ function usage(value, source) {
792
+ return Boolean(value) && isCount(value.inputTokens) && isCount(value.outputTokens)
793
+ && (source === 'provider'
794
+ ? isCount(value.cachedInputTokens) && value.cachedInputTokens <= value.inputTokens
795
+ : value.cachedInputTokens === null)
796
+ && (value.costUsd === null || (Number.isFinite(value.costUsd) && value.costUsd >= 0));
797
+ }
798
+ function usageAccounting(value, source) {
799
+ return Boolean(value) && typeof value === 'object'
800
+ && Object.keys(value).sort().join() === [...USAGE_ARMS].sort().join()
801
+ && USAGE_ARMS.every(arm => usage(value[arm], source));
802
+ }
803
+ /** Baseline arm minus every pruned-arm call (downstream, decision, fallback, transformation). */
804
+ function totalSavings(value, basis) {
805
+ const tokens = (item) => item.inputTokens + item.outputTokens
806
+ - (basis === 'uncached' ? item.cachedInputTokens ?? 0 : 0);
807
+ return tokens(value.baseline) - USAGE_ARMS.filter(arm => arm !== 'baseline').reduce((sum, arm) => sum + tokens(value[arm]), 0);
808
+ }
809
+ function providerUsageSavings(value) {
810
+ const costs = USAGE_ARMS.map(arm => value[arm].costUsd);
811
+ return {
812
+ totalTokens: totalSavings(value, 'total'),
813
+ uncachedTokens: totalSavings(value, 'uncached'),
814
+ costUsd: costs.some(cost => cost === null) ? null
815
+ : value.baseline.costUsd - USAGE_ARMS.filter(arm => arm !== 'baseline').reduce((sum, arm) => sum + value[arm].costUsd, 0),
816
+ cachedInputTokensDelta: value.prunedDownstream.cachedInputTokens - value.baseline.cachedInputTokens,
817
+ };
818
+ }
819
+ function sourceTypeForBudget(candidate) {
820
+ if (candidate.source.kind === 'system' || candidate.source.kind === 'developer' || candidate.source.kind === 'project')
821
+ return 'system';
822
+ if (candidate.source.kind === 'user')
823
+ return 'user';
824
+ if (candidate.protectedHints.includes('explicit-mention'))
825
+ return 'at-mention';
826
+ return 'auto';
827
+ }
828
+ function uniqueCandidates(candidates) {
829
+ const byId = new Map(candidates.map(candidate => [candidate.itemId, candidate]));
830
+ if (byId.size !== candidates.length)
831
+ throw new ContextPruningError('candidate item IDs must be unique', 'semantic');
832
+ for (const candidate of candidates)
833
+ validateContextPruningCandidate(candidate);
834
+ for (const candidate of candidates) {
835
+ for (const dependencyId of candidate.dependencies) {
836
+ if (!byId.has(dependencyId))
837
+ throw new ContextPruningError(`unknown context dependency '${dependencyId}'`, 'semantic');
838
+ }
839
+ }
840
+ return byId;
841
+ }
842
+ function requirePlain(value, label) {
843
+ if (!value || typeof value !== 'object' || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
844
+ throw new ContextPruningError(`${label} must be a plain object`, 'schema');
845
+ }
846
+ }
847
+ //# sourceMappingURL=context-pruning.js.map