@aiwg/cli 2026.9.21 → 2026.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +2 -1
  2. package/dist/src/artifacts/cli.js +23 -18
  3. package/dist/src/artifacts/discovery-shadow-evaluator.js +177 -0
  4. package/dist/src/artifacts/discovery-shadow.js +195 -0
  5. package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -2
  6. package/dist/src/artifacts/index-builder.js +47 -9
  7. package/dist/src/artifacts/query-engine.js +108 -5
  8. package/dist/src/artifacts/types.js +1 -0
  9. package/dist/src/cli/handlers/decision.js +88 -0
  10. package/dist/src/cli/handlers/gates.js +154 -0
  11. package/dist/src/cli/handlers/index.js +5 -1
  12. package/dist/src/cli/handlers/sessions.js +10 -7
  13. package/dist/src/cli/handlers/utilities.js +20 -0
  14. package/dist/src/config/aiwg-config.js +15 -2
  15. package/dist/src/decision/acceptance.js +25 -0
  16. package/dist/src/decision/adapters/jev.js +24 -2
  17. package/dist/src/decision/calibration/registry.js +9 -0
  18. package/dist/src/decision/compose.js +74 -15
  19. package/dist/src/decision/context-live-promotion.js +344 -0
  20. package/dist/src/decision/context-live-qualification.js +591 -0
  21. package/dist/src/decision/context-pruning.js +847 -0
  22. package/dist/src/decision/driver.js +430 -0
  23. package/dist/src/decision/egress-live-qualification.js +874 -0
  24. package/dist/src/decision/ensemble/contract.js +64 -1
  25. package/dist/src/decision/ensemble/index.js +1 -0
  26. package/dist/src/decision/ensemble/report.js +8 -42
  27. package/dist/src/decision/ensemble/runtime.js +571 -0
  28. package/dist/src/decision/ensemble-study/artifacts.js +85 -0
  29. package/dist/src/decision/ensemble-study/calibration.js +443 -0
  30. package/dist/src/decision/ensemble-study/corpus.js +229 -0
  31. package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
  32. package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
  33. package/dist/src/decision/ensemble-study/multifact.js +241 -0
  34. package/dist/src/decision/ensemble-study/protocol.js +77 -0
  35. package/dist/src/decision/ensemble-study/score.js +462 -0
  36. package/dist/src/decision/ensemble-study/staged.js +87 -0
  37. package/dist/src/decision/ensemble-study/statistics.js +84 -0
  38. package/dist/src/decision/evaluate.js +227 -14
  39. package/dist/src/decision/export.js +4 -0
  40. package/dist/src/decision/feature-export.js +1074 -0
  41. package/dist/src/decision/graph-index.js +16 -0
  42. package/dist/src/decision/graph-live-qualification.js +1041 -0
  43. package/dist/src/decision/graph-live-workload.js +245 -0
  44. package/dist/src/decision/graph-skill-bridge.js +21 -3
  45. package/dist/src/decision/heldout/calibration.js +140 -0
  46. package/dist/src/decision/heldout/collector.js +430 -0
  47. package/dist/src/decision/heldout/contract.js +309 -0
  48. package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
  49. package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
  50. package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
  51. package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
  52. package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
  53. package/dist/src/decision/heldout/d29-pools.js +288 -0
  54. package/dist/src/decision/heldout/d29-v6.js +169 -0
  55. package/dist/src/decision/heldout/d29-v7.js +202 -0
  56. package/dist/src/decision/heldout/d29-v8.js +586 -0
  57. package/dist/src/decision/heldout/generator-registry.js +67 -0
  58. package/dist/src/decision/heldout/generators.js +696 -0
  59. package/dist/src/decision/heldout/index.js +8 -0
  60. package/dist/src/decision/heldout/journal.js +346 -0
  61. package/dist/src/decision/heldout/study-generators.js +64 -0
  62. package/dist/src/decision/heldout/types.js +2 -0
  63. package/dist/src/decision/host-policy.js +61 -0
  64. package/dist/src/decision/index.js +11 -0
  65. package/dist/src/decision/interop-profile.v1.json +112 -0
  66. package/dist/src/decision/interop.js +653 -0
  67. package/dist/src/decision/issue-triage/index.js +3 -0
  68. package/dist/src/decision/issue-triage/runtime.js +1069 -0
  69. package/dist/src/decision/issue-triage/types.js +2 -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/load.js +8 -1
  77. package/dist/src/decision/qualification/quality.js +57 -49
  78. package/dist/src/decision/qualification/release.js +76 -3
  79. package/dist/src/decision/qualification/runner.js +5 -2
  80. package/dist/src/decision/routing/contract.js +175 -0
  81. package/dist/src/decision/routing/index.js +5 -0
  82. package/dist/src/decision/routing/report.js +216 -0
  83. package/dist/src/decision/routing/runtime.js +685 -0
  84. package/dist/src/decision/routing/types.js +2 -0
  85. package/dist/src/decision/sdlc-screening.js +1157 -0
  86. package/dist/src/decision/sensitivity/comparative.js +154 -0
  87. package/dist/src/decision/sensitivity/contract.js +218 -0
  88. package/dist/src/decision/sensitivity/index.js +5 -0
  89. package/dist/src/decision/sensitivity/runtime.js +912 -0
  90. package/dist/src/decision/sensitivity/types.js +2 -0
  91. package/dist/src/decision/validate.js +26 -0
  92. package/dist/src/extensions/commands/definitions.js +38 -0
  93. package/dist/src/extensions/manifest.js +44 -0
  94. package/dist/src/gates/discovery.js +315 -0
  95. package/dist/src/gates/driver.js +388 -0
  96. package/dist/src/gates/evaluate.js +719 -0
  97. package/dist/src/gates/floors.js +255 -0
  98. package/dist/src/gates/index.js +17 -0
  99. package/dist/src/gates/providers/evidence.js +38 -0
  100. package/dist/src/gates/providers/index.js +21 -0
  101. package/dist/src/gates/providers/isolated-child-source.js +446 -0
  102. package/dist/src/gates/providers/loader.js +1069 -0
  103. package/dist/src/gates/providers/paired.js +52 -0
  104. package/dist/src/gates/providers/proportion.js +60 -0
  105. package/dist/src/gates/providers/registry.js +40 -0
  106. package/dist/src/gates/providers/scalar.js +41 -0
  107. package/dist/src/gates/providers/screening.js +53 -0
  108. package/dist/src/gates/providers/types.js +25 -0
  109. package/dist/src/gates/registry.js +1003 -0
  110. package/dist/src/gates/report.js +102 -0
  111. package/dist/src/gates/schema.js +56 -0
  112. package/dist/src/gates/stats/binomial.js +26 -0
  113. package/dist/src/gates/stats/bootstrap.js +62 -0
  114. package/dist/src/gates/stats/clopper-pearson.js +95 -0
  115. package/dist/src/gates/stats/digest.js +26 -0
  116. package/dist/src/gates/stats/error.js +5 -0
  117. package/dist/src/gates/stats/index.js +9 -0
  118. package/dist/src/gates/stats/normal.js +35 -0
  119. package/dist/src/gates/stats/paired.js +120 -0
  120. package/dist/src/gates/stats/splits.js +30 -0
  121. package/dist/src/gates/types.js +8 -0
  122. package/dist/src/governance/redaction.js +14 -4
  123. package/dist/src/mcp/muse-native-config.mjs +123 -0
  124. package/dist/src/mcp/tools/decision.mjs +188 -0
  125. package/dist/src/mcp/tools/subsystems.mjs +2 -0
  126. package/dist/src/providers/capability-matrix.yaml +2 -1
  127. package/dist/src/providers/provider-definitions.js +2 -1
  128. package/dist/src/sessions/adapters/muse.js +336 -39
  129. package/dist/src/sessions/batch-import.js +7 -0
  130. package/dist/src/sessions/workspace-discovery.js +13 -2
  131. package/package.json +1 -1
  132. package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
  133. package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
  134. package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
  135. package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
  136. package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
  137. package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
  138. package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
  139. package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
  140. package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
  141. package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
  142. package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
  143. package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
  144. package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
  145. package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
  146. package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
  147. package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
  148. package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
  149. package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
  150. package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
  151. package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
  152. package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
  153. package/schemas/decision/D29Study.v1.schema.json +1903 -0
  154. package/schemas/decision/D29Study.v2.schema.json +1344 -0
  155. package/schemas/decision/D29Study.v3.schema.json +609 -0
  156. package/schemas/decision/D29Study.v4.schema.json +1133 -0
  157. package/schemas/decision/D29Study.v6.schema.json +1205 -0
  158. package/schemas/decision/D29Study.v7.schema.json +1471 -0
  159. package/schemas/decision/D29Study.v8.schema.json +671 -0
  160. package/schemas/decision/D29Study.v9.schema.json +778 -0
  161. package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
  162. package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
  163. package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
  164. package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
  165. package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
  166. package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
  167. package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
  168. package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
  169. package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
  170. package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
  171. package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
  172. package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
  173. package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
  174. package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
  175. package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
  176. package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
  177. package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
  178. package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
  179. package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
  180. package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
  181. package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
  182. package/schemas/decision/DecisionRuleset.schema.json +8 -2
  183. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
  184. package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
  185. package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
  186. package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
  187. package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
  188. package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
  189. package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
  190. package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
  191. package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
  192. package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
  193. package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
  194. package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
  195. package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
  196. package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
  197. package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
  198. package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
  199. package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
  200. package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
  201. package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
  202. package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
  203. package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
  204. package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
  205. package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
  206. package/tools/agents/deploy-agents.mjs +1 -1
  207. package/tools/agents/providers/base.mjs +26 -7
  208. package/tools/agents/providers/factory.mjs +10 -2
  209. package/tools/agents/providers/muse-hooks.mjs +1 -29
  210. package/tools/agents/providers/muse.mjs +6 -3
@@ -0,0 +1,84 @@
1
+ import { pairedBinaryDifferenceInterval, pairedMeanDifferenceBootstrap, pairedNonInferiority, wilsonScoreInterval } from '../qualification/quality.js';
2
+ import { pairedDeltaFinding } from '../ensemble/contract.js';
3
+ import { D17_ANALYSIS, validateD17Analysis } from './protocol.js';
4
+ const mean = (values) => values.reduce((sum, n) => sum + n, 0) / values.length;
5
+ const p95 = (values) => [...values].sort((a, b) => a - b)[Math.ceil(values.length * 0.95) - 1];
6
+ /** Abstentions are false for correctness, but true events in their own paired endpoint. */
7
+ function interval(pairs, endpoint, levelBps) {
8
+ const counts = { both: 0, candidateOnly: 0, baselineOnly: 0, neither: 0 };
9
+ for (const pair of pairs) {
10
+ const c = endpoint(pair.challenger), b = endpoint(pair.champion);
11
+ counts[c ? b ? 'both' : 'candidateOnly' : b ? 'baselineOnly' : 'neither']++;
12
+ }
13
+ return pairedBinaryDifferenceInterval({ counts, levelBps, method: 'newcombe-10' });
14
+ }
15
+ /** Local descriptive statistics, never a release authorization. Missing endpoints withhold native reporting. */
16
+ export function d17Statistics(pairs, expectedIds, protocol = D17_ANALYSIS) {
17
+ validateD17Analysis(protocol);
18
+ const p = protocol;
19
+ if (new Set(expectedIds).size !== expectedIds.length || new Set(pairs.map(row => row.id)).size !== pairs.length
20
+ || pairs.some(row => !expectedIds.includes(row.id) || !p.slices.some(slice => slice === row.slice)))
21
+ throw new Error('D17 pair membership');
22
+ for (const row of pairs)
23
+ for (const arm of [row.champion, row.challenger]) {
24
+ if (typeof arm.accepted !== 'boolean' || typeof arm.correct !== 'boolean' || !arm.accepted && arm.correct
25
+ || [arm.probability, arm.brier].some(n => typeof n !== 'number' || !Number.isFinite(n) || n < 0 || n > 1)
26
+ || [arm.latencyMs, arm.reservedCostMicros].some(n => typeof n !== 'number' || !Number.isFinite(n) || n < 0)
27
+ || arm.tokens !== null && (!Number.isSafeInteger(arm.tokens) || arm.tokens < 0))
28
+ throw new Error('D17 unknown or invalid measurement');
29
+ }
30
+ const findings = [];
31
+ if (pairs.length !== expectedIds.length || pairs.length < p.minimumPairs)
32
+ findings.push('incomplete-pairs');
33
+ const slices = p.slices.map(slice => {
34
+ const rows = pairs.filter(row => row.slice === slice);
35
+ const delta = rows.length ? mean(rows.map(row => Number(row.challenger.correct) - Number(row.champion.correct))) : null;
36
+ if (rows.length < p.minimumPerSlice)
37
+ findings.push(`slice-insufficient:${slice}`);
38
+ return { slice, n: rows.length, qualityDelta: delta, harm: delta !== null && delta < 0, descriptiveOnly: true };
39
+ });
40
+ const metrics = (arm) => ({ quality: Number(arm.correct), calibration: arm.brier,
41
+ 'risk-coverage': Number(arm.accepted && !arm.correct), abstention: Number(!arm.accepted), latency: arm.latencyMs,
42
+ tokens: arm.tokens, cost: arm.reservedCostMicros, slice: Number(arm.correct) });
43
+ const pairedDeltas = p.native.map(threshold => {
44
+ const complete = pairs.filter(row => metrics(row.champion)[threshold.metric] !== null && metrics(row.challenger)[threshold.metric] !== null);
45
+ const item = { metric: threshold.metric, pairs: complete.length, delta: complete.length ? mean(complete.map(row => metrics(row.challenger)[threshold.metric] - metrics(row.champion)[threshold.metric])) : null };
46
+ const finding = pairedDeltaFinding(threshold, item);
47
+ if (finding)
48
+ findings.push(finding);
49
+ return item;
50
+ });
51
+ const quality = pairs.length ? interval(pairs, arm => arm.correct, p.interval.levelBps) : null;
52
+ const abstention = pairs.length ? interval(pairs, arm => !arm.accepted, p.abstention.levelBps) : null;
53
+ const brier = pairs.length >= 2 ? pairedMeanDifferenceBootstrap({ differences: pairs.map(row => row.challenger.brier - row.champion.brier),
54
+ levelBps: p.brier.levelBps, seed: p.brier.seed, resamples: p.brier.resamples, bounds: [-1, 1] }) : null;
55
+ const accepted = pairs.filter(row => row.challenger.accepted), errors = accepted.filter(row => !row.challenger.correct).length;
56
+ const risk = accepted.length ? wilsonScoreInterval({ events: errors, n: accepted.length, levelBps: p.risk.levelBps }) : null;
57
+ const coverage = pairs.length ? wilsonScoreInterval({ events: accepted.length, n: pairs.length, levelBps: p.risk.levelBps }) : null;
58
+ const acceptedErrorUpperBps = risk ? Math.ceil(risk[1] * 10000) : null;
59
+ const coverageLowerBps = coverage ? Math.floor(coverage[0] * 10000) : null;
60
+ const p95LatencyIncreaseMs = pairs.length ? p95(pairs.map(row => row.challenger.latencyMs)) - p95(pairs.map(row => row.champion.latencyMs)) : null;
61
+ if (!quality || pairedNonInferiority({ interval: quality, marginBps: p.interval.nonInferiorityMarginBps }).decision !== 'non-inferior')
62
+ findings.push('quality-ni');
63
+ if (!brier || brier.upperBps > p.brier.maximumUpperBps)
64
+ findings.push('brier-upper');
65
+ if (!abstention || abstention.upperBps > p.abstention.maximumUpperBps)
66
+ findings.push('abstention-upper');
67
+ if (acceptedErrorUpperBps === null || acceptedErrorUpperBps > p.risk.maximumAcceptedErrorUpperBps)
68
+ findings.push('accepted-risk-upper');
69
+ if (coverageLowerBps === null || coverageLowerBps < p.risk.minimumCoverageLowerBps)
70
+ findings.push('coverage-lower');
71
+ if (p95LatencyIncreaseMs === null || p95LatencyIncreaseMs > p.maximumP95LatencyIncreaseMs)
72
+ findings.push('latency-p95');
73
+ const observed = new Set(pairs.map(row => row.id));
74
+ return { sampleN: pairs.length, missingIds: expectedIds.filter(id => !observed.has(id)), slices, pairedDeltas,
75
+ quality, brier, abstention, acceptedN: accepted.length, acceptedErrors: errors, acceptedErrorUpperBps, coverageLowerBps,
76
+ p95LatencyIncreaseMs, latencyDifferencesMs: pairs.map(row => row.challenger.latencyMs - row.champion.latencyMs),
77
+ netSavingsMicros: pairs.reduce((sum, row) => sum + row.champion.reservedCostMicros - row.challenger.reservedCostMicros, 0),
78
+ costBasis: 'nonrefundable-reservations-including-failed-attempts',
79
+ benefitSupported: quality !== null && quality.lowerBps > p.interval.benefitLowerExclusiveBps,
80
+ findings: findings.sort(), statisticalGate: findings.length ? 'HOLD' : 'pass',
81
+ worstCaseFailureAsError: { n: expectedIds.length, championCorrect: pairs.filter(row => row.champion.correct).length,
82
+ challengerCorrect: pairs.filter(row => row.challenger.correct).length, missingPairsCountedAsErrors: expectedIds.length - pairs.length } };
83
+ }
84
+ //# sourceMappingURL=statistics.js.map
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { canonicalJson } from '../security/artifact-trust.js';
3
3
  import { composeRuleset } from './compose.js';
4
- import { applyPrimitiveAcceptance, validatePrimitiveAcceptancePolicy } from './acceptance.js';
4
+ import { applyTargetAcceptance, validatePrimitiveAcceptancePolicy } from './acceptance.js';
5
5
  import { DecisionPreDispatchError, decisionInvocationFingerprint, nextReceipt } from './receipts.js';
6
6
  import { admitEntry, EntryAdmissionError } from './entry.js';
7
7
  import { correlateAtomicBatch, decisionBatchQuestionId, planNativeDecisionBatches } from './batch.js';
@@ -16,6 +16,7 @@ import { providerPrefixEvidence } from './compile-cache/prefix.js';
16
16
  import { digestCachedResult, RESULT_CACHE_KEY_VERSION } from './result-cache/index.js';
17
17
  import { DecisionProjectionError, normalizeProjectionOrigin, projectDecisionState, } from './projection.js';
18
18
  import { DecisionRuntimeTrace, runtimeTraceOf } from './telemetry/runtime.js';
19
+ import { gatePreprocessedEvidenceDispatch, isEmptyPreprocessingLineage, isWellFormedPreprocessingLineage, PREPROCESSING_LOCAL_ORIGIN, } from './preprocessed-evidence.js';
19
20
  import { assertContextQualified } from './context-qualification.js';
20
21
  import { assertContextPlanCurrent, ContextPlanError, planDecisionContext, recordContextActualUsage, } from './context-plan.js';
21
22
  import { DECISION_API_VERSION, DECISION_API_VERSION_STRUCTURED } from './types.js';
@@ -34,6 +35,10 @@ export async function evaluateDecisionRuleset(request) {
34
35
  /** A cache hit is a historical result accompanied by a NEW caller receipt, not a fresh adapter attempt. */
35
36
  async function evaluateWithResultCache(request) {
36
37
  const config = request.resultCache;
38
+ // A cache hit would bypass the D24 pre-dispatch lineage gate, so lineage is never cached.
39
+ if (preprocessingGateApplies(request)) {
40
+ throw new Error('Result cache does not support preprocessing lineage');
41
+ }
37
42
  if (!config.policy.sideEffectFree || !request.receiptStore || !request.calibrationPin || !request.policyPin
38
43
  || !config.recordCallerReceipt) {
39
44
  throw new Error('Result cache requires side-effect-free policy, durable receipts, policy and calibration pins');
@@ -163,10 +168,15 @@ async function evaluateDecisionRulesetUngated(request) {
163
168
  rulesetPin = artifactPin(request.ruleset);
164
169
  bindingPin = artifactPin(request.binding);
165
170
  // Pin and execute immutable snapshots; adapters receive separate per-attempt copies.
171
+ // D24 lineage and verification are host-supplied too: the gate and the recorded
172
+ // result share one snapshot, so caller-owned mutation during evaluation cannot
173
+ // change the verdict or the recorded lineage.
166
174
  request = {
167
175
  ...request,
168
176
  ruleset: structuredClone(request.ruleset), binding: structuredClone(request.binding),
169
177
  definitions: structuredClone(request.definitions), input: structuredClone(request.input),
178
+ preprocessingLineage: snapshotPreprocessingLineage(request.preprocessingLineage),
179
+ preprocessingVerification: snapshotPreprocessingVerification(request.preprocessingVerification),
170
180
  };
171
181
  base = resultBase(request, rulesetPin, bindingPin);
172
182
  validateBinding(request.binding, request.ruleset);
@@ -195,6 +205,19 @@ async function evaluateDecisionRulesetUngated(request) {
195
205
  if (contextPlan)
196
206
  base = withRulesetContext(base, contextPlan, []);
197
207
  runtimeTraceOf(request)?.validated(false);
208
+ // D24: stored preprocessing lineage is gated before receipts, credentials and transport.
209
+ if (preprocessingGateApplies(request)) {
210
+ const gate = gatePreprocessedEvidenceDispatch(request.preprocessingLineage, request.preprocessingVerification, resolved.flatMap(item => request.binding.spec.evaluations[item.alias].targets
211
+ .map(target => preprocessingDestination(request, item.alias, target))), request.input, request.ruleset.spec.evaluations.map(evaluation => evaluation.inputPointer));
212
+ base = { ...base, spec: { ...base.spec, preprocessingLineage: base.spec.preprocessingLineage
213
+ ? { ...base.spec.preprocessingLineage, dispatchGate: gate }
214
+ : { schemaVersion: 'decision-preprocessing-lineage/v1', status: 'review', references: [], traces: [], dispatchGate: gate } } };
215
+ if (gate.outcome === 'refused')
216
+ return failureResult(base, 'data-boundary-denied');
217
+ if (gate.outcome === 'review') {
218
+ return { ...base, spec: { ...base.spec, status: 'review', reason: 'insufficient-information', matchedRules: [], evaluations: {} } };
219
+ }
220
+ }
198
221
  const batchPolicy = request.batchReceipts;
199
222
  if (batchPolicy?.unknownCostBound && (!Number.isSafeInteger(batchPolicy.unknownCostBound.upperBoundMicros)
200
223
  || batchPolicy.unknownCostBound.upperBoundMicros < 0 || !batchPolicy.unknownCostBound.policyId
@@ -205,6 +228,14 @@ async function evaluateDecisionRulesetUngated(request) {
205
228
  || batchPolicy.unknownCostBound.upperBoundMicros > batchPolicy.maxCostMicros)) {
206
229
  return failureResult(base, 'budget-exhausted');
207
230
  }
231
+ // #2797: a context-planned invocation clones the full plan into every evaluation, so the
232
+ // completed result can exceed entry admission after provider work. Preflight the worst-case
233
+ // completion envelope here — before receipts, credentials, admission control and transport —
234
+ // and reject without dispatch when no completion could be admissible.
235
+ if (contextPlan && !contextResultEnvelopeFits(contextPlan, resolved, request.binding)) {
236
+ const rejected = failureResult(base, 'invalid-input');
237
+ return { ...rejected, spec: { ...rejected.spec, contextFailure: { schemaVersion: 'decision-context-failure/v1', reason: 'invalid-input' } } };
238
+ }
208
239
  const fingerprint = decisionInvocationFingerprint({
209
240
  invocationId: request.invocationId,
210
241
  value: request.input,
@@ -437,10 +468,24 @@ async function evaluateDecisionRulesetUngated(request) {
437
468
  const batchSpan = runtimeTraceOf(request)?.startBatch(plan.candidates.map(candidate => candidate.alias), plan.groupId, durableReceipt ? durableReceipt.attempts.length + 1 : dispatchCount);
438
469
  if (batchSpan)
439
470
  batchSpans.push(batchSpan);
471
+ // A request owns its usage even when it fails before an answer is available.
472
+ // Durable mode already has that owner in its batch receipt.
473
+ const usageOwner = !durableReceipt ? {
474
+ groupId: plan.groupId, ordinal: dispatchCount, questionIds: [...questionIds],
475
+ usage: { inputTokens: null, outputTokens: null, costUsd: null },
476
+ requestId: null,
477
+ } : undefined;
478
+ if (usageOwner)
479
+ (base.spec.batchRequests ??= []).push(usageOwner);
440
480
  const response = await adapter.evaluateMany({ decisionSubject: plan.decisionSubject,
441
481
  requests: preparedRequests, ...(batchSpan ? { traceContext: batchSpan.traceContext } : {}) });
442
482
  if (batchSpan)
443
483
  batchSpan.response = response;
484
+ if (usageOwner) {
485
+ usageOwner.usage = structuredClone(response.sharedUsage);
486
+ const ids = uniqueNonNull(response.answers.map(answer => answer.observation.requestId));
487
+ usageOwner.requestId = ids.length === 1 && validOpaqueRequestId(ids[0]) ? ids[0] : null;
488
+ }
444
489
  if (contextPlan && response.sharedUsage.inputTokens !== null) {
445
490
  recordRuntimeContextUsage(contextPlan, questionIds, response.sharedUsage.inputTokens, contextUsage);
446
491
  }
@@ -581,6 +626,8 @@ async function evaluateDecisionRulesetUngated(request) {
581
626
  }
582
627
  }
583
628
  runtimeTraceOf(request)?.finishBatch(batchSpans, durableReceipt);
629
+ // Both durable and result-owned native requests keep accounting off answers.
630
+ observations = resultOnlyBatchObservations(observations);
584
631
  plan.candidates.forEach((candidate, index) => {
585
632
  const item = resolved.find(value => value.alias === candidate.alias);
586
633
  const evidence = { mode: 'native', groupId: plan.groupId, questionId: questionIds[index] };
@@ -920,6 +967,65 @@ async function invokeWithDeadline(context, adapter, target, deadlineEpochMs, ord
920
967
  }
921
968
  /** Requests whose projection evidence prohibited automatic action (allowed incomplete context). */
922
969
  const projectionBlockedAutomaticAction = new WeakSet();
970
+ /**
971
+ * D24 snapshot of host-supplied lineage. Uncloneable content cannot be well-formed
972
+ * lineage, so it is passed through for the gate to refuse exactly as before.
973
+ */
974
+ function snapshotPreprocessingLineage(lineage) {
975
+ if (lineage === undefined)
976
+ return undefined;
977
+ try {
978
+ return structuredClone(lineage);
979
+ }
980
+ catch {
981
+ return lineage;
982
+ }
983
+ }
984
+ /**
985
+ * D24 snapshot of host-supplied verification. Manifests, lifecycle and bindings are
986
+ * data and snapshotted with the lineage; `now` is host code and carried by reference
987
+ * like the other request callbacks. Uncloneable data is passed through for the gate
988
+ * to judge.
989
+ */
990
+ function snapshotPreprocessingVerification(verification) {
991
+ if (verification === undefined)
992
+ return undefined;
993
+ const { now, ...rest } = verification;
994
+ try {
995
+ const snapshot = structuredClone(rest);
996
+ return now === undefined ? snapshot : { ...snapshot, now };
997
+ }
998
+ catch {
999
+ return verification;
1000
+ }
1001
+ }
1002
+ /**
1003
+ * D24 applies when lineage is supplied or the host supplied verification expecting lineage.
1004
+ * Requests with neither keep the text-native path unchanged.
1005
+ */
1006
+ function preprocessingGateApplies(request) {
1007
+ return !isEmptyPreprocessingLineage(request.preprocessingLineage) || request.preprocessingVerification !== undefined;
1008
+ }
1009
+ /**
1010
+ * The D10 destination a target would send derived text to: the projection policy's provider and
1011
+ * origin, or the local no-egress destination when no projection is configured (projection then
1012
+ * denies any network adapter before credentials). `null` means the destination cannot be bound.
1013
+ */
1014
+ function preprocessingDestination(request, alias, target) {
1015
+ const projection = request.projection;
1016
+ if (!projection)
1017
+ return { provider: target.adapter, origin: PREPROCESSING_LOCAL_ORIGIN };
1018
+ if (isUnprojectedLocalOptOut(projection))
1019
+ return null;
1020
+ try {
1021
+ const policy = projection.resolve({ alias, target: structuredClone(target) });
1022
+ return typeof policy?.provider === 'string' && typeof policy.origin === 'string'
1023
+ ? { provider: policy.provider, origin: policy.origin } : null;
1024
+ }
1025
+ catch {
1026
+ return null;
1027
+ }
1028
+ }
923
1029
  function isUnprojectedLocalOptOut(projection) {
924
1030
  return !!projection && projection.mode === 'unprojected-local';
925
1031
  }
@@ -1062,17 +1168,7 @@ function normalizeObservation(definition, target, observation) {
1062
1168
  validateDecisionValue(definition, observation.value);
1063
1169
  if (observation.uncertainty?.distribution)
1064
1170
  validateDistribution(definition, observation.uncertainty.distribution);
1065
- if (target.acceptance.mode === 'typed-value')
1066
- return observation;
1067
- if (target.acceptance.mode === 'primitive-policy')
1068
- return applyPrimitiveAcceptance(definition, target.acceptance, observation);
1069
- if (!observation.uncertainty || observation.uncertainty.confidence === null)
1070
- return observationFailure('missing-confidence', 'abstained', observation);
1071
- if (observation.uncertainty.profile !== target.acceptance.profile)
1072
- return observationFailure('confidence-profile-mismatch', 'abstained', observation);
1073
- if (observation.uncertainty.confidence * 10_000 < target.acceptance.minimumBps)
1074
- return observationFailure('low-confidence', 'abstained', observation);
1075
- return observation;
1171
+ return applyTargetAcceptance(definition, target, observation);
1076
1172
  }
1077
1173
  function normalizeObservationForRuntime(input) {
1078
1174
  let observation = input.observation;
@@ -1143,7 +1239,10 @@ function resultBase(request, ruleset, binding) {
1143
1239
  apiVersion: resultVersion(request), kind: 'RulesetResult',
1144
1240
  metadata: { id: request.invocationId, version: '1.0.0', description: `Ruleset result for ${request.ruleset.metadata.id}` },
1145
1241
  spec: { ruleset, binding, runId: request.runId, invocationId: request.invocationId, status: 'error', reason: 'evaluation-failed', matchedRules: [], evaluations: {},
1146
- ...(isUnprojectedLocalOptOut(request.projection) ? { projection: { mode: 'unprojected-local', authority: 'host' } } : {}) },
1242
+ ...(isUnprojectedLocalOptOut(request.projection) ? { projection: { mode: 'unprojected-local', authority: 'host' } } : {}),
1243
+ // Only a lineage the D24 gate can read is copied; a malformed one is recorded by the gate as refused.
1244
+ ...(!isEmptyPreprocessingLineage(request.preprocessingLineage) && isWellFormedPreprocessingLineage(request.preprocessingLineage)
1245
+ ? { preprocessingLineage: structuredClone(request.preprocessingLineage) } : {}) },
1147
1246
  };
1148
1247
  }
1149
1248
  /**
@@ -1158,7 +1257,7 @@ function resultVersion(request) {
1158
1257
  || request.ruleset.apiVersion === DECISION_API_VERSION_STRUCTURED || request.binding.apiVersion === DECISION_API_VERSION_STRUCTURED
1159
1258
  || Object.values(request.definitions).some(definition => definition.apiVersion === DECISION_API_VERSION_STRUCTURED)
1160
1259
  || request.batching || request.batchReceipts || request.scheduler?.enabled || request.providerPrefix || request.context
1161
- || request.resultCache?.policy.enabled) {
1260
+ || request.resultCache?.policy.enabled || preprocessingGateApplies(request)) {
1162
1261
  return DECISION_API_VERSION_STRUCTURED;
1163
1262
  }
1164
1263
  return DECISION_API_VERSION;
@@ -1259,6 +1358,120 @@ function contextPartitionedBatchPlans(plans, contextPlan) {
1259
1358
  }
1260
1359
  return split;
1261
1360
  }
1361
+ /**
1362
+ * Pre-dispatch worst-case bound (#2797) on the completed result envelope of a context-planned
1363
+ * invocation.
1364
+ *
1365
+ * Every evaluation carries one full clone of the context plan alongside the ruleset-level plan,
1366
+ * so the envelope grows with the alias count even when the provider request fits. Usage is
1367
+ * recorded cumulatively as evaluations complete — evaluation i carries i+1 usage entries — and
1368
+ * entries are upserted by key, so retries add none; every evaluation carrying all n entries is
1369
+ * therefore the true upper bound. Attempts are bounded per alias by the binding: no alias can
1370
+ * record more than its targets' retries plus one, nor more than the invocation-wide maxAttempts.
1371
+ * Each probe attempt carries the same evidence a real single dispatch records (requestIdSource
1372
+ * and the single-dispatch batch object), and each probe distribution spans the definition's full
1373
+ * answer domain. Entry admission is monotonic in every counted dimension, so a probe that cannot
1374
+ * satisfy admission proves no completion of the invocation could either; the caller must then
1375
+ * reject before receipts, credentials, admission control or transport instead of discarding
1376
+ * completed provider work after dispatch. A fitting envelope still passes through writer-version
1377
+ * admission when the completed result is written. Null, unknown or non-safe-integer counts fail
1378
+ * closed.
1379
+ */
1380
+ export function contextResultEnvelopeFits(plan, items, binding) {
1381
+ if (!binding || !Number.isSafeInteger(binding.spec?.maxAttempts) || binding.spec.maxAttempts < 1)
1382
+ return false;
1383
+ for (const item of items) {
1384
+ const configured = binding.spec.evaluations?.[item.alias];
1385
+ if (!configured || !Array.isArray(configured.targets) || !configured.targets.length)
1386
+ return false;
1387
+ let perAlias = 0;
1388
+ for (const target of configured.targets) {
1389
+ const maxRetries = target?.retry?.maxRetries;
1390
+ if (!Number.isSafeInteger(maxRetries) || maxRetries < 0)
1391
+ return false;
1392
+ perAlias += maxRetries + 1;
1393
+ }
1394
+ if (!Number.isSafeInteger(perAlias) || Math.min(binding.spec.maxAttempts, perAlias) < 1)
1395
+ return false;
1396
+ if (contextResultEnvelopeDistributionKeys(item.definition).length === 0)
1397
+ return false;
1398
+ }
1399
+ try {
1400
+ admitEntry(contextResultEnvelopeProbe(plan, items, binding));
1401
+ return true;
1402
+ }
1403
+ catch (error) {
1404
+ if (error instanceof EntryAdmissionError)
1405
+ return false;
1406
+ throw error;
1407
+ }
1408
+ }
1409
+ /** Answer-domain keys of a definition, matching distribution validation support. */
1410
+ function contextResultEnvelopeDistributionKeys(definition) {
1411
+ const answer = definition?.spec?.answer;
1412
+ if (!answer)
1413
+ return [];
1414
+ if (answer.kind === 'choice')
1415
+ return answer.options.map(option => option.id);
1416
+ if (answer.kind === 'ordinal-score')
1417
+ return answer.levels.map((_, index) => String(index));
1418
+ return ['false', 'true'];
1419
+ }
1420
+ function contextResultEnvelopeProbe(plan, items, binding) {
1421
+ const pin = (id) => ({ id, version: '1.0.0', digest: `sha256:${'0'.repeat(64)}` });
1422
+ const firstPartition = plan.partitions[0];
1423
+ const usageFor = (alias) => firstPartition && firstPartition.questionIds.length
1424
+ ? recordContextActualUsage(plan, firstPartition.id, 0, firstPartition.questionIds.includes(alias) ? alias : firstPartition.questionIds[0])
1425
+ : { schemaVersion: 'decision-context-usage/v1', planDigest: plan.planDigest, partitionId: 'probe', questionIds: [alias],
1426
+ estimator: { ...plan.estimator }, providerProfile: { id: plan.providerProfile.id, version: plan.providerProfile.version },
1427
+ estimatedInputTokens: 0, actualInputTokens: 0, estimationErrorTokens: 0, estimationErrorBps: null };
1428
+ // Worst case: every evaluation carries every usage entry (cumulative recording, upserted by key).
1429
+ const usages = items.map(item => usageFor(item.alias));
1430
+ const evaluations = Object.fromEntries(items.map(item => {
1431
+ const configured = binding.spec.evaluations[item.alias];
1432
+ const target = configured.targets[0];
1433
+ const questionId = decisionBatchQuestionId(item.alias);
1434
+ let perAlias = 0;
1435
+ for (const candidate of configured.targets)
1436
+ perAlias += candidate.retry.maxRetries + 1;
1437
+ const attemptCount = Math.min(binding.spec.maxAttempts, perAlias);
1438
+ const keys = contextResultEnvelopeDistributionKeys(item.definition);
1439
+ const peak = keys.reduce((longest, key) => key.length >= longest.length ? key : longest, keys[0]);
1440
+ const value = item.definition.spec.answer.kind === 'ordinal-score' ? Number(peak)
1441
+ : item.definition.spec.answer.kind === 'choice' ? peak : peak === 'true';
1442
+ const distribution = Object.fromEntries(keys.map(key => [key, key === peak ? 1 : 0]));
1443
+ const attempts = Array.from({ length: attemptCount }, (_, index) => ({
1444
+ ordinal: index + 1, adapter: target.adapter, adapterVersion: target.adapterVersion,
1445
+ requestedModel: target.model, actualModel: target.model,
1446
+ subagent: target.subagent ?? null, status: 'success', reason: 'none', durationMs: 0,
1447
+ usage: { inputTokens: 0, outputTokens: 0, costUsd: null },
1448
+ requestId: 'probe', requestIdSource: 'typesafe', httpStatus: 200,
1449
+ // A retried attempt records its backoff; only the final attempt has none.
1450
+ ...(index < attemptCount - 1 ? { retryDelayMs: 0 } : {}),
1451
+ // Single-dispatch evidence, as recorded when native batching does not apply. The native
1452
+ // batch object carries a subset of these fields, so the single shape bounds both phases.
1453
+ batch: { mode: 'single', groupId: `single_${questionId.slice(2)}`, questionId, degradationReason: 'unsupported' },
1454
+ }));
1455
+ return [item.alias, {
1456
+ apiVersion: DECISION_API_VERSION_STRUCTURED, kind: 'DecisionResult',
1457
+ metadata: { id: `context-result-envelope-probe-${item.alias}`, version: '1.0.0', description: `Decision result for ${item.alias}` },
1458
+ spec: { decision: pin(`probe-${item.alias}`), ruleset: pin('probe-ruleset'), binding: pin('probe-binding'),
1459
+ alias: item.alias, runId: 'context-result-envelope-probe', invocationId: 'context-result-envelope-probe',
1460
+ status: 'success', value, reason: 'none',
1461
+ uncertainty: { source: 'provider', profile: 'typesafe-distribution-v1', calibration: 'vendor-claimed', confidence: 1,
1462
+ distribution, calibrationRef: null },
1463
+ attempts, context: { plan: structuredClone(plan), actualUsage: usages.map(usage => structuredClone(usage)) } },
1464
+ }];
1465
+ }));
1466
+ return { apiVersion: DECISION_API_VERSION_STRUCTURED, kind: 'RulesetResult',
1467
+ metadata: { id: 'context-result-envelope-probe', version: '1.0.0', description: 'Context result envelope probe' },
1468
+ // A matched completion carries its outcome and matched rule ids; ruleset-declared outcomes are
1469
+ // bounded by artifact admission of the ruleset itself, so the probe carries representative ones.
1470
+ spec: { ruleset: pin('probe-ruleset'), binding: pin('probe-binding'), runId: 'context-result-envelope-probe',
1471
+ invocationId: 'context-result-envelope-probe', status: 'completed', reason: 'none', matchedRules: ['review'],
1472
+ outcome: 'review',
1473
+ evaluations, context: { plan: structuredClone(plan), actualUsage: usages } } };
1474
+ }
1262
1475
  function recordRuntimeContextUsage(plan, questionIds, actualInputTokens, evidence) {
1263
1476
  const ids = typeof questionIds === 'string' ? [questionIds] : questionIds;
1264
1477
  const partition = plan.partitions.find(candidate => ids.every(id => candidate.questionIds.includes(id)));
@@ -3,6 +3,10 @@ export function decisionResultForExport(result, policy = {}) {
3
3
  const copy = structuredClone(result);
4
4
  if (policy.includeProviderRequestIds)
5
5
  return copy;
6
+ if (copy.kind === 'RulesetResult') {
7
+ for (const request of copy.spec.batchRequests ?? [])
8
+ request.requestId = null;
9
+ }
6
10
  const decisions = copy.kind === 'DecisionResult'
7
11
  ? [copy] : Object.values(copy.spec.evaluations);
8
12
  for (const decision of decisions) {