@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.
- package/agentic/code/providers/capability-matrix.yaml +2 -1
- package/dist/src/artifacts/discovery-shadow-evaluator.js +177 -0
- package/dist/src/artifacts/discovery-shadow.js +195 -0
- package/dist/src/artifacts/index-builder.js +47 -9
- package/dist/src/artifacts/query-engine.js +94 -1
- package/dist/src/artifacts/types.js +1 -0
- package/dist/src/cli/handlers/decision.js +177 -0
- package/dist/src/cli/handlers/gates.js +154 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/sessions.js +10 -7
- package/dist/src/cli/handlers/utilities.js +20 -0
- package/dist/src/config/aiwg-config.js +15 -2
- package/dist/src/decision/acceptance.js +25 -0
- package/dist/src/decision/adapters/jev.js +29 -3
- package/dist/src/decision/ask.js +352 -0
- package/dist/src/decision/calibration/registry.js +9 -0
- package/dist/src/decision/compose.js +74 -15
- package/dist/src/decision/context-live-promotion.js +344 -0
- package/dist/src/decision/context-live-qualification.js +591 -0
- package/dist/src/decision/context-pruning.js +847 -0
- package/dist/src/decision/driver.js +432 -0
- package/dist/src/decision/egress-live-qualification.js +874 -0
- package/dist/src/decision/ensemble/contract.js +64 -1
- package/dist/src/decision/ensemble/index.js +1 -0
- package/dist/src/decision/ensemble/report.js +8 -42
- package/dist/src/decision/ensemble/runtime.js +571 -0
- package/dist/src/decision/ensemble-study/artifacts.js +85 -0
- package/dist/src/decision/ensemble-study/calibration.js +443 -0
- package/dist/src/decision/ensemble-study/corpus.js +229 -0
- package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
- package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
- package/dist/src/decision/ensemble-study/multifact.js +241 -0
- package/dist/src/decision/ensemble-study/protocol.js +77 -0
- package/dist/src/decision/ensemble-study/score.js +462 -0
- package/dist/src/decision/ensemble-study/staged.js +87 -0
- package/dist/src/decision/ensemble-study/statistics.js +84 -0
- package/dist/src/decision/evaluate.js +227 -14
- package/dist/src/decision/export.js +4 -0
- package/dist/src/decision/feature-export.js +1074 -0
- package/dist/src/decision/graph-index.js +16 -0
- package/dist/src/decision/graph-live-qualification.js +1041 -0
- package/dist/src/decision/graph-live-workload.js +245 -0
- package/dist/src/decision/graph-skill-bridge.js +21 -3
- package/dist/src/decision/heldout/calibration.js +140 -0
- package/dist/src/decision/heldout/collector.js +430 -0
- package/dist/src/decision/heldout/contract.js +309 -0
- package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
- package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
- package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
- package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
- package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
- package/dist/src/decision/heldout/d29-pools.js +288 -0
- package/dist/src/decision/heldout/d29-v6.js +169 -0
- package/dist/src/decision/heldout/d29-v7.js +202 -0
- package/dist/src/decision/heldout/d29-v8.js +586 -0
- package/dist/src/decision/heldout/generator-registry.js +67 -0
- package/dist/src/decision/heldout/generators.js +696 -0
- package/dist/src/decision/heldout/index.js +8 -0
- package/dist/src/decision/heldout/journal.js +346 -0
- package/dist/src/decision/heldout/study-generators.js +64 -0
- package/dist/src/decision/heldout/types.js +2 -0
- package/dist/src/decision/host-policy.js +61 -0
- package/dist/src/decision/index.js +13 -0
- package/dist/src/decision/interop-profile.v1.json +112 -0
- package/dist/src/decision/interop.js +653 -0
- package/dist/src/decision/issue-triage/index.js +3 -0
- package/dist/src/decision/issue-triage/runtime.js +1069 -0
- package/dist/src/decision/issue-triage/types.js +2 -0
- package/dist/src/decision/jev-credentials.js +121 -0
- package/dist/src/decision/patterns/catalog.js +28 -8
- package/dist/src/decision/patterns/playground.js +28 -11
- package/dist/src/decision/patterns/specs.js +19 -10
- package/dist/src/decision/preprocessed-evidence.js +724 -0
- package/dist/src/decision/qualification/capture.js +8 -2
- package/dist/src/decision/qualification/gate-evidence.js +19 -4
- package/dist/src/decision/qualification/quality.js +57 -49
- package/dist/src/decision/qualification/release.js +76 -3
- package/dist/src/decision/qualification/runner.js +5 -2
- package/dist/src/decision/routing/contract.js +175 -0
- package/dist/src/decision/routing/index.js +5 -0
- package/dist/src/decision/routing/report.js +216 -0
- package/dist/src/decision/routing/runtime.js +685 -0
- package/dist/src/decision/routing/types.js +2 -0
- package/dist/src/decision/sdlc-screening.js +1157 -0
- package/dist/src/decision/sensitivity/comparative.js +154 -0
- package/dist/src/decision/sensitivity/contract.js +218 -0
- package/dist/src/decision/sensitivity/index.js +5 -0
- package/dist/src/decision/sensitivity/runtime.js +912 -0
- package/dist/src/decision/sensitivity/types.js +2 -0
- package/dist/src/decision/validate.js +26 -0
- package/dist/src/extensions/commands/definitions.js +38 -0
- package/dist/src/extensions/manifest.js +44 -0
- package/dist/src/gates/discovery.js +315 -0
- package/dist/src/gates/driver.js +388 -0
- package/dist/src/gates/evaluate.js +719 -0
- package/dist/src/gates/floors.js +255 -0
- package/dist/src/gates/index.js +17 -0
- package/dist/src/gates/providers/evidence.js +38 -0
- package/dist/src/gates/providers/index.js +21 -0
- package/dist/src/gates/providers/isolated-child-source.js +446 -0
- package/dist/src/gates/providers/loader.js +1069 -0
- package/dist/src/gates/providers/paired.js +52 -0
- package/dist/src/gates/providers/proportion.js +60 -0
- package/dist/src/gates/providers/registry.js +40 -0
- package/dist/src/gates/providers/scalar.js +41 -0
- package/dist/src/gates/providers/screening.js +53 -0
- package/dist/src/gates/providers/types.js +25 -0
- package/dist/src/gates/registry.js +1003 -0
- package/dist/src/gates/report.js +102 -0
- package/dist/src/gates/schema.js +56 -0
- package/dist/src/gates/stats/binomial.js +26 -0
- package/dist/src/gates/stats/bootstrap.js +62 -0
- package/dist/src/gates/stats/clopper-pearson.js +95 -0
- package/dist/src/gates/stats/digest.js +26 -0
- package/dist/src/gates/stats/error.js +5 -0
- package/dist/src/gates/stats/index.js +9 -0
- package/dist/src/gates/stats/normal.js +35 -0
- package/dist/src/gates/stats/paired.js +120 -0
- package/dist/src/gates/stats/splits.js +30 -0
- package/dist/src/gates/types.js +8 -0
- package/dist/src/governance/redaction.js +14 -4
- package/dist/src/mcp/muse-native-config.mjs +123 -0
- package/dist/src/mcp/tools/decision.mjs +228 -0
- package/dist/src/mcp/tools/subsystems.mjs +2 -0
- package/dist/src/providers/capability-matrix.yaml +2 -1
- package/dist/src/providers/provider-definitions.js +2 -1
- package/dist/src/sessions/adapters/muse.js +336 -39
- package/dist/src/sessions/batch-import.js +7 -0
- package/dist/src/sessions/workspace-discovery.js +13 -2
- package/package.json +1 -1
- package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
- package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
- package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
- package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
- package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
- package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
- package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
- package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
- package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
- package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
- package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
- package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
- package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
- package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
- package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
- package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
- package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
- package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
- package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
- package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
- package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
- package/schemas/decision/D29Study.v1.schema.json +1903 -0
- package/schemas/decision/D29Study.v2.schema.json +1344 -0
- package/schemas/decision/D29Study.v3.schema.json +609 -0
- package/schemas/decision/D29Study.v4.schema.json +1133 -0
- package/schemas/decision/D29Study.v6.schema.json +1205 -0
- package/schemas/decision/D29Study.v7.schema.json +1471 -0
- package/schemas/decision/D29Study.v8.schema.json +671 -0
- package/schemas/decision/D29Study.v9.schema.json +778 -0
- package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
- package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
- package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
- package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
- package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
- package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
- package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
- package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
- package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
- package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
- package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
- package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
- package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
- package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
- package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
- package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
- package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
- package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
- package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
- package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
- package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
- package/schemas/decision/DecisionRuleset.schema.json +8 -2
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
- package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
- package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
- package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
- package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
- package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
- package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
- package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
- package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
- package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
- package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
- package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
- package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
- package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
- package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
- package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
- package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
- package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
- package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
- package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
- package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
- package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
- package/tools/agents/deploy-agents.mjs +1 -1
- package/tools/agents/providers/base.mjs +40 -18
- package/tools/agents/providers/factory.mjs +10 -2
- package/tools/agents/providers/muse-hooks.mjs +1 -29
- package/tools/agents/providers/muse.mjs +6 -3
|
@@ -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
|