@aiwg/cli 2026.9.24 → 2026.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +2 -1
  2. package/dist/src/artifacts/discovery-shadow-evaluator.js +177 -0
  3. package/dist/src/artifacts/discovery-shadow.js +195 -0
  4. package/dist/src/artifacts/index-builder.js +47 -9
  5. package/dist/src/artifacts/query-engine.js +94 -1
  6. package/dist/src/artifacts/types.js +1 -0
  7. package/dist/src/cli/handlers/decision.js +88 -0
  8. package/dist/src/cli/handlers/gates.js +154 -0
  9. package/dist/src/cli/handlers/index.js +5 -1
  10. package/dist/src/cli/handlers/sessions.js +10 -7
  11. package/dist/src/cli/handlers/utilities.js +20 -0
  12. package/dist/src/config/aiwg-config.js +15 -2
  13. package/dist/src/decision/acceptance.js +25 -0
  14. package/dist/src/decision/adapters/jev.js +24 -2
  15. package/dist/src/decision/calibration/registry.js +9 -0
  16. package/dist/src/decision/compose.js +74 -15
  17. package/dist/src/decision/context-live-promotion.js +344 -0
  18. package/dist/src/decision/context-live-qualification.js +591 -0
  19. package/dist/src/decision/context-pruning.js +847 -0
  20. package/dist/src/decision/driver.js +430 -0
  21. package/dist/src/decision/egress-live-qualification.js +874 -0
  22. package/dist/src/decision/ensemble/contract.js +64 -1
  23. package/dist/src/decision/ensemble/index.js +1 -0
  24. package/dist/src/decision/ensemble/report.js +8 -42
  25. package/dist/src/decision/ensemble/runtime.js +571 -0
  26. package/dist/src/decision/ensemble-study/artifacts.js +85 -0
  27. package/dist/src/decision/ensemble-study/calibration.js +443 -0
  28. package/dist/src/decision/ensemble-study/corpus.js +229 -0
  29. package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
  30. package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
  31. package/dist/src/decision/ensemble-study/multifact.js +241 -0
  32. package/dist/src/decision/ensemble-study/protocol.js +77 -0
  33. package/dist/src/decision/ensemble-study/score.js +462 -0
  34. package/dist/src/decision/ensemble-study/staged.js +87 -0
  35. package/dist/src/decision/ensemble-study/statistics.js +84 -0
  36. package/dist/src/decision/evaluate.js +227 -14
  37. package/dist/src/decision/export.js +4 -0
  38. package/dist/src/decision/feature-export.js +1074 -0
  39. package/dist/src/decision/graph-index.js +16 -0
  40. package/dist/src/decision/graph-live-qualification.js +1041 -0
  41. package/dist/src/decision/graph-live-workload.js +245 -0
  42. package/dist/src/decision/graph-skill-bridge.js +21 -3
  43. package/dist/src/decision/heldout/calibration.js +140 -0
  44. package/dist/src/decision/heldout/collector.js +430 -0
  45. package/dist/src/decision/heldout/contract.js +309 -0
  46. package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
  47. package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
  48. package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
  49. package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
  50. package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
  51. package/dist/src/decision/heldout/d29-pools.js +288 -0
  52. package/dist/src/decision/heldout/d29-v6.js +169 -0
  53. package/dist/src/decision/heldout/d29-v7.js +202 -0
  54. package/dist/src/decision/heldout/d29-v8.js +586 -0
  55. package/dist/src/decision/heldout/generator-registry.js +67 -0
  56. package/dist/src/decision/heldout/generators.js +696 -0
  57. package/dist/src/decision/heldout/index.js +8 -0
  58. package/dist/src/decision/heldout/journal.js +346 -0
  59. package/dist/src/decision/heldout/study-generators.js +64 -0
  60. package/dist/src/decision/heldout/types.js +2 -0
  61. package/dist/src/decision/host-policy.js +61 -0
  62. package/dist/src/decision/index.js +11 -0
  63. package/dist/src/decision/interop-profile.v1.json +112 -0
  64. package/dist/src/decision/interop.js +653 -0
  65. package/dist/src/decision/issue-triage/index.js +3 -0
  66. package/dist/src/decision/issue-triage/runtime.js +1069 -0
  67. package/dist/src/decision/issue-triage/types.js +2 -0
  68. package/dist/src/decision/patterns/catalog.js +28 -8
  69. package/dist/src/decision/patterns/playground.js +28 -11
  70. package/dist/src/decision/patterns/specs.js +19 -10
  71. package/dist/src/decision/preprocessed-evidence.js +724 -0
  72. package/dist/src/decision/qualification/capture.js +8 -2
  73. package/dist/src/decision/qualification/gate-evidence.js +19 -4
  74. package/dist/src/decision/qualification/quality.js +57 -49
  75. package/dist/src/decision/qualification/release.js +76 -3
  76. package/dist/src/decision/qualification/runner.js +5 -2
  77. package/dist/src/decision/routing/contract.js +175 -0
  78. package/dist/src/decision/routing/index.js +5 -0
  79. package/dist/src/decision/routing/report.js +216 -0
  80. package/dist/src/decision/routing/runtime.js +685 -0
  81. package/dist/src/decision/routing/types.js +2 -0
  82. package/dist/src/decision/sdlc-screening.js +1157 -0
  83. package/dist/src/decision/sensitivity/comparative.js +154 -0
  84. package/dist/src/decision/sensitivity/contract.js +218 -0
  85. package/dist/src/decision/sensitivity/index.js +5 -0
  86. package/dist/src/decision/sensitivity/runtime.js +912 -0
  87. package/dist/src/decision/sensitivity/types.js +2 -0
  88. package/dist/src/decision/validate.js +26 -0
  89. package/dist/src/extensions/commands/definitions.js +38 -0
  90. package/dist/src/extensions/manifest.js +44 -0
  91. package/dist/src/gates/discovery.js +315 -0
  92. package/dist/src/gates/driver.js +388 -0
  93. package/dist/src/gates/evaluate.js +719 -0
  94. package/dist/src/gates/floors.js +255 -0
  95. package/dist/src/gates/index.js +17 -0
  96. package/dist/src/gates/providers/evidence.js +38 -0
  97. package/dist/src/gates/providers/index.js +21 -0
  98. package/dist/src/gates/providers/isolated-child-source.js +446 -0
  99. package/dist/src/gates/providers/loader.js +1069 -0
  100. package/dist/src/gates/providers/paired.js +52 -0
  101. package/dist/src/gates/providers/proportion.js +60 -0
  102. package/dist/src/gates/providers/registry.js +40 -0
  103. package/dist/src/gates/providers/scalar.js +41 -0
  104. package/dist/src/gates/providers/screening.js +53 -0
  105. package/dist/src/gates/providers/types.js +25 -0
  106. package/dist/src/gates/registry.js +1003 -0
  107. package/dist/src/gates/report.js +102 -0
  108. package/dist/src/gates/schema.js +56 -0
  109. package/dist/src/gates/stats/binomial.js +26 -0
  110. package/dist/src/gates/stats/bootstrap.js +62 -0
  111. package/dist/src/gates/stats/clopper-pearson.js +95 -0
  112. package/dist/src/gates/stats/digest.js +26 -0
  113. package/dist/src/gates/stats/error.js +5 -0
  114. package/dist/src/gates/stats/index.js +9 -0
  115. package/dist/src/gates/stats/normal.js +35 -0
  116. package/dist/src/gates/stats/paired.js +120 -0
  117. package/dist/src/gates/stats/splits.js +30 -0
  118. package/dist/src/gates/types.js +8 -0
  119. package/dist/src/governance/redaction.js +14 -4
  120. package/dist/src/mcp/muse-native-config.mjs +123 -0
  121. package/dist/src/mcp/tools/decision.mjs +188 -0
  122. package/dist/src/mcp/tools/subsystems.mjs +2 -0
  123. package/dist/src/providers/capability-matrix.yaml +2 -1
  124. package/dist/src/providers/provider-definitions.js +2 -1
  125. package/dist/src/sessions/adapters/muse.js +336 -39
  126. package/dist/src/sessions/batch-import.js +7 -0
  127. package/dist/src/sessions/workspace-discovery.js +13 -2
  128. package/package.json +1 -1
  129. package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
  130. package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
  131. package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
  132. package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
  133. package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
  134. package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
  135. package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
  136. package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
  137. package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
  138. package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
  139. package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
  140. package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
  141. package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
  142. package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
  143. package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
  144. package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
  145. package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
  146. package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
  147. package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
  148. package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
  149. package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
  150. package/schemas/decision/D29Study.v1.schema.json +1903 -0
  151. package/schemas/decision/D29Study.v2.schema.json +1344 -0
  152. package/schemas/decision/D29Study.v3.schema.json +609 -0
  153. package/schemas/decision/D29Study.v4.schema.json +1133 -0
  154. package/schemas/decision/D29Study.v6.schema.json +1205 -0
  155. package/schemas/decision/D29Study.v7.schema.json +1471 -0
  156. package/schemas/decision/D29Study.v8.schema.json +671 -0
  157. package/schemas/decision/D29Study.v9.schema.json +778 -0
  158. package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
  159. package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
  160. package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
  161. package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
  162. package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
  163. package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
  164. package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
  165. package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
  166. package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
  167. package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
  168. package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
  169. package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
  170. package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
  171. package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
  172. package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
  173. package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
  174. package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
  175. package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
  176. package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
  177. package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
  178. package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
  179. package/schemas/decision/DecisionRuleset.schema.json +8 -2
  180. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
  181. package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
  182. package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
  183. package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
  184. package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
  185. package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
  186. package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
  187. package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
  188. package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
  189. package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
  190. package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
  191. package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
  192. package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
  193. package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
  194. package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
  195. package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
  196. package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
  197. package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
  198. package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
  199. package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
  200. package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
  201. package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
  202. package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
  203. package/tools/agents/deploy-agents.mjs +1 -1
  204. package/tools/agents/providers/base.mjs +26 -7
  205. package/tools/agents/providers/factory.mjs +10 -2
  206. package/tools/agents/providers/muse-hooks.mjs +1 -29
  207. package/tools/agents/providers/muse.mjs +6 -3
@@ -0,0 +1,719 @@
1
+ import { qualificationIntegrityAllowlistProblems } from '../decision/qualification/release.js';
2
+ import { artifactDigest } from '../decision/validate.js';
3
+ import { evaluatePredicate } from '../decision/predicates.js';
4
+ import { clopperPearsonInterval } from './stats/clopper-pearson.js';
5
+ import { wilsonScoreInterval } from './stats/binomial.js';
6
+ import { pairedMeanDifferenceBootstrap } from './stats/bootstrap.js';
7
+ import { PairedDifferenceError } from './stats/error.js';
8
+ import { pairedBinaryDifferenceInterval, pairedNonInferiority } from './stats/paired.js';
9
+ import { GateRegistry, authoredGatePacksOf, gateProvidersOf, resolveGateBinding } from './registry.js';
10
+ import { GATE_PROVIDER_DEFAULT_TIMEOUT_MS, GATE_PROVIDER_MAX_OUTPUT_BYTES, deriveIsolatedSeed, isLoaderIssuedProvider, runIsolatedProviderSync, sealProviderSection, } from './providers/loader.js';
11
+ import { qualifyGateParameter } from './types.js';
12
+ export class GateEvaluationError extends Error {
13
+ constructor(message) { super(message); this.name = 'GateEvaluationError'; }
14
+ }
15
+ const RANK = { PROMOTE: 0, HOLD: 1, ROLLBACK: 2 };
16
+ /** Shared outcome lattice. The maximum never upgrades any component. */
17
+ export function maxOutcome(...outcomes) {
18
+ return outcomes.reduce((worst, outcome) => (RANK[outcome] > RANK[worst] ? outcome : worst), 'PROMOTE');
19
+ }
20
+ /**
21
+ * Per-gate outcome from its own status: `pass` promotes, `fail` takes the
22
+ * gate's `onFail`, `insufficient` takes `onInsufficient` (always HOLD by load
23
+ * validation; ROLLBACK needs an observed blocking failure). Shortfalls never PROMOTE.
24
+ */
25
+ export function gateStatusOutcome(status, gate) {
26
+ if (status === 'pass')
27
+ return 'PROMOTE';
28
+ if (status === 'fail')
29
+ return gate.onFail;
30
+ return gate.onInsufficient ?? 'HOLD';
31
+ }
32
+ /** Binds an integrity report for evaluation. The digest is re-derived on every use; a forged copy is refused. */
33
+ export function sealUpstream(metadata) {
34
+ return { metadata, digest: artifactDigest(metadata) };
35
+ }
36
+ /**
37
+ * Seals trusted holdout inputs for evaluation. The seal covers
38
+ * `{frozenDigest, firstAccessedAt}` and is re-derived by `evaluateGates` on
39
+ * every use, following the HeldoutFrozen seal (`readHeldoutFrozen` checks
40
+ * `frozen.digest === heldoutDigest(frozen.bundle)`): a spread-copied or edited
41
+ * record is refused. The seal is an unkeyed integrity digest, not an
42
+ * authenticity proof, so the holdout inputs are trusted caller inputs built from
43
+ * verified HeldoutFrozen and access records (verification of the frozen record
44
+ * itself is tracked in #2833). `frozenDigest` is the binding digest in the frozen record.
45
+ */
46
+ export function sealGateHoldout(input) {
47
+ const payload = { frozenDigest: input.frozenDigest, firstAccessedAt: input.firstAccessedAt };
48
+ return { ...payload, digest: artifactDigest(payload) };
49
+ }
50
+ const fail = (message) => { throw new GateEvaluationError(message); };
51
+ const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
52
+ function supportOf(observation) {
53
+ const n = observation?.n;
54
+ return typeof n === 'number' && Number.isSafeInteger(n) && n >= 0 ? n : null;
55
+ }
56
+ function poolObservations(kind, observations) {
57
+ if (!observations.length)
58
+ return null;
59
+ if (kind === 'proportion') {
60
+ let n = 0;
61
+ let events = 0;
62
+ for (const observation of observations) {
63
+ if (supportOf(observation) === null || typeof observation.events !== 'number'
64
+ || !Number.isSafeInteger(observation.events) || observation.events < 0)
65
+ return null;
66
+ if (observation.events > observation.n)
67
+ return null;
68
+ n += observation.n;
69
+ events += observation.events;
70
+ }
71
+ return { n, events };
72
+ }
73
+ if (kind === 'paired') {
74
+ const total = { both: 0, candidateOnly: 0, baselineOnly: 0, neither: 0 };
75
+ let n = 0;
76
+ for (const observation of observations) {
77
+ const cells = isRecord(observation?.paired)
78
+ ? ['both', 'candidateOnly', 'baselineOnly', 'neither'].map(key => observation.paired[key])
79
+ : [];
80
+ if (supportOf(observation) === null || cells.length !== 4
81
+ || cells.some(cell => !Number.isSafeInteger(cell) || cell < 0)) {
82
+ return null;
83
+ }
84
+ const [both, candidateOnly, baselineOnly, neither] = cells;
85
+ // The counted support must equal the cell sums; a disagreement is unknown, fail closed.
86
+ if (observation.n !== both + candidateOnly + baselineOnly + neither)
87
+ return null;
88
+ n += observation.n;
89
+ total.both += both;
90
+ total.candidateOnly += candidateOnly;
91
+ total.baselineOnly += baselineOnly;
92
+ total.neither += neither;
93
+ }
94
+ return { n, paired: total };
95
+ }
96
+ if (kind === 'scalar') {
97
+ let n = 0;
98
+ let weighted = 0;
99
+ for (const observation of observations) {
100
+ if (supportOf(observation) === null || typeof observation.value !== 'number' || !Number.isFinite(observation.value))
101
+ return null;
102
+ n += observation.n;
103
+ weighted += observation.n * observation.value;
104
+ }
105
+ return n === 0 ? null : { n, value: weighted / n };
106
+ }
107
+ if (kind === 'differences') {
108
+ const differences = [];
109
+ let n = 0;
110
+ for (const observation of observations) {
111
+ if (supportOf(observation) === null || !Array.isArray(observation.differences)
112
+ || observation.differences.some(value => typeof value !== 'number' || !Number.isFinite(value)))
113
+ return null;
114
+ if (observation.n !== observation.differences.length)
115
+ return null;
116
+ n += observation.n;
117
+ differences.push(...observation.differences);
118
+ }
119
+ return { n, differences };
120
+ }
121
+ if (kind === 'evidence') {
122
+ const available = observations.flatMap(observation => observation?.available ?? []);
123
+ if (observations.some(observation => !Array.isArray(observation?.available)))
124
+ return null;
125
+ return { available };
126
+ }
127
+ return null;
128
+ }
129
+ function seriesFor(gate, pack, metrics) {
130
+ if (gate.metric === undefined)
131
+ return null;
132
+ const declared = pack.resolved.spec.metrics[gate.metric.name];
133
+ if (declared === undefined || declared.provider !== gate.metric.provider)
134
+ return null;
135
+ return metrics.providers[gate.metric.provider]?.metrics[gate.metric.name] ?? null;
136
+ }
137
+ function metricKindOf(gate, pack) {
138
+ if (gate.metric === undefined)
139
+ return '';
140
+ return pack.resolved.spec.metrics[gate.metric.name]?.kind ?? '';
141
+ }
142
+ function expandScope(gate, kind, series, slices) {
143
+ const mode = gate.scope.mode;
144
+ if (mode === 'all')
145
+ return [{ slice: null, observation: series?.pooled ?? null }];
146
+ if (mode === 'pooled') {
147
+ const observations = (gate.scope.slices ?? []).map(slice => series?.bySlice[slice] ?? null);
148
+ if (observations.some(observation => observation === null)) {
149
+ return [{ slice: null, observation: null }];
150
+ }
151
+ return [{ slice: null, observation: poolObservations(kind, observations) }];
152
+ }
153
+ const excluded = new Set(gate.scope.except ?? []);
154
+ const names = mode === 'each' ? slices.filter(slice => !excluded.has(slice)) : (gate.scope.slices ?? []);
155
+ return names.map(slice => ({ slice, observation: series?.bySlice[slice] ?? null }));
156
+ }
157
+ function pairedCells(observation) {
158
+ if (!isRecord(observation?.paired))
159
+ return null;
160
+ const cells = ['both', 'candidateOnly', 'baselineOnly', 'neither'].map(key => observation.paired[key]);
161
+ if (cells.some(cell => !Number.isSafeInteger(cell) || cell < 0))
162
+ return null;
163
+ const [both, candidateOnly, baselineOnly, neither] = cells;
164
+ return { both, candidateOnly, baselineOnly, neither };
165
+ }
166
+ /**
167
+ * Resolve a literal-or-parameter gate number against the binding's
168
+ * preregistered values. Null when a `{param}` reference has no resolved
169
+ * value: callers hold as `parameter-missing`, never promote.
170
+ */
171
+ function resolveGateNumber(value, packId, parameters) {
172
+ if (value === undefined)
173
+ return null;
174
+ if (typeof value === 'number')
175
+ return value;
176
+ if (value !== null && typeof value === 'object' && typeof value.param === 'string') {
177
+ const resolved = parameters[qualifyGateParameter(packId, value.param)];
178
+ return typeof resolved === 'number' ? resolved : null;
179
+ }
180
+ return null;
181
+ }
182
+ function evaluateObservation(gate, observation, context) {
183
+ const { packId, parameters, metrics, upstream, nowMs, slice, slices } = context;
184
+ const base = { slice, n: supportOf(observation) };
185
+ const minimumN = gate.minimumN === undefined ? 0 : resolveGateNumber(gate.minimumN, packId, parameters);
186
+ // Evidence gates observe attestations, not counted support; upstream and predicate
187
+ // gates read the ceiling and the whole document. None of them take an n.
188
+ if (gate.kind !== 'upstream-ceiling' && gate.kind !== 'predicate' && gate.kind !== 'evidence') {
189
+ if (observation === null || observation === undefined)
190
+ return { ...base, status: 'insufficient', reason: 'metric-missing' };
191
+ if (base.n === null)
192
+ return { ...base, status: 'insufficient', reason: 'support-unknown' };
193
+ if (minimumN === null)
194
+ return { ...base, status: 'insufficient', reason: 'parameter-missing' };
195
+ if (base.n < minimumN)
196
+ return { ...base, status: 'insufficient', reason: 'below-minimum-n' };
197
+ }
198
+ const thresholdValue = () => {
199
+ if (gate.threshold?.value !== undefined)
200
+ return gate.threshold.value;
201
+ if (gate.threshold?.param !== undefined) {
202
+ const value = parameters[qualifyGateParameter(packId, gate.threshold.param)];
203
+ return typeof value === 'number' ? value : null;
204
+ }
205
+ return null;
206
+ };
207
+ switch (gate.kind) {
208
+ case 'minimum-n': {
209
+ const target = gate.minimumN === undefined ? null : resolveGateNumber(gate.minimumN, packId, parameters);
210
+ if (target === null) {
211
+ return { ...base, status: 'insufficient', reason: 'parameter-missing',
212
+ statistic: { method: 'support-count' } };
213
+ }
214
+ const passed = base.n >= target;
215
+ return { ...base, status: passed ? 'pass' : 'insufficient', reason: passed ? 'support-met' : 'below-minimum-n',
216
+ statistic: { method: 'support-count' } };
217
+ }
218
+ case 'interval-bound': {
219
+ const events = observation?.events;
220
+ if (typeof events !== 'number' || !Number.isSafeInteger(events) || events < 0) {
221
+ return { ...base, status: 'insufficient', reason: 'events-unknown', events: null };
222
+ }
223
+ if (events > base.n)
224
+ return { ...base, status: 'insufficient', reason: 'events-exceed-n', events };
225
+ const target = thresholdValue();
226
+ if (target === null)
227
+ return { ...base, status: 'insufficient', reason: 'parameter-missing', events };
228
+ const method = gate.statistic?.method ?? 'wilson';
229
+ const levelBps = resolveGateNumber(gate.statistic?.levelBps, packId, parameters);
230
+ if (levelBps === null)
231
+ return { ...base, status: 'insufficient', reason: 'parameter-missing', events };
232
+ let lower;
233
+ let upper;
234
+ try {
235
+ if (method === 'clopper-pearson') {
236
+ ({ lower, upper } = clopperPearsonInterval({ events, n: base.n, levelBps }));
237
+ }
238
+ else {
239
+ [lower, upper] = wilsonScoreInterval({ events, n: base.n, levelBps });
240
+ }
241
+ }
242
+ catch (error) {
243
+ if (error instanceof PairedDifferenceError) {
244
+ return { ...base, status: 'insufficient', reason: 'statistic-invalid', events };
245
+ }
246
+ throw error;
247
+ }
248
+ const lowerBps = Math.max(0, Math.floor(lower * 10000));
249
+ const upperBps = Math.min(10000, Math.ceil(upper * 10000));
250
+ const estimateBps = Math.round(events / base.n * 10000);
251
+ const passed = gate.statistic?.bound === 'upper' ? upperBps <= target : lowerBps >= target;
252
+ return { ...base, status: passed ? 'pass' : 'fail', reason: passed ? 'bound-within-threshold' : 'bound-breaches-threshold',
253
+ events, statistic: { method, levelBps, lowerBps, upperBps, estimateBps },
254
+ threshold: { op: gate.threshold?.op, ...(gate.threshold?.value !== undefined
255
+ ? { value: gate.threshold.value } : { param: gate.threshold?.param }), resolved: target } };
256
+ }
257
+ case 'paired-difference': {
258
+ const cells = pairedCells(observation);
259
+ if (cells === null)
260
+ return { ...base, status: 'insufficient', reason: 'paired-unknown' };
261
+ if (base.n !== cells.both + cells.candidateOnly + cells.baselineOnly + cells.neither) {
262
+ return { ...base, status: 'insufficient', reason: 'observation-mismatch' };
263
+ }
264
+ const method = gate.statistic?.method ?? 'newcombe';
265
+ const levelBps = resolveGateNumber(gate.statistic?.levelBps, packId, parameters);
266
+ if (levelBps === null)
267
+ return { ...base, status: 'insufficient', reason: 'parameter-missing' };
268
+ let interval;
269
+ try {
270
+ interval = pairedBinaryDifferenceInterval({ counts: cells, levelBps, method: method === 'tango' ? 'tango' : 'newcombe-10' });
271
+ }
272
+ catch (error) {
273
+ if (error instanceof PairedDifferenceError)
274
+ return { ...base, status: 'insufficient', reason: 'paired-invalid' };
275
+ throw error;
276
+ }
277
+ const mode = gate.statistic?.mode ?? 'non-inferiority';
278
+ const margin = gate.statistic?.marginBps ?? 0;
279
+ const statistic = { method, levelBps, lowerBps: interval.lowerBps, upperBps: interval.upperBps,
280
+ estimateBps: interval.estimateBps };
281
+ const threshold = { op: 'gte', resolved: margin };
282
+ if (mode === 'non-inferiority') {
283
+ const verdict = pairedNonInferiority({ interval, marginBps: margin });
284
+ if (verdict.decision === 'insufficient')
285
+ return { ...base, status: 'insufficient', reason: 'paired-invalid', statistic, threshold };
286
+ const passed = verdict.decision === 'non-inferior';
287
+ return { ...base, status: passed ? 'pass' : 'fail',
288
+ reason: passed ? 'lower-bound-at-or-above-margin' : 'lower-bound-below-margin', statistic, threshold };
289
+ }
290
+ const passed = interval.lowerBps > margin;
291
+ return { ...base, status: passed ? 'pass' : 'fail',
292
+ reason: passed ? 'lower-bound-above-margin' : 'lower-bound-at-or-below-margin', statistic, threshold };
293
+ }
294
+ case 'bootstrap-bound': {
295
+ const differences = observation?.differences;
296
+ if (!Array.isArray(differences))
297
+ return { ...base, status: 'insufficient', reason: 'differences-unknown' };
298
+ if (differences.length !== base.n)
299
+ return { ...base, status: 'insufficient', reason: 'observation-mismatch' };
300
+ const target = thresholdValue();
301
+ if (target === null)
302
+ return { ...base, status: 'insufficient', reason: 'parameter-missing' };
303
+ const levelBps = resolveGateNumber(gate.statistic?.levelBps, packId, parameters);
304
+ if (levelBps === null)
305
+ return { ...base, status: 'insufficient', reason: 'parameter-missing' };
306
+ let interval;
307
+ try {
308
+ interval = pairedMeanDifferenceBootstrap({ differences, levelBps,
309
+ seed: gate.statistic?.seed, resamples: gate.statistic?.resamples,
310
+ bounds: gate.statistic?.bounds });
311
+ }
312
+ catch (error) {
313
+ if (error instanceof PairedDifferenceError)
314
+ return { ...base, status: 'insufficient', reason: 'statistic-invalid' };
315
+ throw error;
316
+ }
317
+ const passed = gate.statistic?.bound === 'upper' ? interval.upperBps <= target : interval.lowerBps >= target;
318
+ return { ...base, status: passed ? 'pass' : 'fail', reason: passed ? 'bound-within-threshold' : 'bound-breaches-threshold',
319
+ statistic: { method: 'bootstrap', levelBps, lowerBps: interval.lowerBps,
320
+ upperBps: interval.upperBps, estimateBps: interval.estimateBps },
321
+ threshold: { op: gate.threshold?.op, ...(gate.threshold?.value !== undefined
322
+ ? { value: gate.threshold.value } : { param: gate.threshold?.param }), resolved: target } };
323
+ }
324
+ case 'count-max':
325
+ case 'count-min': {
326
+ const events = observation?.events;
327
+ if (typeof events !== 'number' || !Number.isSafeInteger(events) || events < 0) {
328
+ return { ...base, status: 'insufficient', reason: 'events-unknown', events: null };
329
+ }
330
+ if (events > base.n)
331
+ return { ...base, status: 'insufficient', reason: 'events-exceed-n', events };
332
+ const target = thresholdValue();
333
+ if (target === null)
334
+ return { ...base, status: 'insufficient', reason: 'parameter-missing', events };
335
+ const passed = gate.kind === 'count-max' ? events <= target : events >= target;
336
+ return { ...base, status: passed ? 'pass' : 'fail', reason: passed ? 'count-within-threshold' : 'count-breaches-threshold',
337
+ events, statistic: { method: 'exact-count' },
338
+ threshold: { op: gate.threshold?.op, ...(gate.threshold?.value !== undefined
339
+ ? { value: gate.threshold.value } : { param: gate.threshold?.param }), resolved: target } };
340
+ }
341
+ case 'value-threshold': {
342
+ const value = observation?.value;
343
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
344
+ return { ...base, status: 'insufficient', reason: 'value-unknown' };
345
+ }
346
+ const target = thresholdValue();
347
+ if (target === null)
348
+ return { ...base, status: 'insufficient', reason: 'parameter-missing' };
349
+ const passed = gate.threshold?.op === 'lte' ? value <= target : value >= target;
350
+ return { ...base, status: passed ? 'pass' : 'fail', reason: passed ? 'value-within-threshold' : 'value-breaches-threshold',
351
+ statistic: { method: 'exact-value' },
352
+ threshold: { op: gate.threshold?.op, ...(gate.threshold?.value !== undefined
353
+ ? { value: gate.threshold.value } : { param: gate.threshold?.param }), resolved: target } };
354
+ }
355
+ case 'evidence': {
356
+ const available = observation?.available;
357
+ if (!Array.isArray(available))
358
+ return { ...base, status: 'insufficient', reason: 'evidence-unknown' };
359
+ for (const id of gate.evidence?.required ?? []) {
360
+ const entry = available.find(candidate => candidate?.id === id);
361
+ if (entry === undefined)
362
+ return { ...base, status: 'insufficient', reason: 'evidence-missing', statistic: { method: 'evidence-present' } };
363
+ if (entry.passed !== true) {
364
+ return { ...base, status: 'fail', reason: 'evidence-failed', statistic: { method: 'evidence-present' } };
365
+ }
366
+ if (entry.expiresAt !== undefined && entry.expiresAt !== null) {
367
+ const expiry = Date.parse(entry.expiresAt);
368
+ if (!Number.isFinite(expiry) || expiry <= nowMs) {
369
+ return { ...base, status: 'fail', reason: 'evidence-expired', statistic: { method: 'evidence-present' } };
370
+ }
371
+ }
372
+ }
373
+ return { ...base, status: 'pass', reason: 'evidence-present', statistic: { method: 'evidence-present' } };
374
+ }
375
+ case 'predicate': {
376
+ let value = 'unknown';
377
+ try {
378
+ value = evaluatePredicate(gate.predicate, { metrics, parameters, slices }, {});
379
+ }
380
+ catch {
381
+ value = 'unknown';
382
+ }
383
+ if (value === true)
384
+ return { ...base, status: 'pass', reason: 'predicate-true', statistic: { method: 'predicate' } };
385
+ if (value === false)
386
+ return { ...base, status: 'fail', reason: 'predicate-false', statistic: { method: 'predicate' } };
387
+ return { ...base, status: 'insufficient', reason: 'predicate-unknown', statistic: { method: 'predicate' } };
388
+ }
389
+ case 'upstream-ceiling': {
390
+ if (upstream === null)
391
+ return { ...base, status: 'insufficient', reason: 'upstream-missing',
392
+ statistic: { method: 'upstream-integrity' } };
393
+ const problems = qualificationIntegrityAllowlistProblems(upstream.metadata);
394
+ if (!problems.length) {
395
+ return { ...base, status: 'pass', reason: 'upstream-clean', statistic: { method: 'upstream-integrity' } };
396
+ }
397
+ const compromised = problems.includes('compromised')
398
+ || upstream.metadata.release_gate?.decision === 'ROLLBACK'
399
+ || upstream.metadata.integrity_state === 'compromised';
400
+ return { ...base, status: 'fail', reason: compromised ? 'upstream-compromised' : 'upstream-allowlist',
401
+ statistic: { method: 'upstream-integrity' } };
402
+ }
403
+ }
404
+ }
405
+ function digestRecordsOrFail(records, providerId) {
406
+ try {
407
+ return artifactDigest(records);
408
+ }
409
+ catch {
410
+ return fail(`metric provider ${providerId} records are not digestible`);
411
+ }
412
+ }
413
+ function serializeRecordsOrFail(records, providerId) {
414
+ try {
415
+ const serialized = JSON.stringify(records);
416
+ if (typeof serialized !== 'string')
417
+ throw new Error('unserializable');
418
+ return serialized;
419
+ }
420
+ catch {
421
+ return fail(`metric provider ${providerId} records are not JSON-serializable`);
422
+ }
423
+ }
424
+ function rerunProviderOrFail(provider, args, providerId) {
425
+ // C2: execute only the registry's loader-issued provider. The runner makes
426
+ // its own copy of the loader-captured bytes, re-digests exactly the bytes it
427
+ // sends to the sandbox and refuses unless they match the pinned digest.
428
+ // Caller-supplied runtime objects (snapshot paths, digests) are never used.
429
+ if (!isLoaderIssuedProvider(provider)) {
430
+ return fail(`metric provider ${providerId} was not issued by the bundle loader: refusing to re-run it`);
431
+ }
432
+ try {
433
+ return runIsolatedProviderSync(provider, {
434
+ mode: 'invoke', recordsJson: args.recordsJson, clockMs: args.clockMs, seed: args.seed,
435
+ expectedCodeDigest: args.codeDigest,
436
+ }, { timeoutMs: args.timeoutMs, maxOutputBytes: GATE_PROVIDER_MAX_OUTPUT_BYTES }).result;
437
+ }
438
+ catch (error) {
439
+ return fail(`metric provider ${providerId} re-run failed: ${error instanceof Error ? error.message : 'unknown'}`);
440
+ }
441
+ }
442
+ function upstreamOutcome(upstream) {
443
+ if (upstream === null)
444
+ return { ceiling: 'HOLD', problems: ['upstream-ceiling-missing'] };
445
+ const problems = qualificationIntegrityAllowlistProblems(upstream.metadata);
446
+ const release = upstream.metadata.release_gate?.decision;
447
+ const compromised = problems.includes('compromised')
448
+ || release === 'ROLLBACK'
449
+ || upstream.metadata.integrity_state === 'compromised';
450
+ if (compromised)
451
+ return { ceiling: 'ROLLBACK', problems };
452
+ if (problems.length || release === 'HOLD')
453
+ return { ceiling: 'HOLD', problems };
454
+ return { ceiling: 'PROMOTE', problems };
455
+ }
456
+ /**
457
+ * Pure, deterministic gate evaluation. The binding is resolved internally
458
+ * against the registry: pack pins (authored + composed digests), provider pins
459
+ * and parameters are re-derived, so no caller-supplied resolution is accepted.
460
+ * Precondition violations (untrusted digests, provider pin mismatches,
461
+ * missing trusted holdout inputs, frozen-after-holdout bindings) throw before
462
+ * any gate runs, so failure paths never discard completed results. Per-gate
463
+ * shortfalls report `insufficient`, which never promotes.
464
+ *
465
+ * Interval levels are two-sided confidence levels applied to one-sided bounds:
466
+ * the reported bound is the one-sided edge of a two-sided interval, which is
467
+ * conservative (wider) relative to a one-sided interval at the same level.
468
+ */
469
+ export function evaluateGates(input) {
470
+ const { binding, registry, trustedBindingDigest, holdout, metrics, upstream, now } = input;
471
+ if ('resolved' in input
472
+ && input.resolved !== undefined) {
473
+ fail('caller-supplied gate resolution is not accepted: pass the binding and registry');
474
+ }
475
+ if (!(registry instanceof GateRegistry))
476
+ fail('a GateRegistry is required for internal resolution');
477
+ if (input.floors === undefined) {
478
+ fail('project floors are required: pass resolveProjectFloors(config) or \'none-explicit-opt-out\' with a documented reason');
479
+ }
480
+ const floors = input.floors === 'none-explicit-opt-out' ? undefined : input.floors;
481
+ // Reserve before dispatch: resolve and fully validate the binding (pins,
482
+ // providers, parameters, slices) before any gate observes any metric. The
483
+ // resolution runs through the pure `resolveGateBinding` over a standalone
484
+ // snapshot of the registry's authored packs: no overridable `GateRegistry`
485
+ // instance method is called for anything security-relevant, so a subclass
486
+ // that overrides `resolveBinding` (or a duck-typed `{resolveBinding}`)
487
+ // cannot empty gates, loosen parameters or forge digests.
488
+ const resolved = resolveGateBinding(binding, authoredGatePacksOf(registry), gateProvidersOf(registry), floors);
489
+ const { packs, parameters } = resolved;
490
+ // 1:1 pins<->packs: the pin list and the resolved packs must match exactly.
491
+ if (packs.length !== binding.spec.packs.length)
492
+ fail('binding pins do not match resolved packs 1:1');
493
+ for (const pack of packs) {
494
+ // Recompute the authored digest: the stored pin is never trusted on its own.
495
+ if (artifactDigest(pack.authored) !== pack.digest) {
496
+ fail(`pack digest mismatch for ${pack.authored.metadata.id}`);
497
+ }
498
+ // Recompute the composed digest from the evaluator's own pure composition
499
+ // (`resolveGateBinding` above): registry-provided resolved bytes and
500
+ // digests are never trusted.
501
+ if (artifactDigest(pack.resolved) !== pack.resolvedDigest) {
502
+ fail(`composed pack digest mismatch for ${pack.authored.metadata.id}`);
503
+ }
504
+ const pin = binding.spec.packs.find(candidate => candidate.id === pack.authored.metadata.id);
505
+ if (pin === undefined || pin.version !== pack.authored.metadata.version
506
+ || pin.digest !== pack.digest || pin.resolvedDigest !== pack.resolvedDigest) {
507
+ fail(`pack pin mismatch for ${pack.authored.metadata.id}`);
508
+ }
509
+ }
510
+ // Every metric section consumed must be pinned; an unpinned section is
511
+ // refused rather than evaluated. Null version/digests never match. Bundle
512
+ // sections (#2831 rework) additionally carry the trusted code digest and
513
+ // the input-records digest sealed by the host: the section code digest must
514
+ // match the binding pin (verified above against the loaded provider), never
515
+ // a provider-declared field on its own. Sections covered by an isolated
516
+ // re-run are reproduced below and their caller-asserted values ignored, so
517
+ // a missing caller section is tolerated only with runtime coverage.
518
+ const runtimeById = new Map((input.providerRuntime?.providers ?? []).map(provider => [provider.id, provider]));
519
+ const runtimeCovers = (id) => runtimeById.has(id) && input.providerRuntime?.records[id] !== undefined;
520
+ for (const pin of binding.spec.metricProviders) {
521
+ const section = metrics.providers[pin.id];
522
+ if (section === undefined && runtimeCovers(pin.id))
523
+ continue;
524
+ if (section === undefined || section.version !== pin.version || section.sourceDigest !== pin.sourceDigest) {
525
+ fail(`metric provider pin mismatch for ${pin.id}`);
526
+ }
527
+ const pinnedCode = pin.codeDigest;
528
+ const sectionCode = section.codeDigest;
529
+ const sectionRecords = section.recordsDigest;
530
+ if (typeof pinnedCode === 'string') {
531
+ if (sectionCode !== pinnedCode)
532
+ fail(`metric provider pin mismatch for ${pin.id}`);
533
+ if (typeof sectionRecords !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(sectionRecords)) {
534
+ fail(`metric provider pin mismatch for ${pin.id}`);
535
+ }
536
+ }
537
+ else if (sectionCode !== undefined) {
538
+ fail(`metric provider pin mismatch for ${pin.id}`);
539
+ }
540
+ }
541
+ for (const id of Object.keys(metrics.providers)) {
542
+ if (!binding.spec.metricProviders.some(pin => pin.id === id)) {
543
+ fail(`metric provider ${id} is not pinned by the binding`);
544
+ }
545
+ }
546
+ if (upstream !== null && artifactDigest(upstream.metadata) !== upstream.digest) {
547
+ fail('upstream integrity digest does not match its report');
548
+ }
549
+ // Omitting the upstream record must never downgrade a bound ceiling to
550
+ // HOLD: when the resolved binding observes any upstream-ceiling gate
551
+ // (including the default floor's), evaluation without a sealed upstream is
552
+ // refused here in the evaluator, not just at the CLI.
553
+ if (upstream === null
554
+ && packs.some(pack => pack.resolved.spec.gates.some(gate => gate.kind === 'upstream-ceiling'))) {
555
+ fail('evaluation requires a sealed upstream record: the binding observes an upstream-ceiling gate');
556
+ }
557
+ const nowMs = Date.parse(now);
558
+ if (!Number.isFinite(nowMs))
559
+ fail('evaluation timestamp is not a valid date-time');
560
+ if (artifactDigest(binding) !== trustedBindingDigest)
561
+ fail('binding digest does not match its trusted pin');
562
+ // Holdout freeze is enforced from the REQUIRED sealed holdout record, never
563
+ // from binding fields (which the study author controls). The seal is
564
+ // re-derived here: `frozenDigest` and `firstAccessedAt` come only from a
565
+ // `sealGateHoldout` record whose digest matches its content.
566
+ if (holdout === undefined || holdout === null)
567
+ fail('trusted holdout inputs are required for evaluation');
568
+ const sealed = holdout;
569
+ if (typeof sealed.digest !== 'string'
570
+ || artifactDigest({ frozenDigest: sealed.frozenDigest, firstAccessedAt: sealed.firstAccessedAt }) !== sealed.digest) {
571
+ fail('holdout record seal does not match its content');
572
+ }
573
+ if (sealed.frozenDigest !== trustedBindingDigest)
574
+ fail('binding digest does not match the frozen record digest');
575
+ const frozenDigest = sealed.frozenDigest;
576
+ const firstAccessedAt = sealed.firstAccessedAt;
577
+ if (firstAccessedAt !== null && typeof firstAccessedAt !== 'string') {
578
+ fail('trusted holdout first-access time must be a date-time or null');
579
+ }
580
+ // Null access is allowed only when the binding declares no held-out split:
581
+ // a split binding with a null record would disable the freeze check.
582
+ const declaresSplit = binding.spec.splitDigest !== undefined
583
+ || binding.spec.corpusDigest !== undefined || binding.spec.goldDigest !== undefined;
584
+ if (firstAccessedAt === null && declaresSplit) {
585
+ fail('holdout record reports no access but the binding declares held-out data');
586
+ }
587
+ const frozenAt = Date.parse(binding.spec.frozenAt);
588
+ if (!Number.isFinite(frozenAt))
589
+ fail('binding freeze timestamp is not a valid date-time');
590
+ if (frozenAt > nowMs)
591
+ fail('binding freezes after the evaluation timestamp');
592
+ if (firstAccessedAt !== null) {
593
+ const accessedAt = Date.parse(firstAccessedAt);
594
+ if (!Number.isFinite(accessedAt))
595
+ fail('holdout access timestamp is not a valid date-time');
596
+ if (frozenAt >= accessedAt)
597
+ fail('binding froze at or after holdout access: evaluation refused');
598
+ if (accessedAt > nowMs)
599
+ fail('holdout access timestamp is after the evaluation timestamp');
600
+ }
601
+ // Records binding (P4): every bundle-provider pin is reproduced by an
602
+ // isolated re-run of the pinned provider over records digesting to the
603
+ // pin's records digest, and the re-run output replaces the caller-asserted
604
+ // section. Without runtime coverage the section cannot be reproduced and
605
+ // evaluation refuses (fail closed, including the file path).
606
+ const effectiveProviders = { ...metrics.providers };
607
+ for (const pin of binding.spec.metricProviders) {
608
+ const pinnedCode = pin.codeDigest;
609
+ if (typeof pinnedCode !== 'string')
610
+ continue;
611
+ const pinnedRecords = pin.recordsDigest;
612
+ const snapshot = runtimeById.get(pin.id) ?? fail(`metric provider ${pin.id} requires an isolated re-run`
613
+ + ' over its pinned records: caller-asserted metrics cannot reproduce provider output');
614
+ const records = input.providerRuntime?.records[pin.id] ?? fail(`metric provider ${pin.id} requires an isolated re-run`
615
+ + ' over its pinned records: caller-asserted metrics cannot reproduce provider output');
616
+ const registered = gateProvidersOf(registry).get(pin.id);
617
+ if (registered.version !== pin.version || registered.sourceDigest !== pin.sourceDigest
618
+ || registered.codeDigest !== pinnedCode
619
+ || snapshot.version !== pin.version || snapshot.sourceDigest !== pin.sourceDigest
620
+ || snapshot.codeDigest !== pinnedCode) {
621
+ fail(`metric provider pin mismatch for ${pin.id}`);
622
+ }
623
+ if (!Array.isArray(records))
624
+ fail(`metric provider ${pin.id} records must be an array`);
625
+ const actualRecordsDigest = digestRecordsOrFail(records, pin.id);
626
+ if (actualRecordsDigest !== pinnedRecords) {
627
+ fail(`metric provider ${pin.id} records do not match the pinned records digest`);
628
+ }
629
+ const recordsJson = serializeRecordsOrFail(records, pin.id);
630
+ const reproduced = rerunProviderOrFail(registered, {
631
+ recordsJson, clockMs: nowMs, seed: deriveIsolatedSeed(pinnedCode, pinnedRecords),
632
+ timeoutMs: input.providerRuntime?.timeoutMs ?? GATE_PROVIDER_DEFAULT_TIMEOUT_MS,
633
+ codeDigest: pinnedCode,
634
+ }, pin.id);
635
+ try {
636
+ effectiveProviders[pin.id] = sealProviderSection(registered, records, reproduced);
637
+ }
638
+ catch (error) {
639
+ fail(`metric provider ${pin.id} re-run failed: ${error instanceof Error ? error.message : 'unknown'}`);
640
+ }
641
+ }
642
+ const effectiveMetrics = { ...metrics, providers: effectiveProviders };
643
+ const gateEvidence = [];
644
+ for (const pack of packs) {
645
+ for (const gate of pack.resolved.spec.gates) {
646
+ const kind = metricKindOf(gate, pack);
647
+ const series = seriesFor(gate, pack, effectiveMetrics);
648
+ const targets = gate.kind === 'upstream-ceiling' || gate.kind === 'predicate'
649
+ ? [{ slice: null, observation: null }]
650
+ : expandScope(gate, kind, series, binding.spec.slices);
651
+ if (!targets.length) {
652
+ // Unreachable through resolveBinding (vacuous scopes are load errors),
653
+ // but a zero-target gate must never PROMOTE vacuously.
654
+ gateEvidence.push({
655
+ gateId: gate.id, kind: gate.kind,
656
+ scope: { mode: gate.scope.mode, ...(gate.scope.slices === undefined ? {} : { slices: [...gate.scope.slices] }) },
657
+ slice: null, n: null, status: 'insufficient', outcome: gate.onInsufficient ?? 'HOLD',
658
+ reasons: [`${gate.id}:no-evaluation-targets`],
659
+ });
660
+ continue;
661
+ }
662
+ for (const target of targets) {
663
+ const evaluation = evaluateObservation(gate, target.observation, { packId: pack.authored.metadata.id, parameters, metrics: effectiveMetrics, upstream, nowMs,
664
+ slice: target.slice, slices: binding.spec.slices });
665
+ // Upstream-ceiling gates mirror the upstream verdict exactly: compromise
666
+ // rolls back, allowlist problems hold, clean promotes. The gate's own
667
+ // onFail never escalates (or softens) the trusted verdict.
668
+ const outcome = gate.kind === 'upstream-ceiling' && evaluation.status === 'fail'
669
+ ? upstreamOutcome(upstream).ceiling
670
+ : gateStatusOutcome(evaluation.status, gate);
671
+ gateEvidence.push({
672
+ gateId: gate.id, kind: gate.kind,
673
+ scope: { mode: gate.scope.mode, ...(gate.scope.slices === undefined ? {} : { slices: [...gate.scope.slices] }) },
674
+ slice: evaluation.slice, n: evaluation.n,
675
+ ...(evaluation.events === undefined ? {} : { events: evaluation.events }),
676
+ ...(evaluation.statistic === undefined ? {} : { statistic: evaluation.statistic }),
677
+ ...(evaluation.threshold === undefined ? {} : { threshold: evaluation.threshold }),
678
+ status: evaluation.status,
679
+ outcome,
680
+ reasons: [`${gate.id}:${evaluation.reason}`],
681
+ });
682
+ }
683
+ }
684
+ }
685
+ const packOutcome = maxOutcome(...gateEvidence.map(entry => entry.outcome));
686
+ const { ceiling: upstreamCeiling, problems: upstreamProblems } = upstreamOutcome(upstream);
687
+ const bindingCeiling = binding.spec.ceiling ?? 'PROMOTE';
688
+ const decision = maxOutcome(packOutcome, upstreamCeiling, bindingCeiling);
689
+ const body = {
690
+ apiVersion: binding.apiVersion,
691
+ kind: 'GateReport',
692
+ metadata: { id: binding.metadata.id, version: binding.metadata.version,
693
+ description: `GateReport for ${binding.metadata.id}` },
694
+ binding: { id: binding.metadata.id, version: binding.metadata.version, digest: trustedBindingDigest },
695
+ packs: packs.map(pack => ({ id: pack.authored.metadata.id, version: pack.authored.metadata.version,
696
+ digest: pack.digest, resolvedDigest: pack.resolvedDigest })),
697
+ metricProviders: binding.spec.metricProviders.map(pin => ({ ...pin })),
698
+ metricsDigest: artifactDigest(effectiveMetrics),
699
+ floors: input.floors === 'none-explicit-opt-out'
700
+ ? { mode: 'opt-out' }
701
+ : { mode: 'applied', digest: artifactDigest(input.floors) },
702
+ holdout: { digest: sealed.digest, frozenDigest, firstAccessedAt },
703
+ gateEvidence,
704
+ references: binding.spec.references.map(reference => ({
705
+ name: reference.name, kind: reference.kind, observed: effectiveMetrics.references?.[reference.name] !== undefined,
706
+ })),
707
+ upstream: upstream === null ? null : {
708
+ decision: upstreamCeiling,
709
+ reasons: upstreamProblems.map(problem => `upstream:${problem}`),
710
+ digest: upstream.digest,
711
+ },
712
+ ceilings: { packOutcome, upstreamCeiling, bindingCeiling },
713
+ decision,
714
+ evaluatedAt: now,
715
+ ...(input.attestation === undefined ? {} : { attestation: input.attestation }),
716
+ };
717
+ return { ...body, digest: artifactDigest(body) };
718
+ }
719
+ //# sourceMappingURL=evaluate.js.map