@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
@@ -72,6 +72,14 @@ export interface DeployContext {
72
72
  * against missing values.
73
73
  */
74
74
  unresolvedInputs?: string[];
75
+ /**
76
+ * The addresses this component's steps narrow to (#4300), from
77
+ * `--target <component>:<address>`, sorted. A step's `plan` plans only
78
+ * these, and its `run` applies only these. Every capability step of the
79
+ * component must accept targets ({@link Capability.targets}), or the
80
+ * component refuses before anything plans or runs. Absent: the whole step.
81
+ */
82
+ targets?: string[];
75
83
  }
76
84
 
77
85
  /**
@@ -101,6 +109,14 @@ export interface CapabilityPlan {
101
109
  * applies.
102
110
  */
103
111
  outputs?: PlannedOutputs;
112
+ /**
113
+ * The targets this plan narrowed to (#4300), sorted. A plan made with
114
+ * `DeployContext.targets` gives them back, which says it planned only
115
+ * those. Planning binds them into the member's digest
116
+ * ({@link targetedPlanDigest} in ./wave-plan.ts), so an approval of a
117
+ * targeted plan covers that plan and no other.
118
+ */
119
+ targets?: string[];
104
120
  }
105
121
 
106
122
  /**
@@ -176,11 +192,44 @@ export interface Capability<In = unknown, Out = unknown> {
176
192
  * compensation gap. See ../lint/rules/comp/comp003-mutating-no-rollback.ts.
177
193
  */
178
194
  readonly rollbackPolicy?: RollbackPolicy;
195
+ /**
196
+ * Whether this verb narrows to `DeployContext.targets` (#4300). `"accept"`:
197
+ * its `plan` and `run` act on the targets alone, and its plan gives them
198
+ * back in {@link CapabilityPlan.targets}. `{ refuse }`: it can't, and says
199
+ * why. Unset refuses too, with a reason that names the verb.
200
+ */
201
+ readonly targets?: TargetSupport;
179
202
  }
180
203
 
181
204
  /** A capability's relationship to rollback — see `Capability.rollbackPolicy`. */
182
205
  export type RollbackPolicy = "native" | "none-by-design" | "needs-opt-out";
183
206
 
207
+ /** Whether a capability accepts `DeployContext.targets` (#4300). See `Capability.targets`. */
208
+ export type TargetSupport = "accept" | { refuse: string };
209
+
210
+ /**
211
+ * A component was asked to narrow to targets (#4300) and one of its steps'
212
+ * capabilities refuses them. Nothing in the component plans or runs.
213
+ */
214
+ export class TargetsRefusedError extends Error {
215
+ constructor(
216
+ readonly component: string,
217
+ readonly kind: string,
218
+ readonly reason: string,
219
+ ) {
220
+ super(`component "${component}": ${kind} refuses targets: ${reason}`);
221
+ this.name = "TargetsRefusedError";
222
+ }
223
+ }
224
+
225
+ /** Why `capability` refuses targets, or `undefined` when it accepts them (#4300). */
226
+ export function targetRefusal(capability: Pick<Capability<never, unknown>, "kind" | "targets">): string | undefined {
227
+ const t = capability.targets;
228
+ if (t === "accept") return undefined;
229
+ if (t && typeof t === "object" && typeof t.refuse === "string") return t.refuse;
230
+ return `${capability.kind} does not say it can narrow to targets`;
231
+ }
232
+
184
233
  /** Extract a capability's `In` type. */
185
234
  export type CapabilityInput<C> = C extends Capability<infer In, unknown> ? In : never;
186
235
  /** Extract a capability's `Out` type. */
@@ -257,3 +306,25 @@ export class CapabilityRegistry {
257
306
  return [...this.capabilities.keys()].sort();
258
307
  }
259
308
  }
309
+
310
+ /**
311
+ * `--target <component>:<address>` values (#4300), grouped by component with
312
+ * each component's addresses sorted and unique. The address is everything
313
+ * after the first `:`, so it may hold colons of its own. Throws for a value
314
+ * with no component or no address, and, given `known`, for a component not
315
+ * among them.
316
+ */
317
+ export function parseComponentTargets(values: readonly string[], known?: ReadonlySet<string>): Record<string, string[]> {
318
+ const out: Record<string, Set<string>> = {};
319
+ for (const value of values) {
320
+ const at = value.indexOf(":");
321
+ const component = at > 0 ? value.slice(0, at).trim() : "";
322
+ const address = at > 0 ? value.slice(at + 1).trim() : "";
323
+ if (component === "" || address === "") throw new Error(`--target needs a component and an address: --target <component>:<address>, got "${value}"`);
324
+ if (known && !known.has(component)) {
325
+ throw new Error(`--target ${value}: no component is named ${component} (components: ${[...known].sort().join(", ") || "none"})`);
326
+ }
327
+ (out[component] ??= new Set()).add(address);
328
+ }
329
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)).map(([c, s]) => [c, [...s].sort()]));
330
+ }
@@ -390,6 +390,12 @@ export function findComponentGate(component: DriverComponent): { gate: string }
390
390
  export interface RunComponentsOptions {
391
391
  /** Target environment name, threaded into every capability's `DeployContext.env` (default: "local"). */
392
392
  env?: string;
393
+ /**
394
+ * The addresses each component's steps narrow to (#4300), by component
395
+ * (`--target <component>:<address>`). A name the run does not run refuses
396
+ * the run before anything starts.
397
+ */
398
+ targets?: Readonly<Record<string, readonly string[]>>;
393
399
  /**
394
400
  * chant #1051 — opt-in: discover `*.component.ts` files in a sandboxed
395
401
  * child process (`chant run --components <name|all> --sandbox`) instead of
@@ -664,6 +670,12 @@ export async function runComponents(
664
670
  return { success: false, selected: [], error: resolved.error };
665
671
  }
666
672
  const targets = resolved.targets;
673
+ const narrowed = options.targets ?? {};
674
+ const outside = Object.keys(narrowed).filter((name) => !targets.some((c) => c.name === name));
675
+ if (outside.length > 0) {
676
+ return { success: false, selected: [], error: `--target names ${outside.join(", ")}, which this run does not run` };
677
+ }
678
+ const targeted = Object.keys(narrowed).length > 0 ? { targets: narrowed } : {};
667
679
 
668
680
  // (#629) Resolve `chant.config.ts`'s `sbom`/`signing`/`vulnPolicy` sections
669
681
  // and fill their defaults into every recognized step (`generate-sbom`,
@@ -712,6 +724,7 @@ export async function runComponents(
712
724
  const run = await runInterpretDriver(resolvedTargets, registry, {
713
725
  env,
714
726
  releaseIdentity,
727
+ ...targeted,
715
728
  componentOutputs: seedOutputs,
716
729
  ...(emit ? { onRunEvent: emit } : {}),
717
730
  ...(gates ? { gates } : {}),
@@ -755,7 +768,7 @@ export async function runComponents(
755
768
  emit?.({ type: "component-start", wave: 1, component: componentName });
756
769
  const componentResult = await runComponentDeploy(
757
770
  resolvedTargets[0],
758
- deployContext({ env, releaseIdentity }, componentName),
771
+ deployContext({ env, releaseIdentity, ...targeted }, componentName),
759
772
  registry,
760
773
  componentOutputs,
761
774
  undefined,
@@ -55,7 +55,7 @@ import { componentPlanDigest } from "./gate-plan";
55
55
  import { componentDeployInputs } from "./deploy-inputs";
56
56
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
57
57
  import type { CapabilityRegistry, DeployContext } from "./capability";
58
- import { isPlanMovedError, PLAN_MOVED_EXIT_CODE } from "./capability";
58
+ import { isPlanMovedError, PLAN_MOVED_EXIT_CODE, TargetsRefusedError, targetRefusal } from "./capability";
59
59
  import { isPromotedArtifact } from "./auto-release";
60
60
  import type { ReleaseIdentity } from "../telemetry-attribution";
61
61
  import type { RunProgressEvent } from "./run-progress";
@@ -889,6 +889,18 @@ export async function runComponentDeploy(
889
889
  onRunEvent?: RunEventSink,
890
890
  ): Promise<DriverComponentResult> {
891
891
  const emit = joinRunEventSinks(onRunEvent, progressSinkOf(onProgress));
892
+ // #4300: a component narrowed to targets runs only when every step can narrow.
893
+ try {
894
+ assertTargetsAccepted(component, registry, ctx);
895
+ } catch (err) {
896
+ if (!(err instanceof TargetsRefusedError)) throw err;
897
+ return {
898
+ component: component.name,
899
+ ok: false,
900
+ status: "fail",
901
+ records: [{ component: component.name, phase: "Targets", kind: err.kind, status: "fail", durationMs: 0, error: err.message }],
902
+ };
903
+ }
892
904
  // Each component tracks its own release: a digest one component promoted never stamps another's deploy.
893
905
  if (ctx.release) ctx = { ...ctx, release: { ...ctx.release } };
894
906
  const phaseOutputs: Record<string, Record<string, unknown>> = {};
@@ -908,6 +920,7 @@ export async function runComponentDeploy(
908
920
  component,
909
921
  ...(ctx.vars ? { vars: ctx.vars } : {}),
910
922
  ...(release !== undefined ? { release } : {}),
923
+ ...(ctx.targets ? { targets: ctx.targets } : {}),
911
924
  // #4155: the files the steps deploy from, read where the steps read
912
925
  // them. Only a gate reads the digest, so a run with none skips the hashing.
913
926
  ...(hasGateStep(component) ? { inputs: componentDeployInputs(component.deploy, process.cwd()).inputs } : {}),
@@ -1121,6 +1134,8 @@ export interface InterpretRunOptions {
1121
1134
  * step promotes an artifact. Absent: no deploy step sees a release.
1122
1135
  */
1123
1136
  releaseIdentity?: (component: string) => ReleaseIdentity | undefined;
1137
+ /** The addresses each component narrows to (#4300), by component: its `DeployContext.targets`. */
1138
+ targets?: Readonly<Record<string, readonly string[]>>;
1124
1139
  }
1125
1140
 
1126
1141
  /**
@@ -1144,11 +1159,47 @@ export interface InterpretRunOptions {
1144
1159
  */
1145
1160
  /** A component's `DeployContext` for a run: its env, vars and, when the caller knows it, its release. */
1146
1161
  export function deployContext(
1147
- options: { env: string; vars?: Record<string, unknown>; releaseIdentity?: (component: string) => ReleaseIdentity | undefined },
1162
+ options: {
1163
+ env: string;
1164
+ vars?: Record<string, unknown>;
1165
+ releaseIdentity?: (component: string) => ReleaseIdentity | undefined;
1166
+ targets?: Readonly<Record<string, readonly string[]>>;
1167
+ },
1148
1168
  component: string,
1149
1169
  ): DeployContext {
1150
1170
  const release = options.releaseIdentity?.(component);
1151
- return { env: options.env, component, ...(options.vars ? { vars: options.vars } : {}), ...(release ? { release: { ...release } } : {}) };
1171
+ const targets = options.targets?.[component];
1172
+ return {
1173
+ env: options.env,
1174
+ component,
1175
+ ...(options.vars ? { vars: options.vars } : {}),
1176
+ ...(release ? { release: { ...release } } : {}),
1177
+ ...(targets && targets.length > 0 ? { targets: [...new Set(targets)].sort() } : {}),
1178
+ };
1179
+ }
1180
+
1181
+ /**
1182
+ * Refuse a component asked to narrow to `ctx.targets` (#4300) when one of its
1183
+ * capability steps can't: throws {@link TargetsRefusedError} naming the first
1184
+ * such verb, in composition order. A step whose kind is not registered
1185
+ * refuses too. Does nothing when `ctx.targets` is absent.
1186
+ */
1187
+ export function assertTargetsAccepted(component: DriverComponent, registry: CapabilityRegistry, ctx: DeployContext): void {
1188
+ if (!ctx.targets || ctx.targets.length === 0) return;
1189
+ const walk = (steps: DriverPhase["steps"]): void => {
1190
+ for (const step of steps) {
1191
+ if (typeof (step as DriverPhase).phase === "string" && Array.isArray((step as DriverPhase).steps)) {
1192
+ walk((step as DriverPhase).steps);
1193
+ continue;
1194
+ }
1195
+ const kind = (step as { kind?: unknown }).kind;
1196
+ if (kind === "gate" || typeof kind !== "string") continue;
1197
+ if (!registry.has(kind)) throw new TargetsRefusedError(component.name, kind, `no capability is registered for ${kind}`);
1198
+ const reason = targetRefusal(registry.resolve(kind));
1199
+ if (reason !== undefined) throw new TargetsRefusedError(component.name, kind, reason);
1200
+ }
1201
+ };
1202
+ for (const phase of component.deploy) walk(phase.steps);
1152
1203
  }
1153
1204
 
1154
1205
  export async function runInterpretDriver(
@@ -54,6 +54,7 @@ import type { CapabilityRegistry } from "./capability";
54
54
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
55
55
  import type { DeployContext } from "./capability";
56
56
  import { planWaveComponent, type WaveComponentPlan } from "./wave-plan";
57
+ import { computePlanDigest } from "../lifecycle/plan-digest";
57
58
  import { decideWaveGate, runWaveLoop, waveGateName, waveSetDigest, type WaveMember, type WaveRecord } from "../wave-engine";
58
59
 
59
60
  /** The sink a fan-out emits to: run events, and the older stream through them. */
@@ -129,6 +130,19 @@ export interface FanOutWaveGate {
129
130
  * do not exist yet. A later wave is left for its own job.
130
131
  */
131
132
  only?: number;
133
+ /**
134
+ * With `only` (#4291): members of earlier waves the record says failed to
135
+ * plan or to apply. Each blocks its dependents in this wave and names
136
+ * itself as what blocked them; the rest of the wave runs. An earlier
137
+ * member that neither applied nor failed still refuses the run.
138
+ */
139
+ failedEarlier?: readonly string[];
140
+ /**
141
+ * With `only` (#4291): earlier waves the record says stopped at their gate.
142
+ * A member left in one of them stops this wave too ({@link EarlierWaveWaitingError}),
143
+ * since a gate nobody approved stops every wave after it.
144
+ */
145
+ waitingEarlier?: ReadonlyArray<{ wave: number; gate: string; components: readonly string[] }>;
132
146
  }
133
147
 
134
148
  /** Thrown when `FanOutWaveGate.only` names a wave whose predecessors have not all applied. */
@@ -146,6 +160,22 @@ export class EarlierWaveNotAppliedError extends Error {
146
160
  }
147
161
  }
148
162
 
163
+ /**
164
+ * Thrown when `FanOutWaveGate.only` names a wave after one that waits at its
165
+ * gate (#4291): nothing in this wave runs until that gate is approved and
166
+ * its job runs again. The CLI exits 3, as the waiting wave's job does.
167
+ */
168
+ export class EarlierWaveWaitingError extends Error {
169
+ constructor(
170
+ readonly wave: number,
171
+ readonly earlier: number,
172
+ readonly gate: string,
173
+ ) {
174
+ super(`wave ${wave} does not run yet: wave ${earlier} waits at its gate ${gate}. Approve it and run wave ${earlier} again, then this wave.`);
175
+ this.name = "EarlierWaveWaitingError";
176
+ }
177
+ }
178
+
149
179
  /** The single gate over a whole fan-out. Omit it to run without one. */
150
180
  export interface FanOutGate {
151
181
  /** The name `chant approve <op> <gate>` takes — the fan-out's name, not a component's. */
@@ -158,6 +188,8 @@ export interface FanOutGate {
158
188
  export interface FanOutRunOptions {
159
189
  env: string;
160
190
  vars?: Record<string, unknown>;
191
+ /** The addresses each component narrows to (#4300), by component: its steps' `DeployContext.targets`. */
192
+ targets?: Readonly<Record<string, readonly string[]>>;
161
193
  /** Outputs of components this run is not running: the ones the plan lists in `seeds`. */
162
194
  componentOutputs?: Record<string, Record<string, unknown>>;
163
195
  onProgress?: (event: RunProgressEvent) => void;
@@ -290,7 +322,7 @@ export async function runFanOut(
290
322
  evaluateGate(gates.port, {
291
323
  op: gate.op,
292
324
  gate: gate.gate,
293
- planDigest: active.digest,
325
+ planDigest: fanOutGateDigest(active.digest, options.targets),
294
326
  ...(gate.description ? { description: gate.description } : {}),
295
327
  ...(gate.timeout ? { timeout: gate.timeout } : {}),
296
328
  ...(options.now ? { now: options.now } : {}),
@@ -426,6 +458,21 @@ export async function runFanOut(
426
458
  };
427
459
  }
428
460
 
461
+ /**
462
+ * The digest a gate over the whole set binds: the derivation's digest, and,
463
+ * when the run narrows components to targets (#4300), the digest over it and
464
+ * every component's sorted targets. An approval of the whole set then covers
465
+ * no narrowed run, and one narrowed run's approval covers no other.
466
+ */
467
+ export function fanOutGateDigest(digest: string, targets: FanOutRunOptions["targets"]): string {
468
+ const narrowed = Object.entries(targets ?? {})
469
+ .filter(([, t]) => t.length > 0)
470
+ .map(([component, t]) => [component, [...new Set(t)].sort()] as const)
471
+ .sort(([a], [b]) => a.localeCompare(b));
472
+ if (narrowed.length === 0) return digest;
473
+ return computePlanDigest("fan-out-targets", { plan: digest, targets: Object.fromEntries(narrowed) });
474
+ }
475
+
429
476
  /** The run's `deployContext`, with the carried state and the carry callback (#3459) when the caller gave them. */
430
477
  function fanOutContext(options: FanOutRunOptions, component: string): DeployContext {
431
478
  return {
@@ -518,7 +565,13 @@ async function runGatedWaves(
518
565
  if (!wave) return { end: "stop" };
519
566
  const runnable = wave.filter((name) => left.has(name) && !blockedBy.has(name));
520
567
  if (waveGate.only !== undefined && waveNum < waveGate.only) {
521
- if (runnable.length > 0) throw new EarlierWaveNotAppliedError(waveGate.only, waveNum, runnable);
568
+ // #4291: an earlier member that failed blocks only its dependents; one that waits at its gate stops this wave.
569
+ const failedBefore = new Set(waveGate.failedEarlier ?? []);
570
+ const open = runnable.filter((name) => !failedBefore.has(name));
571
+ const waiting = waveGate.waitingEarlier?.find((w) => w.wave === waveNum && open.some((name) => w.components.includes(name)));
572
+ if (waiting) throw new EarlierWaveWaitingError(waveGate.only, waveNum, waiting.gate);
573
+ if (open.length > 0) throw new EarlierWaveNotAppliedError(waveGate.only, waveNum, open);
574
+ for (const name of runnable) markBlocked(name);
522
575
  return { end: "next" };
523
576
  }
524
577
  if (waveGate.only !== undefined && waveNum > waveGate.only) return { end: "stop" };
@@ -664,7 +717,9 @@ async function runGatedWaves(
664
717
  .sort(([a], [b]) => a.localeCompare(b))
665
718
  .map(([component, by]) => ({ component, reason: "blocked" as const, blockedBy: by }));
666
719
 
667
- const status: FanOutRunResult["status"] = failed.length > 0 ? "fail" : gatedFact ? "gated" : "ok";
720
+ // #4291: a member of this job's wave that an earlier job's failure blocked fails the job, though the rest of the wave applied.
721
+ const blockedHere = waveGate.only !== undefined && (plan.waves[waveGate.only - 1] ?? []).some((name) => blockedBy.has(name));
722
+ const status: FanOutRunResult["status"] = failed.length > 0 || blockedHere ? "fail" : gatedFact ? "gated" : "ok";
668
723
  emit?.({ type: "run-settle", status: status === "fail" ? "failed" : status, durationMs: null });
669
724
 
670
725
  return {
@@ -11,7 +11,7 @@ import { describe, expect, test } from "vitest";
11
11
  import { CapabilityRegistry, type DeployContext } from "./capability";
12
12
  import { memoryGateLedgerPort } from "../op/gate";
13
13
  import { planFanOut } from "./fan-out";
14
- import { EarlierWaveNotAppliedError, runFanOut, type FanOutRunOptions } from "./fan-out-run";
14
+ import { EarlierWaveNotAppliedError, EarlierWaveWaitingError, runFanOut, type FanOutRunOptions } from "./fan-out-run";
15
15
  import { computePlanDigest } from "../lifecycle/plan-digest";
16
16
  import type { DriverComponent } from "./driver";
17
17
  import type { GateResolutionRecord, PendingGateRecord } from "../lifecycle/gate-ledger";
@@ -354,6 +354,50 @@ describe("one wave per CI job", () => {
354
354
  });
355
355
  });
356
356
 
357
+ describe("a failed member blocks only its dependents across wave jobs (#4291)", () => {
358
+ /** net, then a and b, then app (reads a) and web (reads b). */
359
+ const WIDE: DriverComponent[] = [...ESTATE, root("web", ["b"])];
360
+
361
+ test("wave 3's job blocks what an earlier job's failure holds, and plans the rest", async () => {
362
+ const w = world();
363
+ const plan = planFanOut({ components: WIDE, changed: ["net"] });
364
+ const result = await runFanOut(plan, WIDE, w.registry, {
365
+ ...waveOptions(ledger()),
366
+ waveGate: { op: "fan-out", gate: "release", only: 3, failedEarlier: ["a"] },
367
+ progress: { completed: ["net", "b"] },
368
+ });
369
+ expect(result.blocked).toEqual([{ component: "app", reason: "blocked", blockedBy: "a" }]);
370
+ expect(w.planned.map((p) => p.root)).toEqual(["web"]);
371
+ // A member of the job's own wave was blocked, so the job fails though web went on to its gate.
372
+ expect(result.status).toBe("fail");
373
+ });
374
+
375
+ test("an earlier member that neither applied nor failed still refuses the job", async () => {
376
+ const w = world();
377
+ const plan = planFanOut({ components: WIDE, changed: ["net"] });
378
+ await expect(
379
+ runFanOut(plan, WIDE, w.registry, {
380
+ ...waveOptions(ledger()),
381
+ waveGate: { op: "fan-out", gate: "release", only: 3, failedEarlier: ["a"] },
382
+ progress: { completed: ["net"] },
383
+ }),
384
+ ).rejects.toThrow(EarlierWaveNotAppliedError);
385
+ });
386
+
387
+ test("an earlier wave that waits at its gate stops the job", async () => {
388
+ const w = world();
389
+ const plan = planFanOut({ components: WIDE, changed: ["net"] });
390
+ await expect(
391
+ runFanOut(plan, WIDE, w.registry, {
392
+ ...waveOptions(ledger()),
393
+ waveGate: { op: "fan-out", gate: "release", only: 3, waitingEarlier: [{ wave: 2, gate: "release-wave-2", components: ["a", "b"] }] },
394
+ progress: { completed: ["net"] },
395
+ }),
396
+ ).rejects.toThrow(EarlierWaveWaitingError);
397
+ expect(w.planned).toEqual([]);
398
+ });
399
+ });
400
+
357
401
  describe("both modes", () => {
358
402
  test("a run takes one gate over the set or one per wave, not both", async () => {
359
403
  const w = world();
@@ -45,6 +45,8 @@ export interface ComponentGatePlan {
45
45
  release?: string;
46
46
  /** What the steps deploy from (`componentDeployInputs`), when the caller read it. */
47
47
  inputs?: readonly DeployInputDigest[];
48
+ /** The addresses the component narrows to (#4300), sorted. A gate's approval then covers that narrowing alone. */
49
+ targets?: readonly string[];
48
50
  }
49
51
 
50
52
  /** The plan digest a component gate records and approves. */
@@ -58,5 +60,6 @@ export function componentPlanDigest(plan: ComponentGatePlan): string {
58
60
  vars: plan.vars ?? null,
59
61
  release: plan.release ?? null,
60
62
  ...(plan.inputs && plan.inputs.length > 0 ? { inputs: plan.inputs } : {}),
63
+ ...(plan.targets && plan.targets.length > 0 ? { targets: [...plan.targets].sort() } : {}),
61
64
  });
62
65
  }