@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
@@ -26,6 +26,7 @@ import { dirname, join, resolve } from "node:path";
26
26
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
27
27
  import { loadChantConfig, type ChantConfig } from "../../config";
28
28
  import { promotionOrder } from "../../workspace/promotion-order";
29
+ import { releaseTargetRefusal } from "../../components/pr-environment";
29
30
  import { resolvePromotionSource } from "../../components/promote-order";
30
31
  import { fetchLifecycle, getHeadCommit, pushLifecycle } from "../../lifecycle/git";
31
32
  import {
@@ -897,6 +898,14 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
897
898
  console.error(formatError({ message: "--to <env> is required", hint: PROMOTE_USAGE }));
898
899
  return 1;
899
900
  }
901
+ // #4298: a pull request's environment is neither a release's target nor its source.
902
+ for (const env of [to, args.migrateFrom]) {
903
+ const refusal = env ? releaseTargetRefusal(env) : undefined;
904
+ if (refusal) {
905
+ console.error(formatError({ message: refusal }));
906
+ return 1;
907
+ }
908
+ }
900
909
  const who = actorOrRefuse(ctx);
901
910
  if (typeof who === "number") return who;
902
911
 
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `chant query "<sql>" [dir] [--member <name>] [--json]` (#4297): one
3
+ * read-only SQL statement over the snapshots, Op runs, gates, deploys and
4
+ * releases on the local `chant/lifecycle` branch. The tables and the engine
5
+ * live in `../../lifecycle/query.ts`; this reads the ledgers and prints.
6
+ *
7
+ * Inside a workspace every member's ledger is read, each row naming its
8
+ * member; `--member` keeps the members it names. Outside one the project's
9
+ * own ledger is read and `member` is NULL. It reads the local branch and
10
+ * never fetches.
11
+ */
12
+ import { existsSync } from "node:fs";
13
+ import { resolve } from "node:path";
14
+ import { formatError, formatWarning } from "../format";
15
+ import type { CommandContext } from "../registry";
16
+ import type { QueryLedger } from "../../lifecycle/query";
17
+ import { findWorkspaceRoot } from "../../project-root";
18
+
19
+ const USAGE = 'chant query "<sql>" [dir] [--member <name>] [--json]';
20
+
21
+ /** The ledgers `chant query` reads from `cwd`: every member's in a workspace, else the project's own. */
22
+ export async function queryLedgers(cwd: string, members?: readonly string[]): Promise<QueryLedger[]> {
23
+ const { gitLedgerReadPort } = await import("../../lifecycle/git");
24
+ const found = findWorkspaceRoot(cwd);
25
+ if (!found) return [{ member: null, port: gitLedgerReadPort({ cwd }) }];
26
+ // Workspace code loads only when there is a workspace, so a level-0 project never pays for it.
27
+ const { readDeclaration, WorkspaceReadError } = await import("../../workspace/declaration");
28
+ const { workingTree } = await import("../../workspace/tree");
29
+ const { QueryError } = await import("../../lifecycle/query");
30
+ let declaration;
31
+ try {
32
+ declaration = readDeclaration(workingTree(found.dir), "");
33
+ } catch (err) {
34
+ if (err instanceof WorkspaceReadError) throw new QueryError(`${err.code}: ${err.message}`);
35
+ throw err;
36
+ }
37
+ const wanted = members && members.length > 0 ? new Set(members) : null;
38
+ if (wanted) {
39
+ const unknown = [...wanted].filter((n) => !declaration.members.some((m) => m.name === n));
40
+ if (unknown.length > 0) throw new QueryError(`--member ${unknown.join(", ")} names no member of the workspace`);
41
+ }
42
+ return declaration.members
43
+ .filter((m) => !wanted || wanted.has(m.name))
44
+ .map((m) => ({ member: m.name, port: gitLedgerReadPort({ cwd: found.dir, member: m.dir === "." ? "." : m.name }) }));
45
+ }
46
+
47
+ export async function runQueryCommand(ctx: CommandContext): Promise<number> {
48
+ const { args } = ctx;
49
+ const sql = args.positionals?.[0];
50
+ if (sql === undefined || sql.trim() === "") {
51
+ console.error(formatError({ message: "chant query needs a statement", hint: `${USAGE}, such as chant query "SELECT env, count(*) FROM resources GROUP BY env"` }));
52
+ return 1;
53
+ }
54
+ const cwd = resolve(args.positionals?.[1] ?? ".");
55
+ if (!existsSync(cwd)) {
56
+ console.error(formatError({ message: `${cwd} does not exist`, hint: USAGE }));
57
+ return 1;
58
+ }
59
+ const { loadQueryData, runQuery, formatQueryResult, QueryError } = await import("../../lifecycle/query");
60
+ try {
61
+ const load = await loadQueryData(await queryLedgers(cwd, args.members));
62
+ const result = await runQuery(sql, load);
63
+ if (args.json) {
64
+ console.log(JSON.stringify(result, null, 2));
65
+ } else {
66
+ console.log(formatQueryResult(result));
67
+ const bad = Object.entries(result.malformed).filter(([, n]) => n > 0);
68
+ if (bad.length > 0) console.error(formatWarning({ message: `left out what could not be read: ${bad.map(([t, n]) => `${n} in ${t}`).join(", ")}` }));
69
+ }
70
+ return 0;
71
+ } catch (err) {
72
+ if (err instanceof QueryError) {
73
+ console.error(formatError({ message: err.message, hint: USAGE }));
74
+ return 1;
75
+ }
76
+ throw err;
77
+ }
78
+ }
@@ -34,6 +34,7 @@ import { appliedDigestOf, extractRunDigest, maybeRecordAutoRelease, type Applied
34
34
  import { maybePersistBuildManifest } from "../../components/manifest-persistence";
35
35
  import { failedRunExitCode, type DriverComponentResult } from "../../components/driver";
36
36
  import { isComponentList, type ComponentRunPlan, type RunComponentsResult } from "../../components/cli-support";
37
+ import { parseComponentTargets } from "../../components/capability";
37
38
  import { isApplyRecord, resolveCiRun, type ReleaseRecord, type RunOrigin } from "../../lifecycle/release-ledger";
38
39
  import { gateAdmission, gateApprover } from "../../components/promote";
39
40
  import { runOpGenerate } from "./run-generate";
@@ -1202,10 +1203,19 @@ async function runComponentSelection(ctx: CommandContext): Promise<number> {
1202
1203
  if (plan.added.length > 0) console.error(formatInfo(`added as dependencies: ${plan.added.join(", ")}`));
1203
1204
  }
1204
1205
  : undefined;
1206
+ // #4300: `--target <component>:<address>`, the addresses a component's steps narrow to.
1207
+ let targets: Record<string, string[]> | undefined;
1208
+ try {
1209
+ targets = ctx.args.targets && ctx.args.targets.length > 0 ? parseComponentTargets(ctx.args.targets) : undefined;
1210
+ } catch (err) {
1211
+ console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
1212
+ return 1;
1213
+ }
1205
1214
  let result: RunComponentsResult;
1206
1215
  try {
1207
1216
  result = await runtime.runComponents(projectPath, selector, {
1208
1217
  env: ctx.args.env,
1218
+ ...(targets ? { targets } : {}),
1209
1219
  componentOutputs: seededOutputs,
1210
1220
  onProgress,
1211
1221
  ...(events ? { onRunEvent: events.emit } : {}),
package/src/cli/main.ts CHANGED
@@ -173,6 +173,11 @@ Commands:
173
173
  terms; --live or --at <ref> with --env <name> reads the
174
174
  estate or a recorded snapshot; --show <attr>,
175
175
  --check-live, --check-snapshot, --fail-on-drift)
176
+ query "<sql>" [dir] Run one read-only SQL statement over chant/lifecycle:
177
+ the resources and snapshots each lexicon observed, Op
178
+ runs, gates, deploys, releases and cuts, loaded into
179
+ node:sqlite (schema v1, listed in chant_schema).
180
+ --member <name> keeps members; --json prints rows
176
181
  generate Generate typed code from sources chant.config declares
177
182
  (k8s.crds, helm.charts) into src/generated/<lexicon>/
178
183
  (codegen.outDir); --check exits 1 when the committed
@@ -342,7 +347,7 @@ CI:
342
347
  key. --trust commits its runner entry on a branch of its
343
348
  own and pushes it for review; the target branch is never
344
349
  written
345
- ci init --workflow trunk-curated|trunk-curated+apply-on-merge
350
+ ci init --workflow trunk-curated|trunk-curated+apply-on-merge|apply-before-merge
346
351
  [--forge github|gitlab|forgejo] [--security <group>]... [--yes]
347
352
  Set the workspace up for a prescribed delivery workflow:
348
353
  print what it would write as a diff, then write it on
@@ -558,6 +563,15 @@ Ops:
558
563
  applies it once, if .chant/overriders at base lists
559
564
  --actor
560
565
 
566
+ lock <target>... Lock targets for a pull request on chant/lifecycle
567
+ (--pr <n>, --stage plan|apply|lock, --head <sha>,
568
+ --by <who>, --gate <name>, --json). --from-report
569
+ <pr-plan.json> locks the components a plan selected.
570
+ --forge <kind> reads which holders are still open and
571
+ posts chant/lock on --head. Exits 3 when another open
572
+ pull request holds a target. --list prints the locks
573
+ unlock [<target>...] Release the locks a pull request holds (--pr <n>,
574
+ --gate <name>, --json)
561
575
  graph Show Op dependency graph (--stacks for cross-stack order,
562
576
  --format ir|mermaid|dot|layout for the lint-gated graph IR,
563
577
  a Mermaid flowchart, Graphviz DOT, or node positions;
@@ -997,17 +1011,33 @@ Component release ledger + status:
997
1011
  when the wave is reached, with --canary <name>
998
1012
  for wave 1 and --wave <n> for one wave per CI
999
1013
  job; --resume <file> finishes an attempt that
1000
- stopped)
1014
+ stopped; --target <component>:<address> plans
1015
+ and applies only that address)
1001
1016
  components pr-plan Plan the components a pull request reaches and
1002
1017
  the ones that depend on them, and write the
1003
1018
  report and note (--base <ref> --pr <n>
1004
1019
  [--forge github|gitlab|forgejo] posts them);
1005
1020
  applies nothing
1021
+ components pr-post Post the plan note and the chant/plan status
1022
+ from the report pr-plan --no-status wrote
1023
+ (--pr <n> --head <sha> --env <env> --forge
1024
+ <kind>), in a job that runs none of the
1025
+ change's code (#4295)
1006
1026
  components pr-record On merge (#4168): plan the same members again,
1007
1027
  store the change set and write the merge record
1008
1028
  _merges/<commit>.json; applies and refuses
1009
1029
  nothing, and a plan that differs from the
1010
1030
  approved one sets a failing chant/record status
1031
+ components pr-env An environment per open pull request (#4298): up
1032
+ --base <ref> --pr <n> applies the change to pr-<n>,
1033
+ derived from ci.prEnvironment.template, once an
1034
+ approval of its plan stands (chant approve pr-<n>
1035
+ pr-env --plan <digest>); post --pr <n> --head
1036
+ <sha> keeps the record up --no-push handed on and
1037
+ posts the note; down --pr <n> tears down
1038
+ only what carries pr-<n>'s ownership marker; sweep
1039
+ tears down each one whose TTL passed or whose pull
1040
+ request closed. Never a release target
1011
1041
  components pr-apply With --apply-on-merge: plan the same members again and apply
1012
1042
  them only if an approval stands for that digest
1013
1043
  (chant approve pr-<n> pr-apply --plan <digest>);
@@ -1026,6 +1056,11 @@ Component release ledger + status:
1026
1056
  from the one last applied to --env, under the
1027
1057
  approved plans of the merges since, else at gate
1028
1058
  op green-<sha>;
1059
+ --before-merge --pr <n>: apply the open pull
1060
+ request's head under its gate, once the
1061
+ declaration at base sets ci.workflow to
1062
+ apply-before-merge and the forge says it is
1063
+ approved, mergeable, undiverged and green;
1029
1064
  --events-json, both commands: the run-event
1030
1065
  stream on stdout; --report-url <s3 or file URL>,
1031
1066
  both commands: copy the report, the change set
@@ -1201,7 +1236,8 @@ Options:
1201
1236
  chant.config.ts's build.sandbox: true; #1045)
1202
1237
  --param <name=value> (build, graph, run --components, components fan-out,
1203
1238
  components promote, components rollback,
1204
- components redeploy, release cut) Bind a
1239
+ components redeploy, components pr-env up,
1240
+ release cut) Bind a
1205
1241
  declared build-time parameter (chant.config.ts's buildParams)
1206
1242
  to a value, for source to read as params.<name>
1207
1243
  (#1064) instead of process.env — repeatable.
@@ -1213,7 +1249,8 @@ Options:
1213
1249
  chant.template.json declares instead (#2627).
1214
1250
  --params-file <path> (build, graph, run --components, components fan-out,
1215
1251
  components promote, components rollback,
1216
- components redeploy, release cut) JSON file
1252
+ components redeploy, components pr-env up,
1253
+ release cut) JSON file
1217
1254
  of { "name": value } build-time parameter values
1218
1255
  (#1064). Second precedence, after --param.
1219
1256
  init --from refuses it: pass --param instead.
@@ -1441,6 +1478,8 @@ export const commandRegistry: CommandDef[] = [
1441
1478
  { name: "describe", handler: runDescribe },
1442
1479
  { name: "explain", handler: runExplain },
1443
1480
  { name: "search", handler: runSearch },
1481
+ // #4297 — SQL over the snapshots, runs, gates, deploys and releases on chant/lifecycle. Imported on first use.
1482
+ { name: "query", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/query")).runQueryCommand(ctx) },
1444
1483
  // chant#2591 — `import --agents` and `audit` read chant.config statically
1445
1484
  // and never evaluate it (chant#2589 for audit, which audits code it must not run).
1446
1485
  { name: "import", handler: runImport, runsNoConfig: (args) => args.agents === true },
@@ -1524,6 +1563,9 @@ export const commandRegistry: CommandDef[] = [
1524
1563
  { name: "operator log", handler: runOperatorLog },
1525
1564
  { name: "operator", handler: runOperator },
1526
1565
  { name: "approve", handler: runApprove },
1566
+ // #4293 — target locks a pull request holds on chant/lifecycle. Imported on first use.
1567
+ { name: "lock", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/lock")).runLock(ctx) },
1568
+ { name: "unlock", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/lock")).runUnlock(ctx) },
1527
1569
 
1528
1570
  { name: "graph", handler: runGraph },
1529
1571
  { name: "vendor", handler: runVendor },
@@ -1613,7 +1655,10 @@ export const commandRegistry: CommandDef[] = [
1613
1655
  // #3183 — the pull-request loop the generated CI runs. Imported on first use.
1614
1656
  { name: "components pr-plan", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrPlan(ctx) },
1615
1657
  { name: "components pr-apply", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrApply(ctx) },
1658
+ // #4295: posts from the plan report in a job that runs none of the change's code, so it loads no plugins.
1659
+ { name: "components pr-post", handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrPost(ctx) },
1616
1660
  { name: "components pr-record", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrRecord(ctx) },
1661
+ { name: "components pr-env", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr-env")).runComponentsPrEnv(ctx) },
1617
1662
  { name: "components status", requiresPlugins: true, handler: runComponentsStatus },
1618
1663
  { name: "components release", handler: runComponentsReleaseRecord },
1619
1664
  { name: "components export", handler: runComponentsExport },
@@ -43,6 +43,7 @@ const BOOLEAN_FLAGS = new Set([
43
43
  "--stacks",
44
44
  "--components",
45
45
  "--pr-build",
46
+ "--pr-env",
46
47
  "--up",
47
48
  "--down",
48
49
  "--include-dependents",
@@ -72,6 +73,7 @@ const BOOLEAN_FLAGS = new Set([
72
73
  "--in-place",
73
74
  "--require-runner-start",
74
75
  "--no-push",
76
+ "--list",
75
77
  "--no-require-review",
76
78
  "--expire",
77
79
  "--check-live",
@@ -376,6 +378,11 @@ export function parseArgs(args: string[]): ParsedArgs {
376
378
  const share = Number(raw);
377
379
  if (!Number.isInteger(share) || share < 1) throw new Error(`--share needs a share number from 1: got "${raw ?? ""}"`);
378
380
  result.share = share;
381
+ } else if (arg === "--target") {
382
+ // `chant components fan-out|run --components --target <component>:<address>` (#4300), repeatable.
383
+ const raw = args[++i];
384
+ if (!raw || raw.startsWith("-") || !raw.includes(":")) throw new Error("--target needs a component and an address: --target <component>:<address>");
385
+ (result.targets ??= []).push(raw);
379
386
  } else if (arg === "--canary") {
380
387
  const raw = args[++i];
381
388
  if (!raw || raw.startsWith("-")) throw new Error("--canary needs a component name: --canary <name>[,<name>...]");
@@ -396,11 +403,26 @@ export function parseArgs(args: string[]): ParsedArgs {
396
403
  if (!result.forge || result.forge.startsWith("-")) throw new Error("--forge needs a forge: --forge github|gitlab|forgejo");
397
404
  } else if (arg === "--require-review") {
398
405
  result.requireReview = true;
406
+ } else if (arg === "--list") {
407
+ // `chant lock --list` (#4293): print the target locks instead of taking any.
408
+ result.list = true;
409
+ } else if (arg === "--stage") {
410
+ // `chant lock --stage plan|apply|lock` (#4293): how the lock was taken.
411
+ result.lockStage = args[++i];
412
+ if (!result.lockStage || result.lockStage.startsWith("-")) throw new Error("--stage needs plan, apply or lock: --stage <stage>");
413
+ } else if (arg === "--from-report") {
414
+ // `chant lock --from-report <pr-plan.json>` (#4293): lock the components a pull-request report lists.
415
+ result.fromReport = args[++i];
416
+ if (!result.fromReport || result.fromReport.startsWith("-")) throw new Error("--from-report needs a report file: --from-report <pr-plan.json>");
399
417
  } else if (arg === "--report-url") {
400
418
  // `chant components pr-plan|pr-apply --report-url <url>` (#4007).
401
419
  const url = args[++i];
402
420
  if (!url || url.startsWith("-")) throw new Error("--report-url needs a URL: --report-url s3://<bucket>/<prefix> or file://<directory>");
403
421
  result.reportUrl = url;
422
+ } else if (arg === "--result") {
423
+ // `chant components pr-post --result <result>` (#4295): the plan job's own result, which fails the status when it failed.
424
+ result.planResult = args[++i];
425
+ if (!result.planResult || result.planResult.startsWith("-")) throw new Error("--result needs the plan job's result: --result success|failure|cancelled");
404
426
  } else if (arg === "--no-status") {
405
427
  // `chant ci run|serve --no-status` (#4097): post no commit status for the job.
406
428
  result.noStatus = true;
@@ -419,9 +441,18 @@ export function parseArgs(args: string[]): ParsedArgs {
419
441
  } else if (arg === "--green") {
420
442
  // `chant components pr-apply --green` (#4168 step 9): apply the green commit at HEAD.
421
443
  result.green = true;
444
+ } else if (arg === "--before-merge") {
445
+ // `chant components pr-apply --before-merge` (#4301, ws-106): apply an open pull request's plan from its head.
446
+ result.beforeMerge = true;
447
+ } else if (arg === "--apply-before-merge") {
448
+ // `chant build --pr-loop --apply-before-merge` and `chant ci run apply --apply-before-merge` (#4301): the job that applies before merge.
449
+ result.applyBeforeMerge = true;
422
450
  } else if (arg === "--apply-on-merge") {
423
451
  // `chant build --pr-loop --apply-on-merge` and `chant ci serve --apply-on-merge` (#4168): opt into applying on merge.
424
452
  result.applyOnMerge = true;
453
+ } else if (arg === "--pr-env") {
454
+ // `chant build --pr-loop --pr-env` (#4298): add the pull request environment's jobs.
455
+ result.prEnv = true;
425
456
  } else if (arg === "--pr-build") {
426
457
  // `chant build --pr-loop --pr-build` (#4088), and `chant ci run --pr-build` (#4094).
427
458
  result.prBuild = true;
@@ -864,8 +895,9 @@ export function parseArgs(args: string[]): ParsedArgs {
864
895
  } else if (arg === "--expire") {
865
896
  result.expire = true;
866
897
  } else if (arg === "--role") {
867
- // #2508 — a role the approver holds, for a gate whose quorum names
868
- // roles. Repeatable, and a comma list works too.
898
+ // #2508 — a role the approver claims, which a gate's policy sees. A
899
+ // quorum reads roles from .chant/trust.json at base instead (#4290).
900
+ // Repeatable, and a comma list works too.
869
901
  const value = args[++i] ?? "";
870
902
  result.roles = [...(result.roles ?? []), ...value.split(",").map((r) => r.trim()).filter(Boolean)];
871
903
  } else if (arg === "--agent") {
@@ -294,6 +294,12 @@ export interface ParsedArgs {
294
294
  share?: number;
295
295
  /** `chant run wave --group <name>` (#3914) — apply one job group's runs of a decided wave. */
296
296
  group?: string;
297
+ /**
298
+ * `chant components fan-out --target <component>:<address>` and `chant run
299
+ * --components --target ...` (#4300), repeatable: the addresses a
300
+ * component's steps narrow to. Raw values; `parseComponentTargets` groups them.
301
+ */
302
+ targets?: string[];
297
303
  /** `chant components fan-out --canary <component>` (#3049), repeatable — components that form wave 1. */
298
304
  canary?: string[];
299
305
  /**
@@ -302,6 +308,12 @@ export interface ParsedArgs {
302
308
  * pr-apply finds it from the commit through `--forge` when omitted.
303
309
  */
304
310
  pr?: number;
311
+ /** `chant lock --list` (#4293): print the target locks instead of taking any. */
312
+ list?: boolean;
313
+ /** `chant lock --stage plan|apply|lock` (#4293): how the lock is taken. Default `lock`. */
314
+ lockStage?: string;
315
+ /** `chant lock --from-report <file>` (#4293): lock the components a `pr-plan.json` lists. */
316
+ fromReport?: string;
305
317
  /** Every `--pr <n>`, in order: `chant release compose` (#4168 4.3) takes several. */
306
318
  prs?: number[];
307
319
  /** `chant release cut --channel <tag>` (#4168 4.3): the tag a publish of the release moves in place of `latest`. */
@@ -345,6 +357,21 @@ export interface ParsedArgs {
345
357
  * approved plans of the merges between or a gate of its own.
346
358
  */
347
359
  green?: boolean;
360
+ /** `chant build --pr-loop --pr-env` (#4298) — add the env-up, env-down and env-sweep jobs of a pull request's environment. */
361
+ prEnv?: boolean;
362
+ /**
363
+ * `chant components pr-apply --before-merge` (#4301, ws-106): apply an open
364
+ * pull request's plan from its head, once the declaration at base sets
365
+ * `ci.workflow: "apply-before-merge"` and the pull request is approved,
366
+ * mergeable, undiverged and green.
367
+ */
368
+ beforeMerge?: boolean;
369
+ /**
370
+ * `chant build --pr-loop --apply-before-merge` and `chant ci run apply
371
+ * --apply-before-merge` (#4301): the pipeline's apply job applies an open
372
+ * pull request before it merges, and a merge records.
373
+ */
374
+ applyBeforeMerge?: boolean;
348
375
  /** `chant build --pr-loop --pr-build` (#4088) — build and lint before pr-plan and pr-apply. */
349
376
  prBuild?: boolean;
350
377
  /** `chant ci serve --build-command <line>` (#4169) — the line that builds a commit a `ci/build/<sha>` tag asks for, in a worktree of it. */
@@ -477,7 +504,7 @@ export interface ParsedArgs {
477
504
  records?: boolean;
478
505
  /** `chant workspace wip save --label <text>` (#3172): what the caller calls the snapshot, such as a turn id. */
479
506
  label?: string;
480
- /** `chant workspace wip --branch <branch>` (#3172): list one branch's snapshots. */
507
+ /** `chant workspace wip --branch <branch>` (#3172): list one branch's snapshots. `chant components pr-apply|pr-record --branch <branch>` (#4292): the branch pushed, which `ci.branches` routes members by; default the forge's GITHUB_REF_NAME or CI_COMMIT_BRANCH. */
481
508
  branch?: string;
482
509
  /** `chant ci workflow --workflow <name>`, repeatable (#3573): a workflow whose completion triggers a tick, beside those the scan finds. Also `chant ci init --workflow <name>` (#4168 step 6), the prescribed workflow to set up. */
483
510
  ciWorkflows?: string[];
@@ -491,6 +518,8 @@ export interface ParsedArgs {
491
518
  ciSecrets?: string;
492
519
  /** `chant ci workflow --chant <command>` (#3573): the command the tick step runs chant with, in place of the npx pin. Also `chant ci run --chant` (#4094): what runs chant in the job. */
493
520
  ciChant?: string;
521
+ /** `chant components pr-post --result <result>` (#4295): the plan job's own result. `failure` or `cancelled` fails the `chant/plan` status. */
522
+ planResult?: string;
494
523
  /** `chant ci run|serve --no-status` (#4097): post no commit status for the job. `chant components pr-plan|pr-apply --no-status` (#4161): post neither the status nor the note. */
495
524
  noStatus?: boolean;
496
525
  /** `chant ci run|serve --status-url <template>` (#4097): the link a job's status carries. `{key}` is `<pr>/<commit>[/<member>]`, as `--report-url` keys it. */
@@ -737,7 +766,7 @@ export interface ParsedArgs {
737
766
  * on a model-authored channel.
738
767
  */
739
768
  allowSameOrigin?: boolean;
740
- /** `chant approve --role <name>` (#2508) — roles the approver claims, for a gate whose quorum counts only some roles. Repeatable; a comma list also works. */
769
+ /** `chant approve --role <name>` (#2508) — roles the approver claims, which a gate's policy sees. A quorum never counts them (#4290): its roles come from `.chant/trust.json` at base. Repeatable; a comma list also works. */
741
770
  roles?: string[];
742
771
  /** `chant approve --agent` (#2508) — record the approval as an agent's. An agent never counts toward a quorum; it passes a gate only through a policy permit in `enforce` mode. */
743
772
  agent?: boolean;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Resource targeting (#4300): `--target <component>:<address>` narrows a
3
+ * component's steps, each capability accepts or refuses by name, and the
4
+ * targets join the plan digest an approval binds.
5
+ */
6
+
7
+ import { describe, expect, test } from "vitest";
8
+ import { CapabilityRegistry, parseComponentTargets, targetRefusal, type CapabilityPlan, type DeployContext } from "./capability";
9
+ import { deployContext, runComponentDeploy, type DriverComponent } from "./driver";
10
+ import { bindTargets, planWaveComponent, targetedPlanDigest } from "./wave-plan";
11
+ import { fanOutGateDigest, runFanOut } from "./fan-out-run";
12
+ import { planFanOut } from "./fan-out";
13
+ import { memoryGateLedgerPort } from "../op/gate";
14
+
15
+ const component = (name: string, ...kinds: string[]): DriverComponent => ({
16
+ name,
17
+ deploy: [{ phase: "Deploy", steps: kinds.map((kind) => ({ kind })) }],
18
+ });
19
+
20
+ /** A registry with a verb that accepts targets, one that refuses, and one that says nothing. */
21
+ function registry(seen: DeployContext[] = []): CapabilityRegistry {
22
+ const r = new CapabilityRegistry();
23
+ r.register({
24
+ kind: "narrow",
25
+ targets: "accept",
26
+ async plan(ctx: DeployContext): Promise<CapabilityPlan> {
27
+ seen.push(ctx);
28
+ return { member: ctx.component, planDigest: "sha256:base", ...(ctx.targets ? { targets: ctx.targets } : {}) };
29
+ },
30
+ async run(ctx: DeployContext) {
31
+ seen.push(ctx);
32
+ return { ok: true };
33
+ },
34
+ } as never);
35
+ r.register({
36
+ kind: "whole",
37
+ targets: { refuse: "whole applies everything" },
38
+ async plan(ctx: DeployContext): Promise<CapabilityPlan> {
39
+ return { member: ctx.component, planDigest: "sha256:whole" };
40
+ },
41
+ async run() {
42
+ return { ok: true };
43
+ },
44
+ } as never);
45
+ r.register({
46
+ kind: "silent",
47
+ async run() {
48
+ return { ok: true };
49
+ },
50
+ } as never);
51
+ return r;
52
+ }
53
+
54
+ describe("parseComponentTargets", () => {
55
+ test("groups by component, sorted and unique, splitting at the first colon", () => {
56
+ expect(parseComponentTargets(["net:aws_vpc.main", "net:aws_subnet.a", "net:aws_vpc.main", "app:Deployment/prod/web", "net:x[\"a:b\"]"])).toEqual({
57
+ app: ["Deployment/prod/web"],
58
+ net: ["aws_subnet.a", "aws_vpc.main", 'x["a:b"]'],
59
+ });
60
+ });
61
+
62
+ test("refuses a value with no component or no address, and an unknown component", () => {
63
+ expect(() => parseComponentTargets([":aws_vpc.main"])).toThrow(/--target <component>:<address>/);
64
+ expect(() => parseComponentTargets(["net:"])).toThrow(/--target <component>:<address>/);
65
+ expect(() => parseComponentTargets(["nope:a"], new Set(["net"]))).toThrow(/no component is named nope/);
66
+ });
67
+ });
68
+
69
+ describe("targetRefusal", () => {
70
+ test("accept, refuse with its reason, and unset refusing by name", () => {
71
+ expect(targetRefusal({ kind: "a", targets: "accept" })).toBeUndefined();
72
+ expect(targetRefusal({ kind: "b", targets: { refuse: "no" } })).toBe("no");
73
+ expect(targetRefusal({ kind: "c" })).toMatch(/^c does not say/);
74
+ });
75
+ });
76
+
77
+ describe("deployContext", () => {
78
+ test("gives a component its own targets, sorted, and none to another", () => {
79
+ const options = { env: "dev", targets: { net: ["b", "a", "a"] } };
80
+ expect(deployContext(options, "net").targets).toEqual(["a", "b"]);
81
+ expect(deployContext(options, "app").targets).toBeUndefined();
82
+ });
83
+ });
84
+
85
+ describe("planning a targeted component", () => {
86
+ test("binds the targets into the member's digest and its change-set part", async () => {
87
+ const seen: DeployContext[] = [];
88
+ const ctx = deployContext({ env: "dev", targets: { net: ["aws_vpc.main"] } }, "net");
89
+ const planned = await planWaveComponent(component("net", "narrow"), registry(seen), ctx, {});
90
+ expect("error" in planned).toBe(false);
91
+ if ("error" in planned) return;
92
+ expect(seen[0]!.targets).toEqual(["aws_vpc.main"]);
93
+ const digest = targetedPlanDigest("sha256:base", ["aws_vpc.main"]);
94
+ expect(planned.members).toEqual([{ member: "net", planDigest: digest }]);
95
+ expect(planned.parts[0]!.member.planDigest).toBe(digest);
96
+ });
97
+
98
+ test("another set of targets is another digest, and none is the plan's own", async () => {
99
+ const a = targetedPlanDigest("sha256:base", ["x"]);
100
+ const b = targetedPlanDigest("sha256:base", ["y"]);
101
+ expect(a).not.toBe(b);
102
+ expect(targetedPlanDigest("sha256:base", ["y", "x"])).toBe(targetedPlanDigest("sha256:base", ["x", "y"]));
103
+ const untargeted = await planWaveComponent(component("net", "narrow"), registry(), deployContext({ env: "dev" }, "net"), {});
104
+ expect("error" in untargeted ? undefined : untargeted.members[0]!.planDigest).toBe("sha256:base");
105
+ });
106
+
107
+ test("a verb that refuses names itself, and so does one that says nothing", async () => {
108
+ const ctx = deployContext({ env: "dev", targets: { net: ["a"] } }, "net");
109
+ const refused = await planWaveComponent(component("net", "narrow", "whole"), registry(), ctx, {});
110
+ expect(refused).toEqual({ component: "net", error: 'component "net": whole refuses targets: whole applies everything' });
111
+ const silent = await planWaveComponent(component("net", "silent"), registry(), ctx, {});
112
+ expect("error" in silent && silent.error).toMatch(/silent refuses targets: silent does not say/);
113
+ });
114
+
115
+ test("a plan that does not give the targets back refuses", () => {
116
+ const ctx: DeployContext = { env: "dev", component: "net", targets: ["a", "b"] };
117
+ expect(() => bindTargets("narrow", ctx, { member: "net", planDigest: "d" })).toThrow(/names no targets/);
118
+ expect(() => bindTargets("narrow", ctx, { member: "net", planDigest: "d", targets: ["a"] })).toThrow(/names a$/);
119
+ });
120
+ });
121
+
122
+ describe("running a targeted component", () => {
123
+ test("refuses before any step runs when one verb can't narrow", async () => {
124
+ const seen: DeployContext[] = [];
125
+ const ctx = deployContext({ env: "dev", targets: { net: ["a"] } }, "net");
126
+ const result = await runComponentDeploy(component("net", "narrow", "whole"), ctx, registry(seen), {}, undefined, { port: memoryGateLedgerPort() });
127
+ expect(result.status).toBe("fail");
128
+ expect(result.records[0]).toMatchObject({ phase: "Targets", kind: "whole", status: "fail" });
129
+ expect(seen).toEqual([]);
130
+ });
131
+
132
+ test("hands the targets to a verb that accepts them", async () => {
133
+ const seen: DeployContext[] = [];
134
+ const ctx = deployContext({ env: "dev", targets: { net: ["a"] } }, "net");
135
+ const result = await runComponentDeploy(component("net", "narrow"), ctx, registry(seen), {}, undefined, { port: memoryGateLedgerPort() });
136
+ expect(result.status).toBe("ok");
137
+ expect(seen[0]!.targets).toEqual(["a"]);
138
+ });
139
+ });
140
+
141
+ describe("a gate over the whole set", () => {
142
+ test("binds the targets, so the untargeted approval does not cover a targeted run", async () => {
143
+ expect(fanOutGateDigest("sha256:set", undefined)).toBe("sha256:set");
144
+ expect(fanOutGateDigest("sha256:set", { net: [] })).toBe("sha256:set");
145
+ const narrowed = fanOutGateDigest("sha256:set", { net: ["a"] });
146
+ expect(narrowed).not.toBe("sha256:set");
147
+ expect(fanOutGateDigest("sha256:set", { net: ["b"] })).not.toBe(narrowed);
148
+
149
+ const estate = [component("net", "narrow")];
150
+ const plan = planFanOut({ components: estate, changed: ["net"] });
151
+ const gates = memoryGateLedgerPort();
152
+ const result = await runFanOut(plan, estate, registry(), {
153
+ env: "dev",
154
+ gates,
155
+ now: "2026-01-01T00:00:00Z",
156
+ gate: { op: "fan-out", gate: "set" },
157
+ targets: { net: ["a"] },
158
+ });
159
+ expect(result.status).toBe("gated");
160
+ expect(result.gate?.planDigest).toBe(fanOutGateDigest(plan.digest, { net: ["a"] }));
161
+ });
162
+ });