@intentius/chant 0.124.40 → 0.124.42

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 (346) hide show
  1. package/dist/__fixtures__/oidc-emulator.d.ts +49 -0
  2. package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
  3. package/dist/__fixtures__/oidc-emulator.js +89 -0
  4. package/dist/ci-handover.d.ts +117 -0
  5. package/dist/ci-handover.d.ts.map +1 -0
  6. package/dist/ci-handover.js +217 -0
  7. package/dist/ci-identity.d.ts +190 -0
  8. package/dist/ci-identity.d.ts.map +1 -0
  9. package/dist/ci-identity.js +219 -0
  10. package/dist/cli/handlers/approve.d.ts +2 -1
  11. package/dist/cli/handlers/approve.d.ts.map +1 -1
  12. package/dist/cli/handlers/approve.js +20 -6
  13. package/dist/cli/handlers/build.d.ts.map +1 -1
  14. package/dist/cli/handlers/build.js +18 -1
  15. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  16. package/dist/cli/handlers/fan-out.js +42 -5
  17. package/dist/cli/handlers/gate-resume.d.ts +3 -0
  18. package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
  19. package/dist/cli/handlers/gate-resume.js +15 -2
  20. package/dist/cli/handlers/lock.d.ts +8 -0
  21. package/dist/cli/handlers/lock.d.ts.map +1 -0
  22. package/dist/cli/handlers/lock.js +197 -0
  23. package/dist/cli/handlers/pr-env.d.ts +34 -0
  24. package/dist/cli/handlers/pr-env.d.ts.map +1 -0
  25. package/dist/cli/handlers/pr-env.js +436 -0
  26. package/dist/cli/handlers/pr.d.ts +76 -0
  27. package/dist/cli/handlers/pr.d.ts.map +1 -1
  28. package/dist/cli/handlers/pr.js +615 -55
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/promote.js +9 -0
  31. package/dist/cli/handlers/query.d.ts +6 -0
  32. package/dist/cli/handlers/query.d.ts.map +1 -0
  33. package/dist/cli/handlers/query.js +80 -0
  34. package/dist/cli/handlers/run.d.ts.map +1 -1
  35. package/dist/cli/handlers/run.js +11 -0
  36. package/dist/cli/main.d.ts.map +1 -1
  37. package/dist/cli/main.js +49 -4
  38. package/dist/cli/parse-args.d.ts.map +1 -1
  39. package/dist/cli/parse-args.js +46 -2
  40. package/dist/cli/registry.d.ts +31 -2
  41. package/dist/cli/registry.d.ts.map +1 -1
  42. package/dist/components/capability.d.ts +47 -0
  43. package/dist/components/capability.d.ts.map +1 -1
  44. package/dist/components/capability.js +44 -0
  45. package/dist/components/cli-support.d.ts +6 -0
  46. package/dist/components/cli-support.d.ts.map +1 -1
  47. package/dist/components/cli-support.js +8 -1
  48. package/dist/components/driver.d.ts +10 -0
  49. package/dist/components/driver.d.ts.map +1 -1
  50. package/dist/components/driver.js +52 -2
  51. package/dist/components/fan-out-run.d.ts +37 -0
  52. package/dist/components/fan-out-run.d.ts.map +1 -1
  53. package/dist/components/fan-out-run.js +44 -4
  54. package/dist/components/gate-plan.d.ts +2 -0
  55. package/dist/components/gate-plan.d.ts.map +1 -1
  56. package/dist/components/gate-plan.js +1 -0
  57. package/dist/components/pr-environment.d.ts +298 -0
  58. package/dist/components/pr-environment.d.ts.map +1 -0
  59. package/dist/components/pr-environment.js +406 -0
  60. package/dist/components/pr-hooks.d.ts +66 -0
  61. package/dist/components/pr-hooks.d.ts.map +1 -0
  62. package/dist/components/pr-hooks.js +119 -0
  63. package/dist/components/pr-pipeline.d.ts +92 -2
  64. package/dist/components/pr-pipeline.d.ts.map +1 -1
  65. package/dist/components/pr-pipeline.js +150 -21
  66. package/dist/components/pr-policy.d.ts +109 -0
  67. package/dist/components/pr-policy.d.ts.map +1 -0
  68. package/dist/components/pr-policy.js +150 -0
  69. package/dist/components/pr-run.d.ts +6 -0
  70. package/dist/components/pr-run.d.ts.map +1 -1
  71. package/dist/components/pr-run.js +8 -3
  72. package/dist/components/wave-plan.d.ts +14 -1
  73. package/dist/components/wave-plan.d.ts.map +1 -1
  74. package/dist/components/wave-plan.js +34 -2
  75. package/dist/gate-decision.d.ts +15 -2
  76. package/dist/gate-decision.d.ts.map +1 -1
  77. package/dist/gate-decision.js +17 -2
  78. package/dist/gate-policy.d.ts +140 -0
  79. package/dist/gate-policy.d.ts.map +1 -0
  80. package/dist/gate-policy.js +242 -0
  81. package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
  82. package/dist/lifecycle/gate-checkpoint.js +1 -1
  83. package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
  84. package/dist/lifecycle/gate-seal.d.ts.map +1 -0
  85. package/dist/lifecycle/query.d.ts +68 -0
  86. package/dist/lifecycle/query.d.ts.map +1 -0
  87. package/dist/lifecycle/query.js +246 -0
  88. package/dist/lifecycle/run-ledger.d.ts +13 -0
  89. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  90. package/dist/lifecycle/run-ledger.js +16 -0
  91. package/dist/lifecycle/target-lock.d.ts +97 -0
  92. package/dist/lifecycle/target-lock.d.ts.map +1 -0
  93. package/dist/lifecycle/target-lock.js +286 -0
  94. package/dist/op/activities/activity-contracts.d.ts +30 -0
  95. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  96. package/dist/op/activities/activity-contracts.js +26 -0
  97. package/dist/op/activities/index.d.ts +2 -0
  98. package/dist/op/activities/index.d.ts.map +1 -1
  99. package/dist/op/activities/index.js +4 -0
  100. package/dist/op/activities/pr-refs.d.ts +10 -0
  101. package/dist/op/activities/pr-refs.d.ts.map +1 -0
  102. package/dist/op/activities/pr-refs.js +26 -0
  103. package/dist/op/activities/predict-behaviour.d.ts +2 -4
  104. package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
  105. package/dist/op/activities/predict-behaviour.js +2 -21
  106. package/dist/op/activities/review-agent.d.ts +137 -0
  107. package/dist/op/activities/review-agent.d.ts.map +1 -0
  108. package/dist/op/activities/review-agent.js +266 -0
  109. package/dist/op/builders.d.ts +9 -0
  110. package/dist/op/builders.d.ts.map +1 -1
  111. package/dist/op/builders.js +11 -0
  112. package/dist/op/cron.d.ts +8 -0
  113. package/dist/op/cron.d.ts.map +1 -1
  114. package/dist/op/cron.js +51 -0
  115. package/dist/op/gate-approval.d.ts +20 -13
  116. package/dist/op/gate-approval.d.ts.map +1 -1
  117. package/dist/op/gate-approval.js +13 -18
  118. package/dist/op/gate-quorum.d.ts +131 -0
  119. package/dist/op/gate-quorum.d.ts.map +1 -0
  120. package/dist/op/gate-quorum.js +183 -0
  121. package/dist/op/gate-resume.d.ts +4 -2
  122. package/dist/op/gate-resume.d.ts.map +1 -1
  123. package/dist/op/gate-resume.js +5 -4
  124. package/dist/op/gate-review.js +1 -1
  125. package/dist/op/gate.d.ts +32 -4
  126. package/dist/op/gate.d.ts.map +1 -1
  127. package/dist/op/gate.js +40 -24
  128. package/dist/op/index.d.ts +6 -4
  129. package/dist/op/index.d.ts.map +1 -1
  130. package/dist/op/index.js +4 -3
  131. package/dist/op/local-executor.d.ts.map +1 -1
  132. package/dist/op/local-executor.js +9 -3
  133. package/dist/op/op-waves-run.d.ts +28 -43
  134. package/dist/op/op-waves-run.d.ts.map +1 -1
  135. package/dist/op/op-waves-run.js +170 -120
  136. package/dist/op/op-waves.d.ts +55 -3
  137. package/dist/op/op-waves.d.ts.map +1 -1
  138. package/dist/op/op-waves.js +63 -8
  139. package/dist/op/schedule-overdue.d.ts +57 -0
  140. package/dist/op/schedule-overdue.d.ts.map +1 -0
  141. package/dist/op/schedule-overdue.js +138 -0
  142. package/dist/op/stage-hooks.d.ts +132 -0
  143. package/dist/op/stage-hooks.d.ts.map +1 -0
  144. package/dist/op/stage-hooks.js +142 -0
  145. package/dist/override-gate.d.ts +2 -2
  146. package/dist/override-gate.d.ts.map +1 -1
  147. package/dist/override-gate.js +1 -1
  148. package/dist/pr-forge.d.ts +64 -0
  149. package/dist/pr-forge.d.ts.map +1 -1
  150. package/dist/pr-forge.js +331 -44
  151. package/dist/pr-loop.d.ts +107 -1
  152. package/dist/pr-loop.d.ts.map +1 -1
  153. package/dist/pr-loop.js +84 -3
  154. package/dist/pr-post.d.ts +72 -0
  155. package/dist/pr-post.d.ts.map +1 -0
  156. package/dist/pr-post.js +134 -0
  157. package/dist/pr-requires.d.ts +38 -0
  158. package/dist/pr-requires.d.ts.map +1 -0
  159. package/dist/pr-requires.js +48 -0
  160. package/dist/wave-blocked.d.ts +79 -0
  161. package/dist/wave-blocked.d.ts.map +1 -0
  162. package/dist/wave-blocked.js +245 -0
  163. package/dist/wave-engine.d.ts +6 -0
  164. package/dist/wave-engine.d.ts.map +1 -1
  165. package/dist/wave-engine.js +2 -0
  166. package/dist/wave-runtime.mjs +243 -243
  167. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
  168. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
  169. package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
  170. package/dist/workspace/checks/ci-workflow.d.ts.map +1 -1
  171. package/dist/workspace/checks/ci-workflow.js +19 -4
  172. package/dist/workspace/ci-branches.d.ts +51 -0
  173. package/dist/workspace/ci-branches.d.ts.map +1 -0
  174. package/dist/workspace/ci-branches.js +69 -0
  175. package/dist/workspace/ci-cloud-identity.d.ts +11 -0
  176. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
  177. package/dist/workspace/ci-cloud-identity.js +24 -0
  178. package/dist/workspace/ci-init.d.ts.map +1 -1
  179. package/dist/workspace/ci-init.js +5 -4
  180. package/dist/workspace/ci-locks.d.ts +12 -0
  181. package/dist/workspace/ci-locks.d.ts.map +1 -0
  182. package/dist/workspace/ci-locks.js +6 -0
  183. package/dist/workspace/ci-run.d.ts +17 -1
  184. package/dist/workspace/ci-run.d.ts.map +1 -1
  185. package/dist/workspace/ci-run.js +66 -4
  186. package/dist/workspace/ci-workflow-at.d.ts +14 -0
  187. package/dist/workspace/ci-workflow-at.d.ts.map +1 -0
  188. package/dist/workspace/ci-workflow-at.js +17 -0
  189. package/dist/workspace/ci-workflow.d.ts +11 -3
  190. package/dist/workspace/ci-workflow.d.ts.map +1 -1
  191. package/dist/workspace/ci-workflow.js +10 -2
  192. package/dist/workspace/declaration.d.ts +69 -3
  193. package/dist/workspace/declaration.d.ts.map +1 -1
  194. package/dist/workspace/declaration.js +72 -1
  195. package/dist/workspace/declaration.schema.json +149 -2
  196. package/dist/workspace/identity-gate-rule.d.ts +1 -1
  197. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  198. package/dist/workspace/identity-gate-rule.js +1 -1
  199. package/dist/workspace/identity.d.ts +1 -1
  200. package/dist/workspace/identity.d.ts.map +1 -1
  201. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  202. package/dist/workspace/lineage-upgrade.js +8 -5
  203. package/dist/workspace/member-pipeline.d.ts +6 -0
  204. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  205. package/dist/workspace/member-pipeline.js +23 -0
  206. package/dist/workspace/pr-report.schema.json +94 -3
  207. package/dist/workspace/reason-codes.d.ts +1 -1
  208. package/dist/workspace/reason-codes.d.ts.map +1 -1
  209. package/dist/workspace/reason-codes.js +1 -1
  210. package/dist/workspace/security.d.ts.map +1 -1
  211. package/dist/workspace/security.js +4 -0
  212. package/dist/workspace/status-gates.d.ts +3 -2
  213. package/dist/workspace/status-gates.d.ts.map +1 -1
  214. package/dist/workspace/status-gates.js +8 -7
  215. package/dist/workspace/status.d.ts.map +1 -1
  216. package/dist/workspace/status.js +4 -1
  217. package/dist/workspace/trust/seal.d.ts +2 -2
  218. package/dist/workspace/trust/seal.d.ts.map +1 -1
  219. package/dist/workspace/trust/seal.js +2 -2
  220. package/package.json +1 -1
  221. package/src/__fixtures__/oidc-emulator.ts +118 -0
  222. package/src/ci-handover.test.ts +209 -0
  223. package/src/ci-handover.ts +237 -0
  224. package/src/ci-identity.test.ts +201 -0
  225. package/src/ci-identity.ts +310 -0
  226. package/src/cli/handlers/approve.ts +18 -5
  227. package/src/cli/handlers/build.ts +18 -1
  228. package/src/cli/handlers/fan-out.ts +43 -6
  229. package/src/cli/handlers/gate-resume.ts +16 -2
  230. package/src/cli/handlers/lock.test.ts +98 -0
  231. package/src/cli/handlers/lock.ts +202 -0
  232. package/src/cli/handlers/operator.test.ts +3 -1
  233. package/src/cli/handlers/pr-before-merge.test.ts +276 -0
  234. package/src/cli/handlers/pr-env.ts +462 -0
  235. package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
  236. package/src/cli/handlers/pr.test.ts +184 -0
  237. package/src/cli/handlers/pr.ts +605 -61
  238. package/src/cli/handlers/promote.ts +9 -0
  239. package/src/cli/handlers/query.ts +78 -0
  240. package/src/cli/handlers/run.ts +10 -0
  241. package/src/cli/main.ts +49 -4
  242. package/src/cli/parse-args.ts +34 -2
  243. package/src/cli/registry.ts +31 -2
  244. package/src/components/capability-targets.test.ts +162 -0
  245. package/src/components/capability.ts +71 -0
  246. package/src/components/cli-support.ts +14 -1
  247. package/src/components/driver.ts +54 -3
  248. package/src/components/fan-out-run.ts +58 -3
  249. package/src/components/fan-out-waves.test.ts +45 -1
  250. package/src/components/gate-plan.ts +3 -0
  251. package/src/components/pr-environment.test.ts +249 -0
  252. package/src/components/pr-environment.ts +538 -0
  253. package/src/components/pr-hooks.test.ts +118 -0
  254. package/src/components/pr-hooks.ts +157 -0
  255. package/src/components/pr-pipeline.ts +194 -21
  256. package/src/components/pr-policy.test.ts +145 -0
  257. package/src/components/pr-policy.ts +211 -0
  258. package/src/components/pr-run.ts +14 -2
  259. package/src/components/wave-plan.ts +38 -3
  260. package/src/gate-decision-bundle.test.ts +4 -3
  261. package/src/gate-decision.ts +39 -2
  262. package/src/gate-policy-bundle.test.ts +28 -0
  263. package/src/gate-policy.test.ts +162 -0
  264. package/src/gate-policy.ts +298 -0
  265. package/src/lifecycle/gate-checkpoint.ts +1 -1
  266. package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
  267. package/src/lifecycle/query.test.ts +91 -0
  268. package/src/lifecycle/query.ts +315 -0
  269. package/src/lifecycle/run-ledger.ts +18 -0
  270. package/src/lifecycle/target-lock.test.ts +195 -0
  271. package/src/lifecycle/target-lock.ts +347 -0
  272. package/src/op/activities/activity-contracts.ts +31 -0
  273. package/src/op/activities/index.ts +6 -0
  274. package/src/op/activities/pr-refs.ts +28 -0
  275. package/src/op/activities/predict-behaviour.ts +3 -23
  276. package/src/op/activities/review-agent.test.ts +240 -0
  277. package/src/op/activities/review-agent.ts +355 -0
  278. package/src/op/builders.ts +13 -0
  279. package/src/op/cron.ts +44 -0
  280. package/src/op/gate-alias.test.ts +2 -2
  281. package/src/op/gate-approval.test.ts +30 -9
  282. package/src/op/gate-approval.ts +23 -26
  283. package/src/op/gate-quorum.test.ts +139 -0
  284. package/src/op/gate-quorum.ts +248 -0
  285. package/src/op/gate-resume.ts +6 -3
  286. package/src/op/gate-review.ts +1 -1
  287. package/src/op/gate.ts +64 -23
  288. package/src/op/index.ts +13 -1
  289. package/src/op/local-executor.ts +9 -2
  290. package/src/op/op-waves-hook-targets.test.ts +126 -0
  291. package/src/op/op-waves-policy-hold.test.ts +66 -0
  292. package/src/op/op-waves-quorum.test.ts +101 -0
  293. package/src/op/op-waves-run.ts +197 -183
  294. package/src/op/op-waves.ts +83 -10
  295. package/src/op/schedule-overdue.test.ts +220 -0
  296. package/src/op/schedule-overdue.ts +161 -0
  297. package/src/op/stage-hooks.ts +251 -0
  298. package/src/override-gate-bundle.test.ts +1 -1
  299. package/src/override-gate.ts +2 -2
  300. package/src/pr-forge-bundle.test.ts +2 -2
  301. package/src/pr-forge.test.ts +33 -0
  302. package/src/pr-forge.ts +347 -35
  303. package/src/pr-loop-bundle.test.ts +2 -2
  304. package/src/pr-loop.test.ts +58 -0
  305. package/src/pr-loop.ts +170 -4
  306. package/src/pr-post.test.ts +141 -0
  307. package/src/pr-post.ts +159 -0
  308. package/src/pr-requires.test.ts +184 -0
  309. package/src/pr-requires.ts +79 -0
  310. package/src/review-agent-bundle.test.ts +35 -0
  311. package/src/stage-hooks-bundle.test.ts +38 -0
  312. package/src/wave-blocked-bundle.test.ts +25 -0
  313. package/src/wave-blocked.test.ts +146 -0
  314. package/src/wave-blocked.ts +271 -0
  315. package/src/wave-engine.ts +8 -0
  316. package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
  317. package/src/workspace/checks/ci-workflow.test.ts +9 -0
  318. package/src/workspace/checks/ci-workflow.ts +18 -4
  319. package/src/workspace/ci-branches.test.ts +62 -0
  320. package/src/workspace/ci-branches.ts +79 -0
  321. package/src/workspace/ci-cloud-identity.test.ts +53 -0
  322. package/src/workspace/ci-cloud-identity.ts +23 -0
  323. package/src/workspace/ci-init.ts +5 -4
  324. package/src/workspace/ci-locks.ts +16 -0
  325. package/src/workspace/ci-run-index.test.ts +2 -1
  326. package/src/workspace/ci-run.test.ts +107 -8
  327. package/src/workspace/ci-run.ts +74 -5
  328. package/src/workspace/ci-workflow-at.ts +23 -0
  329. package/src/workspace/ci-workflow.ts +13 -2
  330. package/src/workspace/declaration.schema.json +149 -2
  331. package/src/workspace/declaration.test.ts +76 -0
  332. package/src/workspace/declaration.ts +140 -5
  333. package/src/workspace/identity-gate-rule.ts +1 -1
  334. package/src/workspace/identity.ts +1 -1
  335. package/src/workspace/lineage-upgrade.ts +11 -8
  336. package/src/workspace/member-pipeline.test.ts +29 -0
  337. package/src/workspace/member-pipeline.ts +30 -0
  338. package/src/workspace/pr-report.schema.json +94 -3
  339. package/src/workspace/reason-codes.ts +1 -1
  340. package/src/workspace/security.ts +4 -0
  341. package/src/workspace/status-gates.ts +9 -6
  342. package/src/workspace/status.ts +4 -1
  343. package/src/workspace/trust/gate-seal.test.ts +1 -1
  344. package/src/workspace/trust/seal.ts +2 -2
  345. package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
  346. /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Gate-time policy on the pull-request path (#4294): `ci.policy` in the
3
+ * workspace declaration, run by `chant components pr-plan` and `pr-apply`
4
+ * over the change-set document the gate binds (`CapabilityPlan.changeSet`,
5
+ * composed by `planPrSet`).
6
+ *
7
+ * The settings and the policy directory are both read at the base the job
8
+ * measures the change from, so a pull request cannot loosen, edit or delete
9
+ * the policy that judges it. The engine and its result shapes are
10
+ * `../gate-policy.ts`.
11
+ *
12
+ * - deny: pr-apply applies nothing unless an override counts
13
+ * (`../override-gate.ts`, subject the gate's op, such as `pr-12`). The
14
+ * first refusal records the denial on `_gates/policy-override.jsonl`, so
15
+ * `chant approve policy-override <op> --rule <id> --reason <text>` can
16
+ * answer it. The gate's own approval is still needed. The override is
17
+ * spent just before anything applies.
18
+ * - hold: the plan needs a person's approval of its own digest. pr-apply
19
+ * always asks for one, so a hold changes one path: a green commit's plan
20
+ * no longer applies under the approvals of the merges it carries.
21
+ * - error: the policy did not run or gave no readable answer. pr-apply
22
+ * applies nothing.
23
+ */
24
+
25
+ import { readDeclaration, WorkspaceReadError } from "../workspace/declaration";
26
+ import { locateWorkspace } from "../workspace/which-chant";
27
+ import type { WorkspaceTree } from "../workspace/tree";
28
+ import {
29
+ checkGatePolicy,
30
+ DEFAULT_POLICY_PATH,
31
+ exportGatePolicy,
32
+ type GatePolicyExec,
33
+ type GatePolicySettings,
34
+ type GatePolicyVerdict,
35
+ } from "../gate-policy";
36
+ import type { ChangeSetDocument } from "../change-set";
37
+ import type { GateLedgerPort } from "../op/gate";
38
+ import type { PrPolicyReport } from "../pr-loop";
39
+
40
+ /** `ci.policy` as read at base. */
41
+ export interface PrLoopPolicy {
42
+ /** Absent when the declaration at base sets none. */
43
+ settings?: GatePolicySettings;
44
+ /** The commit it was read at. */
45
+ base: string;
46
+ /** The tree of the declaration's directory at base, where the policy directory is read. */
47
+ tree?: WorkspaceTree;
48
+ /** A line for the log when no policy runs because the base has no declaration, or could not be read. */
49
+ note?: string;
50
+ }
51
+
52
+ /** Whether the working tree's declaration names a `ci.policy`. */
53
+ function checkoutNamesPolicy(cwd: string): boolean {
54
+ try {
55
+ return readDeclaration(locateWorkspace(cwd).tree).ci?.policy !== undefined;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * The policy a pull-request job in `cwd` runs, from the declaration at
63
+ * `base`. Throws when the declaration at base cannot be read and the
64
+ * working tree's names a policy: running none would let the change waive it.
65
+ */
66
+ export function prLoopPolicyAt(cwd: string, base: string): PrLoopPolicy {
67
+ try {
68
+ const located = locateWorkspace(cwd, base);
69
+ const settings = readDeclaration(located.tree).ci?.policy;
70
+ return { base, ...(settings ? { settings, tree: located.tree } : {}) };
71
+ } catch (err) {
72
+ const missing = err instanceof WorkspaceReadError && (err.code === "declaration-missing" || err.code === "not-a-git-repository");
73
+ const why = err instanceof Error ? err.message : String(err);
74
+ if (checkoutNamesPolicy(cwd)) {
75
+ if (missing) return { base, note: `ci.policy: the base ${base.slice(0, 12)} has no workspace declaration, so the policy this change declares runs once it merges` };
76
+ throw new Error(`ci.policy is read from the workspace declaration at ${base.slice(0, 12)}, and it could not be read (${why}), so the policy cannot be run`);
77
+ }
78
+ return missing ? { base } : { base, note: `ci.policy: the declaration at ${base.slice(0, 12)} could not be read (${why}); this checkout names no policy, so none runs` };
79
+ }
80
+ }
81
+
82
+ /** What the engine reads as `input`. */
83
+ export interface PrPolicyInput {
84
+ version: 1;
85
+ stage: "plan" | "apply";
86
+ /** The gate's op, `pr-<n>`. */
87
+ op: string;
88
+ gate: string;
89
+ env: string;
90
+ pr: number | null;
91
+ base: string;
92
+ head: string;
93
+ /** The change-set document the gate binds. Its `entries` are every change the plan proposes. */
94
+ changeSet: ChangeSetDocument;
95
+ }
96
+
97
+ /** Run the policy over `input`. Undefined when the base sets no policy. */
98
+ export function runPrPolicy(read: PrLoopPolicy, input: PrPolicyInput, exec?: GatePolicyExec): GatePolicyVerdict | undefined {
99
+ if (!read.settings || !read.tree) return undefined;
100
+ const engine = read.settings.engine ?? "conftest";
101
+ const path = read.settings.path ?? DEFAULT_POLICY_PATH;
102
+ const exported = exportGatePolicy(read.tree, path);
103
+ if ("error" in exported) return { result: "error", engine, denials: [], rules: [], holds: [], warnings: [], error: exported.error };
104
+ try {
105
+ return checkGatePolicy({ settings: read.settings, input, policyDir: exported.dir, cwd: process.cwd(), ...(exec ? { exec } : {}) });
106
+ } finally {
107
+ exported.cleanup();
108
+ }
109
+ }
110
+
111
+ /** The verdict as the report keeps it. */
112
+ export function prPolicyReport(verdict: GatePolicyVerdict, base: string): PrPolicyReport {
113
+ return {
114
+ result: verdict.result,
115
+ engine: verdict.engine,
116
+ base,
117
+ denials: verdict.denials,
118
+ ...(verdict.rules.length > 0 ? { rules: verdict.rules } : {}),
119
+ holds: verdict.holds,
120
+ warnings: verdict.warnings,
121
+ ...(verdict.error !== undefined ? { error: verdict.error } : {}),
122
+ };
123
+ }
124
+
125
+ export interface PrPolicyOverrideInput {
126
+ port: GateLedgerPort;
127
+ /** The override's subject: the gate's op. */
128
+ subject: string;
129
+ /** The digest the gate binds: the change-set document's. */
130
+ digest: string;
131
+ rules: readonly string[];
132
+ /** The commit the overriders and signers are read at. */
133
+ base: string;
134
+ cwd: string;
135
+ /** Count only sealed overrides, as a sealed gate counts only sealed approvals. */
136
+ sealed: boolean;
137
+ /** Record the denial when no override counts and none stands. The plan stage records nothing. */
138
+ record: boolean;
139
+ description?: string;
140
+ now?: string;
141
+ }
142
+
143
+ /** Where an override of a denial stands. */
144
+ export type PrPolicyOverride =
145
+ | { status: "overridden"; override: NonNullable<PrPolicyReport["override"]> }
146
+ | { status: "none"; refusals: string[]; command: string; overriders: string[]; recorded: boolean; moved?: { by: string } };
147
+
148
+ /**
149
+ * Decide a denied plan against the override ledger, as an Op waves job
150
+ * decides a denied run (`settleDenials` in `../op/op-waves-run.ts`).
151
+ */
152
+ export async function decidePrPolicyOverride(input: PrPolicyOverrideInput): Promise<PrPolicyOverride> {
153
+ const og = await import("../override-gate");
154
+ const overriders = og.overridersAt(input.cwd, input.base).principals;
155
+ const signers = input.sealed ? (await import("../gate-decision")).gateSignersAt(input.cwd, input.base) : undefined;
156
+ const ledger = await input.port.read(og.OVERRIDE_OP);
157
+ const applied = ((await input.port.readApplied?.(og.OVERRIDE_OP)) ?? []) as unknown as import("../override-gate").AppliedOverrideRecord[];
158
+ const now = input.now ?? new Date().toISOString();
159
+ const verdict = og.decideOverride({
160
+ ledger,
161
+ applied,
162
+ subject: input.subject,
163
+ planDigest: input.digest,
164
+ rules: input.rules,
165
+ overriders,
166
+ mode: input.sealed ? "sealed" : "ledger",
167
+ ...(signers ? { signers } : {}),
168
+ now,
169
+ });
170
+ if (verdict.status === "overridden") {
171
+ const o = verdict.override;
172
+ return { status: "overridden", override: { by: o.by, at: o.at, reason: o.reason, digest: o.digest } };
173
+ }
174
+ let recorded = false;
175
+ if (input.record && !verdict.standing) {
176
+ const { version: _v, kind: _k, ...fact } = og.overridePending({
177
+ subject: input.subject,
178
+ planDigest: input.digest,
179
+ rules: input.rules,
180
+ timestamp: now,
181
+ expiresAt: new Date(new Date(now).getTime() + 48 * 3600 * 1000).toISOString(),
182
+ ...(input.description ? { description: input.description } : {}),
183
+ });
184
+ await input.port.appendPending(fact);
185
+ recorded = true;
186
+ }
187
+ return {
188
+ status: "none",
189
+ refusals: verdict.refusals,
190
+ command: og.overrideCommand(input.subject, input.rules, input.sealed),
191
+ overriders,
192
+ recorded,
193
+ ...(verdict.status === "moved" ? { moved: { by: verdict.by } } : {}),
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Spend the override the apply runs under: append its applied record before
199
+ * anything applies. Throws when it could not be written or did not reach the
200
+ * remote, and the caller applies nothing.
201
+ */
202
+ export async function spendPrPolicyOverride(port: GateLedgerPort, subject: string, override: NonNullable<PrPolicyReport["override"]>, at: { now?: string; commit?: string } = {}): Promise<void> {
203
+ const og = await import("../override-gate");
204
+ if (!port.appendApplied) throw new Error("this gate ledger cannot record an applied override");
205
+ const record = og.appliedOverride({ subject, digest: override.digest, at: override.at, by: override.by }, { timestamp: at.now ?? new Date().toISOString(), ...(at.commit ? { commit: at.commit } : {}) });
206
+ const pushed = await port.appendApplied(record as unknown as Parameters<NonNullable<GateLedgerPort["appendApplied"]>>[0]);
207
+ const { isLifecycleLocalNote } = await import("../lifecycle/local-note");
208
+ if (!pushed.pushed && !isLifecycleLocalNote(pushed.pushWarning)) {
209
+ throw new Error(`the applied record did not reach the remote the next run reads: ${pushed.pushWarning ?? "the push did not land"}`);
210
+ }
211
+ }
@@ -110,6 +110,8 @@ export interface PrPlanSetOptions {
110
110
  vars?: Record<string, unknown>;
111
111
  /** Outputs supplied by the caller (`--seed-outputs`). They win over outputs read here. */
112
112
  seededOutputs?: Record<string, Record<string, unknown>>;
113
+ /** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
114
+ targets?: Readonly<Record<string, readonly string[]>>;
113
115
  /**
114
116
  * Where the planning's events go (#4041): a `plan` phase with no
115
117
  * component, and in it one `plan` step per component, started as its wave
@@ -384,6 +386,8 @@ export interface PrApplyOptions {
384
386
  vars?: Record<string, unknown>;
385
387
  /** The set {@link planPrSet} planned and the gate approved, or {@link resumePrSet}'s set. */
386
388
  set: PrPlanSet;
389
+ /** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
390
+ targets?: Readonly<Record<string, readonly string[]>>;
387
391
  onProgress?: (event: RunProgressEvent) => void;
388
392
  /** Where the apply's events go (#4041): the fan-out's run events. */
389
393
  onRunEvent?: RunEventSink;
@@ -467,6 +471,7 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
467
471
  env: options.env,
468
472
  ...(options.vars ? { vars: options.vars } : {}),
469
473
  plans,
474
+ ...(options.targets ? { targets: options.targets } : {}),
470
475
  componentOutputs: outputs,
471
476
  hold: (name, now) => heldReason(byName.get(name)!, read, now, inChange),
472
477
  ...(options.onProgress ? { onProgress: options.onProgress } : {}),
@@ -491,6 +496,7 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
491
496
  env: options.env,
492
497
  ...(options.vars ? { vars: options.vars } : {}),
493
498
  seededOutputs: run.componentOutputs,
499
+ ...(options.targets ? { targets: options.targets } : {}),
494
500
  });
495
501
  for (const name of plan.order) roundOf.set(name, { round, set });
496
502
  const record = (status: PrRound["status"], approval: PrApproval, message?: string): void => {
@@ -514,7 +520,7 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
514
520
 
515
521
  const completed = new Set(run.completed);
516
522
  const held = new Map((run.held ?? []).map((h) => [h.component, h]));
517
- const blocked = new Set(run.blocked.map((b) => b.component));
523
+ const blocked = new Map(run.blocked.map((b) => [b.component, b.blockedBy]));
518
524
  const errorOf = (component: string): string | undefined => {
519
525
  const result = [...run.results].reverse().find((r) => r.component === component);
520
526
  if (result?.status === "gated") return `stopped at its own gate ${result.gate?.gate ?? ""}`.trim();
@@ -529,7 +535,10 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
529
535
  const h = held.get(m.component);
530
536
  if (h) return { ...asPlanned, status: "waiting", held: h.heldBy ? `${h.heldBy} is held: ${h.reason}` : h.reason };
531
537
  if (fresh?.status === "plan-failed") return { ...asPlanned, status: "plan-failed", ...(fresh.error ? { error: fresh.error } : {}) };
532
- if (blocked.has(m.component)) return { ...asPlanned, status: "blocked" };
538
+ if (blocked.has(m.component)) {
539
+ const by = blocked.get(m.component);
540
+ return { ...asPlanned, status: "blocked", ...(by ? { blockedBy: by } : {}) };
541
+ }
533
542
  const error = errorOf(m.component);
534
543
  return { ...asPlanned, status: "failed", ...(error ? { error } : {}) };
535
544
  });
@@ -623,6 +632,8 @@ export interface PrResumeOptions {
623
632
  env: string;
624
633
  vars?: Record<string, unknown>;
625
634
  seededOutputs?: Record<string, Record<string, unknown>>;
635
+ /** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
636
+ targets?: Readonly<Record<string, readonly string[]>>;
626
637
  /** The approved set, from the attempt record. */
627
638
  record: PrApplyRecord;
628
639
  /** Components an earlier attempt applied. */
@@ -674,6 +685,7 @@ export async function resumePrSet(options: PrResumeOptions): Promise<PrResume> {
674
685
  env: options.env,
675
686
  ...(options.vars ? { vars: options.vars } : {}),
676
687
  seededOutputs: { ...(options.seededOutputs ?? {}), ...options.record.planOutputs },
688
+ ...(options.targets ? { targets: options.targets } : {}),
677
689
  });
678
690
  const reasons = fresh.doc.members
679
691
  .map((m) => planCoveredBy(options.record.changeSet, fresh.doc, m.member))
@@ -27,11 +27,11 @@
27
27
  * `@<component>.publish.*`) resolves.
28
28
  */
29
29
 
30
- import { resolveStepInput, unresolvedWiringSources, type DriverComponent, type DriverPhase, type DriverStep } from "./driver";
30
+ import { assertTargetsAccepted, resolveStepInput, unresolvedWiringSources, type DriverComponent, type DriverPhase, type DriverStep } from "./driver";
31
31
  import { componentPlanDigest } from "./gate-plan";
32
32
  import { componentDeployInputs, type ComponentDeployInputs } from "./deploy-inputs";
33
33
  import { computePlanDigest } from "../lifecycle/plan-digest";
34
- import type { CapabilityRegistry, DeployContext, PlannedOutputs } from "./capability";
34
+ import type { CapabilityPlan, CapabilityRegistry, DeployContext, PlannedOutputs } from "./capability";
35
35
  import type { WaveMember } from "../gated-waves";
36
36
  import type { ChangeSetPart } from "../change-set";
37
37
 
@@ -101,12 +101,13 @@ export async function planWaveComponent(
101
101
  const plans: Record<string, unknown> = {};
102
102
  const parts: ChangeSetPart[] = [];
103
103
  let outputs: PlannedOutputs | undefined;
104
+ assertTargetsAccepted(component, registry, ctx);
104
105
  for (const step of capabilitySteps(component.deploy)) {
105
106
  const capability = registry.resolve(step.kind);
106
107
  if (!capability.plan) continue;
107
108
  const { kind: _kind, ...rest } = step;
108
109
  const input = resolveStepInput(rest, {}, componentOutputs);
109
- const planned = await capability.plan(planContext(ctx, rest, componentOutputs), input as never);
110
+ const planned = bindTargets(step.kind, ctx, await capability.plan(planContext(ctx, rest, componentOutputs), input as never));
110
111
  const member = { member: planned.member, planDigest: planned.planDigest };
111
112
  members.push(member);
112
113
  parts.push(planned.changeSet ?? emptyPart(member));
@@ -124,6 +125,7 @@ export async function planWaveComponent(
124
125
  component,
125
126
  ...(ctx.vars ? { vars: ctx.vars } : {}),
126
127
  inputs: read.inputs,
128
+ ...(ctx.targets ? { targets: ctx.targets } : {}),
127
129
  }),
128
130
  };
129
131
  members.push(member);
@@ -145,6 +147,39 @@ export async function planWaveComponent(
145
147
  }
146
148
  }
147
149
 
150
+ /**
151
+ * A member's digest with its targets in it (#4300), the way terragucci's
152
+ * cone plan digest binds a cone: the digest over the plan's own digest and
153
+ * the sorted targets, so an approval of one narrowing covers no other.
154
+ */
155
+ export function targetedPlanDigest(planDigest: string, targets: readonly string[]): string {
156
+ return computePlanDigest("capability-targets", { plan: planDigest, targets: [...new Set(targets)].sort() });
157
+ }
158
+
159
+ /**
160
+ * A step's plan made with `ctx.targets` (#4300), its digest bound to them.
161
+ * The plan must give back the targets it was asked for, which says it planned
162
+ * those alone; one that gives back others, or none, refuses. Without
163
+ * `ctx.targets`, the plan as it came.
164
+ */
165
+ export function bindTargets(kind: string, ctx: DeployContext, planned: CapabilityPlan): CapabilityPlan {
166
+ const asked = ctx.targets;
167
+ if (!asked || asked.length === 0) return planned;
168
+ const want = [...new Set(asked)].sort();
169
+ const got = [...new Set(planned.targets ?? [])].sort();
170
+ if (got.length !== want.length || got.some((t, i) => t !== want[i])) {
171
+ throw new Error(
172
+ `component "${ctx.component}": ${kind} was asked to plan ${want.join(", ")} but its plan of ${planned.member} ` +
173
+ (got.length === 0 ? "names no targets" : `names ${got.join(", ")}`),
174
+ );
175
+ }
176
+ const planDigest = targetedPlanDigest(planned.planDigest, want);
177
+ const changeSet = planned.changeSet
178
+ ? { ...planned.changeSet, member: { ...planned.changeSet.member, planDigest } }
179
+ : undefined;
180
+ return { ...planned, planDigest, targets: want, ...(changeSet ? { changeSet } : {}) };
181
+ }
182
+
148
183
  /** `next` over `prior`, as a later step's outputs win over an earlier one's in a run. */
149
184
  function mergePlannedOutputs(prior: PlannedOutputs | undefined, next: PlannedOutputs): PlannedOutputs {
150
185
  const known = { ...(prior?.known ?? {}), ...next.known };
@@ -41,20 +41,21 @@ describe("the gate-decision bundle", () => {
41
41
  "packages/core/src/intrinsic.ts",
42
42
  "packages/core/src/lifecycle/gate-applied.ts",
43
43
  "packages/core/src/lifecycle/gate-ledger-select.ts",
44
+ "packages/core/src/lifecycle/gate-seal.ts",
44
45
  "packages/core/src/lifecycle/plan-digest.ts",
45
46
  "packages/core/src/notify-verify.ts",
46
47
  "packages/core/src/notify.ts",
47
48
  "packages/core/src/op/gate-point.ts",
49
+ "packages/core/src/op/gate-quorum.ts",
48
50
  "packages/core/src/op/op-waves.ts",
49
51
  "packages/core/src/wave-layout.ts",
50
52
  "packages/core/src/workspace/class-data.ts",
51
53
  "packages/core/src/workspace/identity-gate-rule.ts",
52
54
  "packages/core/src/workspace/jsonc.ts",
53
- "packages/core/src/workspace/trust/gate-seal.ts",
54
55
  "packages/core/src/workspace/trust/trust-files.ts",
55
56
  ]);
56
- // About 43 KB unminified today, 7 KB of it the pinned classes read as data (#3888).
57
+ // About 49 KB unminified today: 7 KB of it the pinned classes read as data (#3888), 6 KB the approval rules (#4290).
57
58
  const bytes = result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0);
58
- expect(bytes).toBeLessThan(48 * 1024);
59
+ expect(bytes).toBeLessThan(54 * 1024);
59
60
  });
60
61
  });
@@ -27,6 +27,11 @@
27
27
  * counts for nothing, as terragucci's wave gate has it: the caller hands in
28
28
  * the lines of `_gates/<op>/applied.jsonl` ({@link parseAppliedGateLines}).
29
29
  *
30
+ * Approval rules (#4290), a quorum as a list of `{ targets, count, roles,
31
+ * notAuthor }`, are judged with `./op/gate-quorum.ts`, re-exported here:
32
+ * {@link trustRolesAt} and {@link gateSignersAt} read what
33
+ * `trustQuorumJudge` judges by, at base.
34
+ *
30
35
  * The module reads the ledger lines and the review it is handed; reading
31
36
  * them is the caller's. It imports no TypeScript, esbuild, zod, ajv, lint,
32
37
  * fold or codegen module, no package's code, and none of chant's git ledger
@@ -46,7 +51,7 @@ import {
46
51
  } from "./lifecycle/gate-ledger-select";
47
52
  import { samePlanDigest } from "./lifecycle/plan-digest";
48
53
  import { isApprovalSpent, type AppliedGateRecord } from "./lifecycle/gate-applied";
49
- import { gateSealRefusal, type GateSigners, type SealedGateApproval } from "./workspace/trust/gate-seal";
54
+ import { gateSealRefusal, type GateSigners, type SealedGateApproval } from "./lifecycle/gate-seal";
50
55
  import {
51
56
  DEFAULT_SIGNERS_PATH,
52
57
  TRUST_CONFIG_PATH,
@@ -79,7 +84,7 @@ export {
79
84
  type GateSigners,
80
85
  type SealedGateApproval,
81
86
  type SshSignatureCheck,
82
- } from "./workspace/trust/gate-seal";
87
+ } from "./lifecycle/gate-seal";
83
88
  export {
84
89
  DEFAULT_SIGNERS_PATH,
85
90
  TRUST_CONFIG_PATH,
@@ -105,6 +110,28 @@ export {
105
110
  type AppliedGateRecord,
106
111
  } from "./lifecycle/gate-applied";
107
112
  export type { MergedReview } from "./op/gate-review";
113
+ export {
114
+ quorumNeed,
115
+ quorumProblems,
116
+ quorumRuleLine,
117
+ quorumRuleProgress,
118
+ quorumRules,
119
+ quorumRulesFor,
120
+ quorumWaiting,
121
+ rolesOf,
122
+ ruleNeedsProof,
123
+ samePrincipal,
124
+ targetGlobMatch,
125
+ trustQuorumJudge,
126
+ UNPROVEN_JUDGE,
127
+ type GateQuorumJudge,
128
+ type GateQuorumRule,
129
+ type GateQuorumRuleProgress,
130
+ type GateQuorumSpec,
131
+ type QuorumApprover,
132
+ type QuorumJudgement,
133
+ type TrustQuorumInput,
134
+ } from "./op/gate-quorum";
108
135
 
109
136
  // ── reading at base ──────────────────────────────────────────────────────
110
137
 
@@ -172,6 +199,16 @@ export function gateSignersAt(repo: string, base: string): GateSigners {
172
199
  return trustAt(repo, base).signers;
173
200
  }
174
201
 
202
+ /**
203
+ * The role grants of `.chant/trust.json` at `base` (#4290): role name to the
204
+ * principals that hold it, as chant reads them. A missing file gives none,
205
+ * and so does one that is not valid, which turns chant's trust off.
206
+ * {@link trustQuorumJudge} takes them with {@link gateSignersAt}'s signers.
207
+ */
208
+ export function trustRolesAt(repo: string, base: string): Record<string, string[]> {
209
+ return trustAt(repo, base).roles;
210
+ }
211
+
175
212
  /** A gate the declaration at base names under `identity.gates`, and the principal class its signer must be in, if any. */
176
213
  export interface IdentityGate {
177
214
  gate: string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The gate policy bundles alone (#4294), as the override gate does: a
3
+ * product runs the engine and reads its answer from one bundled file with
4
+ * no TypeScript toolchain. This bundles `@intentius/chant/gate-policy` and
5
+ * reads esbuild's list of what went in.
6
+ */
7
+
8
+ import { join } from "node:path";
9
+ import { build } from "esbuild";
10
+ import { describe, expect, test } from "vitest";
11
+ import { REPO } from "./workspace/__fixtures__/contract-repo";
12
+
13
+ describe("the gate-policy bundle", () => {
14
+ test("holds the policy module and nothing else", async () => {
15
+ const result = await build({
16
+ entryPoints: [join(REPO, "packages", "core", "src", "gate-policy.ts")],
17
+ bundle: true,
18
+ platform: "node",
19
+ format: "esm",
20
+ write: false,
21
+ metafile: true,
22
+ outdir: "out",
23
+ logLevel: "silent",
24
+ });
25
+ expect(Object.keys(result.metafile.inputs).map((i) => i.replace(/^.*?packages\//, "packages/"))).toEqual(["packages/core/src/gate-policy.ts"]);
26
+ expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(16 * 1024);
27
+ });
28
+ });
@@ -0,0 +1,162 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ assertGatePolicySettings,
6
+ checkGatePolicy,
7
+ conftestFindings,
8
+ exportGatePolicy,
9
+ gatePolicyArgv,
10
+ opaFindings,
11
+ type GatePolicyExec,
12
+ type GatePolicyTree,
13
+ } from "./gate-policy";
14
+
15
+ /** conftest's JSON for one input: a deny with its query, an older deny without one, and a warning. */
16
+ const CONFTEST_DENY = JSON.stringify([
17
+ {
18
+ filename: "input.json",
19
+ namespace: "main",
20
+ successes: 2,
21
+ failures: [
22
+ { msg: "bucket logs is public", metadata: { query: "data.main.deny_public_bucket" } },
23
+ { msg: { msg: "no owner tag" } },
24
+ ],
25
+ warnings: [{ msg: "the plan deletes 3 resources" }],
26
+ },
27
+ ]);
28
+
29
+ const opa = (value: unknown): string => JSON.stringify({ result: [{ expressions: [{ value, text: "data.main", location: { row: 1, col: 1 } }] }] });
30
+
31
+ describe("reading the engine's answer (#4294)", () => {
32
+ test("conftest: failures deny by their query, warnings are advice", () => {
33
+ expect(conftestFindings(CONFTEST_DENY)).toEqual({
34
+ denials: [
35
+ { rule: "main.deny_public_bucket", message: "bucket logs is public" },
36
+ { rule: "main.deny", message: "no owner tag" },
37
+ ],
38
+ holds: [],
39
+ warnings: ["the plan deletes 3 resources"],
40
+ });
41
+ expect(conftestFindings(JSON.stringify([{ filename: "input.json", namespace: "main", successes: 1 }]))).toEqual({ denials: [], holds: [], warnings: [] });
42
+ expect(conftestFindings("not json")).toBeUndefined();
43
+ expect(conftestFindings(JSON.stringify({ result: [] }))).toBeUndefined();
44
+ });
45
+
46
+ test("opa: deny, violation, hold and warn rules by name; a child package one level down; allow_unattended is no rule", () => {
47
+ const found = opaFindings(
48
+ opa({
49
+ deny: ["public bucket"],
50
+ violation_size: "too many deletes",
51
+ hold_iam: ["an IAM role changes"],
52
+ hold: true,
53
+ warn: ["consider tags"],
54
+ allow_unattended: true,
55
+ allow_unattended_docs: ["docs only"],
56
+ other: 3,
57
+ aws: { deny_region: ["us-west-1 is not allowed"] },
58
+ }),
59
+ );
60
+ expect(found).toEqual({
61
+ denials: [
62
+ { rule: "main.aws.deny_region", message: "us-west-1 is not allowed" },
63
+ { rule: "main.deny", message: "public bucket" },
64
+ { rule: "main.violation_size", message: "too many deletes" },
65
+ ],
66
+ holds: [
67
+ { rule: "main.hold", message: "hold" },
68
+ { rule: "main.hold_iam", message: "an IAM role changes" },
69
+ ],
70
+ warnings: ["consider tags"],
71
+ });
72
+ });
73
+
74
+ test("opa: an undefined query matches nothing, an array is a deny set, errors and other shapes are no answer", () => {
75
+ expect(opaFindings(JSON.stringify({}))).toEqual({ denials: [], holds: [], warnings: [] });
76
+ expect(opaFindings(opa(["a", { msg: "b" }]), "policy.deny")).toEqual({ denials: [{ rule: "policy.deny", message: "a" }, { rule: "policy.deny", message: "b" }], holds: [], warnings: [] });
77
+ expect(opaFindings(JSON.stringify({ errors: [{ message: "rego_parse_error" }] }))).toBeUndefined();
78
+ expect(opaFindings(opa(7))).toBeUndefined();
79
+ expect(opaFindings("{")).toBeUndefined();
80
+ });
81
+ });
82
+
83
+ describe("running the engine (#4294)", () => {
84
+ const fake = (out: { status: number | null; stdout?: string; stderr?: string }, seen: string[][] = []): GatePolicyExec => (argv) => {
85
+ seen.push(argv);
86
+ return { status: out.status, stdout: out.stdout ?? "", stderr: out.stderr ?? "" };
87
+ };
88
+
89
+ test("the engines' own argv, and a command's placeholders filled", () => {
90
+ expect(gatePolicyArgv({}, { policy: "/p", input: "/i.json" })).toEqual(["conftest", "test", "--no-color", "--output", "json", "--policy", "/p", "--all-namespaces", "/i.json"]);
91
+ expect(gatePolicyArgv({ engine: "conftest", namespace: "infra" }, { policy: "/p", input: "/i.json" })).toContain("--namespace");
92
+ expect(gatePolicyArgv({ engine: "opa" }, { policy: "/p", input: "/i.json" })).toEqual(["opa", "eval", "--format", "json", "--data", "/p", "--input", "/i.json", "data.main"]);
93
+ expect(gatePolicyArgv({ engine: "opa", command: ["./bin/check", "--rules={policy}", "{input}"] }, { policy: "/p", input: "/i.json" })).toEqual(["./bin/check", "--rules=/p", "/i.json"]);
94
+ });
95
+
96
+ test("the input is written to the file the engine reads, and a denial is a verdict with its sorted rules", () => {
97
+ let input: unknown;
98
+ const exec: GatePolicyExec = (argv) => {
99
+ input = JSON.parse(readFileSync(argv[argv.length - 1]!, "utf-8"));
100
+ return { status: 1, stdout: CONFTEST_DENY, stderr: "" };
101
+ };
102
+ const verdict = checkGatePolicy({ settings: {}, input: { changeSet: { entries: [] } }, policyDir: "/p", exec });
103
+ expect(input).toEqual({ changeSet: { entries: [] } });
104
+ expect(verdict).toMatchObject({ result: "denied", engine: "conftest", rules: ["main.deny", "main.deny_public_bucket"], warnings: ["the plan deletes 3 resources"] });
105
+ });
106
+
107
+ test("a hold with no denial is held; nothing is passed", () => {
108
+ expect(checkGatePolicy({ settings: { engine: "opa" }, input: {}, policyDir: "/p", exec: fake({ status: 0, stdout: opa({ hold_iam: ["iam"] }) }) })).toMatchObject({ result: "held", holds: [{ rule: "main.hold_iam" }] });
109
+ expect(checkGatePolicy({ settings: { engine: "opa" }, input: {}, policyDir: "/p", exec: fake({ status: 0, stdout: opa({ deny: [], warn: [] }) }) })).toMatchObject({ result: "passed", denials: [], holds: [] });
110
+ expect(checkGatePolicy({ settings: { engine: "opa" }, input: {}, policyDir: "/p", exec: fake({ status: 0, stdout: opa({ deny: ["x"], hold: true }) }) }).result).toBe("denied");
111
+ });
112
+
113
+ test("an engine that does not start, prints no result or fails without a denial is an error", () => {
114
+ expect(checkGatePolicy({ settings: {}, input: {}, policyDir: "/p", exec: fake({ status: null, stderr: "spawn conftest ENOENT" }) })).toMatchObject({ result: "error", error: expect.stringContaining("did not run") });
115
+ expect(checkGatePolicy({ settings: {}, input: {}, policyDir: "/p", exec: fake({ status: 2, stdout: "Error: rego_parse_error" }) })).toMatchObject({ result: "error", error: expect.stringContaining("gave no conftest result") });
116
+ expect(checkGatePolicy({ settings: {}, input: {}, policyDir: "/p", exec: fake({ status: 2, stdout: "[]", stderr: "could not load policy" }) })).toMatchObject({ result: "error", error: expect.stringContaining("exited 2") });
117
+ });
118
+ });
119
+
120
+ describe("the policy at base (#4294)", () => {
121
+ const files: Record<string, string> = { "policy/main.rego": "package main\n", "policy/lib/util.rego": "package lib\n", "src/app.ts": "" };
122
+ const tree: GatePolicyTree = {
123
+ stat: (p) => (files[p] !== undefined ? "file" : Object.keys(files).some((f) => f.startsWith(`${p}/`)) ? "dir" : undefined),
124
+ list: (p) => {
125
+ const names = new Map<string, "file" | "dir">();
126
+ for (const f of Object.keys(files).filter((f) => f.startsWith(`${p}/`))) {
127
+ const rest = f.slice(p.length + 1).split("/");
128
+ names.set(rest[0]!, rest.length > 1 ? "dir" : "file");
129
+ }
130
+ return names.size ? [...names].map(([name, type]) => ({ name, type })) : undefined;
131
+ },
132
+ read: (p) => files[p]!,
133
+ };
134
+
135
+ test("the directory is copied out of the tree, and removed again", () => {
136
+ const out = exportGatePolicy(tree, "./policy/");
137
+ if ("error" in out) throw new Error(out.error);
138
+ expect(readFileSync(join(out.dir, "main.rego"), "utf-8")).toBe("package main\n");
139
+ expect(readFileSync(join(out.dir, "lib", "util.rego"), "utf-8")).toBe("package lib\n");
140
+ expect(existsSync(join(out.dir, "app.ts"))).toBe(false);
141
+ out.cleanup();
142
+ expect(existsSync(out.dir)).toBe(false);
143
+ });
144
+
145
+ test("a directory the base does not have is an error", () => {
146
+ expect(exportGatePolicy(tree, "rules")).toEqual({ error: "the policy directory rules does not exist at base" });
147
+ });
148
+ });
149
+
150
+ describe("ci.policy settings (#4294)", () => {
151
+ test("the four settings are read, and an unattended outcome is refused", () => {
152
+ expect(() => assertGatePolicySettings("ci.policy", { engine: "opa", path: "policy", namespace: "main", command: ["opa", "eval", "{input}"] })).not.toThrow();
153
+ expect(() => assertGatePolicySettings("ci.policy", {})).not.toThrow();
154
+ expect(() => assertGatePolicySettings("ci.policy", { engine: "sentinel" })).toThrow(/conftest or opa/);
155
+ expect(() => assertGatePolicySettings("ci.policy", { path: "../elsewhere" })).toThrow(/inside the workspace/);
156
+ expect(() => assertGatePolicySettings("ci.policy", { command: ["check"] })).toThrow(/\{input\}/);
157
+ expect(() => assertGatePolicySettings("ci.policy", { outcomes: ["deny", "allow_unattended"] })).toThrow(/no unattended outcome/);
158
+ expect(() => assertGatePolicySettings("ci.policy", { allow_unattended: true })).toThrow(/no unattended outcome/);
159
+ expect(() => assertGatePolicySettings("ci.policy", { rules: [] })).toThrow(/not a setting/);
160
+ expect(() => assertGatePolicySettings("ci.policy", [])).toThrow(/must be/);
161
+ });
162
+ });