@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,1003 @@
1
+ import { canonicalJson } from '../security/artifact-trust.js';
2
+ import { artifactDigest } from '../decision/validate.js';
3
+ import { validateGateDocument } from './schema.js';
4
+ import { qualifyGateParameter } from './types.js';
5
+ import { INTEGRITY_CEILING_FLOOR_PACK_ID, assertCeilingDefaults, projectCeilingSatisfied } from './floors.js';
6
+ export class GateRegistryError extends Error {
7
+ constructor(message) { super(message); this.name = 'GateRegistryError'; }
8
+ }
9
+ export const GATE_NAMESPACES = ['aiwg', 'framework', 'addon', 'extension', 'project'];
10
+ export function splitPackId(id) {
11
+ if (typeof id !== 'string')
12
+ throw new GateRegistryError('pack id must be a string');
13
+ const separator = id.indexOf(':');
14
+ const namespace = separator < 0 ? '' : id.slice(0, separator);
15
+ const rest = separator < 0 ? '' : id.slice(separator + 1);
16
+ if (!GATE_NAMESPACES.includes(namespace) || !rest || /\s/.test(rest)) {
17
+ throw new GateRegistryError(`pack id must be <namespace>:<path> with namespace ${GATE_NAMESPACES.join('/')}: '${id}'`);
18
+ }
19
+ if (namespace === 'project') {
20
+ if (rest.includes(':') || rest.includes('/'))
21
+ throw new GateRegistryError(`project pack ids are project:<name>: '${id}'`);
22
+ }
23
+ else if (!/^[^/\s:]+\/[^/\s:]+$/.test(rest)) {
24
+ throw new GateRegistryError(`pack id must be <namespace>:<bundle>/<name>: '${id}'`);
25
+ }
26
+ return { namespace: namespace, rest };
27
+ }
28
+ const fail = (message) => { throw new GateRegistryError(message); };
29
+ const STATISTICAL_KINDS = ['interval-bound', 'paired-difference', 'bootstrap-bound'];
30
+ function checkThresholdBlock(gate, params) {
31
+ const needsThreshold = gate.kind === 'interval-bound' || gate.kind === 'bootstrap-bound'
32
+ || gate.kind === 'count-max' || gate.kind === 'count-min' || gate.kind === 'value-threshold';
33
+ const threshold = gate.threshold;
34
+ if (threshold === undefined) {
35
+ if (needsThreshold)
36
+ fail(`gate ${gate.id} of kind ${gate.kind} requires a threshold`);
37
+ return;
38
+ }
39
+ if (!needsThreshold)
40
+ fail(`gate ${gate.id} of kind ${gate.kind} must not carry a threshold`);
41
+ const param = threshold.param === undefined ? undefined : params[threshold.param];
42
+ if (threshold.param !== undefined && param === undefined)
43
+ fail(`gate ${gate.id} references unknown parameter '${threshold.param}'`);
44
+ const expectedParamType = gate.kind === 'interval-bound' || gate.kind === 'bootstrap-bound' ? 'bps'
45
+ : gate.kind === 'count-max' || gate.kind === 'count-min' ? 'count' : 'value';
46
+ if (param !== undefined && param.type !== expectedParamType) {
47
+ fail(`gate ${gate.id} parameter '${threshold.param}' must have type ${expectedParamType}`);
48
+ }
49
+ if (threshold.value !== undefined) {
50
+ if (!Number.isFinite(threshold.value))
51
+ fail(`gate ${gate.id} threshold value must be finite`);
52
+ if (expectedParamType === 'bps' && (threshold.value < 0 || threshold.value > 10000)) {
53
+ fail(`gate ${gate.id} threshold must be within 0..10000 bps`);
54
+ }
55
+ if (expectedParamType === 'count' && (!Number.isSafeInteger(threshold.value) || threshold.value < 0)) {
56
+ fail(`gate ${gate.id} threshold must be a non-negative integer count`);
57
+ }
58
+ }
59
+ const op = gate.kind === 'count-max' ? 'lte' : gate.kind === 'count-min' ? 'gte' : threshold.op;
60
+ if (gate.kind === 'count-max' && threshold.op !== 'lte')
61
+ fail(`gate ${gate.id} count-max requires op lte`);
62
+ if (gate.kind === 'count-min' && threshold.op !== 'gte')
63
+ fail(`gate ${gate.id} count-min requires op gte`);
64
+ if (gate.kind === 'interval-bound' && gate.statistic?.bound === 'upper' && threshold.op !== 'lte') {
65
+ fail(`gate ${gate.id} upper-bound gates require op lte`);
66
+ }
67
+ if (gate.kind === 'interval-bound' && gate.statistic?.bound === 'lower' && threshold.op !== 'gte') {
68
+ fail(`gate ${gate.id} lower-bound gates require op gte`);
69
+ }
70
+ if (gate.kind === 'value-threshold') {
71
+ if (threshold.op === 'lte' && gate.direction !== 'lower-is-stricter')
72
+ fail(`gate ${gate.id} lte requires direction lower-is-stricter`);
73
+ if (threshold.op === 'gte' && gate.direction !== 'higher-is-stricter')
74
+ fail(`gate ${gate.id} gte requires direction higher-is-stricter`);
75
+ }
76
+ if (gate.kind === 'interval-bound' || gate.kind === 'bootstrap-bound') {
77
+ if (op === 'lte' && gate.direction !== 'lower-is-stricter')
78
+ fail(`gate ${gate.id} upper-bound gates require direction lower-is-stricter`);
79
+ if (op === 'gte' && gate.direction !== 'higher-is-stricter')
80
+ fail(`gate ${gate.id} lower-bound gates require direction higher-is-stricter`);
81
+ }
82
+ }
83
+ const isParamRef = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
84
+ && typeof value.param === 'string';
85
+ /**
86
+ * Resolve a literal-or-parameter number against pack parameter defaults.
87
+ * Returns the default for a `{param}` reference, or null when the reference
88
+ * names an unknown parameter, the type mismatches, or no default pins it.
89
+ */
90
+ export function resolveNumberDefault(value, params, expectedType) {
91
+ if (value === undefined)
92
+ return null;
93
+ if (typeof value === 'number')
94
+ return value;
95
+ if (!isParamRef(value))
96
+ return null;
97
+ const declared = params[value.param];
98
+ if (declared === undefined || declared.type !== expectedType)
99
+ return null;
100
+ return declared.default ?? null;
101
+ }
102
+ /** A literal-or-parameter reference names a known parameter of the expected type. */
103
+ function checkParamRef(gateId, value, params, expectedType, what) {
104
+ if (value === undefined || typeof value === 'number')
105
+ return;
106
+ if (!isParamRef(value) || params[value.param] === undefined) {
107
+ fail(`gate ${gateId} ${what} references unknown parameter '${value?.param}'`);
108
+ }
109
+ if (params[value.param].type !== expectedType) {
110
+ fail(`gate ${gateId} ${what} parameter '${value.param}' must have type ${expectedType}`);
111
+ }
112
+ }
113
+ /** Statistic coherence per kind. Irrelevant statistic fields are rejected so confused authoring fails at load. */
114
+ function checkStatisticBlock(gate, params) {
115
+ const statistic = gate.statistic;
116
+ if (statistic === undefined) {
117
+ if (STATISTICAL_KINDS.includes(gate.kind)) {
118
+ fail(`gate ${gate.id} of kind ${gate.kind} requires a statistic`);
119
+ }
120
+ return;
121
+ }
122
+ if (!STATISTICAL_KINDS.includes(gate.kind)) {
123
+ fail(`gate ${gate.id} of kind ${gate.kind} must not carry a statistic`);
124
+ }
125
+ if (statistic.kind !== gate.kind)
126
+ fail(`gate ${gate.id} statistic kind must match the gate kind`);
127
+ const { method, bound, levelBps, mode, marginBps, seed, resamples, bounds } = statistic;
128
+ if (gate.kind === 'interval-bound') {
129
+ if (method !== undefined && method !== 'wilson' && method !== 'clopper-pearson') {
130
+ fail(`gate ${gate.id} interval-bound method must be wilson or clopper-pearson`);
131
+ }
132
+ if (bound !== 'upper' && bound !== 'lower')
133
+ fail(`gate ${gate.id} interval-bound requires bound upper|lower`);
134
+ if (levelBps === undefined)
135
+ fail(`gate ${gate.id} interval-bound requires levelBps`);
136
+ checkParamRef(gate.id, levelBps, params, 'level', 'levelBps');
137
+ if (mode !== undefined || marginBps !== undefined || seed !== undefined || resamples !== undefined || bounds !== undefined) {
138
+ fail(`gate ${gate.id} interval-bound must not carry mode, margin, seed, resamples or bounds`);
139
+ }
140
+ }
141
+ else if (gate.kind === 'paired-difference') {
142
+ if (method !== undefined && method !== 'newcombe' && method !== 'tango') {
143
+ fail(`gate ${gate.id} paired-difference method must be newcombe or tango`);
144
+ }
145
+ if (bound !== undefined)
146
+ fail(`gate ${gate.id} paired-difference must not carry bound`);
147
+ if (levelBps === undefined)
148
+ fail(`gate ${gate.id} paired-difference requires levelBps`);
149
+ checkParamRef(gate.id, levelBps, params, 'level', 'levelBps');
150
+ const resolvedMode = mode ?? 'non-inferiority';
151
+ if (resolvedMode !== 'non-inferiority' && resolvedMode !== 'superiority')
152
+ fail(`gate ${gate.id} unknown paired mode`);
153
+ if (resolvedMode === 'non-inferiority' && marginBps === undefined)
154
+ fail(`gate ${gate.id} non-inferiority requires marginBps`);
155
+ if (resolvedMode === 'non-inferiority' && marginBps !== undefined && marginBps > 0) {
156
+ fail(`gate ${gate.id} non-inferiority marginBps must be non-positive`);
157
+ }
158
+ if (resolvedMode === 'superiority' && marginBps !== undefined && marginBps < 0) {
159
+ fail(`gate ${gate.id} superiority marginBps must be non-negative`);
160
+ }
161
+ if (seed !== undefined || resamples !== undefined || bounds !== undefined) {
162
+ fail(`gate ${gate.id} paired-difference must not carry seed, resamples or bounds`);
163
+ }
164
+ }
165
+ else {
166
+ if (method !== undefined && method !== 'bootstrap')
167
+ fail(`gate ${gate.id} bootstrap-bound method must be bootstrap`);
168
+ if (bound !== 'upper' && bound !== 'lower')
169
+ fail(`gate ${gate.id} bootstrap-bound requires bound upper|lower`);
170
+ if (levelBps === undefined || seed === undefined || resamples === undefined || bounds === undefined) {
171
+ fail(`gate ${gate.id} bootstrap-bound requires levelBps, seed, resamples and bounds`);
172
+ }
173
+ checkParamRef(gate.id, levelBps, params, 'level', 'levelBps');
174
+ if (mode !== undefined || marginBps !== undefined)
175
+ fail(`gate ${gate.id} bootstrap-bound must not carry mode or margin`);
176
+ }
177
+ }
178
+ function checkParameter(name, parameter) {
179
+ if (parameter.default !== undefined) {
180
+ if (!Number.isFinite(parameter.default))
181
+ fail(`parameter ${name} default must be finite`);
182
+ if (parameter.type === 'bps' && (parameter.default < 0 || parameter.default > 10000)) {
183
+ fail(`parameter ${name} bps default must be within 0..10000`);
184
+ }
185
+ if (parameter.type === 'level' && (!Number.isSafeInteger(parameter.default)
186
+ || parameter.default <= 5000 || parameter.default >= 9999)) {
187
+ fail(`parameter ${name} level default must be an integer strictly between 5000 and 9999`);
188
+ }
189
+ if (parameter.type === 'count' && (!Number.isSafeInteger(parameter.default) || parameter.default < 0)) {
190
+ fail(`parameter ${name} count default must be a non-negative integer`);
191
+ }
192
+ }
193
+ }
194
+ /** Semantic validation of one fully resolved pack against the provider registry. */
195
+ export function validateResolvedPack(pack, providers) {
196
+ const ids = pack.spec.gates.map(gate => gate.id);
197
+ if (new Set(ids).size !== ids.length)
198
+ fail(`pack ${pack.metadata.id} has duplicate gate ids`);
199
+ const params = pack.spec.parameters ?? {};
200
+ for (const [name, parameter] of Object.entries(params))
201
+ checkParameter(name, parameter);
202
+ for (const [name, metric] of Object.entries(pack.spec.metrics)) {
203
+ if (!providers.has(metric.provider))
204
+ fail(`pack ${pack.metadata.id} metric ${name} names unknown provider ${metric.provider}`);
205
+ const provider = providers.get(metric.provider);
206
+ const declared = provider.metrics[name];
207
+ if (declared === undefined)
208
+ fail(`pack ${pack.metadata.id} metric ${name} is not computed by ${metric.provider}`);
209
+ if (declared.kind !== metric.kind) {
210
+ fail(`pack ${pack.metadata.id} metric ${name} kind ${metric.kind} does not match provider kind ${declared.kind}`);
211
+ }
212
+ }
213
+ for (const gate of pack.spec.gates) {
214
+ if (gate.onFail !== 'HOLD' && gate.onFail !== 'ROLLBACK') {
215
+ fail(`gate ${gate.id} onFail must be HOLD or ROLLBACK, never PROMOTE`);
216
+ }
217
+ // Insufficient evidence always holds: ROLLBACK needs an observed blocking
218
+ // failure, never a shortfall. Rejected here at load, not just in schema.
219
+ if (gate.onInsufficient !== undefined && gate.onInsufficient !== 'HOLD') {
220
+ fail(`gate ${gate.id} onInsufficient must be HOLD: insufficient evidence never rolls back`);
221
+ }
222
+ if (gate.metric !== undefined) {
223
+ const declared = pack.spec.metrics[gate.metric.name];
224
+ if (declared === undefined || declared.provider !== gate.metric.provider) {
225
+ fail(`gate ${gate.id} references undeclared metric ${gate.metric.provider}:${gate.metric.name}`);
226
+ }
227
+ }
228
+ const metricKind = gate.metric === undefined ? undefined : pack.spec.metrics[gate.metric.name]?.kind;
229
+ const expectedKind = gate.kind === 'interval-bound' || gate.kind === 'count-max' || gate.kind === 'count-min' ? 'proportion'
230
+ : gate.kind === 'paired-difference' ? 'paired' : gate.kind === 'bootstrap-bound' ? 'differences'
231
+ : gate.kind === 'value-threshold' ? 'scalar' : gate.kind === 'evidence' ? 'evidence' : undefined;
232
+ if (expectedKind !== undefined && metricKind !== expectedKind) {
233
+ fail(`gate ${gate.id} of kind ${gate.kind} requires a ${expectedKind} metric`);
234
+ }
235
+ checkThresholdBlock(gate, params);
236
+ checkStatisticBlock(gate, params);
237
+ checkParamRef(gate.id, gate.minimumN, params, 'count', 'minimumN');
238
+ if (gate.kind === 'minimum-n') {
239
+ if (gate.minimumN === undefined)
240
+ fail(`gate ${gate.id} minimum-n requires minimumN >= 1`);
241
+ // A default is the enforced minimum, so a defaulted reference must pin
242
+ // at least 1; a defaultless reference is supplied by the binding and an
243
+ // unresolvable value holds at evaluation, never promotes.
244
+ const resolved = resolveNumberDefault(gate.minimumN, params, 'count');
245
+ if (typeof gate.minimumN === 'number' && gate.minimumN < 1) {
246
+ fail(`gate ${gate.id} minimum-n requires minimumN >= 1`);
247
+ }
248
+ if (isParamRef(gate.minimumN) && params[gate.minimumN.param]?.default !== undefined && (resolved ?? 0) < 1) {
249
+ fail(`gate ${gate.id} minimum-n requires minimumN >= 1`);
250
+ }
251
+ if (metricKind === 'evidence')
252
+ fail(`gate ${gate.id} minimum-n cannot observe an evidence metric`);
253
+ }
254
+ else if (gate.minimumN !== undefined && typeof gate.minimumN === 'number'
255
+ && (!Number.isSafeInteger(gate.minimumN) || gate.minimumN < 0)) {
256
+ fail(`gate ${gate.id} minimumN must be a non-negative integer`);
257
+ }
258
+ if (gate.kind === 'evidence' && (gate.evidence === undefined || !gate.evidence.required.length)) {
259
+ fail(`gate ${gate.id} evidence requires a nonempty required list`);
260
+ }
261
+ if (gate.kind !== 'evidence' && gate.evidence !== undefined)
262
+ fail(`gate ${gate.id} of kind ${gate.kind} must not carry evidence`);
263
+ if (gate.kind !== 'predicate' && gate.predicate !== undefined)
264
+ fail(`gate ${gate.id} of kind ${gate.kind} must not carry a predicate`);
265
+ if ((gate.scope.mode === 'listed' || gate.scope.mode === 'pooled')
266
+ && (gate.scope.slices === undefined || !gate.scope.slices.length)) {
267
+ fail(`gate ${gate.id} scope ${gate.scope.mode} requires a nonempty slices list`);
268
+ }
269
+ // `except` is honoured only by `each` (per-slice minus exclusions). Any other
270
+ // mode silently ignoring it would be a fail-open scoping bug, so it is a load error.
271
+ if (gate.scope.except !== undefined && gate.scope.mode !== 'each') {
272
+ fail(`gate ${gate.id} scope except is only allowed with mode each`);
273
+ }
274
+ }
275
+ }
276
+ /** Resolve a `$param` threshold against pack parameter defaults. Null when no default pins it. */
277
+ export function resolveThresholdDefault(gate, params) {
278
+ if (gate.threshold === undefined)
279
+ return null;
280
+ if (gate.threshold.value !== undefined)
281
+ return gate.threshold.value;
282
+ if (gate.threshold.param === undefined)
283
+ return null;
284
+ return params[gate.threshold.param]?.default ?? null;
285
+ }
286
+ /**
287
+ * Scope-tightening rules per gate kind. A scope change is a tightening only when
288
+ * the child observes every slice the parent observes under every possible
289
+ * binding inventory (so a breach the parent sees can never go unobserved)
290
+ * and every world that fails the child also fails the parent:
291
+ * - `listed` may only widen (more slices fail in a superset of worlds);
292
+ * - `pooled` sets are fixed;
293
+ * - `listed -> each` widens coverage to the whole binding inventory, so the
294
+ * child's `except` must exclude none of the parent's listed slices;
295
+ * - `all -> each` covers the whole inventory only with no exceptions, and is a
296
+ * tightening only when per-slice checks are strictly harder than the pooled
297
+ * check (`count-min`, `minimum-n`); for `count-max` the pooled bound is
298
+ * strictly harder (pooled<=2 implies per-slice<=2, not vice versa), and for
299
+ * interval/paired/bootstrap/value gates pooled and per-slice bounds are
300
+ * incomparable — so the change is rejected for every other kind.
301
+ * Every other cross-mode change crosses aggregation or drops coverage and is
302
+ * rejected for every kind.
303
+ */
304
+ function assertScopeTightens(parent, child) {
305
+ const from = parent.scope;
306
+ const to = child.scope;
307
+ if (from.mode === to.mode) {
308
+ if (to.mode === 'listed' || to.mode === 'pooled') {
309
+ const before = new Set(from.slices ?? []);
310
+ const after = new Set(to.slices ?? []);
311
+ if (to.mode === 'listed') {
312
+ // A wider listed set fails in a superset of worlds, so it tightens.
313
+ for (const slice of before) {
314
+ if (!after.has(slice))
315
+ fail(`gate ${child.id} scope listed may not drop inherited slices`);
316
+ }
317
+ }
318
+ else if (after.size !== before.size || [...after].some(slice => !before.has(slice))) {
319
+ fail(`gate ${child.id} scope pooled may not change its slice set`);
320
+ }
321
+ }
322
+ const beforeExcept = new Set(from.except ?? []);
323
+ for (const slice of to.except ?? []) {
324
+ if (!beforeExcept.has(slice))
325
+ fail(`gate ${child.id} scope may only narrow inherited exceptions`);
326
+ }
327
+ return;
328
+ }
329
+ if (from.mode === 'listed' && to.mode === 'each') {
330
+ const excluded = new Set(to.except ?? []);
331
+ for (const slice of from.slices ?? []) {
332
+ if (excluded.has(slice))
333
+ fail(`gate ${child.id} scope listed -> each may not except inherited slice '${slice}'`);
334
+ }
335
+ return;
336
+ }
337
+ if (from.mode === 'all' && to.mode === 'each'
338
+ && (child.kind === 'count-min' || child.kind === 'minimum-n')) {
339
+ // `all` watches the whole inventory: any exception drops a watched slice.
340
+ if ((to.except ?? []).length)
341
+ fail(`gate ${child.id} scope all -> each may not except slices`);
342
+ return;
343
+ }
344
+ fail(`gate ${child.id} scope change ${from.mode} -> ${to.mode} is not a monotone tightening`);
345
+ }
346
+ /**
347
+ * Proves monotone tightening of a child gate over its parent. Any loosening —
348
+ * a relaxed threshold, a literal rewritten as a bindable parameter, a renamed
349
+ * parameter, a lowered minimumN, a demoted outcome, a widened scope
350
+ * or a changed statistic — is a load error.
351
+ */
352
+ export function assertGateTightens(parent, child, params) {
353
+ if (parent.kind !== child.kind)
354
+ fail(`gate ${child.id} may not change kind ${parent.kind} -> ${child.kind}`);
355
+ if (canonicalJson(parent.metric ?? null) !== canonicalJson(child.metric ?? null)) {
356
+ fail(`gate ${child.id} may not change its metric reference`);
357
+ }
358
+ if (parent.direction !== child.direction)
359
+ fail(`gate ${child.id} may not change its tightening direction`);
360
+ const parentStatistic = parent.statistic;
361
+ const childStatistic = child.statistic;
362
+ if (canonicalJson({ ...parentStatistic, levelBps: 0 }) !== canonicalJson({ ...childStatistic, levelBps: 0 })) {
363
+ fail(`gate ${child.id} may not change its statistic (levelBps aside)`);
364
+ }
365
+ // A literal level is a pinned bound: rewriting it as a parameter hands the
366
+ // bound to the binding, whose allowed range is wider than the literal.
367
+ const parentLevel = parentStatistic?.levelBps;
368
+ const childLevel = childStatistic?.levelBps;
369
+ if (typeof parentLevel === 'number' && isParamRef(childLevel)) {
370
+ fail(`gate ${child.id} may not convert a literal levelBps into a parameter`);
371
+ }
372
+ if (isParamRef(parentLevel) && isParamRef(childLevel) && parentLevel.param !== childLevel.param) {
373
+ fail(`gate ${child.id} may not rename its levelBps parameter`);
374
+ }
375
+ const levelBefore = parentLevel === undefined ? undefined : resolveNumberDefault(parentLevel, params, 'level');
376
+ const levelAfter = childLevel === undefined ? undefined : resolveNumberDefault(childLevel, params, 'level');
377
+ if (levelBefore === null || levelAfter === null) {
378
+ if (canonicalJson(parentLevel ?? null) !== canonicalJson(childLevel ?? null)) {
379
+ fail(`gate ${child.id} levelBps change is unresolvable without parameter defaults`);
380
+ }
381
+ }
382
+ else if ((levelAfter ?? levelBefore) !== undefined && (levelAfter ?? 0) < (levelBefore ?? 0)) {
383
+ fail(`gate ${child.id} may only raise levelBps`);
384
+ }
385
+ if ((parentStatistic?.mode ?? 'non-inferiority') !== (childStatistic?.mode ?? 'non-inferiority')) {
386
+ fail(`gate ${child.id} may not change its paired mode`);
387
+ }
388
+ const marginBefore = parentStatistic?.marginBps;
389
+ const marginAfter = childStatistic?.marginBps;
390
+ if (marginBefore !== undefined && marginAfter !== undefined && marginAfter < marginBefore) {
391
+ fail(`gate ${child.id} may only raise marginBps`);
392
+ }
393
+ // A literal threshold is a pinned bound. Rewriting it as a parameter hands the
394
+ // bound to the binding, whose allowed range (e.g. 0..10000 bps) is wider than
395
+ // the literal — a loosening no default-equality check can see. Forbid it.
396
+ if (parent.threshold?.value !== undefined && child.threshold?.param !== undefined) {
397
+ fail(`gate ${child.id} may not convert a literal threshold into a parameter`);
398
+ }
399
+ // Renaming a parameter rebinds the gate to a fresh default and range. Params
400
+ // may only keep their name with a tighter-or-equal default.
401
+ if (parent.threshold?.param !== undefined && child.threshold?.param !== undefined
402
+ && parent.threshold.param !== child.threshold.param) {
403
+ fail(`gate ${child.id} may not rename its threshold parameter`);
404
+ }
405
+ const before = resolveThresholdDefault(parent, params);
406
+ const after = resolveThresholdDefault(child, params);
407
+ if (before === null || after === null) {
408
+ if (canonicalJson(parent.threshold ?? null) !== canonicalJson(child.threshold ?? null)) {
409
+ fail(`gate ${child.id} threshold change is unresolvable without parameter defaults`);
410
+ }
411
+ }
412
+ else if (child.direction === 'lower-is-stricter' ? after > before : after < before) {
413
+ fail(`gate ${child.id} threshold ${before} -> ${after} loosens direction ${child.direction}`);
414
+ }
415
+ if (typeof parent.minimumN === 'number' && isParamRef(child.minimumN)) {
416
+ fail(`gate ${child.id} may not convert a literal minimumN into a parameter`);
417
+ }
418
+ if (isParamRef(parent.minimumN) && isParamRef(child.minimumN) && parent.minimumN.param !== child.minimumN.param) {
419
+ fail(`gate ${child.id} may not rename its minimumN parameter`);
420
+ }
421
+ const minimumBefore = parent.minimumN === undefined ? 0 : resolveNumberDefault(parent.minimumN, params, 'count');
422
+ const minimumAfter = child.minimumN === undefined ? 0 : resolveNumberDefault(child.minimumN, params, 'count');
423
+ if (minimumBefore === null || minimumAfter === null) {
424
+ if (canonicalJson(parent.minimumN ?? null) !== canonicalJson(child.minimumN ?? null)) {
425
+ fail(`gate ${child.id} minimumN change is unresolvable without parameter defaults`);
426
+ }
427
+ }
428
+ else if (minimumAfter < minimumBefore) {
429
+ fail(`gate ${child.id} may only raise minimumN`);
430
+ }
431
+ if (parent.onFail === 'ROLLBACK' && child.onFail !== 'ROLLBACK') {
432
+ fail(`gate ${child.id} may not demote onFail ROLLBACK -> HOLD`);
433
+ }
434
+ // Insufficient evidence always holds: ROLLBACK needs an observed blocking
435
+ // failure, so neither side may name it. Checked here as well as in schema
436
+ // and pack validation because this pure function is the tightening gate.
437
+ if ((parent.onInsufficient ?? 'HOLD') !== 'HOLD' || (child.onInsufficient ?? 'HOLD') !== 'HOLD') {
438
+ fail(`gate ${child.id} onInsufficient must be HOLD: insufficient evidence never rolls back`);
439
+ }
440
+ if (parent.floor === true && child.floor !== true)
441
+ fail(`gate ${child.id} is a floor gate and cannot be un-floored`);
442
+ assertScopeTightens(parent, child);
443
+ const afterEvidence = new Set(child.evidence?.required ?? []);
444
+ for (const id of parent.evidence?.required ?? []) {
445
+ if (!afterEvidence.has(id))
446
+ fail(`gate ${child.id} may not drop required evidence ${id}`);
447
+ }
448
+ if (canonicalJson(parent.predicate ?? null) !== canonicalJson(child.predicate ?? null)) {
449
+ fail(`gate ${child.id} may not change its predicate`);
450
+ }
451
+ }
452
+ const pinLabel = (pin) => `${pin.id}@${pin.version}`;
453
+ /**
454
+ * Pure `extends` composition: merges a child's declarations over its already
455
+ * resolved parent. No registry state is read, so a `GateRegistry` subclass
456
+ * cannot change tightening semantics by overriding an instance method. The
457
+ * evaluator calls this (via `composeGatePack`/`resolveGateBinding`) instead of
458
+ * any overridable `GateRegistry` method for security-relevant resolution.
459
+ */
460
+ export function applyGateExtends(parentResolved, child) {
461
+ const mergedParams = { ...(parentResolved.spec.parameters ?? {}) };
462
+ for (const [name, parameter] of Object.entries(child.spec.parameters ?? {})) {
463
+ const before = mergedParams[name];
464
+ if (before === undefined) {
465
+ mergedParams[name] = parameter;
466
+ continue;
467
+ }
468
+ if (before.type !== parameter.type || before.direction !== parameter.direction) {
469
+ throw new GateRegistryError(`pack ${child.metadata.id} may not change parameter ${name} type or direction`);
470
+ }
471
+ if (before.default === undefined && parameter.default !== undefined) {
472
+ throw new GateRegistryError(`pack ${child.metadata.id} may not add a default to required parameter ${name}`);
473
+ }
474
+ if (before.default !== undefined && parameter.default === undefined) {
475
+ mergedParams[name] = parameter;
476
+ continue;
477
+ }
478
+ if (before.default !== undefined && parameter.default !== undefined) {
479
+ const tightens = parameter.direction === 'lower-is-stricter'
480
+ ? parameter.default <= before.default : parameter.default >= before.default;
481
+ if (!tightens)
482
+ throw new GateRegistryError(`pack ${child.metadata.id} parameter ${name} default loosens`);
483
+ mergedParams[name] = parameter;
484
+ }
485
+ }
486
+ const parentGates = new Map(parentResolved.spec.gates.map(gate => [gate.id, gate]));
487
+ const merged = [];
488
+ const seen = new Set();
489
+ for (const gate of parentResolved.spec.gates) {
490
+ const override = (child.spec.gates ?? []).find(candidate => candidate.id === gate.id);
491
+ if (override === undefined) {
492
+ // Removing a non-floor gate is allowed; floor gates pin the minimum.
493
+ if (gate.floor === true)
494
+ throw new GateRegistryError(`pack ${child.metadata.id} may not remove floor gate ${gate.id}`);
495
+ continue;
496
+ }
497
+ seen.add(gate.id);
498
+ assertGateTightens(gate, override, mergedParams);
499
+ merged.push(override);
500
+ }
501
+ for (const gate of child.spec.gates ?? []) {
502
+ if (!seen.has(gate.id) && !parentGates.has(gate.id))
503
+ merged.push(gate);
504
+ else if (!seen.has(gate.id))
505
+ fail(`pack ${child.metadata.id} duplicates gate ${gate.id}`);
506
+ }
507
+ const mergedMetrics = { ...parentResolved.spec.metrics };
508
+ for (const [name, metric] of Object.entries(child.spec.metrics ?? {})) {
509
+ const before = mergedMetrics[name];
510
+ if (before !== undefined && (before.provider !== metric.provider || before.kind !== metric.kind)) {
511
+ throw new GateRegistryError(`pack ${child.metadata.id} may not change inherited metric ${name}`);
512
+ }
513
+ mergedMetrics[name] = metric;
514
+ }
515
+ return {
516
+ apiVersion: child.apiVersion, kind: child.kind, metadata: child.metadata,
517
+ spec: {
518
+ extends: child.spec.extends, parameters: mergedParams, metrics: mergedMetrics, gates: merged,
519
+ // Omit `combine` when the child leaves it unset: a present-but-undefined
520
+ // key is not JSON and would poison the composed-pack digest.
521
+ ...(child.spec.combine === undefined ? {} : { combine: child.spec.combine }),
522
+ },
523
+ };
524
+ }
525
+ /**
526
+ * Pure extends-chain composition. The lookup returns authored packs by full id;
527
+ * parent pins (version + authored digest) are enforced here, so a loosened
528
+ * parent always moves the composed digest.
529
+ */
530
+ export function composeGatePack(pack, lookupAuthored) {
531
+ if (pack.spec.extends === undefined)
532
+ return pack;
533
+ const parentAuthored = lookupAuthored(pack.spec.extends.id);
534
+ const parentPin = pack.spec.extends;
535
+ if (parentAuthored.metadata.version !== parentPin.version
536
+ || artifactDigest(parentAuthored) !== parentPin.digest) {
537
+ throw new GateRegistryError(`pack ${pack.metadata.id} parent pin mismatch for ${pinLabel(parentPin)}`);
538
+ }
539
+ return applyGateExtends(composeGatePack(parentAuthored, lookupAuthored), pack);
540
+ }
541
+ /**
542
+ * Whether one expanded floor's `integrity-ceiling` gate genuinely governs the
543
+ * default ceiling: it must be an all-scoped `upstream-ceiling` gate that
544
+ * tightens the shipped default gate (floor flag included). A same-id gate of
545
+ * any other kind or scope never suppresses the default.
546
+ */
547
+ function governsDefaultCeiling(floor, defaultGate) {
548
+ return floor.gates.some(gate => {
549
+ if (gate.id !== 'integrity-ceiling' || gate.kind !== 'upstream-ceiling' || gate.scope.mode !== 'all') {
550
+ return false;
551
+ }
552
+ try {
553
+ assertGateTightens(defaultGate, gate, floor.params);
554
+ return true;
555
+ }
556
+ catch {
557
+ return false;
558
+ }
559
+ });
560
+ }
561
+ /**
562
+ * Expands project floors over an explicit authored-pack map: inline packs are
563
+ * schema-checked again (direct registry callers bypass config validation) and
564
+ * semantically validated, pack references are verified by version and authored
565
+ * digest and composed through the pure `composeGatePack` (so `extends` floors
566
+ * enforce their composed minima). Every floor gate that binds a threshold
567
+ * parameter must declare a default for it: the default is the enforced
568
+ * minimum, and a required (default-less) floor parameter is a load error.
569
+ *
570
+ * The operator default floor — the composed gates of the shipped
571
+ * `aiwg:decision-engine/integrity-ceiling` pack, the single source of truth —
572
+ * is appended unless a configured floor governs it via `governsDefaultCeiling`
573
+ * above. An inline gate that merely reuses the id (any other kind or scope)
574
+ * never suppresses the default; the binding must then satisfy both, which
575
+ * fails closed with a diagnostic.
576
+ */
577
+ export function expandFloorPacks(floors, lookupAuthored, providers) {
578
+ const expanded = (floors.floors ?? []).map(source => {
579
+ if ('packRef' in source) {
580
+ const pin = source.packRef;
581
+ const authored = lookupAuthored(pin.id);
582
+ if (authored.metadata.version !== pin.version
583
+ || artifactDigest(authored) !== pin.digest) {
584
+ throw new GateRegistryError(`project floor pack pin mismatch for ${pinLabel(pin)}`);
585
+ }
586
+ const composed = composeGatePack(authored, lookupAuthored);
587
+ validateResolvedPack(composed, providers);
588
+ return {
589
+ packId: authored.metadata.id,
590
+ gates: composed.spec.gates,
591
+ params: { ...(composed.spec.parameters ?? {}) },
592
+ };
593
+ }
594
+ const pack = validateGateDocument(source.pack);
595
+ if (pack.kind !== 'GatePack')
596
+ throw new GateRegistryError('project floor pack requires a GatePack document');
597
+ const composed = pack.spec.extends === undefined ? pack : composeGatePack(pack, lookupAuthored);
598
+ validateResolvedPack(composed, providers);
599
+ for (const gate of composed.spec.gates) {
600
+ const level = gate.statistic?.levelBps;
601
+ const refs = [gate.threshold?.param, isParamRef(gate.minimumN) ? gate.minimumN.param : undefined,
602
+ isParamRef(level) ? level.param : undefined];
603
+ for (const name of refs) {
604
+ if (name !== undefined && composed.spec.parameters?.[name]?.default === undefined) {
605
+ throw new GateRegistryError(`project floor gate '${gate.id}' parameter '${name}' requires a default: it is the enforced minimum`);
606
+ }
607
+ }
608
+ }
609
+ return {
610
+ packId: pack.metadata.id,
611
+ gates: composed.spec.gates,
612
+ params: { ...(composed.spec.parameters ?? {}) },
613
+ };
614
+ });
615
+ const defaultAuthored = lookupAuthored(INTEGRITY_CEILING_FLOOR_PACK_ID);
616
+ const defaultComposed = composeGatePack(defaultAuthored, lookupAuthored);
617
+ validateResolvedPack(defaultComposed, providers);
618
+ const defaultGate = defaultComposed.spec.gates.find(gate => gate.id === 'integrity-ceiling');
619
+ if (defaultGate === undefined) {
620
+ throw new GateRegistryError(`shipped default floor ${INTEGRITY_CEILING_FLOOR_PACK_ID} has no integrity-ceiling gate`);
621
+ }
622
+ if (!expanded.some(floor => governsDefaultCeiling(floor, defaultGate))) {
623
+ expanded.push({
624
+ packId: defaultAuthored.metadata.id,
625
+ gates: defaultComposed.spec.gates,
626
+ params: { ...(defaultComposed.spec.parameters ?? {}) },
627
+ });
628
+ }
629
+ return expanded;
630
+ }
631
+ /**
632
+ * Resolves a binding gate's threshold parameter to its preregistered binding
633
+ * value, so floor comparison is always value-against-minimum in the gate's
634
+ * declared direction. A binding may parameterize a floor literal: the value,
635
+ * not the spelling, must tighten.
636
+ */
637
+ function synthesizeFloorCandidate(gate, packId, parameters) {
638
+ const materialize = (ref) => {
639
+ const value = parameters[qualifyGateParameter(packId, ref.param)];
640
+ if (value === undefined) {
641
+ throw new GateRegistryError(`binding gate '${gate.id}' parameter '${ref.param}' has no resolved value`);
642
+ }
643
+ return value;
644
+ };
645
+ let candidate = gate;
646
+ if (gate.threshold !== undefined && gate.threshold.param !== undefined) {
647
+ const { op, param } = gate.threshold;
648
+ candidate = { ...candidate, threshold: { op, value: materialize({ param }) } };
649
+ }
650
+ if (isParamRef(gate.minimumN))
651
+ candidate = { ...candidate, minimumN: materialize(gate.minimumN) };
652
+ const statistic = gate.statistic;
653
+ if (statistic !== undefined && isParamRef(statistic.levelBps)) {
654
+ candidate = { ...candidate, statistic: { ...statistic, levelBps: materialize(statistic.levelBps) } };
655
+ }
656
+ return candidate;
657
+ }
658
+ /**
659
+ * Enforces project floors over already-resolved binding packs. Project policy
660
+ * outranks addon and framework packs: a binding that omits a floor gate, or
661
+ * whose gate loosens any floor gate's threshold, parameter value, scope or
662
+ * outcome (via the shared per-kind `assertGateTightens` and the scope-superset
663
+ * rule), is refused with a diagnostic naming the binding, the gate and the
664
+ * floor pack. Every binding gate sharing a floor gate id must tighten it, so
665
+ * a loosened duplicate cannot evade the floor.
666
+ */
667
+ export function enforceProjectFloors(binding, packs, parameters, expanded, ceilings) {
668
+ // The '*' key is the project-wide default ceiling for every binding; a
669
+ // per-study key overrides it for that study only (exact match — renames
670
+ // never inherit). The star rule is re-checked here (C6): floors handed in
671
+ // directly, bypassing `validateGatesConfig`/`resolveProjectFloors`, still
672
+ // cannot carry starless or loosening per-study ceilings.
673
+ try {
674
+ assertCeilingDefaults(ceilings);
675
+ }
676
+ catch (error) {
677
+ throw new GateRegistryError(error instanceof Error ? error.message : 'invalid project ceilings');
678
+ }
679
+ const configured = ceilings[binding.metadata.id] ?? ceilings['*'];
680
+ if (!projectCeilingSatisfied(binding.spec.ceiling, configured)) {
681
+ throw new GateRegistryError(`binding '${binding.metadata.id}' declares ceiling ${binding.spec.ceiling ?? 'PROMOTE'}`
682
+ + ` below the project ceiling ${configured} for this study`);
683
+ }
684
+ const union = [];
685
+ for (const pack of packs) {
686
+ for (const gate of pack.resolved.spec.gates)
687
+ union.push({ gate, packId: pack.authored.metadata.id });
688
+ }
689
+ for (const floor of expanded) {
690
+ for (const floorGate of floor.gates) {
691
+ const matches = union.filter(candidate => candidate.gate.id === floorGate.id);
692
+ if (!matches.length) {
693
+ throw new GateRegistryError(`binding '${binding.metadata.id}' omits project floor gate '${floorGate.id}'`
694
+ + ` (floor pack '${floor.packId}'): every binding in this project must include and tighten it`);
695
+ }
696
+ for (const match of matches) {
697
+ try {
698
+ assertGateTightens(floorGate, synthesizeFloorCandidate(match.gate, match.packId, parameters), floor.params);
699
+ }
700
+ catch (error) {
701
+ throw new GateRegistryError(`binding '${binding.metadata.id}' loosens project floor gate '${floorGate.id}'`
702
+ + ` (floor pack '${floor.packId}'): ${error instanceof Error ? error.message : 'invalid tightening'}`);
703
+ }
704
+ }
705
+ }
706
+ }
707
+ }
708
+ /**
709
+ * Standalone snapshot readers. These are module functions, not `GateRegistry`
710
+ * methods, so a subclass override of `resolveBinding`/`resolvePack`/`getPack`
711
+ * cannot intercept them. The evaluator reads registry state only through these
712
+ * and recomposes through the pure functions above.
713
+ */
714
+ export function authoredGatePacksOf(registry) {
715
+ const packs = registry.packs;
716
+ if (!(packs instanceof Map))
717
+ throw new GateRegistryError('gate registry packs are unavailable');
718
+ return packs;
719
+ }
720
+ export function gateProvidersOf(registry) {
721
+ const providers = registry.providers;
722
+ if (providers === undefined || providers === null)
723
+ throw new GateRegistryError('gate registry providers are unavailable');
724
+ return providers;
725
+ }
726
+ /**
727
+ * Pure binding resolution over an explicit authored-pack map and provider registry.
728
+ * Project floors (`floors`, loaded from `aiwg.config` by the caller via
729
+ * `resolveProjectFloors`) are an optional trusted input: when present, the
730
+ * binding must include and tighten every floor gate and respect its per-study
731
+ * ceiling. When absent the check is skipped entirely and resolution is
732
+ * byte-identical to the floors-unaware path.
733
+ */
734
+ export function resolveGateBinding(doc, authoredById, providers, floors) {
735
+ const binding = validateGateDocument(doc);
736
+ if (binding.kind !== 'GateBinding')
737
+ throw new GateRegistryError('resolveBinding requires a GateBinding document');
738
+ const seenPins = new Set();
739
+ for (const pin of binding.spec.packs) {
740
+ if (seenPins.has(pin.id))
741
+ throw new GateRegistryError(`binding ${binding.metadata.id} pins pack ${pin.id} twice`);
742
+ seenPins.add(pin.id);
743
+ }
744
+ const lookupAuthored = (id) => {
745
+ if (id.includes(':')) {
746
+ const entry = authoredById.get(id);
747
+ if (entry === undefined)
748
+ throw new GateRegistryError(`unknown gate pack: ${id}`);
749
+ return entry.authored;
750
+ }
751
+ const matches = [...authoredById.keys()].filter(key => splitPackId(key).rest === id);
752
+ if (!matches.length)
753
+ throw new GateRegistryError(`unknown gate pack: ${id}`);
754
+ if (matches.length > 1) {
755
+ throw new GateRegistryError(`ambiguous gate pack '${id}': ${matches.sort().join(', ')} (pin the full id)`);
756
+ }
757
+ return authoredById.get(matches[0]).authored;
758
+ };
759
+ const packs = binding.spec.packs.map(pin => {
760
+ const entry = pin.id.includes(':') ? authoredById.get(pin.id) : (() => {
761
+ const matches = [...authoredById.keys()].filter(key => splitPackId(key).rest === pin.id);
762
+ if (!matches.length)
763
+ throw new GateRegistryError(`unknown gate pack: ${pin.id}`);
764
+ if (matches.length > 1) {
765
+ throw new GateRegistryError(`ambiguous gate pack '${pin.id}': ${matches.sort().join(', ')} (pin the full id)`);
766
+ }
767
+ return authoredById.get(matches[0]);
768
+ })();
769
+ if (entry === undefined)
770
+ throw new GateRegistryError(`unknown gate pack: ${pin.id}`);
771
+ if (entry.authored.metadata.id !== pin.id) {
772
+ throw new GateRegistryError(`binding ${binding.metadata.id} must pin the full pack id (got '${pin.id}')`);
773
+ }
774
+ if (artifactDigest(entry.authored) !== entry.digest
775
+ || entry.authored.metadata.version !== pin.version || entry.digest !== pin.digest) {
776
+ throw new GateRegistryError(`binding ${binding.metadata.id} pack pin mismatch for ${pinLabel(pin)}`);
777
+ }
778
+ const resolved = composeGatePack(entry.authored, lookupAuthored);
779
+ if (artifactDigest(resolved) !== pin.resolvedDigest) {
780
+ throw new GateRegistryError(`binding ${binding.metadata.id} composed-pack pin mismatch for ${pinLabel(pin)}`);
781
+ }
782
+ return {
783
+ authored: entry.authored, digest: entry.digest,
784
+ resolvedDigest: pin.resolvedDigest, resolved,
785
+ };
786
+ });
787
+ for (const pin of binding.spec.metricProviders) {
788
+ if (!providers.has(pin.id))
789
+ throw new GateRegistryError(`binding ${binding.metadata.id} pins unknown provider ${pin.id}`);
790
+ const provider = providers.get(pin.id);
791
+ if (provider.version !== pin.version || provider.sourceDigest !== pin.sourceDigest) {
792
+ throw new GateRegistryError(`binding ${binding.metadata.id} provider pin mismatch for ${pin.id}`);
793
+ }
794
+ // Bundle providers (#2831 rework): the pin must carry the loader-computed
795
+ // code digest, verified here against the loaded provider, plus the
796
+ // records digest the evaluator reproduces (records binding, P4). A byte
797
+ // change moves the code digest, so an old pin refuses until re-pinned.
798
+ // Null never verifies. Core pins carry neither digest.
799
+ const expectedCode = provider.codeDigest;
800
+ const pinnedCode = pin.codeDigest;
801
+ const pinnedRecords = pin.recordsDigest;
802
+ if (typeof expectedCode === 'string') {
803
+ if (typeof pinnedCode !== 'string' || pinnedCode !== expectedCode) {
804
+ throw new GateRegistryError(`binding ${binding.metadata.id} provider code pin mismatch for ${pin.id}`);
805
+ }
806
+ if (typeof pinnedRecords !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(pinnedRecords)) {
807
+ throw new GateRegistryError(`binding ${binding.metadata.id} provider records pin mismatch for ${pin.id}`);
808
+ }
809
+ }
810
+ else {
811
+ if (pinnedCode !== undefined) {
812
+ throw new GateRegistryError(`binding ${binding.metadata.id} provider code pin mismatch for ${pin.id}`);
813
+ }
814
+ if (pinnedRecords !== undefined) {
815
+ throw new GateRegistryError(`binding ${binding.metadata.id} provider records pin mismatch for ${pin.id}`);
816
+ }
817
+ }
818
+ }
819
+ const requiredProviders = new Set();
820
+ for (const pack of packs) {
821
+ for (const gate of pack.resolved.spec.gates) {
822
+ if (gate.metric !== undefined)
823
+ requiredProviders.add(gate.metric.provider);
824
+ }
825
+ }
826
+ const pinnedProviders = new Set(binding.spec.metricProviders.map(pin => pin.id));
827
+ for (const id of requiredProviders) {
828
+ if (!pinnedProviders.has(id)) {
829
+ throw new GateRegistryError(`binding ${binding.metadata.id} leaves provider ${id} used by a gate unpinned`);
830
+ }
831
+ }
832
+ const declared = new Map();
833
+ for (const pack of packs) {
834
+ for (const [name, parameter] of Object.entries(pack.resolved.spec.parameters ?? {})) {
835
+ declared.set(qualifyGateParameter(pack.authored.metadata.id, name), parameter);
836
+ }
837
+ }
838
+ const parameters = {};
839
+ for (const [qualified, parameter] of declared) {
840
+ const value = binding.spec.parameters[qualified] ?? parameter.default;
841
+ if (value === undefined)
842
+ throw new GateRegistryError(`binding ${binding.metadata.id} is missing required parameter ${qualified}`);
843
+ checkBindingValue(qualified, parameter, value);
844
+ parameters[qualified] = value;
845
+ }
846
+ for (const name of Object.keys(binding.spec.parameters)) {
847
+ if (!declared.has(name))
848
+ throw new GateRegistryError(`binding ${binding.metadata.id} sets undeclared parameter ${name}`);
849
+ }
850
+ const slices = new Set(binding.spec.slices);
851
+ for (const [group, members] of Object.entries(binding.spec.sliceGroups ?? {})) {
852
+ for (const slice of members) {
853
+ if (!slices.has(slice)) {
854
+ throw new GateRegistryError(`binding ${binding.metadata.id} sliceGroup '${group}' names slice '${slice}' outside the binding inventory`);
855
+ }
856
+ }
857
+ }
858
+ const references = new Set(binding.spec.references.map(reference => reference.name));
859
+ for (const pack of packs) {
860
+ for (const gate of pack.resolved.spec.gates) {
861
+ for (const slice of [...(gate.scope.slices ?? []), ...(gate.scope.except ?? [])]) {
862
+ if (!slices.has(slice))
863
+ throw new GateRegistryError(`gate ${gate.id} names slice '${slice}' outside the binding inventory`);
864
+ }
865
+ if (gate.statistic?.vs !== undefined && !references.has(gate.statistic.vs)) {
866
+ throw new GateRegistryError(`gate ${gate.id} contrasts undeclared reference '${gate.statistic.vs}'`);
867
+ }
868
+ if (gate.scope.mode === 'each') {
869
+ const excluded = new Set(gate.scope.except ?? []);
870
+ if (binding.spec.slices.every(slice => excluded.has(slice))) {
871
+ throw new GateRegistryError(`gate ${gate.id} each-scope excludes every binding slice`);
872
+ }
873
+ }
874
+ }
875
+ }
876
+ const registeredAt = Date.parse(binding.spec.registeredAt);
877
+ const frozenAt = Date.parse(binding.spec.frozenAt);
878
+ if (!Number.isFinite(registeredAt) || !Number.isFinite(frozenAt)) {
879
+ throw new GateRegistryError(`binding ${binding.metadata.id} has unparseable preregistration timestamps`);
880
+ }
881
+ if (registeredAt > frozenAt) {
882
+ throw new GateRegistryError(`binding ${binding.metadata.id} registers after it freezes`);
883
+ }
884
+ // Project floors run after every legacy check, so floors-unaware diagnostics
885
+ // keep their exact messages and order. Floor expansion reuses the same pure
886
+ // lookup the binding pins above, never registry instance state.
887
+ if (floors !== undefined) {
888
+ enforceProjectFloors(binding, packs, parameters, expandFloorPacks(floors, lookupAuthored, providers), floors.ceilings ?? {});
889
+ }
890
+ return { binding, packs, parameters };
891
+ }
892
+ export class GateRegistry {
893
+ providers;
894
+ packs = new Map();
895
+ constructor(providers) {
896
+ this.providers = providers;
897
+ }
898
+ registerPack(doc, origin) {
899
+ const pack = validateGateDocument(doc);
900
+ if (pack.kind !== 'GatePack')
901
+ throw new GateRegistryError('registerPack requires a GatePack document');
902
+ const { namespace, rest } = splitPackId(pack.metadata.id);
903
+ if (namespace !== origin.namespace) {
904
+ throw new GateRegistryError(`pack ${pack.metadata.id} namespace does not match contribution origin ${origin.namespace}`);
905
+ }
906
+ if (namespace !== 'project') {
907
+ if (origin.bundle === undefined || !origin.bundle.trim())
908
+ throw new GateRegistryError('bundle origin is required outside project:');
909
+ if (rest !== `${origin.bundle}/${rest.slice(origin.bundle.length + 1)}` || !rest.startsWith(`${origin.bundle}/`)) {
910
+ throw new GateRegistryError(`pack ${pack.metadata.id} does not belong to bundle ${origin.bundle}`);
911
+ }
912
+ }
913
+ if (this.packs.has(pack.metadata.id))
914
+ throw new GateRegistryError(`duplicate gate pack: ${pack.metadata.id}`);
915
+ // Shipped-name protection, order-independent: an aiwg: rest-path can never
916
+ // coexist with the same rest-path from any other namespace, whichever
917
+ // registers first.
918
+ for (const id of this.packs.keys()) {
919
+ if (splitPackId(id).rest !== rest)
920
+ continue;
921
+ const otherShipped = id.startsWith('aiwg:');
922
+ const nextShipped = pack.metadata.id.startsWith('aiwg:');
923
+ if (otherShipped || nextShipped) {
924
+ const shipped = nextShipped ? pack.metadata.id : id;
925
+ const shadow = nextShipped ? id : pack.metadata.id;
926
+ throw new GateRegistryError(`pack ${shadow} shadows shipped pack ${shipped}`);
927
+ }
928
+ }
929
+ if (pack.spec.extends !== undefined) {
930
+ this.requireParent(pack.metadata.id, pack.spec.extends);
931
+ }
932
+ const resolved = this.composeAuthored(pack);
933
+ validateResolvedPack(resolved, this.providers);
934
+ const { digest } = { digest: artifactDigest(pack) };
935
+ this.packs.set(pack.metadata.id, { authored: pack, digest, namespace });
936
+ return pack;
937
+ }
938
+ getPack(id) {
939
+ return this.lookupPack(id).authored;
940
+ }
941
+ resolvePack(id) {
942
+ const entry = this.lookupPack(id);
943
+ const resolved = this.composeAuthored(entry.authored);
944
+ return {
945
+ authored: entry.authored, digest: entry.digest,
946
+ resolvedDigest: artifactDigest(resolved), resolved,
947
+ };
948
+ }
949
+ /**
950
+ * Full-id lookup, or rest-path lookup when the id carries no namespace.
951
+ * A rest-path shared by two namespaces is `ambiguous`: callers must pin the
952
+ * full id. Bindings always pin full ids, so they never hit ambiguity.
953
+ */
954
+ lookupPack(id) {
955
+ if (id.includes(':')) {
956
+ const entry = this.packs.get(id);
957
+ if (entry === undefined)
958
+ throw new GateRegistryError(`unknown gate pack: ${id}`);
959
+ return entry;
960
+ }
961
+ const matches = [...this.packs.keys()].filter(key => splitPackId(key).rest === id);
962
+ if (!matches.length)
963
+ throw new GateRegistryError(`unknown gate pack: ${id}`);
964
+ if (matches.length > 1) {
965
+ throw new GateRegistryError(`ambiguous gate pack '${id}': ${matches.sort().join(', ')} (pin the full id)`);
966
+ }
967
+ return this.packs.get(matches[0]);
968
+ }
969
+ requireParent(childId, pin) {
970
+ const entry = this.packs.get(pin.id);
971
+ if (entry === undefined)
972
+ throw new GateRegistryError(`pack ${childId} extends unknown pack ${pin.id}`);
973
+ if (entry.authored.metadata.version !== pin.version || entry.digest !== pin.digest) {
974
+ throw new GateRegistryError(`pack ${childId} parent pin mismatch for ${pinLabel(pin)}`);
975
+ }
976
+ return entry;
977
+ }
978
+ composeAuthored(pack) {
979
+ return composeGatePack(pack, id => {
980
+ const parent = this.packs.get(id);
981
+ if (parent === undefined)
982
+ throw new GateRegistryError(`pack ${pack.metadata.id} extends unknown pack ${id}`);
983
+ return parent.authored;
984
+ });
985
+ }
986
+ resolveBinding(doc, floors) {
987
+ return resolveGateBinding(doc, this.packs, this.providers, floors);
988
+ }
989
+ }
990
+ function checkBindingValue(name, parameter, value) {
991
+ if (!Number.isFinite(value))
992
+ throw new GateRegistryError(`binding parameter ${name} must be finite`);
993
+ if (parameter.type === 'bps' && (value < 0 || value > 10000)) {
994
+ throw new GateRegistryError(`binding parameter ${name} must be within 0..10000 bps`);
995
+ }
996
+ if (parameter.type === 'level' && (!Number.isSafeInteger(value) || value <= 5000 || value >= 9999)) {
997
+ throw new GateRegistryError(`binding parameter ${name} must be an integer strictly between 5000 and 9999`);
998
+ }
999
+ if (parameter.type === 'count' && (!Number.isSafeInteger(value) || value < 0)) {
1000
+ throw new GateRegistryError(`binding parameter ${name} must be a non-negative integer`);
1001
+ }
1002
+ }
1003
+ //# sourceMappingURL=registry.js.map