@intentius/chant 0.124.46 → 0.124.49

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 (726) hide show
  1. package/dist/__golden__/wave-loop/op-by-apply-after-fails.json +10 -0
  2. package/dist/__golden__/wave-loop/op-by-apply-hold.json +10 -0
  3. package/dist/__golden__/wave-loop/op-by-apply-ignored.json +10 -0
  4. package/dist/__golden__/wave-loop/op-by-apply-pass.json +10 -0
  5. package/dist/__golden__/wave-loop/op-by-apply-skip.json +10 -0
  6. package/dist/__golden__/wave-loop/op-cost-member-clash.json +6 -0
  7. package/dist/__golden__/wave-loop/op-cost-member.json +13 -0
  8. package/dist/__golden__/wave-loop/op-denied-source.json +16 -0
  9. package/dist/__golden__/wave-loop/op-denied.json +16 -0
  10. package/dist/__golden__/wave-loop/op-discover-rest-short.json +6 -0
  11. package/dist/__golden__/wave-loop/op-discover-rest-stops.json +13 -0
  12. package/dist/__golden__/wave-loop/op-discover-rest.json +26 -0
  13. package/dist/__golden__/wave-loop/op-discover-wide.json +26 -0
  14. package/dist/__golden__/wave-loop/op-held-on-again.json +16 -0
  15. package/dist/__golden__/wave-loop/op-held-on-before-apply.json +16 -0
  16. package/dist/__golden__/wave-loop/op-held-on.json +16 -0
  17. package/dist/__golden__/wave-loop/op-hooks-apply-fails.json +13 -0
  18. package/dist/__golden__/wave-loop/op-hooks-fail-after-apply.json +13 -0
  19. package/dist/__golden__/wave-loop/op-hooks-fail-before-apply.json +13 -0
  20. package/dist/__golden__/wave-loop/op-hooks-fail-before-plan.json +13 -0
  21. package/dist/__golden__/wave-loop/op-hooks-held.json +13 -0
  22. package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +13 -0
  23. package/dist/__golden__/wave-loop/op-hooks-pass.json +13 -0
  24. package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +13 -0
  25. package/dist/__golden__/wave-loop/op-hooks-share.json +33 -0
  26. package/dist/__golden__/wave-loop/op-outcome-held.json +10 -0
  27. package/dist/__golden__/wave-loop/op-outcome-replan-approved.json +10 -0
  28. package/dist/__golden__/wave-loop/op-outcome-replan-forever.json +10 -0
  29. package/dist/__golden__/wave-loop/op-outcome-replan.json +10 -0
  30. package/dist/__golden__/wave-loop/op-outcome-stood-down.json +10 -0
  31. package/dist/__golden__/wave-loop/op-outcome-unreadable.json +10 -0
  32. package/dist/__golden__/wave-loop/op-overridden.json +16 -0
  33. package/dist/__golden__/wave-loop/op-per-run.json +20 -0
  34. package/dist/__golden__/wave-loop/op-point.json +13 -0
  35. package/dist/__golden__/wave-loop/op-pr-review.json +10 -0
  36. package/dist/__golden__/wave-loop/op-sealed.json +10 -0
  37. package/dist/__golden__/wave-loop/op-spend-change-only.json +10 -0
  38. package/dist/__golden__/wave-loop/op-spend.json +10 -0
  39. package/dist/__golden__/wave-loop/op-wave-commands.json +55 -0
  40. package/dist/__golden__/wave-loop/op-wide-refused.json +26 -0
  41. package/dist/__golden__/wave-loop/op-wide-waiting.json +29 -0
  42. package/dist/__golden__/wave-loop/op-wide.json +39 -0
  43. package/dist/action-pins.d.ts +61 -0
  44. package/dist/action-pins.d.ts.map +1 -0
  45. package/dist/action-pins.js +77 -0
  46. package/dist/cli/commands/carve.d.ts +3 -0
  47. package/dist/cli/commands/carve.d.ts.map +1 -1
  48. package/dist/cli/commands/carve.js +36 -7
  49. package/dist/cli/commands/import-report.d.ts +26 -0
  50. package/dist/cli/commands/import-report.d.ts.map +1 -0
  51. package/dist/cli/commands/import-report.js +65 -0
  52. package/dist/cli/commands/import.d.ts +11 -1
  53. package/dist/cli/commands/import.d.ts.map +1 -1
  54. package/dist/cli/commands/import.js +22 -3
  55. package/dist/cli/commands/migrate.d.ts +13 -1
  56. package/dist/cli/commands/migrate.d.ts.map +1 -1
  57. package/dist/cli/commands/migrate.js +134 -10
  58. package/dist/cli/handlers/approve.d.ts.map +1 -1
  59. package/dist/cli/handlers/approve.js +41 -14
  60. package/dist/cli/handlers/build.d.ts.map +1 -1
  61. package/dist/cli/handlers/build.js +6 -1
  62. package/dist/cli/handlers/graph-reader.d.ts +26 -0
  63. package/dist/cli/handlers/graph-reader.d.ts.map +1 -0
  64. package/dist/cli/handlers/graph-reader.js +41 -0
  65. package/dist/cli/handlers/graph.d.ts.map +1 -1
  66. package/dist/cli/handlers/graph.js +14 -0
  67. package/dist/cli/handlers/ledger.d.ts +3 -0
  68. package/dist/cli/handlers/ledger.d.ts.map +1 -0
  69. package/dist/cli/handlers/ledger.js +55 -0
  70. package/dist/cli/handlers/migrate.d.ts.map +1 -1
  71. package/dist/cli/handlers/migrate.js +18 -11
  72. package/dist/cli/handlers/misc.d.ts.map +1 -1
  73. package/dist/cli/handlers/misc.js +12 -1
  74. package/dist/cli/handlers/pr-env.js +2 -2
  75. package/dist/cli/handlers/pr.d.ts +15 -1
  76. package/dist/cli/handlers/pr.d.ts.map +1 -1
  77. package/dist/cli/handlers/pr.js +183 -40
  78. package/dist/cli/handlers/query.d.ts +7 -2
  79. package/dist/cli/handlers/query.d.ts.map +1 -1
  80. package/dist/cli/handlers/query.js +16 -4
  81. package/dist/cli/handlers/release.d.ts.map +1 -1
  82. package/dist/cli/handlers/release.js +6 -3
  83. package/dist/cli/handlers/search.d.ts +2 -4
  84. package/dist/cli/handlers/search.d.ts.map +1 -1
  85. package/dist/cli/handlers/search.js +13 -39
  86. package/dist/cli/main.d.ts.map +1 -1
  87. package/dist/cli/main.js +87 -14
  88. package/dist/cli/parse-args.d.ts.map +1 -1
  89. package/dist/cli/parse-args.js +47 -2
  90. package/dist/cli/registry.d.ts +22 -4
  91. package/dist/cli/registry.d.ts.map +1 -1
  92. package/dist/components/builders.d.ts +5 -1
  93. package/dist/components/builders.d.ts.map +1 -1
  94. package/dist/components/builders.js +5 -0
  95. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  96. package/dist/components/capability-plugin-loader.js +16 -1
  97. package/dist/components/capability-plugin.d.ts +14 -0
  98. package/dist/components/capability-plugin.d.ts.map +1 -1
  99. package/dist/components/pr-hooks.d.ts +21 -1
  100. package/dist/components/pr-hooks.d.ts.map +1 -1
  101. package/dist/components/pr-hooks.js +72 -4
  102. package/dist/components/pr-pipeline.d.ts +45 -10
  103. package/dist/components/pr-pipeline.d.ts.map +1 -1
  104. package/dist/components/pr-pipeline.js +47 -14
  105. package/dist/components/pr-policy.d.ts +36 -0
  106. package/dist/components/pr-policy.d.ts.map +1 -1
  107. package/dist/components/pr-policy.js +36 -0
  108. package/dist/components/pr-run.d.ts +3 -0
  109. package/dist/components/pr-run.d.ts.map +1 -1
  110. package/dist/components/pr-run.js +1 -0
  111. package/dist/components/starter-plugin.d.ts +1 -0
  112. package/dist/components/starter-plugin.d.ts.map +1 -1
  113. package/dist/components/starter-plugin.js +5 -1
  114. package/dist/components/verbs/index.d.ts +2 -0
  115. package/dist/components/verbs/index.d.ts.map +1 -1
  116. package/dist/components/verbs/index.js +2 -0
  117. package/dist/components/verbs/publish-artifact.d.ts +65 -0
  118. package/dist/components/verbs/publish-artifact.d.ts.map +1 -0
  119. package/dist/components/verbs/publish-artifact.js +74 -0
  120. package/dist/components/verbs/publish-image.d.ts +129 -0
  121. package/dist/components/verbs/publish-image.d.ts.map +1 -0
  122. package/dist/components/verbs/publish-image.js +145 -0
  123. package/dist/config-environments.d.ts +20 -0
  124. package/dist/config-environments.d.ts.map +1 -1
  125. package/dist/config-environments.js +12 -0
  126. package/dist/config.d.ts +25 -1
  127. package/dist/config.d.ts.map +1 -1
  128. package/dist/config.js +10 -2
  129. package/dist/gate-decision.d.ts +20 -2
  130. package/dist/gate-decision.d.ts.map +1 -1
  131. package/dist/gate-decision.js +30 -3
  132. package/dist/graph-ir.d.ts +25 -0
  133. package/dist/graph-ir.d.ts.map +1 -1
  134. package/dist/graph-walk.d.ts +66 -0
  135. package/dist/graph-walk.d.ts.map +1 -0
  136. package/dist/graph-walk.js +70 -0
  137. package/dist/import/parser.d.ts +19 -0
  138. package/dist/import/parser.d.ts.map +1 -1
  139. package/dist/lexicon.d.ts +94 -6
  140. package/dist/lexicon.d.ts.map +1 -1
  141. package/dist/lifecycle/gate-ledger.d.ts +11 -0
  142. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  143. package/dist/lifecycle/git.d.ts +2 -5
  144. package/dist/lifecycle/git.d.ts.map +1 -1
  145. package/dist/lifecycle/ledger-read.d.ts +65 -0
  146. package/dist/lifecycle/ledger-read.d.ts.map +1 -0
  147. package/dist/lifecycle/ledger-read.js +160 -0
  148. package/dist/lifecycle/release-workstation.d.ts +7 -0
  149. package/dist/lifecycle/release-workstation.d.ts.map +1 -1
  150. package/dist/lifecycle/release-workstation.js +25 -0
  151. package/dist/lifecycle/run-ledger.d.ts +4 -3
  152. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  153. package/dist/lifecycle/run-ledger.js +1 -0
  154. package/dist/lint/discover.js +3 -1
  155. package/dist/lint/post-synth.d.ts +127 -1
  156. package/dist/lint/post-synth.d.ts.map +1 -1
  157. package/dist/lint/post-synth.js +84 -2
  158. package/dist/lint/rules/op/index.d.ts +1 -0
  159. package/dist/lint/rules/op/index.d.ts.map +1 -1
  160. package/dist/lint/rules/op/index.js +2 -0
  161. package/dist/lint/rules/op/ops019-workspace-op-target.d.ts +32 -0
  162. package/dist/lint/rules/op/ops019-workspace-op-target.d.ts.map +1 -0
  163. package/dist/lint/rules/op/ops019-workspace-op-target.js +63 -0
  164. package/dist/op/activities/activity-contracts.d.ts +9 -6
  165. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  166. package/dist/op/activities/activity-contracts.js +5 -2
  167. package/dist/op/activities/index.d.ts +2 -2
  168. package/dist/op/activities/index.d.ts.map +1 -1
  169. package/dist/op/activities/index.js +1 -1
  170. package/dist/op/activities/review-agent.d.ts +90 -4
  171. package/dist/op/activities/review-agent.d.ts.map +1 -1
  172. package/dist/op/activities/review-agent.js +217 -19
  173. package/dist/op/activities/work-evidence.d.ts +8 -1
  174. package/dist/op/activities/work-evidence.d.ts.map +1 -1
  175. package/dist/op/activities/work-evidence.js +1 -0
  176. package/dist/op/builders.d.ts +9 -5
  177. package/dist/op/builders.d.ts.map +1 -1
  178. package/dist/op/builders.js +9 -5
  179. package/dist/op/composites/apply-op.d.ts +29 -2
  180. package/dist/op/composites/apply-op.d.ts.map +1 -1
  181. package/dist/op/composites/apply-op.js +20 -1
  182. package/dist/op/gate-approval-base.d.ts +25 -0
  183. package/dist/op/gate-approval-base.d.ts.map +1 -0
  184. package/dist/op/gate-approval-base.js +71 -0
  185. package/dist/op/gate-approval.d.ts +18 -9
  186. package/dist/op/gate-approval.d.ts.map +1 -1
  187. package/dist/op/gate-approval.js +13 -31
  188. package/dist/op/gate-name.d.ts +13 -0
  189. package/dist/op/gate-name.d.ts.map +1 -1
  190. package/dist/op/gate-name.js +14 -1
  191. package/dist/op/gate-quorum.d.ts +100 -3
  192. package/dist/op/gate-quorum.d.ts.map +1 -1
  193. package/dist/op/gate-quorum.js +108 -6
  194. package/dist/op/gate-review.d.ts +9 -0
  195. package/dist/op/gate-review.d.ts.map +1 -1
  196. package/dist/op/gate-review.js +13 -0
  197. package/dist/op/gate.d.ts +47 -5
  198. package/dist/op/gate.d.ts.map +1 -1
  199. package/dist/op/gate.js +25 -4
  200. package/dist/op/index.d.ts +3 -3
  201. package/dist/op/index.d.ts.map +1 -1
  202. package/dist/op/index.js +1 -1
  203. package/dist/op/local-executor.d.ts +45 -3
  204. package/dist/op/local-executor.d.ts.map +1 -1
  205. package/dist/op/local-executor.js +68 -7
  206. package/dist/op/op-waves-run.d.ts +26 -1
  207. package/dist/op/op-waves-run.d.ts.map +1 -1
  208. package/dist/op/op-waves-run.js +259 -13
  209. package/dist/op/op-waves.d.ts +72 -5
  210. package/dist/op/op-waves.d.ts.map +1 -1
  211. package/dist/op/op-waves.js +110 -15
  212. package/dist/op/runtime.d.ts +11 -0
  213. package/dist/op/runtime.d.ts.map +1 -1
  214. package/dist/op/schedule-overdue.d.ts +16 -0
  215. package/dist/op/schedule-overdue.d.ts.map +1 -1
  216. package/dist/op/schedule-overdue.js +36 -0
  217. package/dist/op/stage-hooks.d.ts +9 -2
  218. package/dist/op/stage-hooks.d.ts.map +1 -1
  219. package/dist/op/stage-hooks.js +7 -5
  220. package/dist/op/workspace-run-env.d.ts +22 -0
  221. package/dist/op/workspace-run-env.d.ts.map +1 -0
  222. package/dist/op/workspace-run-env.js +21 -0
  223. package/dist/pr-forge.d.ts +24 -0
  224. package/dist/pr-forge.d.ts.map +1 -1
  225. package/dist/pr-forge.js +47 -0
  226. package/dist/pr-loop.d.ts +10 -0
  227. package/dist/pr-loop.d.ts.map +1 -1
  228. package/dist/pr-loop.js +4 -0
  229. package/dist/pr-migrations.d.ts +62 -0
  230. package/dist/pr-migrations.d.ts.map +1 -0
  231. package/dist/pr-migrations.js +50 -0
  232. package/dist/pr-post.d.ts +42 -0
  233. package/dist/pr-post.d.ts.map +1 -1
  234. package/dist/pr-post.js +68 -2
  235. package/dist/read-filter.d.ts +65 -0
  236. package/dist/read-filter.d.ts.map +1 -0
  237. package/dist/read-filter.js +179 -0
  238. package/dist/run-event.d.ts +1 -1
  239. package/dist/run-event.d.ts.map +1 -1
  240. package/dist/terraform/carve.d.ts +24 -3
  241. package/dist/terraform/carve.d.ts.map +1 -1
  242. package/dist/terraform/carve.js +32 -14
  243. package/dist/terraform/cuts.d.ts +97 -0
  244. package/dist/terraform/cuts.d.ts.map +1 -0
  245. package/dist/terraform/cuts.js +138 -0
  246. package/dist/terraform/folds.d.ts +26 -0
  247. package/dist/terraform/folds.d.ts.map +1 -0
  248. package/dist/terraform/folds.js +38 -0
  249. package/dist/terraform/graph.d.ts.map +1 -1
  250. package/dist/terraform/graph.js +10 -0
  251. package/dist/terraform/reach.d.ts +140 -0
  252. package/dist/terraform/reach.d.ts.map +1 -0
  253. package/dist/terraform/reach.js +170 -0
  254. package/dist/terraform/score.d.ts +14 -0
  255. package/dist/terraform/score.d.ts.map +1 -1
  256. package/dist/terraform/score.js +14 -31
  257. package/dist/terraform/state.d.ts.map +1 -1
  258. package/dist/terraform/state.js +4 -2
  259. package/dist/terraform/types.d.ts +8 -0
  260. package/dist/terraform/types.d.ts.map +1 -1
  261. package/dist/wave-blocked.d.ts +10 -1
  262. package/dist/wave-blocked.d.ts.map +1 -1
  263. package/dist/wave-blocked.js +31 -3
  264. package/dist/wave-runtime.mjs +264 -254
  265. package/dist/workspace/check.schema.json +13 -2
  266. package/dist/workspace/checks/ci-branch.d.ts +44 -0
  267. package/dist/workspace/checks/ci-branch.d.ts.map +1 -0
  268. package/dist/workspace/checks/ci-branch.js +133 -0
  269. package/dist/workspace/checks/gate-approval.d.ts +58 -0
  270. package/dist/workspace/checks/gate-approval.d.ts.map +1 -0
  271. package/dist/workspace/checks/gate-approval.js +103 -0
  272. package/dist/workspace/checks/links.d.ts +3 -0
  273. package/dist/workspace/checks/links.d.ts.map +1 -1
  274. package/dist/workspace/checks/links.js +12 -1
  275. package/dist/workspace/checks/org.d.ts +48 -0
  276. package/dist/workspace/checks/org.d.ts.map +1 -0
  277. package/dist/workspace/checks/org.js +156 -0
  278. package/dist/workspace/checks.d.ts +12 -1
  279. package/dist/workspace/checks.d.ts.map +1 -1
  280. package/dist/workspace/checks.js +63 -7
  281. package/dist/workspace/ci-branches.d.ts +16 -0
  282. package/dist/workspace/ci-branches.d.ts.map +1 -1
  283. package/dist/workspace/ci-branches.js +27 -0
  284. package/dist/workspace/ci-green-cli.js +7 -7
  285. package/dist/workspace/ci-green-workflow.d.ts +1 -1
  286. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  287. package/dist/workspace/ci-green-workflow.js +13 -8
  288. package/dist/workspace/ci-green.d.ts +14 -0
  289. package/dist/workspace/ci-green.d.ts.map +1 -1
  290. package/dist/workspace/ci-green.js +44 -11
  291. package/dist/workspace/ci-run.d.ts +9 -1
  292. package/dist/workspace/ci-run.d.ts.map +1 -1
  293. package/dist/workspace/ci-run.js +67 -6
  294. package/dist/workspace/ci-serve.d.ts +9 -0
  295. package/dist/workspace/ci-serve.d.ts.map +1 -1
  296. package/dist/workspace/ci-serve.js +114 -76
  297. package/dist/workspace/compose-graph.d.ts +14 -0
  298. package/dist/workspace/compose-graph.d.ts.map +1 -1
  299. package/dist/workspace/composites.d.ts +4 -1
  300. package/dist/workspace/composites.d.ts.map +1 -1
  301. package/dist/workspace/composites.js +5 -2
  302. package/dist/workspace/composites.schema.json +22 -0
  303. package/dist/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
  304. package/dist/workspace/cross-root-lint.d.ts +56 -0
  305. package/dist/workspace/cross-root-lint.d.ts.map +1 -0
  306. package/dist/workspace/cross-root-lint.js +111 -0
  307. package/dist/workspace/dbt-models-reader.d.ts +44 -0
  308. package/dist/workspace/dbt-models-reader.d.ts.map +1 -0
  309. package/dist/workspace/dbt-models-reader.js +356 -0
  310. package/dist/workspace/dbt-project-common.d.ts +40 -0
  311. package/dist/workspace/dbt-project-common.d.ts.map +1 -0
  312. package/dist/workspace/dbt-project-common.js +106 -0
  313. package/dist/workspace/dbt-project-reader.d.ts +4 -3
  314. package/dist/workspace/dbt-project-reader.d.ts.map +1 -1
  315. package/dist/workspace/dbt-project-reader.js +14 -108
  316. package/dist/workspace/decision-rank.d.ts +26 -0
  317. package/dist/workspace/decision-rank.d.ts.map +1 -0
  318. package/dist/workspace/decision-rank.js +16 -0
  319. package/dist/workspace/declaration.d.ts +50 -2
  320. package/dist/workspace/declaration.d.ts.map +1 -1
  321. package/dist/workspace/declaration.js +57 -11
  322. package/dist/workspace/declaration.schema.json +166 -24
  323. package/dist/workspace/environments.d.ts +26 -2
  324. package/dist/workspace/environments.d.ts.map +1 -1
  325. package/dist/workspace/environments.js +26 -2
  326. package/dist/workspace/exit-cli.d.ts +44 -0
  327. package/dist/workspace/exit-cli.d.ts.map +1 -0
  328. package/dist/workspace/exit-cli.js +198 -0
  329. package/dist/workspace/exit.d.ts +159 -0
  330. package/dist/workspace/exit.d.ts.map +1 -0
  331. package/dist/workspace/exit.js +480 -0
  332. package/dist/workspace/exit.schema.json +257 -0
  333. package/dist/workspace/flyway-dir.d.ts +39 -0
  334. package/dist/workspace/flyway-dir.d.ts.map +1 -0
  335. package/dist/workspace/flyway-dir.js +104 -0
  336. package/dist/workspace/flyway-layout.d.ts +96 -0
  337. package/dist/workspace/flyway-layout.d.ts.map +1 -0
  338. package/dist/workspace/flyway-layout.js +308 -0
  339. package/dist/workspace/graph-cache.d.ts +3 -1
  340. package/dist/workspace/graph-cache.d.ts.map +1 -1
  341. package/dist/workspace/graph-cache.js +12 -1
  342. package/dist/workspace/graph-cli.d.ts.map +1 -1
  343. package/dist/workspace/graph-cli.js +26 -8
  344. package/dist/workspace/graph.schema.json +34 -0
  345. package/dist/workspace/import-fetch.d.ts +38 -0
  346. package/dist/workspace/import-fetch.d.ts.map +1 -0
  347. package/dist/workspace/import-fetch.js +99 -0
  348. package/dist/workspace/import-init.d.ts +26 -0
  349. package/dist/workspace/import-init.d.ts.map +1 -0
  350. package/dist/workspace/import-init.js +130 -0
  351. package/dist/workspace/import-source.d.ts +147 -0
  352. package/dist/workspace/import-source.d.ts.map +1 -0
  353. package/dist/workspace/import-source.js +370 -0
  354. package/dist/workspace/init.d.ts +4 -0
  355. package/dist/workspace/init.d.ts.map +1 -1
  356. package/dist/workspace/init.js +70 -3
  357. package/dist/workspace/intent.d.ts +2 -3
  358. package/dist/workspace/intent.d.ts.map +1 -1
  359. package/dist/workspace/intent.js +5 -9
  360. package/dist/workspace/kind-link-readers.d.ts +59 -2
  361. package/dist/workspace/kind-link-readers.d.ts.map +1 -1
  362. package/dist/workspace/kind-link-readers.js +17 -1
  363. package/dist/workspace/kind-readers.d.ts +6 -0
  364. package/dist/workspace/kind-readers.d.ts.map +1 -1
  365. package/dist/workspace/kind-readers.js +18 -0
  366. package/dist/workspace/kinds.d.ts +90 -3
  367. package/dist/workspace/kinds.d.ts.map +1 -1
  368. package/dist/workspace/kinds.js +161 -7
  369. package/dist/workspace/ledger.schema.json +158 -0
  370. package/dist/workspace/lexicon-outputs.d.ts +20 -1
  371. package/dist/workspace/lexicon-outputs.d.ts.map +1 -1
  372. package/dist/workspace/lexicon-outputs.js +102 -10
  373. package/dist/workspace/lineage-check.d.ts +3 -0
  374. package/dist/workspace/lineage-check.d.ts.map +1 -1
  375. package/dist/workspace/lineage-check.js +23 -4
  376. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  377. package/dist/workspace/lineage-upgrade.js +13 -4
  378. package/dist/workspace/links.d.ts +8 -1
  379. package/dist/workspace/links.d.ts.map +1 -1
  380. package/dist/workspace/links.js +25 -1
  381. package/dist/workspace/ls.d.ts +8 -0
  382. package/dist/workspace/ls.d.ts.map +1 -1
  383. package/dist/workspace/ls.js +7 -1
  384. package/dist/workspace/ls.schema.json +7 -0
  385. package/dist/workspace/member-commands.d.ts.map +1 -1
  386. package/dist/workspace/member-commands.js +54 -5
  387. package/dist/workspace/migrate-links.d.ts +45 -0
  388. package/dist/workspace/migrate-links.d.ts.map +1 -0
  389. package/dist/workspace/migrate-links.js +100 -0
  390. package/dist/workspace/migration-dirs.d.ts +74 -0
  391. package/dist/workspace/migration-dirs.d.ts.map +1 -0
  392. package/dist/workspace/migration-dirs.js +254 -0
  393. package/dist/workspace/new-migrations.d.ts +47 -0
  394. package/dist/workspace/new-migrations.d.ts.map +1 -0
  395. package/dist/workspace/new-migrations.js +91 -0
  396. package/dist/workspace/pr-report.schema.json +46 -0
  397. package/dist/workspace/promote.d.ts +96 -0
  398. package/dist/workspace/promote.d.ts.map +1 -0
  399. package/dist/workspace/promote.js +263 -0
  400. package/dist/workspace/reason-codes.d.ts +21 -0
  401. package/dist/workspace/reason-codes.d.ts.map +1 -1
  402. package/dist/workspace/reason-codes.js +25 -0
  403. package/dist/workspace/records-cli.d.ts +11 -0
  404. package/dist/workspace/records-cli.d.ts.map +1 -1
  405. package/dist/workspace/records-cli.js +57 -5
  406. package/dist/workspace/records.schema.json +5 -0
  407. package/dist/workspace/release-candidates.d.ts +2 -0
  408. package/dist/workspace/release-candidates.d.ts.map +1 -1
  409. package/dist/workspace/release-candidates.js +5 -2
  410. package/dist/workspace/release-train.d.ts +53 -0
  411. package/dist/workspace/release-train.d.ts.map +1 -0
  412. package/dist/workspace/release-train.js +134 -0
  413. package/dist/workspace/release-workflow.d.ts.map +1 -1
  414. package/dist/workspace/release-workflow.js +4 -3
  415. package/dist/workspace/repo-ref.d.ts +45 -0
  416. package/dist/workspace/repo-ref.d.ts.map +1 -0
  417. package/dist/workspace/repo-ref.js +114 -0
  418. package/dist/workspace/run-event.schema.json +2 -2
  419. package/dist/workspace/runs-cli.d.ts +9 -0
  420. package/dist/workspace/runs-cli.d.ts.map +1 -1
  421. package/dist/workspace/runs-cli.js +48 -3
  422. package/dist/workspace/runs.schema.json +7 -0
  423. package/dist/workspace/search-contract.d.ts +1 -1
  424. package/dist/workspace/search-contract.d.ts.map +1 -1
  425. package/dist/workspace/status-decisions.d.ts +37 -0
  426. package/dist/workspace/status-decisions.d.ts.map +1 -0
  427. package/dist/workspace/status-decisions.js +147 -0
  428. package/dist/workspace/status-gates.d.ts +9 -0
  429. package/dist/workspace/status-gates.d.ts.map +1 -1
  430. package/dist/workspace/status.d.ts +4 -0
  431. package/dist/workspace/status.d.ts.map +1 -1
  432. package/dist/workspace/status.js +14 -2
  433. package/dist/workspace/status.schema.json +6 -0
  434. package/dist/workspace/work-cli.d.ts.map +1 -1
  435. package/dist/workspace/work-cli.js +5 -3
  436. package/dist/workspace/work-evidence-ops.d.ts +83 -0
  437. package/dist/workspace/work-evidence-ops.d.ts.map +1 -0
  438. package/dist/workspace/work-evidence-ops.js +169 -0
  439. package/dist/workspace/work-evidence.d.ts +12 -2
  440. package/dist/workspace/work-evidence.d.ts.map +1 -1
  441. package/dist/workspace/work-evidence.js +21 -3
  442. package/dist/workspace/work-evidence.schema.json +49 -2
  443. package/dist/workspace/workspace-gates.d.ts +110 -0
  444. package/dist/workspace/workspace-gates.d.ts.map +1 -0
  445. package/dist/workspace/workspace-gates.js +140 -0
  446. package/dist/workspace/workspace-kinds.schema.json +108 -10
  447. package/dist/workspace/workspace-ledger.d.ts +76 -0
  448. package/dist/workspace/workspace-ledger.d.ts.map +1 -0
  449. package/dist/workspace/workspace-ledger.js +91 -0
  450. package/dist/workspace/workspace-run.d.ts +112 -0
  451. package/dist/workspace/workspace-run.d.ts.map +1 -0
  452. package/dist/workspace/workspace-run.js +432 -0
  453. package/package.json +1 -1
  454. package/src/__golden__/wave-loop/op-by-apply-after-fails.json +10 -0
  455. package/src/__golden__/wave-loop/op-by-apply-hold.json +10 -0
  456. package/src/__golden__/wave-loop/op-by-apply-ignored.json +10 -0
  457. package/src/__golden__/wave-loop/op-by-apply-pass.json +10 -0
  458. package/src/__golden__/wave-loop/op-by-apply-skip.json +10 -0
  459. package/src/__golden__/wave-loop/op-cost-member-clash.json +6 -0
  460. package/src/__golden__/wave-loop/op-cost-member.json +13 -0
  461. package/src/__golden__/wave-loop/op-denied-source.json +16 -0
  462. package/src/__golden__/wave-loop/op-denied.json +16 -0
  463. package/src/__golden__/wave-loop/op-discover-rest-short.json +6 -0
  464. package/src/__golden__/wave-loop/op-discover-rest-stops.json +13 -0
  465. package/src/__golden__/wave-loop/op-discover-rest.json +26 -0
  466. package/src/__golden__/wave-loop/op-discover-wide.json +26 -0
  467. package/src/__golden__/wave-loop/op-held-on-again.json +16 -0
  468. package/src/__golden__/wave-loop/op-held-on-before-apply.json +16 -0
  469. package/src/__golden__/wave-loop/op-held-on.json +16 -0
  470. package/src/__golden__/wave-loop/op-hooks-apply-fails.json +13 -0
  471. package/src/__golden__/wave-loop/op-hooks-fail-after-apply.json +13 -0
  472. package/src/__golden__/wave-loop/op-hooks-fail-before-apply.json +13 -0
  473. package/src/__golden__/wave-loop/op-hooks-fail-before-plan.json +13 -0
  474. package/src/__golden__/wave-loop/op-hooks-held.json +13 -0
  475. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +13 -0
  476. package/src/__golden__/wave-loop/op-hooks-pass.json +13 -0
  477. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +13 -0
  478. package/src/__golden__/wave-loop/op-hooks-share.json +33 -0
  479. package/src/__golden__/wave-loop/op-outcome-held.json +10 -0
  480. package/src/__golden__/wave-loop/op-outcome-replan-approved.json +10 -0
  481. package/src/__golden__/wave-loop/op-outcome-replan-forever.json +10 -0
  482. package/src/__golden__/wave-loop/op-outcome-replan.json +10 -0
  483. package/src/__golden__/wave-loop/op-outcome-stood-down.json +10 -0
  484. package/src/__golden__/wave-loop/op-outcome-unreadable.json +10 -0
  485. package/src/__golden__/wave-loop/op-overridden.json +16 -0
  486. package/src/__golden__/wave-loop/op-per-run.json +20 -0
  487. package/src/__golden__/wave-loop/op-point.json +13 -0
  488. package/src/__golden__/wave-loop/op-pr-review.json +10 -0
  489. package/src/__golden__/wave-loop/op-sealed.json +10 -0
  490. package/src/__golden__/wave-loop/op-spend-change-only.json +10 -0
  491. package/src/__golden__/wave-loop/op-spend.json +10 -0
  492. package/src/__golden__/wave-loop/op-wave-commands.json +55 -0
  493. package/src/__golden__/wave-loop/op-wide-refused.json +26 -0
  494. package/src/__golden__/wave-loop/op-wide-waiting.json +29 -0
  495. package/src/__golden__/wave-loop/op-wide.json +39 -0
  496. package/src/action-pins.test.ts +78 -0
  497. package/src/action-pins.ts +92 -0
  498. package/src/cli/commands/carve-score-once.test.ts +99 -0
  499. package/src/cli/commands/carve.ts +37 -6
  500. package/src/cli/commands/check-lexicon.test.ts +2 -2
  501. package/src/cli/commands/import-report.ts +71 -0
  502. package/src/cli/commands/import.ts +33 -3
  503. package/src/cli/commands/migrate-dialect.test.ts +145 -0
  504. package/src/cli/commands/migrate.ts +132 -11
  505. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +4 -4
  506. package/src/cli/handlers/approve.ts +33 -4
  507. package/src/cli/handlers/build.ts +7 -1
  508. package/src/cli/handlers/graph-reader.test.ts +31 -0
  509. package/src/cli/handlers/graph-reader.ts +51 -0
  510. package/src/cli/handlers/graph.ts +14 -0
  511. package/src/cli/handlers/ledger.ts +54 -0
  512. package/src/cli/handlers/migrate.ts +21 -12
  513. package/src/cli/handlers/misc.ts +10 -1
  514. package/src/cli/handlers/pr-env.ts +2 -2
  515. package/src/cli/handlers/pr.ts +176 -37
  516. package/src/cli/handlers/query.ts +16 -4
  517. package/src/cli/handlers/release.ts +6 -3
  518. package/src/cli/handlers/search.ts +13 -34
  519. package/src/cli/main.test.ts +17 -0
  520. package/src/cli/main.ts +87 -14
  521. package/src/cli/parse-args.ts +35 -2
  522. package/src/cli/registry.ts +22 -4
  523. package/src/components/builders.ts +8 -0
  524. package/src/components/capability-plugin-loader.ts +13 -1
  525. package/src/components/capability-plugin.ts +16 -0
  526. package/src/components/pr-env-run-bundle.test.ts +1 -0
  527. package/src/components/pr-hooks.test.ts +73 -5
  528. package/src/components/pr-hooks.ts +95 -5
  529. package/src/components/pr-pipeline.ts +64 -16
  530. package/src/components/pr-policy.test.ts +64 -1
  531. package/src/components/pr-policy.ts +53 -0
  532. package/src/components/pr-run.ts +4 -0
  533. package/src/components/registry.test.ts +1 -1
  534. package/src/components/starter-plugin.ts +6 -0
  535. package/src/components/verbs/index.ts +2 -0
  536. package/src/components/verbs/publish-artifact.test.ts +60 -0
  537. package/src/components/verbs/publish-artifact.ts +122 -0
  538. package/src/components/verbs/publish-image.test.ts +101 -0
  539. package/src/components/verbs/publish-image.ts +265 -0
  540. package/src/config-environments.ts +28 -1
  541. package/src/config.ts +24 -1
  542. package/src/gate-decision-bundle.test.ts +5 -2
  543. package/src/gate-decision-identity.test.ts +20 -0
  544. package/src/gate-decision.ts +46 -3
  545. package/src/graph-ir.ts +27 -0
  546. package/src/graph-walk.test.ts +44 -0
  547. package/src/graph-walk.ts +102 -0
  548. package/src/import/parser.ts +20 -0
  549. package/src/lexicon.ts +98 -6
  550. package/src/lifecycle/gate-ledger.ts +8 -0
  551. package/src/lifecycle/git.ts +2 -2
  552. package/src/lifecycle/ledger-read.test.ts +107 -0
  553. package/src/lifecycle/ledger-read.ts +195 -0
  554. package/src/lifecycle/release-workstation.ts +24 -0
  555. package/src/lifecycle/run-ledger.ts +5 -2
  556. package/src/lint/cross-root.test.ts +122 -0
  557. package/src/lint/discover.ts +3 -1
  558. package/src/lint/post-synth.ts +212 -2
  559. package/src/lint/rules/op/index.ts +2 -0
  560. package/src/lint/rules/op/ops019-workspace-op-target.ts +89 -0
  561. package/src/op/activities/activity-contracts.ts +5 -2
  562. package/src/op/activities/index.ts +2 -2
  563. package/src/op/activities/review-agent.test.ts +221 -4
  564. package/src/op/activities/review-agent.ts +265 -22
  565. package/src/op/activities/work-evidence.ts +9 -1
  566. package/src/op/builders.ts +9 -5
  567. package/src/op/composites/apply-op.test.ts +108 -0
  568. package/src/op/composites/apply-op.ts +37 -2
  569. package/src/op/gate-approval-at-base.test.ts +97 -0
  570. package/src/op/gate-approval-base.ts +72 -0
  571. package/src/op/gate-approval.test.ts +6 -1
  572. package/src/op/gate-approval.ts +22 -27
  573. package/src/op/gate-name.ts +16 -1
  574. package/src/op/gate-quorum-at-base.test.ts +211 -0
  575. package/src/op/gate-quorum.ts +156 -8
  576. package/src/op/gate-review.ts +14 -0
  577. package/src/op/gate.ts +69 -9
  578. package/src/op/index.ts +3 -2
  579. package/src/op/local-executor.ts +91 -11
  580. package/src/op/op-waves-depends-on.test.ts +81 -0
  581. package/src/op/op-waves-hook-targets.test.ts +4 -2
  582. package/src/op/op-waves-quorum-product.test.ts +158 -0
  583. package/src/op/op-waves-run.ts +288 -14
  584. package/src/op/op-waves.ts +143 -14
  585. package/src/op/runtime.ts +8 -0
  586. package/src/op/schedule-overdue.ts +33 -0
  587. package/src/op/stage-hooks.ts +13 -5
  588. package/src/op/workspace-run-env.ts +29 -0
  589. package/src/pr-forge-bundle.test.ts +2 -2
  590. package/src/pr-forge.test.ts +47 -0
  591. package/src/pr-forge.ts +68 -0
  592. package/src/pr-loop-bundle.test.ts +3 -2
  593. package/src/pr-loop.test.ts +52 -0
  594. package/src/pr-loop.ts +12 -0
  595. package/src/pr-migrations.ts +97 -0
  596. package/src/pr-post-open.e2e.test.ts +36 -0
  597. package/src/pr-post.test.ts +102 -3
  598. package/src/pr-post.ts +90 -2
  599. package/src/read-filter.test.ts +54 -0
  600. package/src/read-filter.ts +193 -0
  601. package/src/run-event.ts +1 -1
  602. package/src/stage-hooks-bundle.test.ts +1 -0
  603. package/src/terraform/carve.ts +52 -15
  604. package/src/terraform/cuts.test.ts +173 -0
  605. package/src/terraform/cuts.ts +188 -0
  606. package/src/terraform/folds.ts +44 -0
  607. package/src/terraform/graph.ts +9 -0
  608. package/src/terraform/radius.test.ts +90 -0
  609. package/src/terraform/reach.test.ts +146 -0
  610. package/src/terraform/reach.ts +247 -0
  611. package/src/terraform/score.ts +27 -29
  612. package/src/terraform/state.ts +3 -1
  613. package/src/terraform/types.ts +8 -0
  614. package/src/wave-blocked.ts +28 -3
  615. package/src/wave-loop-golden.test.ts +4 -3
  616. package/src/workspace/__fixtures__/import/spacelift.json +9 -0
  617. package/src/workspace/__fixtures__/import/tfc.json +13 -0
  618. package/src/workspace/behold-kinds.test.ts +2 -1
  619. package/src/workspace/check-contract.test.ts +8 -6
  620. package/src/workspace/check.schema.json +13 -2
  621. package/src/workspace/checks/ci-branch.test.ts +128 -0
  622. package/src/workspace/checks/ci-branch.ts +141 -0
  623. package/src/workspace/checks/gate-approval.test.ts +77 -0
  624. package/src/workspace/checks/gate-approval.ts +121 -0
  625. package/src/workspace/checks/links.ts +12 -1
  626. package/src/workspace/checks/org.ts +169 -0
  627. package/src/workspace/checks.ts +68 -7
  628. package/src/workspace/ci-branches.ts +31 -0
  629. package/src/workspace/ci-green-branches.test.ts +153 -0
  630. package/src/workspace/ci-green-cli.ts +7 -7
  631. package/src/workspace/ci-green-workflow.test.ts +4 -3
  632. package/src/workspace/ci-green-workflow.ts +13 -9
  633. package/src/workspace/ci-green.ts +49 -12
  634. package/src/workspace/ci-run.test.ts +72 -1
  635. package/src/workspace/ci-run.ts +68 -8
  636. package/src/workspace/ci-serve.test.ts +95 -0
  637. package/src/workspace/ci-serve.ts +106 -65
  638. package/src/workspace/component-environments.test.ts +25 -2
  639. package/src/workspace/compose-graph.ts +9 -2
  640. package/src/workspace/composites.schema.json +22 -0
  641. package/src/workspace/composites.test.ts +39 -8
  642. package/src/workspace/composites.ts +6 -2
  643. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
  644. package/src/workspace/cross-root-lint.test.ts +90 -0
  645. package/src/workspace/cross-root-lint.ts +126 -0
  646. package/src/workspace/dbt-kind.test.ts +3 -2
  647. package/src/workspace/dbt-models-reader.ts +362 -0
  648. package/src/workspace/dbt-models.test.ts +248 -0
  649. package/src/workspace/dbt-project-common.ts +113 -0
  650. package/src/workspace/dbt-project-reader.ts +29 -112
  651. package/src/workspace/decision-rank.ts +40 -0
  652. package/src/workspace/declaration.schema.json +166 -24
  653. package/src/workspace/declaration.ts +101 -15
  654. package/src/workspace/environments.ts +40 -4
  655. package/src/workspace/exit-cli.ts +219 -0
  656. package/src/workspace/exit.schema.json +257 -0
  657. package/src/workspace/exit.test.ts +185 -0
  658. package/src/workspace/exit.ts +560 -0
  659. package/src/workspace/flyway-dir.ts +113 -0
  660. package/src/workspace/flyway-layout.ts +337 -0
  661. package/src/workspace/graph-cache.ts +12 -2
  662. package/src/workspace/graph-cli.ts +25 -10
  663. package/src/workspace/graph-contract.test.ts +4 -2
  664. package/src/workspace/graph.schema.json +34 -0
  665. package/src/workspace/identity.test.ts +29 -2
  666. package/src/workspace/import-fetch.ts +151 -0
  667. package/src/workspace/import-init.ts +147 -0
  668. package/src/workspace/import-source.test.ts +183 -0
  669. package/src/workspace/import-source.ts +462 -0
  670. package/src/workspace/init.test.ts +55 -1
  671. package/src/workspace/init.ts +66 -4
  672. package/src/workspace/intent.ts +6 -14
  673. package/src/workspace/kind-link-readers.ts +68 -3
  674. package/src/workspace/kind-readers.test.ts +36 -0
  675. package/src/workspace/kind-readers.ts +18 -1
  676. package/src/workspace/kinds.test.ts +35 -7
  677. package/src/workspace/kinds.ts +212 -11
  678. package/src/workspace/ledger.schema.json +158 -0
  679. package/src/workspace/lexicon-outputs.ts +132 -13
  680. package/src/workspace/lineage-check.ts +30 -7
  681. package/src/workspace/lineage-upgrade.test.ts +20 -0
  682. package/src/workspace/lineage-upgrade.ts +16 -7
  683. package/src/workspace/links.ts +24 -1
  684. package/src/workspace/ls-contract.test.ts +1 -1
  685. package/src/workspace/ls.schema.json +7 -0
  686. package/src/workspace/ls.ts +10 -1
  687. package/src/workspace/member-commands.ts +60 -2
  688. package/src/workspace/member-file-schema.test.ts +124 -0
  689. package/src/workspace/migrate-links.test.ts +99 -0
  690. package/src/workspace/migrate-links.ts +108 -0
  691. package/src/workspace/migration-dirs.test.ts +272 -0
  692. package/src/workspace/migration-dirs.ts +267 -0
  693. package/src/workspace/new-migrations.test.ts +98 -0
  694. package/src/workspace/new-migrations.ts +109 -0
  695. package/src/workspace/org-scope.test.ts +238 -0
  696. package/src/workspace/pr-report.schema.json +46 -0
  697. package/src/workspace/promote.test.ts +114 -0
  698. package/src/workspace/promote.ts +302 -0
  699. package/src/workspace/reason-codes.test.ts +10 -1
  700. package/src/workspace/reason-codes.ts +25 -0
  701. package/src/workspace/records-cli.ts +58 -5
  702. package/src/workspace/records.schema.json +5 -0
  703. package/src/workspace/release-candidates.ts +7 -2
  704. package/src/workspace/release-train.ts +157 -0
  705. package/src/workspace/release-workflow.ts +4 -3
  706. package/src/workspace/repo-ref.ts +113 -0
  707. package/src/workspace/run-event.schema.json +2 -2
  708. package/src/workspace/runs-cli.ts +48 -4
  709. package/src/workspace/runs.schema.json +7 -0
  710. package/src/workspace/status-contract.test.ts +46 -1
  711. package/src/workspace/status-decisions.test.ts +100 -0
  712. package/src/workspace/status-decisions.ts +151 -0
  713. package/src/workspace/status-gates.ts +9 -0
  714. package/src/workspace/status.schema.json +6 -0
  715. package/src/workspace/status.ts +17 -3
  716. package/src/workspace/where-filters.test.ts +39 -0
  717. package/src/workspace/work-cli.ts +5 -3
  718. package/src/workspace/work-evidence-ops.test.ts +176 -0
  719. package/src/workspace/work-evidence-ops.ts +189 -0
  720. package/src/workspace/work-evidence.schema.json +49 -2
  721. package/src/workspace/work-evidence.ts +22 -3
  722. package/src/workspace/workspace-gates.ts +200 -0
  723. package/src/workspace/workspace-kinds.schema.json +108 -10
  724. package/src/workspace/workspace-ledger.ts +130 -0
  725. package/src/workspace/workspace-run.test.ts +322 -0
  726. package/src/workspace/workspace-run.ts +461 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * #4404: an Op gate's `policy`, `mode` and `context` are fields of
3
+ * `identity.gates.<gate>` in the workspace declaration at base, as the quorum
4
+ * is (#4324), so a change can't switch its own gate to `log-only` or swap in a
5
+ * permissive policy and be judged by that.
6
+ */
7
+
8
+ import { afterEach, describe, expect, test } from "vitest";
9
+ import type { GateStep, OpConfig } from "./types";
10
+ import type { ActivityFn } from "./activity-registry";
11
+ import { resolveGateApproval, runOpLocally } from "./local-executor";
12
+ import { gitGateLedgerPort, memoryGateLedgerPort, type GateLedgerPort } from "./gate";
13
+ import { gatePolicyVersion, type GatePolicyRef } from "./gate-approval";
14
+ import { stepOutput } from "./step-output-ref";
15
+ import { resetGateOrigin } from "../lifecycle/gate-origin";
16
+ import { hasSshKeygen, TestRepo } from "../workspace/trust/test-repo";
17
+
18
+ const policy = (name: string, text: string): GatePolicyRef => ({ kind: "gate-policy", lexicon: "cedar", name, version: gatePolicyVersion(text), text });
19
+ const STRICT = policy("strict", "forbid unless maintainer");
20
+ const PERMISSIVE = policy("permissive", "permit all");
21
+
22
+ const step = (approval?: GateStep["approval"]): GateStep => ({ kind: "gate", gate: "ship", ...(approval ? { approval } : {}) }) as GateStep;
23
+
24
+ describe("resolveGateApproval reads policy, mode and context at base (#4404)", () => {
25
+ test("the base policy and mode replace the Op's own", () => {
26
+ const opSide = step({ policy: PERMISSIVE, mode: "log-only" });
27
+ const got = resolveGateApproval(opSide, new Map(), undefined, null, { policy: STRICT, mode: "enforce" });
28
+ expect(got).toEqual({ policy: STRICT, mode: "enforce" });
29
+ });
30
+
31
+ test("a gate whose Op dropped its approval block still takes the base policy and mode", () => {
32
+ expect(resolveGateApproval(step(), new Map(), undefined, null, { policy: STRICT, mode: "enforce" })).toEqual({ policy: STRICT, mode: "enforce" });
33
+ });
34
+
35
+ test("a field the entry leaves out comes from the Op", () => {
36
+ const opSide = step({ policy: PERMISSIVE, mode: "enforce" });
37
+ expect(resolveGateApproval(opSide, new Map(), undefined, null, { mode: "log-only" })).toEqual({ policy: PERMISSIVE, mode: "log-only" });
38
+ expect(resolveGateApproval(opSide, new Map(), undefined, null, { policy: STRICT })).toEqual({ policy: STRICT, mode: "enforce" });
39
+ });
40
+
41
+ test("the base context wins key by key, and a key only the Op sets still reaches the policy", () => {
42
+ const opSide = step({ policy: STRICT, context: { risk: "low", team: "payments" } });
43
+ const got = resolveGateApproval(opSide, new Map(), undefined, null, { context: { risk: "high" } });
44
+ expect(got?.context).toEqual({ risk: "high", team: "payments" });
45
+ });
46
+
47
+ test("a step output reference in the Op's context still resolves beside a base key", () => {
48
+ const opSide = step({ policy: STRICT, context: { rows: stepOutput("verify", "rows") } });
49
+ const got = resolveGateApproval(opSide, new Map([["verify", { rows: 12 }]]), undefined, null, { context: { risk: "high" } });
50
+ expect(got?.context).toEqual({ risk: "high", rows: 12 });
51
+ });
52
+
53
+ test("with nothing at base, the Op's own applies", () => {
54
+ expect(resolveGateApproval(step({ policy: PERMISSIVE, mode: "enforce" }), new Map(), undefined, null, { policy: null, mode: null, context: null })).toEqual({ policy: PERMISSIVE, mode: "enforce" });
55
+ expect(resolveGateApproval(step(), new Map(), undefined, null, { policy: null, mode: null, context: null })).toBeUndefined();
56
+ });
57
+ });
58
+
59
+ describe.skipIf(!hasSshKeygen)("an Op gate's policy and mode at base (#4404)", () => {
60
+ const repos: TestRepo[] = [];
61
+ afterEach(() => {
62
+ resetGateOrigin();
63
+ while (repos.length) repos.pop()!.cleanup();
64
+ });
65
+
66
+ const declaration = (gate: Record<string, unknown>): string => JSON.stringify({ name: "acme", schema: 1, members: [], identity: { gates: { ship: gate } } });
67
+ const config = (approval: NonNullable<GateStep["approval"]>): OpConfig => ({
68
+ name: "release",
69
+ overview: "",
70
+ phases: [
71
+ { name: "Gate", steps: [{ kind: "gate", gate: "ship", approval }] },
72
+ { name: "Apply", steps: [{ kind: "activity", fn: "ship", args: {} }] },
73
+ ],
74
+ });
75
+ const activities = new Map<string, ActivityFn>([["ship", async () => ({})]]);
76
+
77
+ test("a change that switches its gate to a permissive log-only policy is judged by the base's", async () => {
78
+ const r = new TestRepo("4404-policy");
79
+ repos.push(r);
80
+ r.git(["config", "user.email", "t@example.test"]);
81
+ r.git(["config", "user.name", "t"]);
82
+ r.git(["config", "commit.gpgsign", "false"]);
83
+ r.write("chant.workspace.json", declaration({ policy: STRICT, mode: "enforce", context: { risk: "high" } }));
84
+ r.commit("base");
85
+ // The pull request rewrites the declaration's entry too; only base counts.
86
+ r.git(["checkout", "-q", "-b", "loosen"]);
87
+ r.write("chant.workspace.json", declaration({ policy: PERMISSIVE, mode: "log-only", context: { risk: "low" } }));
88
+ r.commit("loosen the gate");
89
+
90
+ const git = gitGateLedgerPort({ cwd: r.dir });
91
+ const gates: GateLedgerPort = { ...memoryGateLedgerPort(), approvalRule: git.approvalRule };
92
+ // The Op the change builds says permissive, log-only and low risk.
93
+ const run = await runOpLocally(config({ policy: PERMISSIVE, mode: "log-only", context: { risk: "low" } }), activities, {}, undefined, { gates, now: "2026-10-23T12:00:00.000Z" });
94
+ expect(run.status).toBe("gated");
95
+ expect(run.gate?.approval).toMatchObject({ policy: STRICT, mode: "enforce", context: { risk: "high" } });
96
+ });
97
+ });
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The parts of an Op gate's approval that an `identity.gates.<gate>` entry
3
+ * also carries (#4404): the modes, the policy check and the entry check. Its
4
+ * only runtime import is `node:crypto`, so `gate-decision.ts` (which checks an
5
+ * entry) bundles without the Op runtime or zod (gate-decision-bundle.test.ts).
6
+ * `gate-approval.ts` re-exports everything here.
7
+ */
8
+
9
+ import { createHash } from "node:crypto";
10
+ import { GATE_PLAN_CONTEXT_KEY } from "./gate-plan-context";
11
+ import type { GateApprovalMode, GatePolicyRef } from "./gate-approval";
12
+
13
+ export const GATE_APPROVAL_MODES: readonly GateApprovalMode[] = ["log-only", "enforce"];
14
+
15
+ // The brand `stepOutput()` stamps (`step-output-ref.ts`), as a global symbol so
16
+ // this module need not import that file and its zod schemas; the same copy
17
+ // `activity-contract.ts` keeps for the same reason.
18
+ const STEP_OUTPUT_REF_BRAND = Symbol.for("chant.op.stepOutputRef");
19
+ function isStepOutputRef(value: unknown): boolean {
20
+ return typeof value === "object" && value !== null && (value as Record<symbol, unknown>)[STEP_OUTPUT_REF_BRAND] === true;
21
+ }
22
+
23
+ /** The version of a gate policy: `sha256:` and the hex digest of its text. A lexicon's helper stamps it, and OPS015 checks it still matches. */
24
+ export function gatePolicyVersion(text: string): string {
25
+ return `sha256:${createHash("sha256").update(text).digest("hex")}`;
26
+ }
27
+
28
+ export function isGatePolicyRef(value: unknown): value is GatePolicyRef {
29
+ if (typeof value !== "object" || value === null) return false;
30
+ const v = value as Record<string, unknown>;
31
+ return (
32
+ v.kind === "gate-policy" &&
33
+ typeof v.lexicon === "string" && v.lexicon !== "" &&
34
+ typeof v.name === "string" && v.name !== "" &&
35
+ typeof v.version === "string" && v.version !== "" &&
36
+ typeof v.text === "string" && v.text.trim() !== ""
37
+ );
38
+ }
39
+
40
+ /**
41
+ * What is wrong with the `policy`, `mode` and `context` of an
42
+ * `identity.gates.<gate>` entry (#4404), one line each, named by `at`. Each
43
+ * field stands alone: base `mode: "enforce"` with no base `policy` is fine, as
44
+ * the Op's own policy then applies. `chant workspace check` (WSP153) says so.
45
+ */
46
+ export function gateBaseApprovalProblems(entry: { policy?: unknown; mode?: unknown; context?: unknown }, at: string): string[] {
47
+ const problems: string[] = [];
48
+ if (entry.policy !== undefined) {
49
+ if (!isGatePolicyRef(entry.policy)) problems.push(`\`${at}.policy\` does not resolve to a gate policy set: it needs kind "gate-policy", lexicon, name, version and text`);
50
+ else if (entry.policy.version !== gatePolicyVersion(entry.policy.text)) problems.push(`\`${at}.policy\` "${entry.policy.name}" has a version that is not the digest of its text`);
51
+ }
52
+ if (entry.mode !== undefined && !GATE_APPROVAL_MODES.includes(entry.mode as GateApprovalMode)) {
53
+ problems.push(`\`${at}.mode\` must be one of ${GATE_APPROVAL_MODES.map((m) => `"${m}"`).join(", ")}`);
54
+ }
55
+ if (entry.context !== undefined) {
56
+ if (typeof entry.context !== "object" || entry.context === null || Array.isArray(entry.context)) problems.push(`\`${at}.context\` must be an object of plan attributes`);
57
+ else {
58
+ for (const [key, value] of Object.entries(entry.context)) {
59
+ if (key === GATE_PLAN_CONTEXT_KEY) problems.push(`\`${at}.context.${key}\` is reserved: the run fills it with the plan's change-set summary (#3182)`);
60
+ else if (isStepOutputRef(value) || !isContextValue(value)) problems.push(`\`${at}.context.${key}\` must be a string, number, boolean or list of strings`);
61
+ }
62
+ }
63
+ }
64
+ return problems;
65
+ }
66
+
67
+ export function isContextValue(value: unknown): boolean {
68
+ if (typeof value === "string" || typeof value === "boolean") return true;
69
+ if (typeof value === "number") return Number.isFinite(value);
70
+ if (Array.isArray(value)) return value.every((v) => typeof v === "string");
71
+ return isStepOutputRef(value);
72
+ }
@@ -504,7 +504,7 @@ describe("gate approval — a permit counts only under the context it was given
504
504
  describe("gate approval — the request a policy is asked (#2508)", () => {
505
505
  test("carries the approver, the gate and the context, with the plan digest", () => {
506
506
  expect(gatePolicyRequest({
507
- op: "release", gate: "ship", resolvedBy: "release-bot", approver: { kind: "agent", roles: ["deployer"] },
507
+ op: "release", gate: "ship", resolvedBy: "release-bot", approver: { kind: "agent" }, roles: ["deployer"],
508
508
  planDigest: PLAN_A, context: { risk: "low" },
509
509
  })).toEqual({
510
510
  principal: { kind: "agent", name: "release-bot", roles: ["deployer"] },
@@ -513,6 +513,11 @@ describe("gate approval — the request a policy is asked (#2508)", () => {
513
513
  context: { risk: "low", planDigest: PLAN_A },
514
514
  });
515
515
  });
516
+
517
+ test("never carries the roles the approver claims (#4324)", () => {
518
+ const request = gatePolicyRequest({ op: "release", gate: "ship", resolvedBy: "github:sam", approver: { kind: "human", roles: ["maintainer"] } });
519
+ expect(request.principal.roles).toEqual([]);
520
+ });
516
521
  });
517
522
 
518
523
  describe("gate approval — the approval block is checked where it is written (#2508)", () => {
@@ -33,16 +33,19 @@
33
33
  * permit counts only while the run resolves that same context (#3774).
34
34
  *
35
35
  * A resolution is a local fact, not a signed one (`../lifecycle/gate-ledger.ts`).
36
- * Roles on it are what the approver claimed, and since #4290 the quorum
37
- * never reads them: a role counts only when `.chant/trust.json` at base
38
- * grants it and a seal proves the approver (`./gate-quorum.ts`). A policy
39
- * still sees the claimed roles in its request.
36
+ * Roles on it are what the approver claimed, and nothing trusts them: since
37
+ * #4290 the quorum counts a role only when `.chant/trust.json` at base grants
38
+ * it and a seal proves the approver (`./gate-quorum.ts`), and since #4324 a
39
+ * policy's request carries those same roles, never the claimed ones.
40
40
  */
41
41
 
42
42
  import { createHash } from "node:crypto";
43
43
  import { lexiconModulePath, importLexiconPackage } from "../lexicon-module";
44
44
  import { isStepOutputRef, type StepOutputRef } from "./step-output-ref";
45
45
  import { GATE_PLAN_CONTEXT_KEY } from "./gate-plan-context";
46
+ import { GATE_APPROVAL_MODES, gateBaseApprovalProblems, gatePolicyVersion, isContextValue, isGatePolicyRef } from "./gate-approval-base";
47
+
48
+ export { GATE_APPROVAL_MODES, gateBaseApprovalProblems, gatePolicyVersion, isGatePolicyRef } from "./gate-approval-base";
46
49
  import { quorumProblems, type GateQuorumRule, type GateQuorumSpec } from "./gate-quorum";
47
50
 
48
51
  export type { GateQuorumRule, GateQuorumSpec } from "./gate-quorum";
@@ -50,7 +53,6 @@ export type { GateQuorumRule, GateQuorumSpec } from "./gate-quorum";
50
53
  /** What a policy's decision does to the gate. */
51
54
  export type GateApprovalMode = "log-only" | "enforce";
52
55
 
53
- export const GATE_APPROVAL_MODES: readonly GateApprovalMode[] = ["log-only", "enforce"];
54
56
 
55
57
  /**
56
58
  * A policy set a lexicon rendered for a gate — plain data, so it lands in the
@@ -159,10 +161,6 @@ export interface GatePolicyEvaluator {
159
161
  evaluateGatePolicy(policy: GatePolicyRef, request: GatePolicyRequest): Promise<GatePolicyAnswer> | GatePolicyAnswer;
160
162
  }
161
163
 
162
- /** The version of a gate policy: `sha256:` and the hex digest of its text. A lexicon's helper stamps it, and OPS015 checks it still matches. */
163
- export function gatePolicyVersion(text: string): string {
164
- return `sha256:${createHash("sha256").update(text).digest("hex")}`;
165
- }
166
164
 
167
165
  /**
168
166
  * The digest of a gate's resolved approval context (#3774): `sha256:` and the
@@ -180,17 +178,6 @@ function sortedKeys(_key: string, value: unknown): unknown {
180
178
  return value;
181
179
  }
182
180
 
183
- export function isGatePolicyRef(value: unknown): value is GatePolicyRef {
184
- if (typeof value !== "object" || value === null) return false;
185
- const v = value as Record<string, unknown>;
186
- return (
187
- v.kind === "gate-policy" &&
188
- typeof v.lexicon === "string" && v.lexicon !== "" &&
189
- typeof v.name === "string" && v.name !== "" &&
190
- typeof v.version === "string" && v.version !== "" &&
191
- typeof v.text === "string" && v.text.trim() !== ""
192
- );
193
- }
194
181
 
195
182
  /**
196
183
  * Everything wrong with an `approval` block, one line each. The `gate()`
@@ -256,13 +243,15 @@ export function gateApprovalProblems(approval: unknown): string[] {
256
243
  return problems;
257
244
  }
258
245
 
259
- function isContextValue(value: unknown): boolean {
260
- if (typeof value === "string" || typeof value === "boolean") return true;
261
- if (typeof value === "number") return Number.isFinite(value);
262
- if (Array.isArray(value)) return value.every((v) => typeof v === "string");
263
- return isStepOutputRef(value);
246
+ /** A gate's `policy`, `mode` and `context` as `identity.gates.<gate>` declares them at base (#4404). A step output reference is not data a declaration can hold, so a context value is a plain one. */
247
+ export interface GateBaseApproval {
248
+ policy?: GatePolicyRef | null;
249
+ mode?: GateApprovalMode | null;
250
+ context?: Record<string, string | number | boolean | string[]> | null;
264
251
  }
265
252
 
253
+
254
+
266
255
  /** Import `lexicon`'s evaluator. Throws a message that names the package when it is not installed or exports none. */
267
256
  export async function loadGatePolicyEvaluator(lexicon: string): Promise<GatePolicyEvaluator> {
268
257
  // chant #2520 — a lexicon declared by module path exports its evaluator from that module.
@@ -282,17 +271,23 @@ export async function loadGatePolicyEvaluator(lexicon: string): Promise<GatePoli
282
271
  return mod as GatePolicyEvaluator;
283
272
  }
284
273
 
285
- /** The request a policy is asked about one approval. */
274
+ /**
275
+ * The request a policy is asked about one approval. `roles` are the ones
276
+ * `.chant/trust.json` at base gives a proven approver (#4324), as a quorum
277
+ * judge reads them (`trustQuorumJudge` in `./gate-quorum.ts`). The roles the
278
+ * approver claims on `approver` are never read. Absent, the principal has none.
279
+ */
286
280
  export function gatePolicyRequest(input: {
287
281
  op: string;
288
282
  gate: string;
289
283
  resolvedBy: string;
290
284
  approver: GateApprover;
285
+ roles?: readonly string[];
291
286
  planDigest?: string;
292
287
  context?: Record<string, unknown>;
293
288
  }): GatePolicyRequest {
294
289
  return {
295
- principal: { kind: input.approver.kind, name: input.resolvedBy, roles: input.approver.roles ?? [] },
290
+ principal: { kind: input.approver.kind, name: input.resolvedBy, roles: [...(input.roles ?? [])] },
296
291
  action: "PassGate",
297
292
  resource: { op: input.op, gate: input.gate },
298
293
  context: {
@@ -37,6 +37,21 @@ export const FAN_OUT_GATE_OP = "fan-out";
37
37
  */
38
38
  export const WORKSPACE_UPGRADE_GATE_OP = "workspace-upgrade";
39
39
 
40
+ /**
41
+ * The op name a `chant workspace exit` gate is recorded under (#3177). The gate
42
+ * is the exit's scope (`workspace`, or the member names joined with `+`), and
43
+ * it binds the digest of the exit's patch. Like an upgrade it is a command
44
+ * with no `*.op.ts` behind it, and leaving is a person's decision.
45
+ */
46
+ export const WORKSPACE_EXIT_GATE_OP = "workspace-exit";
47
+
48
+ /**
49
+ * The op name a `chant workspace promote` gate is recorded under (#3178). The
50
+ * gate is the member, and it binds the digest of the promotion's patch. A
51
+ * command with no `*.op.ts` behind it, like the upgrade.
52
+ */
53
+ export const WORKSPACE_PROMOTE_GATE_OP = "workspace-promote";
54
+
40
55
  /**
41
56
  * The op name the override gate records denials and overrides under (#3760):
42
57
  * `_gates/policy-override.jsonl`, gate the subject a policy denied. Like a
@@ -69,7 +84,7 @@ const PR_GATE_OP = /^pr-\d+(?:-[a-z0-9][a-z0-9-]*)?$/;
69
84
  * no Op config to read `neverOverMcp` from, the pending record carries it.
70
85
  */
71
86
  export function isNeverOverMcpGateOp(op: string): boolean {
72
- return op === FAN_OUT_GATE_OP || op === WORKSPACE_UPGRADE_GATE_OP || op === OVERRIDE_GATE_OP || op === RELEASE_GATE_OP || PR_GATE_OP.test(op);
87
+ return op === FAN_OUT_GATE_OP || op === WORKSPACE_UPGRADE_GATE_OP || op === WORKSPACE_EXIT_GATE_OP || op === OVERRIDE_GATE_OP || op === RELEASE_GATE_OP || PR_GATE_OP.test(op);
73
88
  }
74
89
 
75
90
  /** Either spelling of a gate step's name. `gate` since #2202; `signalName` through 0.59.0. */
@@ -0,0 +1,211 @@
1
+ /**
2
+ * #4324: an Op gate's quorum is `identity.gates.<gate>.quorum` in the
3
+ * workspace declaration at base, so a change can't lower the quorum its own
4
+ * run needs, and a gate policy's `principal.roles` come from
5
+ * `.chant/trust.json` at base for a proven approver, never from `chant
6
+ * approve --role`.
7
+ */
8
+
9
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
10
+ import type { GateStep, OpConfig } from "./types";
11
+ import type { ActivityFn } from "./activity-registry";
12
+ import { resolveGateApproval, runOpLocally } from "./local-executor";
13
+ import { gitGateLedgerPort, memoryGateLedgerPort, type GateLedgerPort } from "./gate";
14
+ import type { GatePolicyEvaluator, GatePolicyRef, GatePolicyRequest } from "./gate-approval";
15
+ import { appendPendingGate, type GateResolutionInput } from "../lifecycle/gate-ledger";
16
+ import { resetGateOrigin } from "../lifecycle/gate-origin";
17
+ import { sealGateApproval } from "../workspace/trust/seal";
18
+ import { hasSshKeygen, TestRepo, type Key } from "../workspace/trust/test-repo";
19
+ import { recordGateApproval } from "../cli/handlers/approve";
20
+
21
+ const repos: TestRepo[] = [];
22
+ const home = process.cwd();
23
+ afterEach(() => {
24
+ process.chdir(home);
25
+ resetGateOrigin();
26
+ while (repos.length) repos.pop()!.cleanup();
27
+ });
28
+
29
+ let keys: TestRepo;
30
+ let alice: Key;
31
+ let bob: Key;
32
+ beforeAll(() => {
33
+ if (!hasSshKeygen) return;
34
+ keys = new TestRepo("4324-keys");
35
+ alice = keys.key("alice");
36
+ bob = keys.key("bob");
37
+ });
38
+ afterAll(() => keys?.cleanup());
39
+
40
+ /** A repository whose `main` is the base, with a git identity the ledger writes can use. */
41
+ function repo(label: string, files: Record<string, string>): TestRepo {
42
+ const r = new TestRepo(label);
43
+ repos.push(r);
44
+ r.git(["config", "user.email", "t@example.test"]);
45
+ r.git(["config", "user.name", "t"]);
46
+ r.git(["config", "commit.gpgsign", "false"]);
47
+ for (const [path, text] of Object.entries(files)) r.write(path, text);
48
+ r.commit("base");
49
+ return r;
50
+ }
51
+
52
+ const declaration = (gates: Record<string, unknown>): string =>
53
+ JSON.stringify({ name: "acme", schema: 1, members: [], identity: { gates } });
54
+
55
+ describe("resolveGateApproval reads the quorum at base (#4324)", () => {
56
+ const step = (approval?: GateStep["approval"]): GateStep => ({ kind: "gate", gate: "ship", ...(approval ? { approval } : {}) }) as GateStep;
57
+
58
+ test("the base quorum replaces the Op's own", () => {
59
+ expect(resolveGateApproval(step({ quorum: { count: 1 } }), new Map(), undefined, { count: 2 })).toEqual({ quorum: { count: 2 }, mode: "log-only" });
60
+ });
61
+
62
+ test("a gate whose Op dropped its approval block still takes the base quorum", () => {
63
+ expect(resolveGateApproval(step(), new Map(), undefined, { count: 2 })).toEqual({ quorum: { count: 2 }, mode: "log-only" });
64
+ });
65
+
66
+ test("a base list is matched against the run's environment", () => {
67
+ const atBase = [{ targets: ["prod"], count: 2 }, { count: 1 }];
68
+ expect(resolveGateApproval(step({ quorum: { count: 1 } }), new Map(), "prod", atBase)?.quorum).toEqual(atBase);
69
+ expect(resolveGateApproval(step({ quorum: { count: 1 } }), new Map(), "dev", atBase)?.quorum).toEqual([{ count: 1 }]);
70
+ });
71
+
72
+ test("with no quorum at base, the Op's own applies", () => {
73
+ expect(resolveGateApproval(step({ quorum: { count: 3 } }), new Map(), undefined, null)).toEqual({ quorum: { count: 3 }, mode: "log-only" });
74
+ expect(resolveGateApproval(step(), new Map(), undefined, null)).toBeUndefined();
75
+ });
76
+ });
77
+
78
+ describe.skipIf(!hasSshKeygen)("an Op gate's quorum at base (#4324)", () => {
79
+ const NOW = "2026-10-23T12:00:00.000Z";
80
+ const config = (count: number): OpConfig => ({
81
+ name: "release",
82
+ overview: "",
83
+ phases: [
84
+ { name: "Gate", steps: [{ kind: "gate", gate: "ship", approval: { quorum: { count } } }] },
85
+ { name: "Apply", steps: [{ kind: "activity", fn: "ship", args: {} }] },
86
+ ],
87
+ });
88
+ const activities = new Map<string, ActivityFn>([["ship", async () => ({})]]);
89
+
90
+ test("a change that lowers its own quorum still needs the quorum at base", async () => {
91
+ const r = repo("4324-quorum", {
92
+ "chant.workspace.json": declaration({ ship: { quorum: { count: 2 } } }),
93
+ ".chant/allowed_signers": `github:alice ${alice.pub}\ngithub:bob ${bob.pub}\n`,
94
+ });
95
+ // The pull request lowers both the declaration's quorum and the Op's.
96
+ r.git(["checkout", "-q", "-b", "lower-quorum"]);
97
+ r.write("chant.workspace.json", declaration({ ship: { quorum: { count: 1 } } }));
98
+ r.commit("lower the quorum");
99
+
100
+ const memory = memoryGateLedgerPort();
101
+ const git = gitGateLedgerPort({ cwd: r.dir });
102
+ const gates: GateLedgerPort = { ...memory, approvalRule: git.approvalRule, quorumJudge: git.quorumJudge };
103
+ const approve = async (by: string, key: Key, timestamp: string) => {
104
+ const line = { op: "release", gate: "ship", resolvedBy: by, timestamp, approver: { kind: "human" as const } };
105
+ const input: GateResolutionInput = { ...line, seal: sealGateApproval(key.file, line) };
106
+ await memory.appendResolution!(input);
107
+ };
108
+
109
+ const first = await runOpLocally(config(1), activities, {}, undefined, { gates, now: NOW });
110
+ expect(first.status).toBe("gated");
111
+ expect(first.gate?.approval?.quorum).toEqual({ count: 2 });
112
+ expect(memory.appended[0]?.approval?.quorum).toEqual({ count: 2 });
113
+
114
+ await approve("github:alice", alice, "2026-10-23T12:01:00.000Z");
115
+ const second = await runOpLocally(config(1), activities, {}, undefined, { gates, now: "2026-10-23T12:02:00.000Z" });
116
+ expect(second.status).toBe("gated");
117
+
118
+ await approve("github:bob", bob, "2026-10-23T12:03:00.000Z");
119
+ const third = await runOpLocally(config(1), activities, {}, undefined, { gates, now: "2026-10-23T12:04:00.000Z" });
120
+ expect(third.status).toBe("ok");
121
+ expect(third.records.find((rec) => rec.fn === "gate:ship")?.approval).toMatchObject({ via: "quorum", approvers: ["github:alice", "github:bob"] });
122
+ });
123
+
124
+ test("a gate the declaration names with no quorum keeps the Op's", async () => {
125
+ const r = repo("4324-no-quorum", {
126
+ "chant.workspace.json": declaration({ ship: {} }),
127
+ ".chant/allowed_signers": `github:alice ${alice.pub}\n`,
128
+ });
129
+ const git = gitGateLedgerPort({ cwd: r.dir });
130
+ const gates: GateLedgerPort = { ...memoryGateLedgerPort(), approvalRule: git.approvalRule };
131
+ const run = await runOpLocally(config(3), activities, {}, undefined, { gates, now: NOW });
132
+ expect(run.gate?.approval?.quorum).toEqual({ count: 3 });
133
+ });
134
+ });
135
+
136
+ describe.skipIf(!hasSshKeygen)("a gate policy's roles come from trust.json at base (#4324)", () => {
137
+ const POLICY: GatePolicyRef = { kind: "gate-policy", lexicon: "cedar", name: "ship", version: "sha256:v1", text: "permit when maintainer" };
138
+ /** Permits only a principal with the maintainer role, and remembers what it was asked. */
139
+ function maintainersOnly(): GatePolicyEvaluator & { asked: GatePolicyRequest[] } {
140
+ const asked: GatePolicyRequest[] = [];
141
+ return {
142
+ asked,
143
+ evaluateGatePolicy: (_policy, request) => {
144
+ asked.push(request);
145
+ return request.principal.roles.includes("maintainer")
146
+ ? { decision: "allow", determining: ["maintainers"], errors: [] }
147
+ : { decision: "deny", determining: [], errors: [] };
148
+ },
149
+ };
150
+ }
151
+
152
+ async function gated(): Promise<TestRepo> {
153
+ const r = repo("4324-policy", {
154
+ "README.md": "x\n",
155
+ ".chant/allowed_signers": `github:alice ${alice.pub}\n`,
156
+ ".chant/trust.json": JSON.stringify({ schema: 1, roles: { maintainer: ["github:alice"] } }),
157
+ });
158
+ await appendPendingGate(
159
+ {
160
+ op: "release",
161
+ gate: "ship",
162
+ timestamp: "2026-10-23T12:00:00.000Z",
163
+ origin: "cli",
164
+ expiresAt: "2999-01-01T00:00:00.000Z",
165
+ approval: { policy: POLICY, mode: "enforce" },
166
+ },
167
+ { cwd: r.dir },
168
+ );
169
+ process.chdir(r.dir);
170
+ return r;
171
+ }
172
+
173
+ test("a claimed --role the trust file does not grant fails a policy that requires it", async () => {
174
+ const r = await gated();
175
+ const evaluator = maintainersOnly();
176
+ const outcome = await recordGateApproval("release", "ship", { origin: "cli", actor: "github:sam", roles: ["maintainer"], evaluator, cwd: r.dir });
177
+ expect(outcome.ok).toBe(true);
178
+ expect(evaluator.asked[0]?.principal).toEqual({ kind: "human", name: "github:sam", roles: [] });
179
+ if (!outcome.ok) return;
180
+ expect(outcome.record.policyDecision?.decision).toBe("deny");
181
+ // The claim is still recorded, as data nothing trusts.
182
+ expect(outcome.record.approver).toEqual({ kind: "human", roles: ["maintainer"] });
183
+ });
184
+
185
+ test("an unsealed approval by a granted principal gets no roles either", async () => {
186
+ const r = await gated();
187
+ const evaluator = maintainersOnly();
188
+ const outcome = await recordGateApproval("release", "ship", { origin: "cli", actor: "github:alice", roles: ["maintainer"], evaluator, cwd: r.dir });
189
+ expect(evaluator.asked[0]?.principal.roles).toEqual([]);
190
+ expect(outcome.ok && outcome.record.policyDecision?.decision).toBe("deny");
191
+ });
192
+
193
+ test("a sealed approval gets the roles trust.json at base grants, and only those", async () => {
194
+ const r = await gated();
195
+ const evaluator = maintainersOnly();
196
+ const outcome = await recordGateApproval("release", "ship", { origin: "cli", actor: "github:alice", roles: ["admin"], sign: alice.file, evaluator, cwd: r.dir });
197
+ expect(evaluator.asked[0]?.principal).toEqual({ kind: "human", name: "github:alice", roles: ["maintainer"] });
198
+ expect(outcome.ok && outcome.record.policyDecision?.decision).toBe("allow");
199
+ });
200
+
201
+ test("a grant the change adds to trust.json does not count until it is at base", async () => {
202
+ const r = await gated();
203
+ r.git(["checkout", "-q", "-b", "grant-sam"]);
204
+ r.write(".chant/allowed_signers", `github:alice ${alice.pub}\ngithub:bob ${bob.pub}\n`);
205
+ r.write(".chant/trust.json", JSON.stringify({ schema: 1, roles: { maintainer: ["github:alice", "github:bob"] } }));
206
+ r.commit("grant bob");
207
+ const evaluator = maintainersOnly();
208
+ await recordGateApproval("release", "ship", { origin: "cli", actor: "github:bob", sign: bob.file, evaluator, cwd: r.dir });
209
+ expect(evaluator.asked[0]?.principal.roles).toEqual([]);
210
+ });
211
+ });