@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,571 @@
1
+ import { createTelemetryContext, randomTelemetryIds } from '../telemetry/context.js';
2
+ import { DECISION_TELEMETRY_SCHEMA_VERSION } from '../telemetry/types.js';
3
+ import { recordDecisionSpanMetrics } from '../telemetry/metrics.js';
4
+ import { championChallengerEligibilityProblems, compareEnsembleKeys, ensembleContractDigest, pairedThresholdsDigest, validateChampionChallenger, validateEnsembleIntegrityReport, validateEnsemblePolicy, } from './contract.js';
5
+ import { canonicalJson } from '../../security/artifact-trust.js';
6
+ import { assertDecisionResultWriterVersion } from '../validate.js';
7
+ import { aggregateEnsembleResults } from './aggregate.js';
8
+ import { buildEnsembleIntegrityReport } from './report.js';
9
+ import { resolveDriftResponse } from './drift.js';
10
+ export class EnsembleRuntimeError extends Error {
11
+ reason;
12
+ constructor(message, reason) {
13
+ super(message);
14
+ this.reason = reason;
15
+ this.name = 'EnsembleRuntimeError';
16
+ }
17
+ }
18
+ export async function executeDecisionEnsemble(policyInput, options) {
19
+ const validated = validateEnsemblePolicy(policyInput, {
20
+ hostCeilings: options.hostCeilings,
21
+ calibrationPins: options.calibrationPins,
22
+ });
23
+ const { policy, digest: policyDigest, budget } = validated;
24
+ if (options.enabled !== true || policy.mode === 'disabled') {
25
+ emitEnsembleSpan(options.telemetry, options.now, {
26
+ 'aiwg.ensemble.id': policy.id,
27
+ 'aiwg.ensemble.version': policy.version,
28
+ 'aiwg.ensemble.mode': policy.mode,
29
+ 'aiwg.ensemble.status': 'disabled',
30
+ 'aiwg.ensemble.members': policy.members.length,
31
+ });
32
+ return {
33
+ status: 'disabled',
34
+ reason: options.enabled === true ? 'policy-disabled' : 'disabled',
35
+ policyDigest,
36
+ budget,
37
+ aggregate: null,
38
+ memberResults: [],
39
+ retainedResults: {},
40
+ actuals: { attempts: 0, tokens: 0, costMicros: 0, unknownUsageSamples: 0 },
41
+ };
42
+ }
43
+ if (!options.authorizeMember) {
44
+ throw new EnsembleRuntimeError('ensemble member authorization callback is required before dispatch', 'member-authorization-missing');
45
+ }
46
+ for (const member of [...policy.members].sort((a, b) => compareEnsembleKeys(a.id, b.id))) {
47
+ if (await options.authorizeMember(member) !== true) {
48
+ throw new EnsembleRuntimeError(`ensemble member ${member.id} failed security/privacy/capability authorization`, 'member-disallowed');
49
+ }
50
+ }
51
+ const now = options.now ?? Date.now;
52
+ const deadlineEpochMs = now() + budget.effective.deadlineMs;
53
+ const deadline = createDeadlineSignal(options.signal, budget.effective.deadlineMs, options.delay);
54
+ const signal = deadline.signal;
55
+ const tasks = canonicalSamples(policy);
56
+ const retainedResults = {};
57
+ let actualAttempts = 0;
58
+ let actualTokens = 0;
59
+ let actualCostMicros = 0;
60
+ let unknownUsageSamples = 0;
61
+ let inFlightAttempts = 0;
62
+ let inFlightTokens = 0;
63
+ let inFlightCostMicros = 0;
64
+ const dispatchedMembers = new Set();
65
+ let exhausted = false;
66
+ const reservations = new Map(budget.reservations.map(item => [`${item.memberId}:${item.sampleIndex}`, item]));
67
+ try {
68
+ const memberResults = await runBounded(tasks, budget.effective.concurrency, async ({ member, sampleIndex }) => {
69
+ if (signal.aborted || now() >= deadlineEpochMs)
70
+ return syntheticMemberResult(policy, member, sampleIndex, 'timeout', retainedResults);
71
+ if (exhausted)
72
+ return syntheticMemberResult(policy, member, sampleIndex, 'budget-exhausted', retainedResults);
73
+ // Reserve before dispatch against what is already spent plus every in-flight reservation.
74
+ const reservation = reservations.get(`${member.id}:${sampleIndex}`);
75
+ if (!reservation
76
+ || (!dispatchedMembers.has(member.id) && dispatchedMembers.size + 1 > budget.effective.members)
77
+ || actualAttempts + inFlightAttempts + reservation.attempts > budget.effective.attempts
78
+ || actualTokens + inFlightTokens + reservation.tokens > budget.effective.tokens
79
+ || actualCostMicros + inFlightCostMicros + reservation.costMicros > budget.effective.costMicros) {
80
+ exhausted = true;
81
+ return syntheticMemberResult(policy, member, sampleIndex, 'budget-exhausted', retainedResults);
82
+ }
83
+ dispatchedMembers.add(member.id);
84
+ inFlightAttempts += reservation.attempts;
85
+ inFlightTokens += reservation.tokens;
86
+ inFlightCostMicros += reservation.costMicros;
87
+ let settled = false;
88
+ /** Moves the reservation into actual spend. Without trusted usage the full reserved bound is charged. */
89
+ const settle = (usage, unknownUsage = usage === undefined) => {
90
+ if (settled)
91
+ return;
92
+ settled = true;
93
+ if (unknownUsage)
94
+ unknownUsageSamples += 1;
95
+ usage ??= reservation;
96
+ inFlightAttempts -= reservation.attempts;
97
+ inFlightTokens -= reservation.tokens;
98
+ inFlightCostMicros -= reservation.costMicros;
99
+ actualAttempts += usage.attempts;
100
+ actualTokens += usage.tokens;
101
+ actualCostMicros += usage.costMicros;
102
+ };
103
+ let result;
104
+ try {
105
+ result = await dispatchWithDeadline(options.dispatch({
106
+ policy, policyDigest, member: structuredClone(member), sampleIndex,
107
+ invocationId: `${options.invocationId}:${member.id}:${sampleIndex}`, deadlineEpochMs, signal,
108
+ limits: { attempts: reservation.attempts, tokens: reservation.tokens, costMicros: reservation.costMicros },
109
+ }), signal);
110
+ }
111
+ catch (error) {
112
+ // A timed-out or rejected dispatch may still have spent its reservation; it is never refunded.
113
+ settle();
114
+ const reason = signal.aborted || now() >= deadlineEpochMs || (error instanceof EnsembleRuntimeError && error.reason === 'timeout')
115
+ ? 'timeout' : 'dispatch-error';
116
+ if (reason === 'timeout')
117
+ exhausted = true;
118
+ return syntheticMemberResult(policy, member, sampleIndex, reason, retainedResults);
119
+ }
120
+ try {
121
+ assertDecisionResultWriterVersion(result);
122
+ if (result.kind !== 'DecisionResult')
123
+ throw new EnsembleRuntimeError('ensemble member must return a DecisionResult', 'invalid-output');
124
+ }
125
+ catch {
126
+ settle();
127
+ return syntheticMemberResult(policy, member, sampleIndex, 'invalid-output', retainedResults);
128
+ }
129
+ const attempts = result.spec.attempts.length;
130
+ const reportedTokens = result.spec.attempts.reduce((sum, attempt) => sum + (attempt.usage.inputTokens ?? 0) + (attempt.usage.outputTokens ?? 0), 0);
131
+ // Null token usage is unknown, not zero: charge at least the sample's full token reservation.
132
+ const unknownTokens = result.spec.attempts.some(attempt => attempt.usage.inputTokens === null || attempt.usage.outputTokens === null);
133
+ const tokens = unknownTokens ? Math.max(reportedTokens, reservation.tokens) : reportedTokens;
134
+ const cost = resultCostMicros(policy, result);
135
+ if (cost === null) {
136
+ settle({ attempts: Math.max(attempts, reservation.attempts), tokens: Math.max(tokens, reservation.tokens), costMicros: reservation.costMicros }, true);
137
+ exhausted = true;
138
+ return syntheticMemberResult(policy, member, sampleIndex, 'unknown-cost', retainedResults, result);
139
+ }
140
+ settle({ attempts, tokens, costMicros: cost }, unknownTokens);
141
+ if (attempts > member.fallbackDepth + 1) {
142
+ return syntheticMemberResult(policy, member, sampleIndex, 'fallback-depth-exceeded', retainedResults, result);
143
+ }
144
+ if (attempts > reservation.attempts || tokens > reservation.tokens || cost > reservation.costMicros
145
+ || actualAttempts > budget.effective.attempts || actualTokens > budget.effective.tokens
146
+ || actualCostMicros > budget.effective.costMicros || now() > deadlineEpochMs) {
147
+ exhausted = true;
148
+ return syntheticMemberResult(policy, member, sampleIndex, 'budget-exhausted', retainedResults, result);
149
+ }
150
+ return memberResultFromDecision(member, sampleIndex, result, retainedResults);
151
+ });
152
+ const aggregate = aggregateEnsembleResults(policy, memberResults, {
153
+ hostCeilings: options.hostCeilings,
154
+ calibrationPins: options.calibrationPins,
155
+ });
156
+ emitEnsembleSpan(options.telemetry, options.now, {
157
+ 'aiwg.ensemble.id': policy.id,
158
+ 'aiwg.ensemble.version': policy.version,
159
+ 'aiwg.ensemble.mode': policy.mode,
160
+ 'aiwg.ensemble.status': 'completed',
161
+ 'aiwg.ensemble.members': policy.members.length,
162
+ 'aiwg.ensemble.disagreement_bps': aggregate.disagreement.valueBps,
163
+ 'aiwg.ensemble.outcome': aggregate.outcome.disposition,
164
+ 'aiwg.budget.attempts.planned': budget.demand.attempts,
165
+ 'aiwg.budget.attempts.actual': actualAttempts,
166
+ 'aiwg.budget.tokens.actual': actualTokens,
167
+ 'aiwg.budget.cost_micros.actual': actualCostMicros,
168
+ 'aiwg.budget.unknown_usage_samples': unknownUsageSamples,
169
+ });
170
+ return { status: 'completed', reason: 'completed', policyDigest, budget, aggregate, memberResults, retainedResults,
171
+ actuals: { attempts: actualAttempts, tokens: actualTokens, costMicros: actualCostMicros, unknownUsageSamples } };
172
+ }
173
+ finally {
174
+ deadline.cancel();
175
+ }
176
+ }
177
+ export function championChallengerInputSetDigest(items) {
178
+ return ensembleContractDigest(items.map(item => ({
179
+ id: item.id,
180
+ inputDigest: ensembleContractDigest(item.input),
181
+ ...(item.slice ? { slice: item.slice } : {}),
182
+ })).sort((a, b) => compareEnsembleKeys(a.id, b.id)));
183
+ }
184
+ export function championChallengerShadowBaseline(recordInput, shadow) {
185
+ const { record } = validateChampionChallenger(recordInput);
186
+ const items = new Set(shadow.receipts.map(receipt => receipt.itemId));
187
+ if (shadow.status !== 'completed' || shadow.receipts.length !== record.inputSet.itemCount || items.size !== shadow.receipts.length) {
188
+ throw new EnsembleRuntimeError('a shadow baseline needs one completed receipt per frozen input item', 'shadow-incomplete');
189
+ }
190
+ return {
191
+ ...shadowBaselineIdentity(record),
192
+ pairedDeltasDigest: pairedDeltasDigest(shadow.pairedDeltas),
193
+ receiptsDigest: ensembleContractDigest([...shadow.receipts].sort((a, b) => compareEnsembleKeys(a.itemId, b.itemId))),
194
+ };
195
+ }
196
+ function shadowBaselineIdentity(record) {
197
+ return {
198
+ kind: 'decision-champion-challenger-shadow/v1',
199
+ recordId: record.id,
200
+ inputSetDigest: record.inputSet.digest,
201
+ thresholdsDigest: record.preregistration.thresholdsDigest,
202
+ championIdentityDigest: record.champion.identityDigest,
203
+ challengerIdentityDigest: record.challenger.identityDigest,
204
+ };
205
+ }
206
+ function pairedDeltasDigest(deltas) {
207
+ return ensembleContractDigest(deltas.map(item => ({ metric: item.metric, delta: item.delta, pairs: item.pairs }))
208
+ .sort((a, b) => compareEnsembleKeys(a.metric, b.metric)));
209
+ }
210
+ /** The alias's current revision: the event with the highest revision. */
211
+ function currentAliasEvent(history) {
212
+ return history.reduce((latest, event) => !latest || event.revision > latest.revision ? event : latest, undefined);
213
+ }
214
+ export async function runChampionChallengerShadow(recordInput, options) {
215
+ const { record, digest } = validateChampionChallenger(recordInput, {
216
+ ...(options.eligibility ? { eligibility: options.eligibility } : {}),
217
+ ...(options.aliasHistory ? { aliasHistory: options.aliasHistory } : {}),
218
+ });
219
+ if (options.enabled !== true) {
220
+ emitEnsembleSpan(options.telemetry, options.now, {
221
+ 'aiwg.ensemble.champion_challenger.id': record.id,
222
+ 'aiwg.ensemble.champion_challenger.status': 'disabled',
223
+ 'aiwg.ensemble.alias': record.alias,
224
+ });
225
+ return { status: 'disabled', recordDigest: digest, pairedDeltas: [], receipts: [] };
226
+ }
227
+ if (options.items.length !== record.inputSet.itemCount || championChallengerInputSetDigest(options.items) !== record.inputSet.digest) {
228
+ throw new EnsembleRuntimeError('shadow input set does not match the immutable champion/challenger record', 'input-set-mismatch');
229
+ }
230
+ const signal = options.signal ?? new AbortController().signal;
231
+ const pairs = [];
232
+ for (const item of options.items) {
233
+ const before = ensembleContractDigest(item.input);
234
+ const [champion, challenger] = await Promise.all([
235
+ options.evaluate({ role: 'champion', rolePin: structuredClone(record.champion), item: structuredClone(item),
236
+ invocationId: `${options.invocationId}:${item.id}:champion`, signal }),
237
+ options.evaluate({ role: 'challenger', rolePin: structuredClone(record.challenger), item: structuredClone(item),
238
+ invocationId: `${options.invocationId}:${item.id}:challenger`, signal }),
239
+ ]);
240
+ const after = ensembleContractDigest(item.input);
241
+ if (before !== after || champion.inputDigest !== before || challenger.inputDigest !== before) {
242
+ throw new EnsembleRuntimeError('champion and challenger must evaluate identical immutable inputs', 'input-mutated');
243
+ }
244
+ pairs.push({ item, champion, challenger });
245
+ }
246
+ const pairedDeltas = record.pairedMetrics.map(threshold => observationDelta(threshold.metric, pairs));
247
+ emitEnsembleSpan(options.telemetry, options.now, {
248
+ 'aiwg.ensemble.champion_challenger.id': record.id,
249
+ 'aiwg.ensemble.champion_challenger.status': 'completed',
250
+ 'aiwg.ensemble.alias': record.alias,
251
+ 'aiwg.ensemble.role': 'paired-shadow',
252
+ 'aiwg.ensemble.pairs': pairs.length,
253
+ });
254
+ return {
255
+ status: 'completed',
256
+ recordDigest: digest,
257
+ pairedDeltas,
258
+ receipts: pairs.map(pair => ({ itemId: pair.item.id, champion: pair.champion.receiptDigest, challenger: pair.challenger.receiptDigest })),
259
+ };
260
+ }
261
+ export function pinChampionForRun(recordInput, runId) {
262
+ const { record } = validateChampionChallenger(recordInput);
263
+ return {
264
+ runId,
265
+ alias: record.alias,
266
+ aliasRevision: record.champion.aliasRevision,
267
+ identityDigest: record.champion.identityDigest,
268
+ actualModel: record.champion.actualModel,
269
+ };
270
+ }
271
+ export function promoteChampionChallenger(input) {
272
+ const aliasHistory = input.gateway.aliasHistory(input.record.alias);
273
+ const { record, digest } = validateChampionChallenger(input.record, { eligibility: input.eligibility, aliasHistory });
274
+ // The gateway promotes D09's stored record, so that is the one that must have been validated.
275
+ const stored = input.gateway.promotionEligibility(record.eligibilityId);
276
+ if (!stored || canonicalJson(stored) !== canonicalJson(input.eligibility)) {
277
+ throw new EnsembleRuntimeError('supplied eligibility does not match the D09 stored promotion eligibility', 'promotion-eligibility-mismatch');
278
+ }
279
+ const current = currentAliasEvent(aliasHistory);
280
+ if (!current || current.kind === 'retired' || current.revision !== record.champion.aliasRevision
281
+ || current.actualIdentityDigest !== record.champion.identityDigest) {
282
+ throw new EnsembleRuntimeError('alias no longer holds the champion this record pins', 'promotion-stale-champion');
283
+ }
284
+ if (aliasHistory.some(event => event.promotionEligibilityId === record.eligibilityId)) {
285
+ throw new EnsembleRuntimeError(`promotion eligibility ${record.eligibilityId} was already used`, 'promotion-replayed');
286
+ }
287
+ const report = validateEnsembleIntegrityReport(input.integrityReport);
288
+ // Never trust the supplied report's findings, flags or decision: rebuild it from the trusted
289
+ // record, D09 eligibility and alias history, and require the exact same canonical report.
290
+ const rebuilt = buildEnsembleIntegrityReport({
291
+ record, integrity: report.integrity, eligibility: input.eligibility, aliasHistory,
292
+ pairedDeltas: report.pairedDeltas.map(item => ({ metric: item.metric, delta: item.delta, pairs: item.pairs })),
293
+ });
294
+ if (canonicalJson(rebuilt) !== canonicalJson(report)) {
295
+ throw new EnsembleRuntimeError('integrity report does not match the report rebuilt from the trusted record', 'promotion-report-mismatch');
296
+ }
297
+ const problems = championChallengerEligibilityProblems(record, input.eligibility);
298
+ if (report.decision !== 'PROMOTE' || report.subject.id !== record.id
299
+ || report.subject.digest !== digest || report.subject.eligibilityId !== record.eligibilityId
300
+ || input.eligibility.eligible !== true || problems.length > 0) {
301
+ throw new EnsembleRuntimeError('promotion requires eligible D09 state, verified eval-integrity and a pinned rollback target', 'promotion-gate');
302
+ }
303
+ if (ensembleContractDigest(report.integrity) !== record.evaluationIntegrityReport.digest) {
304
+ throw new EnsembleRuntimeError('integrity fields do not match the record\'s pinned evaluation-integrity report digest', 'promotion-integrity-binding');
305
+ }
306
+ const pairedBaseline = report.integrity.paired_baseline;
307
+ const baseline = pairedBaseline && typeof pairedBaseline === 'object' ? pairedBaseline.championChallengerShadow : null;
308
+ const { receiptsDigest, ...boundFields } = (baseline && typeof baseline === 'object' ? baseline : {});
309
+ if (canonicalJson(boundFields) !== canonicalJson({ ...shadowBaselineIdentity(record), pairedDeltasDigest: pairedDeltasDigest(report.pairedDeltas) })
310
+ || typeof receiptsDigest !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(receiptsDigest)) {
311
+ throw new EnsembleRuntimeError('paired deltas are not bound to a completed shadow run for this record', 'promotion-shadow-unbound');
312
+ }
313
+ const event = input.gateway.promoteAlias(record.eligibilityId, input.at);
314
+ if (!event || event.kind !== 'promoted' || event.alias !== record.alias || event.promotionEligibilityId !== record.eligibilityId
315
+ || event.actualIdentityDigest !== record.challenger.identityDigest || event.actualModel !== record.challenger.actualModel
316
+ || event.revision !== current.revision + 1) {
317
+ throw new EnsembleRuntimeError('D09 gateway returned a promotion event that does not move the alias from the pinned champion to this record\'s challenger; inspect alias history', 'promotion-event-mismatch');
318
+ }
319
+ emitEnsembleSpan(input.telemetry, input.now, {
320
+ 'aiwg.ensemble.champion_challenger.id': record.id,
321
+ 'aiwg.ensemble.alias': record.alias,
322
+ 'aiwg.ensemble.promotion': 'promoted',
323
+ 'aiwg.ensemble.rollback_revision': record.rollbackTarget.aliasRevision,
324
+ });
325
+ return event;
326
+ }
327
+ export function rollbackChampionForNewRuns(input) {
328
+ if (!/^[A-Za-z0-9][A-Za-z0-9:._/@-]{2,127}$/.test(input.approvalReference)) {
329
+ throw new EnsembleRuntimeError('rollback requires a non-empty valid approval reference', 'rollback-approval-reference');
330
+ }
331
+ const aliasHistory = input.gateway.aliasHistory(input.record.alias);
332
+ const { record } = validateChampionChallenger(input.record, { aliasHistory });
333
+ // Only undo this record's own promotion: the alias's current revision must be the promoted
334
+ // challenger, and the revision it replaced must be the pinned champion (the rollback target).
335
+ const current = currentAliasEvent(aliasHistory);
336
+ const replaced = current && currentAliasEvent(aliasHistory.filter(event => event.revision < current.revision));
337
+ if (!current || current.kind !== 'promoted' || current.alias !== record.alias || current.promotionEligibilityId !== record.eligibilityId
338
+ || current.actualIdentityDigest !== record.challenger.identityDigest || current.actualModel !== record.challenger.actualModel) {
339
+ throw new EnsembleRuntimeError('rollback requires the alias to currently hold this record\'s promoted challenger', 'rollback-not-promoted');
340
+ }
341
+ if (!replaced || replaced.revision !== record.rollbackTarget.aliasRevision || replaced.actualIdentityDigest !== record.rollbackTarget.identityDigest) {
342
+ throw new EnsembleRuntimeError('rollback requires the promotion to have directly replaced the pinned champion', 'rollback-champion-mismatch');
343
+ }
344
+ const event = input.gateway.rollbackAlias(record.alias, record.rollbackTarget.aliasRevision, input.approvalReference, input.at);
345
+ emitEnsembleSpan(input.telemetry, input.now, {
346
+ 'aiwg.ensemble.champion_challenger.id': record.id,
347
+ 'aiwg.ensemble.alias': record.alias,
348
+ 'aiwg.ensemble.rollback': 'restored-champion',
349
+ 'aiwg.ensemble.rollback_revision': record.rollbackTarget.aliasRevision,
350
+ });
351
+ return event;
352
+ }
353
+ export async function executeDriftResponse(policyInput, signal, handlers = {}, telemetry, now) {
354
+ const decision = resolveDriftResponse(policyInput, signal);
355
+ if (decision.response) {
356
+ const handler = handlers[decision.response];
357
+ if (!handler)
358
+ throw new EnsembleRuntimeError(`drift response ${decision.response} has no registered handler`, 'drift-handler-missing');
359
+ await handler(decision);
360
+ }
361
+ emitEnsembleSpan(telemetry, now, {
362
+ 'aiwg.ensemble.drift.signal_id': decision.signalId,
363
+ 'aiwg.ensemble.drift.source': decision.source,
364
+ 'aiwg.ensemble.drift.metric': decision.metric,
365
+ 'aiwg.ensemble.drift.state': decision.state,
366
+ 'aiwg.ensemble.drift.response': decision.response,
367
+ 'aiwg.drift.value': signal.source === 'alias-drift' ? 1 : signal.valueBps,
368
+ });
369
+ return { decision, executed: decision.response };
370
+ }
371
+ function createDeadlineSignal(parent, deadlineMs, delay) {
372
+ const controller = new AbortController();
373
+ const timer = new AbortController();
374
+ const abort = () => { if (!controller.signal.aborted)
375
+ controller.abort(); };
376
+ if (parent?.aborted)
377
+ abort();
378
+ else
379
+ parent?.addEventListener('abort', abort, { once: true });
380
+ const wait = delay ?? defaultDelay;
381
+ void wait(deadlineMs, timer.signal).then(abort, () => undefined);
382
+ return {
383
+ signal: controller.signal,
384
+ cancel: () => {
385
+ timer.abort();
386
+ parent?.removeEventListener('abort', abort);
387
+ },
388
+ };
389
+ }
390
+ function defaultDelay(ms, signal) {
391
+ return new Promise((resolve, reject) => {
392
+ const timeout = setTimeout(resolve, ms);
393
+ signal.addEventListener('abort', () => {
394
+ clearTimeout(timeout);
395
+ reject(new Error('cancelled'));
396
+ }, { once: true });
397
+ });
398
+ }
399
+ function dispatchWithDeadline(result, signal) {
400
+ if (signal.aborted)
401
+ return Promise.reject(new EnsembleRuntimeError('ensemble dispatch deadline exceeded', 'timeout'));
402
+ return Promise.race([result, new Promise((_, reject) => {
403
+ signal.addEventListener('abort', () => reject(new EnsembleRuntimeError('ensemble dispatch deadline exceeded', 'timeout')), { once: true });
404
+ })]);
405
+ }
406
+ function canonicalSamples(policy) {
407
+ return [...policy.members].sort((a, b) => compareEnsembleKeys(a.id, b.id))
408
+ .flatMap(member => Array.from({ length: member.samples }, (_, sampleIndex) => ({ member, sampleIndex })));
409
+ }
410
+ async function runBounded(items, concurrency, task) {
411
+ const output = new Array(items.length);
412
+ let next = 0;
413
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
414
+ while (next < items.length) {
415
+ const index = next++;
416
+ output[index] = await task(items[index]);
417
+ }
418
+ });
419
+ await Promise.all(workers);
420
+ return output;
421
+ }
422
+ function memberResultFromDecision(member, sampleIndex, result, retainedResults) {
423
+ const digest = ensembleContractDigest(result);
424
+ retainedResults[digest] = structuredClone(result);
425
+ return {
426
+ memberId: member.id,
427
+ sampleIndex,
428
+ status: result.spec.status === 'success' ? 'succeeded' : result.spec.status === 'abstained' ? 'abstained' : 'failed',
429
+ resultDigest: digest,
430
+ value: result.spec.status === 'success' ? result.spec.value ?? null : null,
431
+ distribution: result.spec.status === 'success' ? result.spec.uncertainty?.distribution ?? null : null,
432
+ uncertaintyProfile: result.spec.status === 'success' ? result.spec.uncertainty?.profile ?? null : null,
433
+ };
434
+ }
435
+ function syntheticMemberResult(policy, member, sampleIndex, reason, retainedResults, source) {
436
+ const failureReason = syntheticFailureReason(reason);
437
+ const result = source ? structuredClone(source) : {
438
+ apiVersion: 'decision.aiwg.io/v1alpha1',
439
+ kind: 'DecisionResult',
440
+ metadata: { id: `${policy.id}:${member.id}:${sampleIndex}`, version: policy.version, description: reason },
441
+ spec: {
442
+ decision: member.definition,
443
+ ruleset: member.definition,
444
+ binding: member.binding,
445
+ alias: member.id,
446
+ runId: policy.id,
447
+ invocationId: `${policy.id}:${member.id}:${sampleIndex}`,
448
+ status: 'error',
449
+ reason: failureReason,
450
+ uncertainty: null,
451
+ attempts: [syntheticAttempt(member, failureReason)],
452
+ },
453
+ };
454
+ const { value: _value, uncertainty: _uncertainty, ...spec } = result.spec;
455
+ return memberResultFromDecision(member, sampleIndex, {
456
+ ...result,
457
+ spec: { ...spec, status: 'error', reason: failureReason, uncertainty: null },
458
+ }, retainedResults);
459
+ }
460
+ function syntheticAttempt(member, reason) {
461
+ return {
462
+ ordinal: 1,
463
+ adapter: member.adapter.id,
464
+ adapterVersion: member.adapter.version,
465
+ requestedModel: member.model.requested,
466
+ actualModel: null,
467
+ subagent: null,
468
+ status: 'error',
469
+ reason,
470
+ durationMs: 0,
471
+ usage: { inputTokens: null, outputTokens: null, costUsd: null },
472
+ requestId: null,
473
+ };
474
+ }
475
+ function syntheticFailureReason(reason) {
476
+ if (reason === 'timeout')
477
+ return 'timeout';
478
+ if (reason === 'invalid-output')
479
+ return 'invalid-output';
480
+ if (reason === 'dispatch-error')
481
+ return 'service-error';
482
+ return 'budget-exhausted';
483
+ }
484
+ function resultCostMicros(policy, result) {
485
+ let cost = 0;
486
+ for (const attempt of result.spec.attempts) {
487
+ if (attempt.usage.costUsd === null) {
488
+ if (policy.ceilings.unknownCost.rule === 'reject')
489
+ return null;
490
+ cost += policy.ceilings.unknownCost.boundMicrosPerAttempt;
491
+ }
492
+ else {
493
+ cost += Math.round(attempt.usage.costUsd * 1_000_000);
494
+ }
495
+ }
496
+ return cost;
497
+ }
498
+ function observationDelta(metric, pairs) {
499
+ const key = metricKey(metric);
500
+ const deltas = pairs.map(pair => {
501
+ const champion = pair.champion[key];
502
+ const challenger = pair.challenger[key];
503
+ return champion === null || challenger === null ? null : challenger - champion;
504
+ }).filter((value) => value !== null && Number.isFinite(value));
505
+ return {
506
+ metric,
507
+ delta: deltas.length ? round(deltas.reduce((sum, value) => sum + value, 0) / deltas.length) : null,
508
+ pairs: deltas.length,
509
+ };
510
+ }
511
+ function metricKey(metric) {
512
+ if (metric === 'risk-coverage')
513
+ return 'riskCoverage';
514
+ if (metric === 'latency')
515
+ return 'latencyMs';
516
+ if (metric === 'cost')
517
+ return 'costMicros';
518
+ return metric;
519
+ }
520
+ function round(value) { return Math.round(value * 1e12) / 1e12; }
521
+ function emitEnsembleSpan(telemetry, nowInput, attributes) {
522
+ if (!telemetry?.hook && !telemetry?.metrics)
523
+ return;
524
+ const now = nowInput ?? Date.now;
525
+ const at = now();
526
+ const context = telemetryContext(telemetry.traceId);
527
+ const clean = Object.fromEntries(Object.entries(attributes).filter(([, value]) => value === null || ['string', 'number', 'boolean'].includes(typeof value)));
528
+ const span = {
529
+ schemaVersion: DECISION_TELEMETRY_SCHEMA_VERSION,
530
+ name: 'decision.workflow',
531
+ context,
532
+ parentSpanId: telemetry.parentSpanId ?? null,
533
+ startTimeUnixMs: at,
534
+ endTimeUnixMs: at,
535
+ status: 'ok',
536
+ attributes: clean,
537
+ provenance: Object.fromEntries(Object.keys(clean).map(key => [key, 'client-derived'])),
538
+ links: [],
539
+ events: [],
540
+ };
541
+ try {
542
+ if (telemetry.metrics)
543
+ recordDecisionSpanMetrics(span, telemetry.metrics);
544
+ }
545
+ catch { /* observability never controls runtime */ }
546
+ try {
547
+ void telemetry.hook?.emit(span);
548
+ }
549
+ catch { /* observability never controls runtime */ }
550
+ }
551
+ function telemetryContext(traceId) {
552
+ try {
553
+ return createTelemetryContext({
554
+ traceId: () => traceId ?? randomTelemetryIds.traceId(),
555
+ spanId: () => randomTelemetryIds.spanId(),
556
+ });
557
+ }
558
+ catch {
559
+ return createTelemetryContext(randomTelemetryIds);
560
+ }
561
+ }
562
+ export function preregisterChampionChallengerThresholds(record) {
563
+ return {
564
+ ...record,
565
+ preregistration: {
566
+ ...record.preregistration,
567
+ thresholdsDigest: pairedThresholdsDigest(record.pairedMetrics),
568
+ },
569
+ };
570
+ }
571
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,85 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import Ajv2020 from 'ajv/dist/2020.js';
5
+ import { admitEntry, DEFAULT_ENTRY_LIMITS } from '../entry.js';
6
+ import { heldoutDigest } from '../heldout/contract.js';
7
+ const schemas = { analysis: 'D17StudyAnalysis', splits: 'D17StudySplits', gold: 'D17StudyGold',
8
+ review: 'D17StudyReview', guide: 'D17StudyGuide', report: 'D17StudyReport',
9
+ nativeTemplates: 'D17StudyNativeTemplates', dryRun: 'D17StudyDryRun',
10
+ // Staged D09 calibration (#2611): new schema versions; the v1 diagnostic schemas are unchanged.
11
+ analysisV2: 'D17StudyAnalysis.v2', dryRunV2: 'D17StudyDryRun.v2', reportV2: 'D17StudyReport.v2',
12
+ calibration: 'D17StudyCalibration' };
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ const schemaDir = [resolve(here, '../../../schemas/decision'), resolve(here, '../../../../schemas/decision')]
15
+ .find(directory => existsSync(resolve(directory, 'D17StudyProtocol.v1.schema.json')));
16
+ if (!schemaDir)
17
+ throw new Error('D17 artifact schema directory is unavailable');
18
+ const readSchema = (name) => JSON.parse(readFileSync(resolve(schemaDir, `${/\.v[0-9]+$/.test(name) ? name : `${name}.v1`}.schema.json`), 'utf8'));
19
+ const ajv = new Ajv2020({ strict: true });
20
+ ajv.addSchema(readSchema('D17StudyProtocol'));
21
+ ajv.addSchema(readSchema('DecisionEnsembleAggregate'));
22
+ ajv.addSchema(readSchema('DecisionEnsembleIntegrityReport'));
23
+ const calibrationSchema = readSchema('D17StudyCalibration');
24
+ ajv.addSchema(calibrationSchema);
25
+ const validators = Object.fromEntries(Object.entries(schemas).map(([name, schema]) => [name,
26
+ name === 'calibration' ? ajv.getSchema(calibrationSchema.$id) : ajv.compile(readSchema(schema))]));
27
+ const limits = { ...DEFAULT_ENTRY_LIMITS, serializedBytes: 8_388_608, properties: 100000, entries: 150000,
28
+ arrayLength: 1800, memoryBytes: 33_554_432 };
29
+ const reportLimits = { ...limits, serializedBytes: 33_554_432, properties: 500000, entries: 1000000, memoryBytes: 134_217_728 };
30
+ /** Closed, bounded preparation artifacts; gold remains local and separate from provider input. */
31
+ export function validateD17Artifact(name, value) {
32
+ admitEntry(value, name === 'report' || name === 'reportV2' ? reportLimits : limits);
33
+ if (!validators[name]?.(value))
34
+ throw new Error(`D17 invalid ${name} artifact`);
35
+ if (name === 'splits') {
36
+ const manifest = value;
37
+ const ids = new Set(), inputs = new Set(), families = new Map();
38
+ for (const [split, group] of Object.entries(manifest.splits)) {
39
+ if (heldoutDigest(group.members) !== group.digest)
40
+ throw new Error('D17 split digest mismatch');
41
+ for (const row of group.members) {
42
+ if (ids.has(row.id) || inputs.has(row.inputDigest) || !/^[0-9a-f]{64}$/.test(row.id)
43
+ || families.has(row.familyId) && families.get(row.familyId) !== split)
44
+ throw new Error('D17 split isolation');
45
+ ids.add(row.id);
46
+ inputs.add(row.inputDigest);
47
+ families.set(row.familyId, split);
48
+ }
49
+ for (const slice of ['direct-facts', 'multi-fact', 'negation', 'authority']) {
50
+ if (group.members.filter(row => row.slice === slice).length !== group.count / 4)
51
+ throw new Error('D17 slice quota');
52
+ }
53
+ }
54
+ }
55
+ if (name === 'gold') {
56
+ const gold = value;
57
+ if (Object.keys(gold.labels).some(id => !Object.hasOwn(gold.worlds, id)))
58
+ throw new Error('D17 gold membership');
59
+ if (Object.keys(gold.worlds).some(id => !Object.hasOwn(gold.labels, id)))
60
+ throw new Error('D17 gold membership');
61
+ }
62
+ if (name === 'review') {
63
+ const review = value;
64
+ if (new Set(review.assessments.map(row => row.assessmentId)).size !== 88)
65
+ throw new Error('D17 review IDs');
66
+ for (const [stage, count] of [['development', 40], ['blind-test', 40], ['delayed-repeat', 8]]) {
67
+ const rows = review.assessments.filter(row => row.stage === stage);
68
+ const framing = stage === 'development' ? 'A fictional archive records this world.'
69
+ : 'The following is an imaginary station log.';
70
+ if (rows.length !== count || new Set(rows.map(row => row.rowId)).size !== count
71
+ || rows.some(row => !/^[0-9a-f]{64}$/.test(row.rowId) || typeof row.payload !== 'string' || !row.payload.startsWith(framing)
72
+ || heldoutDigest({ payload: row.payload }) !== row.inputDigest)) {
73
+ throw new Error('D17 review sample');
74
+ }
75
+ for (const slice of ['direct-facts', 'multi-fact', 'negation', 'authority']) {
76
+ if (rows.filter(row => row.slice === slice).length !== count / 4)
77
+ throw new Error('D17 review slice quota');
78
+ }
79
+ }
80
+ const testIds = new Set(review.assessments.filter(row => row.stage === 'blind-test').map(row => row.rowId));
81
+ if (review.assessments.some(row => row.stage === 'delayed-repeat' && !testIds.has(row.rowId)))
82
+ throw new Error('D17 review repeat');
83
+ }
84
+ }
85
+ //# sourceMappingURL=artifacts.js.map