@intentius/chant 0.124.51 → 0.124.54

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 (593) hide show
  1. package/dist/audit/discover.d.ts.map +1 -1
  2. package/dist/audit/discover.js +3 -1
  3. package/dist/behaviour.d.ts +1 -1
  4. package/dist/behaviour.d.ts.map +1 -1
  5. package/dist/capabilities/templates.d.ts +65 -0
  6. package/dist/capabilities/templates.d.ts.map +1 -0
  7. package/dist/capabilities/templates.js +193 -0
  8. package/dist/capabilities/templates.json +145 -0
  9. package/dist/change-set-graph.d.ts +35 -3
  10. package/dist/change-set-graph.d.ts.map +1 -1
  11. package/dist/change-set-graph.js +60 -9
  12. package/dist/change-set.d.ts +65 -7
  13. package/dist/change-set.d.ts.map +1 -1
  14. package/dist/change-set.js +23 -12
  15. package/dist/ci-identity.d.ts +7 -2
  16. package/dist/ci-identity.d.ts.map +1 -1
  17. package/dist/ci-identity.js +4 -1
  18. package/dist/cli/commands/import-index.d.ts +52 -0
  19. package/dist/cli/commands/import-index.d.ts.map +1 -0
  20. package/dist/cli/commands/import-index.js +226 -0
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/lint.js +4 -2
  23. package/dist/cli/handlers/components.d.ts.map +1 -1
  24. package/dist/cli/handlers/components.js +11 -2
  25. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  26. package/dist/cli/handlers/fan-out.js +16 -3
  27. package/dist/cli/handlers/graph.d.ts.map +1 -1
  28. package/dist/cli/handlers/graph.js +22 -4
  29. package/dist/cli/handlers/index-cloud.d.ts +14 -0
  30. package/dist/cli/handlers/index-cloud.d.ts.map +1 -0
  31. package/dist/cli/handlers/index-cloud.js +22 -0
  32. package/dist/cli/handlers/index-diff.d.ts +18 -0
  33. package/dist/cli/handlers/index-diff.d.ts.map +1 -0
  34. package/dist/cli/handlers/index-diff.js +142 -0
  35. package/dist/cli/handlers/lifecycle-drift.d.ts +25 -0
  36. package/dist/cli/handlers/lifecycle-drift.d.ts.map +1 -0
  37. package/dist/cli/handlers/lifecycle-drift.js +64 -0
  38. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  39. package/dist/cli/handlers/lifecycle.js +188 -28
  40. package/dist/cli/handlers/misc.d.ts +9 -0
  41. package/dist/cli/handlers/misc.d.ts.map +1 -1
  42. package/dist/cli/handlers/misc.js +77 -0
  43. package/dist/cli/handlers/pr.d.ts.map +1 -1
  44. package/dist/cli/handlers/pr.js +17 -3
  45. package/dist/cli/handlers/promote.d.ts.map +1 -1
  46. package/dist/cli/handlers/promote.js +89 -15
  47. package/dist/cli/handlers/query.d.ts.map +1 -1
  48. package/dist/cli/handlers/query.js +28 -4
  49. package/dist/cli/handlers/scenario.d.ts.map +1 -1
  50. package/dist/cli/handlers/scenario.js +8 -7
  51. package/dist/cli/handlers/search.d.ts +34 -4
  52. package/dist/cli/handlers/search.d.ts.map +1 -1
  53. package/dist/cli/handlers/search.js +216 -36
  54. package/dist/cli/main.d.ts.map +1 -1
  55. package/dist/cli/main.js +55 -1
  56. package/dist/cli/mcp/lifecycle-tools.d.ts.map +1 -1
  57. package/dist/cli/mcp/lifecycle-tools.js +3 -2
  58. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  59. package/dist/cli/mcp/resource-handlers.js +4 -3
  60. package/dist/cli/mcp/workspace-tools.js +1 -1
  61. package/dist/cli/parse-args.d.ts.map +1 -1
  62. package/dist/cli/parse-args.js +55 -2
  63. package/dist/cli/registry.d.ts +19 -1
  64. package/dist/cli/registry.d.ts.map +1 -1
  65. package/dist/cloud-lister/aws.d.ts +35 -0
  66. package/dist/cloud-lister/aws.d.ts.map +1 -0
  67. package/dist/cloud-lister/aws.js +203 -0
  68. package/dist/cloud-lister/azure.d.ts +34 -0
  69. package/dist/cloud-lister/azure.d.ts.map +1 -0
  70. package/dist/cloud-lister/azure.js +129 -0
  71. package/dist/cloud-lister/common.d.ts +40 -0
  72. package/dist/cloud-lister/common.d.ts.map +1 -0
  73. package/dist/cloud-lister/common.js +83 -0
  74. package/dist/cloud-lister/credentials.d.ts +50 -0
  75. package/dist/cloud-lister/credentials.d.ts.map +1 -0
  76. package/dist/cloud-lister/credentials.js +86 -0
  77. package/dist/cloud-lister/fakes.d.ts +27 -0
  78. package/dist/cloud-lister/fakes.d.ts.map +1 -0
  79. package/dist/cloud-lister/fakes.js +50 -0
  80. package/dist/cloud-lister/gcp.d.ts +42 -0
  81. package/dist/cloud-lister/gcp.d.ts.map +1 -0
  82. package/dist/cloud-lister/gcp.js +143 -0
  83. package/dist/cloud-lister/guard.d.ts +49 -0
  84. package/dist/cloud-lister/guard.d.ts.map +1 -0
  85. package/dist/cloud-lister/guard.js +103 -0
  86. package/dist/cloud-lister/ids.d.ts +23 -0
  87. package/dist/cloud-lister/ids.d.ts.map +1 -0
  88. package/dist/cloud-lister/ids.js +42 -0
  89. package/dist/cloud-lister/index.d.ts +67 -0
  90. package/dist/cloud-lister/index.d.ts.map +1 -0
  91. package/dist/cloud-lister/index.js +102 -0
  92. package/dist/cloud-lister/kubernetes.d.ts +64 -0
  93. package/dist/cloud-lister/kubernetes.d.ts.map +1 -0
  94. package/dist/cloud-lister/kubernetes.js +276 -0
  95. package/dist/cloud-lister/markers.d.ts +31 -0
  96. package/dist/cloud-lister/markers.d.ts.map +1 -0
  97. package/dist/cloud-lister/markers.js +73 -0
  98. package/dist/cloud-lister/mask.d.ts +31 -0
  99. package/dist/cloud-lister/mask.d.ts.map +1 -0
  100. package/dist/cloud-lister/mask.js +74 -0
  101. package/dist/cloud-lister/sigv4.d.ts +29 -0
  102. package/dist/cloud-lister/sigv4.d.ts.map +1 -0
  103. package/dist/cloud-lister/sigv4.js +54 -0
  104. package/dist/cloud-lister/transport.d.ts +55 -0
  105. package/dist/cloud-lister/transport.d.ts.map +1 -0
  106. package/dist/cloud-lister/transport.js +86 -0
  107. package/dist/cloud-lister/types.d.ts +121 -0
  108. package/dist/cloud-lister/types.d.ts.map +1 -0
  109. package/dist/cloud-lister/types.js +24 -0
  110. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  111. package/dist/components/capability-plugin-loader.js +6 -0
  112. package/dist/components/capability-plugin.d.ts +9 -0
  113. package/dist/components/capability-plugin.d.ts.map +1 -1
  114. package/dist/components/capability.d.ts +224 -0
  115. package/dist/components/capability.d.ts.map +1 -1
  116. package/dist/components/cli-support.d.ts +21 -0
  117. package/dist/components/cli-support.d.ts.map +1 -1
  118. package/dist/components/cli-support.js +58 -14
  119. package/dist/components/covered-round.d.ts +173 -0
  120. package/dist/components/covered-round.d.ts.map +1 -0
  121. package/dist/components/covered-round.js +200 -0
  122. package/dist/components/declared-reads.d.ts +38 -0
  123. package/dist/components/declared-reads.d.ts.map +1 -0
  124. package/dist/components/declared-reads.js +131 -0
  125. package/dist/components/deploy-units.d.ts.map +1 -1
  126. package/dist/components/deploy-units.js +4 -0
  127. package/dist/components/driver.d.ts +42 -1
  128. package/dist/components/driver.d.ts.map +1 -1
  129. package/dist/components/driver.js +74 -3
  130. package/dist/components/fan-out-run.d.ts +23 -1
  131. package/dist/components/fan-out-run.d.ts.map +1 -1
  132. package/dist/components/fan-out-run.js +82 -14
  133. package/dist/components/fan-out-support.d.ts +6 -0
  134. package/dist/components/fan-out-support.d.ts.map +1 -1
  135. package/dist/components/fan-out-support.js +4 -1
  136. package/dist/components/fan-out.d.ts +28 -0
  137. package/dist/components/fan-out.d.ts.map +1 -1
  138. package/dist/components/groups.d.ts +229 -0
  139. package/dist/components/groups.d.ts.map +1 -0
  140. package/dist/components/groups.js +760 -0
  141. package/dist/components/index.d.ts +7 -2
  142. package/dist/components/index.d.ts.map +1 -1
  143. package/dist/components/index.js +5 -0
  144. package/dist/components/plan-shape-conformance.d.ts +48 -0
  145. package/dist/components/plan-shape-conformance.d.ts.map +1 -0
  146. package/dist/components/plan-shape-conformance.js +122 -0
  147. package/dist/components/pr-run.d.ts +62 -7
  148. package/dist/components/pr-run.d.ts.map +1 -1
  149. package/dist/components/pr-run.js +354 -53
  150. package/dist/components/promote-rounds.d.ts +9 -0
  151. package/dist/components/promote-rounds.d.ts.map +1 -1
  152. package/dist/components/promote-rounds.js +23 -2
  153. package/dist/components/promote.d.ts +17 -0
  154. package/dist/components/promote.d.ts.map +1 -1
  155. package/dist/components/promote.js +36 -6
  156. package/dist/components/release-gate.d.ts +23 -1
  157. package/dist/components/release-gate.d.ts.map +1 -1
  158. package/dist/components/release-gate.js +97 -33
  159. package/dist/components/run-groups.d.ts +58 -0
  160. package/dist/components/run-groups.d.ts.map +1 -0
  161. package/dist/components/run-groups.js +78 -0
  162. package/dist/components/starter-plugin.d.ts +1 -0
  163. package/dist/components/starter-plugin.d.ts.map +1 -1
  164. package/dist/components/starter-plugin.js +6 -1
  165. package/dist/components/verbs/index.d.ts +1 -0
  166. package/dist/components/verbs/index.d.ts.map +1 -1
  167. package/dist/components/verbs/index.js +1 -0
  168. package/dist/components/verbs/member-outputs.d.ts +187 -0
  169. package/dist/components/verbs/member-outputs.d.ts.map +1 -0
  170. package/dist/components/verbs/member-outputs.js +218 -0
  171. package/dist/components/wave-plan.d.ts +2 -0
  172. package/dist/components/wave-plan.d.ts.map +1 -1
  173. package/dist/components/wave-plan.js +4 -1
  174. package/dist/config.d.ts +60 -0
  175. package/dist/config.d.ts.map +1 -1
  176. package/dist/config.js +27 -0
  177. package/dist/deep-observation.d.ts +1 -13
  178. package/dist/deep-observation.d.ts.map +1 -1
  179. package/dist/deep-observation.js +5 -22
  180. package/dist/drift-record.d.ts +542 -0
  181. package/dist/drift-record.d.ts.map +1 -0
  182. package/dist/drift-record.js +886 -0
  183. package/dist/engine-facts.d.ts +144 -0
  184. package/dist/engine-facts.d.ts.map +1 -0
  185. package/dist/engine-facts.js +14 -0
  186. package/dist/graph-ir.d.ts +153 -2
  187. package/dist/graph-ir.d.ts.map +1 -1
  188. package/dist/graph-ir.js +224 -6
  189. package/dist/graph-refs.d.ts +145 -0
  190. package/dist/graph-refs.d.ts.map +1 -1
  191. package/dist/graph-refs.js +305 -0
  192. package/dist/graph-schema/fixtures/adopt.json +15 -0
  193. package/dist/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  194. package/dist/graph-schema/fixtures/index.json +68 -0
  195. package/dist/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  196. package/dist/graph-schema/fixtures.d.ts +23 -0
  197. package/dist/graph-schema/fixtures.d.ts.map +1 -0
  198. package/dist/graph-schema/fixtures.js +246 -0
  199. package/dist/graph-schema/generate.d.ts +15 -0
  200. package/dist/graph-schema/generate.d.ts.map +1 -0
  201. package/dist/graph-schema/generate.js +267 -0
  202. package/dist/graph-schema/graph.schema.json +1387 -0
  203. package/dist/health.schema.json +2 -1
  204. package/dist/identity.d.ts.map +1 -1
  205. package/dist/identity.js +2 -0
  206. package/dist/index.d.ts +1 -0
  207. package/dist/index.d.ts.map +1 -1
  208. package/dist/index.js +2 -0
  209. package/dist/instance-nodes.d.ts +159 -0
  210. package/dist/instance-nodes.d.ts.map +1 -0
  211. package/dist/instance-nodes.js +231 -0
  212. package/dist/lexicon.d.ts +72 -0
  213. package/dist/lexicon.d.ts.map +1 -1
  214. package/dist/lifecycle/adoption-file.d.ts +56 -0
  215. package/dist/lifecycle/adoption-file.d.ts.map +1 -0
  216. package/dist/lifecycle/adoption-file.js +123 -0
  217. package/dist/lifecycle/cloud-index.d.ts +186 -0
  218. package/dist/lifecycle/cloud-index.d.ts.map +1 -0
  219. package/dist/lifecycle/cloud-index.js +618 -0
  220. package/dist/lifecycle/derive-edges.d.ts +59 -0
  221. package/dist/lifecycle/derive-edges.d.ts.map +1 -0
  222. package/dist/lifecycle/derive-edges.js +126 -0
  223. package/dist/lifecycle/git.d.ts +10 -9
  224. package/dist/lifecycle/git.d.ts.map +1 -1
  225. package/dist/lifecycle/git.js +7 -1
  226. package/dist/lifecycle/identity-match.d.ts +133 -0
  227. package/dist/lifecycle/identity-match.d.ts.map +1 -0
  228. package/dist/lifecycle/identity-match.js +147 -0
  229. package/dist/lifecycle/index-diff.d.ts +224 -0
  230. package/dist/lifecycle/index-diff.d.ts.map +1 -0
  231. package/dist/lifecycle/index-diff.js +327 -0
  232. package/dist/lifecycle/index-document.d.ts +130 -0
  233. package/dist/lifecycle/index-document.d.ts.map +1 -0
  234. package/dist/lifecycle/index-document.js +189 -0
  235. package/dist/lifecycle/index-drift.d.ts +121 -0
  236. package/dist/lifecycle/index-drift.d.ts.map +1 -0
  237. package/dist/lifecycle/index-drift.js +178 -0
  238. package/dist/lifecycle/index-import.d.ts +168 -0
  239. package/dist/lifecycle/index-import.d.ts.map +1 -0
  240. package/dist/lifecycle/index-import.js +515 -0
  241. package/dist/lifecycle/index-join.d.ts +129 -0
  242. package/dist/lifecycle/index-join.d.ts.map +1 -0
  243. package/dist/lifecycle/index-join.js +248 -0
  244. package/dist/lifecycle/index.d.ts +4 -0
  245. package/dist/lifecycle/index.d.ts.map +1 -1
  246. package/dist/lifecycle/index.js +4 -0
  247. package/dist/lifecycle/observation-baseline.d.ts +61 -21
  248. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  249. package/dist/lifecycle/observation-baseline.js +144 -19
  250. package/dist/lifecycle/observe.d.ts.map +1 -1
  251. package/dist/lifecycle/observe.js +6 -1
  252. package/dist/lifecycle/query.d.ts +45 -4
  253. package/dist/lifecycle/query.d.ts.map +1 -1
  254. package/dist/lifecycle/query.js +89 -7
  255. package/dist/lifecycle/replay.d.ts +10 -3
  256. package/dist/lifecycle/replay.d.ts.map +1 -1
  257. package/dist/lifecycle/replay.js +10 -6
  258. package/dist/lifecycle/scenario.d.ts +4 -3
  259. package/dist/lifecycle/scenario.d.ts.map +1 -1
  260. package/dist/lifecycle/snapshot-store-fake.d.ts +9 -0
  261. package/dist/lifecycle/snapshot-store-fake.d.ts.map +1 -0
  262. package/dist/lifecycle/snapshot-store-fake.js +21 -0
  263. package/dist/lifecycle/snapshot-store-s3.d.ts +102 -0
  264. package/dist/lifecycle/snapshot-store-s3.d.ts.map +1 -0
  265. package/dist/lifecycle/snapshot-store-s3.js +254 -0
  266. package/dist/lifecycle/snapshot-store.d.ts +170 -0
  267. package/dist/lifecycle/snapshot-store.d.ts.map +1 -0
  268. package/dist/lifecycle/snapshot-store.js +832 -0
  269. package/dist/lifecycle/snapshot.d.ts +11 -0
  270. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  271. package/dist/lifecycle/snapshot.js +18 -5
  272. package/dist/lifecycle/type-listing.d.ts +148 -0
  273. package/dist/lifecycle/type-listing.d.ts.map +1 -0
  274. package/dist/lifecycle/type-listing.js +166 -0
  275. package/dist/lint/component-checks.d.ts +15 -1
  276. package/dist/lint/component-checks.d.ts.map +1 -1
  277. package/dist/lint/component-checks.js +1 -0
  278. package/dist/lint/presets/relaxed.json +2 -1
  279. package/dist/lint/presets/strict.json +2 -1
  280. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts +4 -0
  281. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts.map +1 -0
  282. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.js +13 -0
  283. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts +4 -0
  284. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts.map +1 -0
  285. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.js +8 -0
  286. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts +33 -0
  287. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts.map +1 -0
  288. package/dist/lint/rules/comp/comp008-applies-signal-only-root.js +123 -0
  289. package/dist/lint/rules/comp/index.d.ts +2 -1
  290. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  291. package/dist/lint/rules/comp/index.js +4 -1
  292. package/dist/observation.d.ts +19 -2
  293. package/dist/observation.d.ts.map +1 -1
  294. package/dist/observation.js +18 -1
  295. package/dist/op/activities/activity-contracts.d.ts +1 -1
  296. package/dist/op/activities/activity-contracts.js +1 -1
  297. package/dist/op/activities/change-set.d.ts +6 -4
  298. package/dist/op/activities/change-set.d.ts.map +1 -1
  299. package/dist/op/activities/change-set.js +4 -2
  300. package/dist/op/activity-failure.d.ts +27 -0
  301. package/dist/op/activity-failure.d.ts.map +1 -1
  302. package/dist/op/activity-failure.js +31 -0
  303. package/dist/op/index.d.ts +2 -1
  304. package/dist/op/index.d.ts.map +1 -1
  305. package/dist/op/index.js +1 -1
  306. package/dist/op/local-executor.d.ts +5 -1
  307. package/dist/op/local-executor.d.ts.map +1 -1
  308. package/dist/op/local-executor.js +22 -8
  309. package/dist/pr-loop.d.ts +92 -3
  310. package/dist/pr-loop.d.ts.map +1 -1
  311. package/dist/pr-loop.js +62 -3
  312. package/dist/pulumi-urn.d.ts +101 -0
  313. package/dist/pulumi-urn.d.ts.map +1 -0
  314. package/dist/pulumi-urn.js +131 -0
  315. package/dist/release-floors.d.ts +23 -0
  316. package/dist/release-floors.d.ts.map +1 -0
  317. package/dist/release-floors.js +22 -0
  318. package/dist/sensitive-key.d.ts +23 -0
  319. package/dist/sensitive-key.d.ts.map +1 -0
  320. package/dist/sensitive-key.js +31 -0
  321. package/dist/wave-runtime.mjs +234 -234
  322. package/dist/workspace/adoption-report.d.ts +53 -0
  323. package/dist/workspace/adoption-report.d.ts.map +1 -0
  324. package/dist/workspace/adoption-report.js +76 -0
  325. package/dist/workspace/capabilities-contract.d.ts +7 -0
  326. package/dist/workspace/capabilities-contract.d.ts.map +1 -1
  327. package/dist/workspace/capabilities-contract.js +2 -1
  328. package/dist/workspace/capabilities.schema.json +98 -0
  329. package/dist/workspace/change-set.schema.json +42 -3
  330. package/dist/workspace/ci-cloud-identity.d.ts +13 -1
  331. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  332. package/dist/workspace/ci-cloud-identity.js +30 -1
  333. package/dist/workspace/compose-graph.d.ts +10 -1
  334. package/dist/workspace/compose-graph.d.ts.map +1 -1
  335. package/dist/workspace/compose-graph.js +6 -1
  336. package/dist/workspace/conformance/index.d.ts +1 -1
  337. package/dist/workspace/conformance/index.d.ts.map +1 -1
  338. package/dist/workspace/conformance/index.js +6 -1
  339. package/dist/workspace/drift-cli.d.ts +81 -0
  340. package/dist/workspace/drift-cli.d.ts.map +1 -0
  341. package/dist/workspace/drift-cli.js +317 -0
  342. package/dist/workspace/drift.schema.json +672 -0
  343. package/dist/workspace/graph-cli.d.ts +2 -0
  344. package/dist/workspace/graph-cli.d.ts.map +1 -1
  345. package/dist/workspace/graph-cli.js +3 -0
  346. package/dist/workspace/graph.schema.json +72 -0
  347. package/dist/workspace/init.d.ts +5 -1
  348. package/dist/workspace/init.d.ts.map +1 -1
  349. package/dist/workspace/init.js +55 -9
  350. package/dist/workspace/ledger.schema.json +1 -1
  351. package/dist/workspace/nested-graph.d.ts.map +1 -1
  352. package/dist/workspace/nested-graph.js +2 -0
  353. package/dist/workspace/pr-report.schema.json +76 -3
  354. package/dist/workspace/promote-waves.d.ts +112 -0
  355. package/dist/workspace/promote-waves.d.ts.map +1 -0
  356. package/dist/workspace/promote-waves.js +290 -0
  357. package/dist/workspace/promote.d.ts +84 -2
  358. package/dist/workspace/promote.d.ts.map +1 -1
  359. package/dist/workspace/promote.js +347 -61
  360. package/dist/workspace/reason-codes.d.ts +12 -0
  361. package/dist/workspace/reason-codes.d.ts.map +1 -1
  362. package/dist/workspace/reason-codes.js +14 -0
  363. package/dist/workspace/search-contract.d.ts +3 -3
  364. package/dist/workspace/search-contract.d.ts.map +1 -1
  365. package/dist/workspace/search-contract.js +2 -0
  366. package/dist/workspace/search.schema.json +103 -5
  367. package/package.json +14 -1
  368. package/src/__fixtures__/drift-record/README.md +58 -0
  369. package/src/__fixtures__/drift-record/choudoufu-cone.ndjson +4 -0
  370. package/src/__fixtures__/drift-record/choudoufu-resource.ndjson +3 -0
  371. package/src/__fixtures__/drift-record/component-status-ledger.json +65 -0
  372. package/src/__fixtures__/drift-record/component-status.json +91 -0
  373. package/src/__fixtures__/drift-record/deep-diff.json +259 -0
  374. package/src/__fixtures__/drift-record/lifecycle-diff-live.json +825 -0
  375. package/src/__fixtures__/drift-record/live-diff.json +125 -0
  376. package/src/audit/discover.e2e.test.ts +14 -0
  377. package/src/audit/discover.ts +3 -1
  378. package/src/behaviour.ts +1 -1
  379. package/src/capabilities/templates.json +145 -0
  380. package/src/capabilities/templates.test.ts +212 -0
  381. package/src/capabilities/templates.ts +204 -0
  382. package/src/change-set-bundle.test.ts +83 -0
  383. package/src/change-set-graph.ts +80 -9
  384. package/src/change-set.test.ts +24 -0
  385. package/src/change-set.ts +77 -14
  386. package/src/ci-identity-targets.test.ts +23 -0
  387. package/src/ci-identity.ts +10 -4
  388. package/src/cli/commands/import-index.test.ts +117 -0
  389. package/src/cli/commands/import-index.ts +265 -0
  390. package/src/cli/commands/lint.ts +4 -2
  391. package/src/cli/handlers/components.test.ts +9 -0
  392. package/src/cli/handlers/components.ts +12 -2
  393. package/src/cli/handlers/fan-out.ts +17 -3
  394. package/src/cli/handlers/graph.test.ts +42 -0
  395. package/src/cli/handlers/graph.ts +26 -4
  396. package/src/cli/handlers/index-cloud.ts +32 -0
  397. package/src/cli/handlers/index-diff.test.ts +68 -0
  398. package/src/cli/handlers/index-diff.ts +146 -0
  399. package/src/cli/handlers/ledger-root-member.test.ts +161 -0
  400. package/src/cli/handlers/lifecycle-drift.ts +68 -0
  401. package/src/cli/handlers/lifecycle.test.ts +33 -11
  402. package/src/cli/handlers/lifecycle.ts +205 -30
  403. package/src/cli/handlers/misc.ts +73 -0
  404. package/src/cli/handlers/pr-waves-blocked.test.ts +37 -2
  405. package/src/cli/handlers/pr.ts +16 -3
  406. package/src/cli/handlers/promote.test.ts +64 -0
  407. package/src/cli/handlers/promote.ts +81 -6
  408. package/src/cli/handlers/query.ts +27 -4
  409. package/src/cli/handlers/run-declared-reads-cli.test.ts +187 -0
  410. package/src/cli/handlers/run-groups-cli.test.ts +229 -0
  411. package/src/cli/handlers/scenario.test.ts +13 -0
  412. package/src/cli/handlers/scenario.ts +8 -7
  413. package/src/cli/handlers/search-index.test.ts +255 -0
  414. package/src/cli/handlers/search-json.test.ts +1 -1
  415. package/src/cli/handlers/search-ownership.test.ts +194 -0
  416. package/src/cli/handlers/search.ts +271 -38
  417. package/src/cli/main.test.ts +7 -0
  418. package/src/cli/main.ts +56 -1
  419. package/src/cli/mcp/lifecycle-tools.ts +3 -2
  420. package/src/cli/mcp/resource-handlers.ts +4 -3
  421. package/src/cli/mcp/workspace-tools.ts +1 -1
  422. package/src/cli/parse-args.ts +40 -2
  423. package/src/cli/registry.ts +19 -1
  424. package/src/cloud-lister/aws.test.ts +202 -0
  425. package/src/cloud-lister/aws.ts +226 -0
  426. package/src/cloud-lister/azure.test.ts +132 -0
  427. package/src/cloud-lister/azure.ts +142 -0
  428. package/src/cloud-lister/common.ts +115 -0
  429. package/src/cloud-lister/credentials.ts +110 -0
  430. package/src/cloud-lister/fakes.ts +66 -0
  431. package/src/cloud-lister/gcp.test.ts +121 -0
  432. package/src/cloud-lister/gcp.ts +159 -0
  433. package/src/cloud-lister/guard.test.ts +198 -0
  434. package/src/cloud-lister/guard.ts +111 -0
  435. package/src/cloud-lister/ids.ts +54 -0
  436. package/src/cloud-lister/index.ts +134 -0
  437. package/src/cloud-lister/kubernetes.test.ts +160 -0
  438. package/src/cloud-lister/kubernetes.ts +284 -0
  439. package/src/cloud-lister/markers.ts +74 -0
  440. package/src/cloud-lister/mask.ts +77 -0
  441. package/src/cloud-lister/shape.test.ts +81 -0
  442. package/src/cloud-lister/sigv4.ts +70 -0
  443. package/src/cloud-lister/transport.ts +118 -0
  444. package/src/cloud-lister/types.ts +135 -0
  445. package/src/components/capability-plugin-loader.ts +4 -0
  446. package/src/components/capability-plugin.ts +10 -0
  447. package/src/components/capability.ts +238 -0
  448. package/src/components/cli-support.ts +70 -14
  449. package/src/components/covered-round.test.ts +166 -0
  450. package/src/components/covered-round.ts +293 -0
  451. package/src/components/declared-reads.test.ts +103 -0
  452. package/src/components/declared-reads.ts +134 -0
  453. package/src/components/deploy-units.ts +4 -0
  454. package/src/components/driver.ts +101 -4
  455. package/src/components/fan-out-run.ts +95 -15
  456. package/src/components/fan-out-support.ts +10 -1
  457. package/src/components/fan-out.ts +30 -0
  458. package/src/components/groups.test.ts +320 -0
  459. package/src/components/groups.ts +880 -0
  460. package/src/components/index.ts +59 -0
  461. package/src/components/plan-shape-conformance.ts +130 -0
  462. package/src/components/pr-env-run-bundle.test.ts +2 -0
  463. package/src/components/pr-run.test.ts +728 -4
  464. package/src/components/pr-run.ts +395 -55
  465. package/src/components/promote-rounds.ts +25 -2
  466. package/src/components/promote.ts +55 -5
  467. package/src/components/registry.test.ts +1 -1
  468. package/src/components/release-gate.ts +115 -30
  469. package/src/components/release-groups.test.ts +362 -0
  470. package/src/components/run-groups.ts +113 -0
  471. package/src/components/starter-plugin.ts +6 -0
  472. package/src/components/verbs/index.ts +1 -0
  473. package/src/components/verbs/member-outputs.test.ts +205 -0
  474. package/src/components/verbs/member-outputs.ts +308 -0
  475. package/src/components/wave-plan.ts +5 -2
  476. package/src/config.ts +44 -0
  477. package/src/deep-observation.ts +5 -23
  478. package/src/drift-record.test.ts +596 -0
  479. package/src/drift-record.ts +1206 -0
  480. package/src/engine-facts.guard.test.ts +111 -0
  481. package/src/engine-facts.test.ts +178 -0
  482. package/src/engine-facts.ts +154 -0
  483. package/src/gated-waves-bundle.test.ts +2 -0
  484. package/src/graph-ir-live.test.ts +92 -0
  485. package/src/graph-ir.ts +317 -7
  486. package/src/graph-refs.ts +416 -0
  487. package/src/graph-schema/fixtures/adopt.json +15 -0
  488. package/src/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  489. package/src/graph-schema/fixtures/index.json +68 -0
  490. package/src/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  491. package/src/graph-schema/fixtures.ts +267 -0
  492. package/src/graph-schema/generate.ts +263 -0
  493. package/src/graph-schema/graph-schema.test.ts +153 -0
  494. package/src/graph-schema/graph.schema.json +1387 -0
  495. package/src/health.schema.json +2 -1
  496. package/src/identity.ts +2 -0
  497. package/src/index.ts +2 -0
  498. package/src/instance-nodes.ts +288 -0
  499. package/src/lexicon.ts +76 -0
  500. package/src/lifecycle/__fixtures__/index-diff/20261026T120000Z-aaaaaaaaaaaa.json +149 -0
  501. package/src/lifecycle/__fixtures__/index-diff/20261026T130000Z-bbbbbbbbbbbb.json +127 -0
  502. package/src/lifecycle/adoption-file.test.ts +100 -0
  503. package/src/lifecycle/adoption-file.ts +154 -0
  504. package/src/lifecycle/cloud-index.test.ts +517 -0
  505. package/src/lifecycle/cloud-index.ts +736 -0
  506. package/src/lifecycle/derive-edges.test.ts +193 -0
  507. package/src/lifecycle/derive-edges.ts +182 -0
  508. package/src/lifecycle/git.ts +10 -4
  509. package/src/lifecycle/identity-match.test.ts +308 -0
  510. package/src/lifecycle/identity-match.ts +257 -0
  511. package/src/lifecycle/index-diff.test.ts +198 -0
  512. package/src/lifecycle/index-diff.ts +476 -0
  513. package/src/lifecycle/index-document.test.ts +64 -0
  514. package/src/lifecycle/index-document.ts +299 -0
  515. package/src/lifecycle/index-drift.test.ts +191 -0
  516. package/src/lifecycle/index-drift.ts +304 -0
  517. package/src/lifecycle/index-import.test.ts +255 -0
  518. package/src/lifecycle/index-import.ts +634 -0
  519. package/src/lifecycle/index-join.test.ts +250 -0
  520. package/src/lifecycle/index-join.ts +304 -0
  521. package/src/lifecycle/index-store-load-bearing.test.ts +187 -0
  522. package/src/lifecycle/index.ts +4 -0
  523. package/src/lifecycle/member-ledger.test.ts +25 -7
  524. package/src/lifecycle/observation-baseline.test.ts +72 -1
  525. package/src/lifecycle/observation-baseline.ts +190 -27
  526. package/src/lifecycle/observe.ts +6 -1
  527. package/src/lifecycle/query.test.ts +69 -1
  528. package/src/lifecycle/query.ts +103 -10
  529. package/src/lifecycle/replay.test.ts +9 -5
  530. package/src/lifecycle/replay.ts +9 -5
  531. package/src/lifecycle/scenario.ts +4 -3
  532. package/src/lifecycle/snapshot-store-fake.ts +29 -0
  533. package/src/lifecycle/snapshot-store-s3.ts +316 -0
  534. package/src/lifecycle/snapshot-store.test.ts +385 -0
  535. package/src/lifecycle/snapshot-store.ts +981 -0
  536. package/src/lifecycle/snapshot.test.ts +13 -0
  537. package/src/lifecycle/snapshot.ts +23 -6
  538. package/src/lifecycle/type-listing.test.ts +166 -0
  539. package/src/lifecycle/type-listing.ts +309 -0
  540. package/src/lint/component-checks.ts +17 -1
  541. package/src/lint/config.test.ts +3 -0
  542. package/src/lint/presets/relaxed.json +2 -1
  543. package/src/lint/presets/strict.json +2 -1
  544. package/src/lint/rules/__fixtures__/comp/comp008/fail/dns.component.ts +15 -0
  545. package/src/lint/rules/__fixtures__/comp/comp008/pass/dns.component.ts +10 -0
  546. package/src/lint/rules/comp/comp.test.ts +2 -1
  547. package/src/lint/rules/comp/comp008-applies-signal-only-root.ts +121 -0
  548. package/src/lint/rules/comp/comp008.test.ts +77 -0
  549. package/src/lint/rules/comp/index.ts +4 -1
  550. package/src/observation-edges.test.ts +33 -0
  551. package/src/observation.ts +36 -1
  552. package/src/op/activities/activity-contracts.ts +1 -1
  553. package/src/op/activities/change-set.ts +8 -5
  554. package/src/op/activity-failure.ts +36 -0
  555. package/src/op/index.ts +2 -1
  556. package/src/op/local-executor.test.ts +80 -0
  557. package/src/op/local-executor.ts +27 -9
  558. package/src/pr-loop-bundle.test.ts +5 -2
  559. package/src/pr-loop.test.ts +57 -1
  560. package/src/pr-loop.ts +154 -6
  561. package/src/pulumi-urn.ts +151 -0
  562. package/src/release-floors.ts +22 -0
  563. package/src/sensitive-key.ts +34 -0
  564. package/src/workspace/adoption-report.test.ts +61 -0
  565. package/src/workspace/adoption-report.ts +103 -0
  566. package/src/workspace/capabilities-contract.test.ts +4 -0
  567. package/src/workspace/capabilities-contract.ts +9 -1
  568. package/src/workspace/capabilities.schema.json +98 -0
  569. package/src/workspace/change-set.schema.json +42 -3
  570. package/src/workspace/ci-cloud-identity.ts +31 -1
  571. package/src/workspace/compose-graph.test.ts +12 -0
  572. package/src/workspace/compose-graph.ts +12 -1
  573. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  574. package/src/workspace/conformance/index.ts +6 -1
  575. package/src/workspace/drift-cli.test.ts +65 -0
  576. package/src/workspace/drift-cli.ts +365 -0
  577. package/src/workspace/drift.schema.json +672 -0
  578. package/src/workspace/graph-cli.ts +5 -0
  579. package/src/workspace/graph.schema.json +72 -0
  580. package/src/workspace/init.test.ts +38 -1
  581. package/src/workspace/init.ts +50 -8
  582. package/src/workspace/intent-gaps.test.ts +18 -2
  583. package/src/workspace/ledger.schema.json +1 -1
  584. package/src/workspace/nested-graph.ts +1 -0
  585. package/src/workspace/pr-report.schema.json +76 -3
  586. package/src/workspace/promote-waves.test.ts +296 -0
  587. package/src/workspace/promote-waves.ts +340 -0
  588. package/src/workspace/promote.test.ts +158 -4
  589. package/src/workspace/promote.ts +398 -57
  590. package/src/workspace/reason-codes.test.ts +4 -1
  591. package/src/workspace/reason-codes.ts +14 -0
  592. package/src/workspace/search-contract.ts +2 -0
  593. package/src/workspace/search.schema.json +103 -5
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Reads a capability finds in its steps' files (#4466): each becomes a
3
+ * `stackOutput()` reference in the reader's step and a `dependsOn` on the
4
+ * upstream, so it orders the fan-out and counts as a read.
5
+ */
6
+
7
+ import { describe, expect, test } from "vitest";
8
+ import { CapabilityRegistry, type DeclaredRead, type DeclaredReadMember } from "./capability";
9
+ import { applyDeclaredReads, declaredReadsOf, withDeclaredReads } from "./declared-reads";
10
+ import { planFanOut } from "./fan-out";
11
+ import { unresolvedWiringReferences } from "./driver";
12
+ import { capabilitySteps } from "./wave-plan";
13
+ import type { DriverComponent } from "./driver";
14
+
15
+ const stack = (name: string, extra: Record<string, unknown> = {}, dependsOn: string[] = []): DriverComponent => ({
16
+ name,
17
+ dependsOn,
18
+ deploy: [{ phase: "Apply", steps: [{ kind: "fake-stack", stack: name, ...extra }] }],
19
+ });
20
+
21
+ function registry(reads: (members: readonly DeclaredReadMember[]) => DeclaredRead[]) {
22
+ const asked: DeclaredReadMember[][] = [];
23
+ const r = new CapabilityRegistry();
24
+ const unused = async (): Promise<never> => {
25
+ throw new Error("not called here");
26
+ };
27
+ r.register({
28
+ kind: "fake-stack",
29
+ run: unused,
30
+ declaredReads(_ctx: unknown, members: readonly DeclaredReadMember[]) {
31
+ asked.push([...members]);
32
+ return reads(members);
33
+ },
34
+ } as never);
35
+ r.register({ kind: "fake-plain", run: unused } as never);
36
+ return { registry: r, asked };
37
+ }
38
+
39
+ describe("declared reads (#4466)", () => {
40
+ test("a read becomes a stackOutput() reference at its place and a dependsOn, so the reader orders after the upstream", async () => {
41
+ const components = [stack("app"), stack("network")];
42
+ const { registry: r, asked } = registry(() => [{ reader: "app", upstream: "network", output: "VpcId", at: "/imports/network-VpcId" }]);
43
+ const out = await withDeclaredReads(components, r, "/project");
44
+ expect(asked[0]!.map((m) => m.component)).toEqual(["app", "network"]);
45
+ const app = out.find((c) => c.name === "app")!;
46
+ expect(app.dependsOn).toEqual(["network"]);
47
+ expect(capabilitySteps(app.deploy)[0]).toEqual({
48
+ kind: "fake-stack",
49
+ stack: "app",
50
+ imports: { "network-VpcId": { stackOutput: { stack: "network", name: "VpcId" } } },
51
+ });
52
+ // The read is now one core sees: no value for network's VpcId yet.
53
+ const { kind: _kind, ...rest } = capabilitySteps(app.deploy)[0]!;
54
+ expect(unresolvedWiringReferences(rest, {}).map((x) => x.ref)).toEqual(["network.VpcId"]);
55
+ // And it orders the fan-out.
56
+ const plan = planFanOut({ components: out, changed: ["app", "network"] });
57
+ expect(plan.waves).toEqual([["network"], ["app"]]);
58
+ // The upstream is untouched.
59
+ expect(out.find((c) => c.name === "network")).toBe(components[1]);
60
+ });
61
+
62
+ test("a place the declaration already fills is left as it is, and adds no dependsOn", () => {
63
+ const declared = stack("app", { imports: { "network-VpcId": "vpc-pinned" } });
64
+ const [out] = applyDeclaredReads([declared], [{ reader: "app", upstream: "network", output: "VpcId", at: "/imports/network-VpcId", index: 0, kind: "fake-stack" }]);
65
+ expect(out!.dependsOn).toEqual([]);
66
+ expect(capabilitySteps(out!.deploy)[0]).toMatchObject({ imports: { "network-VpcId": "vpc-pinned" } });
67
+ });
68
+
69
+ test("a dependsOn already declared is not repeated", () => {
70
+ const declared = stack("app", {}, ["network"]);
71
+ const [out] = applyDeclaredReads([declared], [{ reader: "app", upstream: "network", output: "VpcId", at: "/imports/a", index: 0, kind: "fake-stack" }]);
72
+ expect(out!.dependsOn).toEqual(["network"]);
73
+ });
74
+
75
+ test("the read goes into the step of the reported kind and index, and an escaped pointer unescapes", () => {
76
+ const declared: DriverComponent = {
77
+ name: "app",
78
+ deploy: [
79
+ { phase: "Build", steps: [{ kind: "fake-plain", note: "x" }] },
80
+ { phase: "Apply", steps: [{ kind: "fake-stack", stack: "one" }, { kind: "fake-stack", stack: "two" }] },
81
+ ],
82
+ };
83
+ const [out] = applyDeclaredReads([declared], [{ reader: "app", upstream: "network", output: "Arn", at: "/imports/a~1b~0c", index: 1, kind: "fake-stack" }]);
84
+ const steps = capabilitySteps(out!.deploy);
85
+ expect(steps[0]).toEqual({ kind: "fake-plain", note: "x" });
86
+ expect(steps[1]).toEqual({ kind: "fake-stack", stack: "one" });
87
+ expect(steps[2]).toEqual({ kind: "fake-stack", stack: "two", imports: { "a/b~c": { stackOutput: { stack: "network", name: "Arn" } } } });
88
+ });
89
+
90
+ test("a read of itself, or of a component the project does not have, is dropped; a capability that throws reports none", async () => {
91
+ const components = [stack("app"), stack("network")];
92
+ const { registry: r } = registry(() => [
93
+ { reader: "app", upstream: "app", output: "X", at: "/imports/x" },
94
+ { reader: "app", upstream: "elsewhere", output: "X", at: "/imports/y" },
95
+ ]);
96
+ expect(await declaredReadsOf(components, r, "/project")).toEqual([]);
97
+ const { registry: throwing } = registry(() => {
98
+ throw new Error("unreadable");
99
+ });
100
+ expect(await declaredReadsOf(components, throwing, "/project")).toEqual([]);
101
+ expect(await withDeclaredReads(components, throwing, "/project")).toEqual(components);
102
+ });
103
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Reads a capability finds in the files its steps name (#4466).
3
+ *
4
+ * A read core can see is a `stackOutput()` or `@<component>.publish.*`
5
+ * reference in a step. A CloudFormation template that imports another
6
+ * stack's export reads that stack too, but the read sits inside the
7
+ * template. A capability that can find such reads says so through
8
+ * `Capability.declaredReads`, and {@link withDeclaredReads} writes each one
9
+ * into the reader's step as the `stackOutput()` reference a person would have
10
+ * written, plus a `dependsOn` on the upstream. From there on it is an ordinary
11
+ * read: it orders the fan-out, it is named under `unknownInputs` while the
12
+ * upstream's plan cannot say the value, and a reader whose plan read another
13
+ * value than the upstream applied is held for a round.
14
+ */
15
+
16
+ import type { CapabilityRegistry, DeclaredRead, DeclaredReadMember } from "./capability";
17
+ import type { DriverComponent, DriverPhase, DriverStep } from "./driver";
18
+ import { capabilitySteps } from "./wave-plan";
19
+
20
+ /** A declared read with the kind of the step it names. */
21
+ export type PlacedRead = DeclaredRead & { index: number; kind: string };
22
+
23
+ /** `pointer`'s segments, unescaped (RFC 6901). */
24
+ function segments(pointer: string): string[] {
25
+ if (pointer === "") return [];
26
+ return pointer
27
+ .split("/")
28
+ .slice(1)
29
+ .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
30
+ }
31
+
32
+ /** `value` with `ref` placed at `path`, copying each object on the way; `undefined` when something already sits there. */
33
+ function placed(value: Record<string, unknown>, path: readonly string[], ref: unknown): Record<string, unknown> | undefined {
34
+ if (path.length === 0) return undefined;
35
+ const [head, ...rest] = path as [string, ...string[]];
36
+ const here = value[head];
37
+ if (rest.length === 0) return here === undefined ? { ...value, [head]: ref } : undefined;
38
+ if (here !== undefined && (here === null || typeof here !== "object" || Array.isArray(here))) return undefined;
39
+ const inner = placed((here as Record<string, unknown> | undefined) ?? {}, rest, ref);
40
+ return inner ? { ...value, [head]: inner } : undefined;
41
+ }
42
+
43
+ /** `phases` with each step `replace` gives a new value for swapped, in place, nested phases included. */
44
+ function mapSteps(phases: readonly DriverPhase[], replace: (step: DriverStep) => DriverStep): DriverPhase[] {
45
+ const walk = (steps: DriverPhase["steps"]): DriverPhase["steps"] =>
46
+ steps.map((step) => {
47
+ if (typeof (step as DriverPhase).phase === "string" && Array.isArray((step as DriverPhase).steps)) {
48
+ return { ...(step as DriverPhase), steps: walk((step as DriverPhase).steps) };
49
+ }
50
+ if ((step as { kind?: unknown }).kind === "gate") return step;
51
+ return replace(step as DriverStep);
52
+ }) as DriverPhase["steps"];
53
+ return phases.map((phase) => ({ ...phase, steps: walk(phase.steps) }));
54
+ }
55
+
56
+ /**
57
+ * Each capability's declared reads among `components` (#4466): every read a
58
+ * capability with `declaredReads` reports between two components of the
59
+ * project, its own reads of itself left out. A capability that throws
60
+ * reports none.
61
+ */
62
+ export async function declaredReadsOf(
63
+ components: readonly DriverComponent[],
64
+ registry: CapabilityRegistry,
65
+ root: string,
66
+ ): Promise<PlacedRead[]> {
67
+ const names = new Set(components.map((c) => c.name));
68
+ const byKind = new Map<string, DeclaredReadMember[]>();
69
+ for (const component of components) {
70
+ const seen = new Map<string, number>();
71
+ for (const step of capabilitySteps(component.deploy)) {
72
+ if (!registry.has(step.kind)) continue;
73
+ const index = seen.get(step.kind) ?? 0;
74
+ seen.set(step.kind, index + 1);
75
+ const { kind, ...rest } = step;
76
+ (byKind.get(kind) ?? byKind.set(kind, []).get(kind)!).push({ component: component.name, index, step: rest });
77
+ }
78
+ }
79
+ const out: PlacedRead[] = [];
80
+ for (const [kind, members] of byKind) {
81
+ const capability = registry.resolve(kind);
82
+ if (typeof capability.declaredReads !== "function") continue;
83
+ let reported: DeclaredRead[];
84
+ try {
85
+ reported = await capability.declaredReads({ root }, members);
86
+ } catch {
87
+ continue;
88
+ }
89
+ for (const r of reported ?? []) {
90
+ if (r.reader === r.upstream || !names.has(r.reader) || !names.has(r.upstream)) continue;
91
+ out.push({ ...r, index: r.index ?? 0, kind });
92
+ }
93
+ }
94
+ return out.sort((a, b) => a.reader.localeCompare(b.reader) || a.index - b.index || a.at.localeCompare(b.at));
95
+ }
96
+
97
+ /**
98
+ * `components` with each declared read written in (#4466): the reader's
99
+ * step gets `{ stackOutput: { stack: upstream, name: output } }` at the
100
+ * read's place, unless something sits there already, and the reader depends
101
+ * on the upstream. Components no read names come back as they were.
102
+ */
103
+ export function applyDeclaredReads(components: readonly DriverComponent[], reads: readonly PlacedRead[]): DriverComponent[] {
104
+ const byReader = new Map<string, PlacedRead[]>();
105
+ for (const r of reads) (byReader.get(r.reader) ?? byReader.set(r.reader, []).get(r.reader)!).push(r);
106
+ return components.map((component) => {
107
+ const mine = byReader.get(component.name);
108
+ if (!mine) return component;
109
+ const kinds = new Map<string, number>();
110
+ const added = new Set<string>();
111
+ const deploy = mapSteps(component.deploy, (step) => {
112
+ const index = kinds.get(step.kind) ?? 0;
113
+ kinds.set(step.kind, index + 1);
114
+ let next: DriverStep = step;
115
+ for (const r of mine.filter((x) => x.kind === step.kind && x.index === index)) {
116
+ const { kind, ...rest } = next;
117
+ const with_ = placed(rest, segments(r.at), { stackOutput: { stack: r.upstream, name: r.output } });
118
+ if (!with_) continue;
119
+ next = { kind, ...with_ } as DriverStep;
120
+ added.add(r.upstream);
121
+ }
122
+ return next;
123
+ });
124
+ const dependsOn = [...(component.dependsOn ?? [])];
125
+ for (const up of [...added].sort()) if (!dependsOn.includes(up)) dependsOn.push(up);
126
+ return { ...component, dependsOn, deploy };
127
+ });
128
+ }
129
+
130
+ /** {@link declaredReadsOf} and {@link applyDeclaredReads} in one: `components` with their declared reads written in. */
131
+ export async function withDeclaredReads(components: readonly DriverComponent[], registry: CapabilityRegistry, root: string): Promise<DriverComponent[]> {
132
+ const reads = await declaredReadsOf(components, registry, root);
133
+ return reads.length > 0 ? applyDeclaredReads(components, reads) : [...components];
134
+ }
@@ -64,6 +64,10 @@ export const DEPLOY_UNIT_RULES: readonly DeployUnitRule[] = [
64
64
  // A Config Connector manifest applied over REST (#4273). Its unit is the
65
65
  // step's `stack`, the member its plan names.
66
66
  { kind: "gcp-apply", field: "stack", lexicon: "gcp" },
67
+ // Another workspace member's outputs (#4455). Its unit is the member, so a
68
+ // change signal naming the member puts the component in the change. No
69
+ // lexicon observes it: the member's own pipeline reports its status.
70
+ { kind: "member-outputs", field: "member", lexicon: "workspace" },
67
71
  ];
68
72
 
69
73
  /** One resolved unit a component's deploy composition targets. */
@@ -54,7 +54,8 @@ import { GateNotRecordableError } from "../lifecycle/no-record";
54
54
  import { componentPlanDigest } from "./gate-plan";
55
55
  import { componentDeployInputs } from "./deploy-inputs";
56
56
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
57
- import type { CapabilityRegistry, DeployContext } from "./capability";
57
+ import type { CapabilityRegistry, DeployContext, GroupMemberStatus } from "./capability";
58
+ import type { FanOutGroup } from "./fan-out";
58
59
  import { isPlanMovedError, PLAN_MOVED_EXIT_CODE, TargetsRefusedError, targetRefusal } from "./capability";
59
60
  import { isPromotedArtifact } from "./auto-release";
60
61
  import type { ReleaseIdentity } from "../telemetry-attribution";
@@ -222,6 +223,8 @@ export interface DriverRunResult {
222
223
  * component that ran in an earlier job.
223
224
  */
224
225
  componentOutputs: Record<string, Record<string, unknown>>;
226
+ /** How each member of a group that ran ended, as its capability said (ws-107, #4465), by component. Absent when no group ran. */
227
+ groupOutcomes?: Record<string, GroupMemberStatus>;
225
228
  }
226
229
 
227
230
  // ── Graph: order + parallel-safe waves from `dependsOn` ──────────────────────
@@ -391,6 +394,18 @@ export function unresolvedWiringReferences(
391
394
  return wiringReferences(input).filter((r) => r.value(componentOutputs) === undefined).map(({ source, ref, publish }) => ({ source, ref, publish }));
392
395
  }
393
396
 
397
+ /**
398
+ * Every cross-component reference in `input`, sorted by `ref` and each once,
399
+ * with whether `componentOutputs` holds a value for it. A pull request's
400
+ * report records each as a read between components, known at plan or not.
401
+ */
402
+ export function wiringReads(
403
+ input: Record<string, unknown>,
404
+ componentOutputs: Record<string, Record<string, unknown>>,
405
+ ): Array<WiringReference & { known: boolean }> {
406
+ return wiringReferences(input).map(({ source, ref, publish, value }) => ({ source, ref, publish, known: value(componentOutputs) !== undefined }));
407
+ }
408
+
394
409
  /**
395
410
  * Every cross-component reference in `input`, each once, sorted by `ref`,
396
411
  * with the value it resolves to against a given outputs map.
@@ -424,6 +439,42 @@ function wiringReferences(input: Record<string, unknown>): Array<WiringReference
424
439
  return [...found.values()].sort((a, b) => a.ref.localeCompare(b.ref));
425
440
  }
426
441
 
442
+ /**
443
+ * Every cross-component reference in `input` with where it sits, as a JSON
444
+ * Pointer into `input` (ws-107), and the output it names (`publish.<field>`
445
+ * for an artifact). A reference made twice is listed at each place. Sorted
446
+ * by `ref`, then by `at`.
447
+ */
448
+ export function wiringReferencesAt(input: Record<string, unknown>): Array<WiringReference & { at: string; output: string }> {
449
+ const found: Array<WiringReference & { at: string; output: string }> = [];
450
+ const pointer = (path: readonly string[]): string => path.map((p) => "/" + p.replace(/~/g, "~0").replace(/\//g, "~1")).join("");
451
+ const walk = (value: unknown, path: string[]): void => {
452
+ if (typeof value === "string") {
453
+ if (PRIOR_STEP_REF.test(value)) return;
454
+ const ref = value.match(COMPONENT_ARTIFACT_REF);
455
+ if (ref) {
456
+ const [, source, field] = ref;
457
+ found.push({ source, ref: `${source}.publish.${field}`, publish: true, at: pointer(path), output: `publish.${field}` });
458
+ }
459
+ return;
460
+ }
461
+ if (Array.isArray(value)) {
462
+ value.forEach((v, i) => walk(v, [...path, String(i)]));
463
+ return;
464
+ }
465
+ if (value && typeof value === "object") {
466
+ if ("stackOutput" in value) {
467
+ const { stack, name } = (value as { stackOutput: { stack: string; name: string } }).stackOutput;
468
+ found.push({ source: stack, ref: `${stack}.${name}`, publish: name === "publish" || name.startsWith("publish."), at: pointer(path), output: name });
469
+ return;
470
+ }
471
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) walk(v, [...path, k]);
472
+ }
473
+ };
474
+ walk(input, []);
475
+ return found.sort((a, b) => a.ref.localeCompare(b.ref) || a.at.localeCompare(b.at));
476
+ }
477
+
427
478
  /**
428
479
  * The cross-component references in `input` that resolve to another value
429
480
  * against `now` than against `read`, sorted (#3999). A pull request's apply
@@ -1136,6 +1187,27 @@ export interface InterpretRunOptions {
1136
1187
  releaseIdentity?: (component: string) => ReleaseIdentity | undefined;
1137
1188
  /** The addresses each component narrows to (#4300), by component: its `DeployContext.targets`. */
1138
1189
  targets?: Readonly<Record<string, readonly string[]>>;
1190
+ /** Groups applied as one (ws-107, #4465). See {@link InterpretGroups}. Absent: every component runs its own deploy. */
1191
+ groups?: InterpretGroups;
1192
+ }
1193
+
1194
+ /**
1195
+ * Groups a run applies as one (ws-107, #4465): a promote under a release and
1196
+ * `run --components` hand them over, already placed by `groupFanOut`. Each
1197
+ * group runs once, through `run`, in the wave its members sit in and in place
1198
+ * of their own deploys. A member that does not end `ok` stops the run after
1199
+ * its wave, as a failed component does.
1200
+ */
1201
+ export interface InterpretGroups {
1202
+ /** The grouped waves, which replace the ones `dependsOn` gives. Every component of the run is in one; an empty wave is passed over. */
1203
+ waves: string[][];
1204
+ /** Each group's members are components of the run, in one wave. */
1205
+ groups: readonly FanOutGroup[];
1206
+ /** Apply one group with the outputs standing, adding each applied member's own: one result per member, and how each ended. */
1207
+ run: (
1208
+ group: FanOutGroup,
1209
+ componentOutputs: Record<string, Record<string, unknown>>,
1210
+ ) => Promise<{ results: DriverComponentResult[]; outcomes: Record<string, GroupMemberStatus> }>;
1139
1211
  }
1140
1212
 
1141
1213
  /**
@@ -1207,8 +1279,20 @@ export async function runInterpretDriver(
1207
1279
  registry: CapabilityRegistry,
1208
1280
  options: InterpretRunOptions,
1209
1281
  ): Promise<DriverRunResult> {
1210
- const { order, waves } = resolveComponentGraph(components);
1282
+ const graph = resolveComponentGraph(components);
1211
1283
  const byName = new Map(components.map((c) => [c.name, c]));
1284
+ // ws-107 (#4465): the grouped waves, and each member's group.
1285
+ const waves = options.groups ? options.groups.waves.map((w) => w.filter((name) => byName.has(name))).filter((w) => w.length > 0) : graph.waves;
1286
+ const order = options.groups ? waves.flat() : graph.order;
1287
+ const placed = new Set(order);
1288
+ const unplaced = components.filter((c) => !placed.has(c.name)).map((c) => c.name);
1289
+ if (unplaced.length > 0) throw new Error(`the grouped waves leave out ${unplaced.join(", ")}, which this run runs`);
1290
+ // A group keeps its members in this run; one left with fewer than two is no group, and its member runs its own deploy.
1291
+ const runGroups = (options.groups?.groups ?? [])
1292
+ .map((g) => ({ ...g, components: g.components.filter((c) => byName.has(c)) }))
1293
+ .filter((g) => g.components.length > 1);
1294
+ const groupOf = new Map(runGroups.flatMap((g) => g.components.map((c) => [c, g] as const)));
1295
+ const groupOutcomes: Record<string, GroupMemberStatus> = {};
1212
1296
  const componentOutputs: Record<string, Record<string, unknown>> = { ...(options.componentOutputs ?? {}) };
1213
1297
  const emit = joinRunEventSinks(options.onRunEvent, progressSinkOf(options.onProgress));
1214
1298
  const gates: GateContext = {
@@ -1227,8 +1311,19 @@ export async function runInterpretDriver(
1227
1311
  waveLoop: for (const [waveIndex, wave] of waves.entries()) {
1228
1312
  const waveNum = waveIndex + 1;
1229
1313
  emit?.({ type: "wave-start", wave: waveNum, components: wave });
1230
- const waveComponents = wave.map((name) => byName.get(name)!);
1231
- const waveResults = await Promise.all(
1314
+ const waveComponents = wave.filter((name) => !groupOf.has(name)).map((name) => byName.get(name)!);
1315
+ const groupsHere = [...new Set(wave.map((name) => groupOf.get(name)).filter((g): g is FanOutGroup => g !== undefined))];
1316
+ // Groups and the wave's other components run side by side, as a wave's components do.
1317
+ const groupResults = Promise.all(
1318
+ groupsHere.map(async (group) => {
1319
+ for (const name of group.components) emit?.({ type: "component-start", wave: waveNum, component: name });
1320
+ const ran = await options.groups!.run(group, componentOutputs);
1321
+ Object.assign(groupOutcomes, ran.outcomes);
1322
+ for (const result of ran.results) emit?.({ type: "component-settle", wave: waveNum, component: result.component, status: progressStatus(result.status) });
1323
+ return ran.results;
1324
+ }),
1325
+ );
1326
+ const ownResults = await Promise.all(
1232
1327
  waveComponents.map(async (component) => {
1233
1328
  emit?.({ type: "component-start", wave: waveNum, component: component.name });
1234
1329
  const result = await runComponentDeploy(
@@ -1244,6 +1339,7 @@ export async function runInterpretDriver(
1244
1339
  return result;
1245
1340
  }),
1246
1341
  );
1342
+ const waveResults = [...ownResults, ...(await groupResults).flat()];
1247
1343
  results.push(...waveResults);
1248
1344
  const stopped = waveResults.find((r) => r.status !== "ok");
1249
1345
  emit?.({ type: "wave-settle", wave: waveNum, status: progressStatus(stopped?.status ?? "ok") });
@@ -1262,6 +1358,7 @@ export async function runInterpretDriver(
1262
1358
  emit?.({ type: "run-settle", status: progressStatus(status), durationMs: null });
1263
1359
  const result: DriverRunResult = {
1264
1360
  order, waves, results, ok, status, failedComponent, componentOutputs,
1361
+ ...(Object.keys(groupOutcomes).length > 0 ? { groupOutcomes } : {}),
1265
1362
  ...(gated
1266
1363
  ? {
1267
1364
  gatedComponent: gated.component,
@@ -46,13 +46,14 @@ import {
46
46
  type GateContext,
47
47
  } from "./driver";
48
48
  import type { ReleaseIdentity } from "../telemetry-attribution";
49
- import { downstreamWithin, remainingFanOut, type FanOutPlan, type FanOutProgress, type FanOutSkip } from "./fan-out";
49
+ import { downstreamWithin, remainingFanOut, type FanOutGroup, type FanOutPlan, type FanOutProgress, type FanOutSkip } from "./fan-out";
50
50
  import { evaluateGate, gitGateLedgerPort, type GateCheck, type GateLedgerPort } from "../op/gate";
51
51
  import type { RunProgressEvent } from "./run-progress";
52
52
  import { emitGateStep, joinRunEventSinks, progressSinkOf, type RunEventSink } from "../run-event-stream";
53
- import type { CapabilityRegistry } from "./capability";
53
+ import type { CapabilityRegistry, GroupMemberStatus, GroupRun } from "./capability";
54
54
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
55
55
  import type { DeployContext } from "./capability";
56
+ import { groupsByComponent, narrowGroups, planAndRunComponentGroup, runComponentGroup } from "./groups";
56
57
  import { planWaveComponent, type WaveComponentPlan } from "./wave-plan";
57
58
  import { computePlanDigest } from "../lifecycle/plan-digest";
58
59
  import { decideWaveGate, runWaveLoop, waveGateName, waveSetDigest, type WaveMember, type WaveRecord } from "../wave-engine";
@@ -247,6 +248,20 @@ export interface FanOutRunOptions {
247
248
  * before it applied.
248
249
  */
249
250
  hold?: (component: string, componentOutputs: Record<string, Record<string, unknown>>) => string | undefined;
251
+ /**
252
+ * The approved plan of each group in `plan.groups` (ws-107), by the
253
+ * group's first component. A group runs through `runGroup` in its wave,
254
+ * in place of its members' own runs, and is held and blocked as one: a
255
+ * member held or blocked holds or blocks the rest. Not with `waveGate`.
256
+ */
257
+ groupRuns?: Record<string, GroupRun>;
258
+ /**
259
+ * A group with no entry in `groupRuns` plans in its wave against the
260
+ * outputs standing, then applies (#4465): `components fan-out` plans
261
+ * nothing before it runs. Without it such a group fails, which is what a
262
+ * pull request's apply wants for a group nobody approved.
263
+ */
264
+ planGroupsAtWave?: boolean;
250
265
  }
251
266
 
252
267
  /** A component {@link FanOutRunOptions.hold} held back, or one downstream of it. */
@@ -276,6 +291,14 @@ export interface FanOutRunResult {
276
291
  waves?: WaveRecord[];
277
292
  /** What {@link FanOutRunOptions.hold} held back, in plan order. Absent when nothing was. */
278
293
  held?: FanOutHeld[];
294
+ /**
295
+ * Members of a group whose apply stopped before their part began (ws-107),
296
+ * sorted. Each has a `fail` result but is not in `failed`; what depends on
297
+ * it outside the group is blocked by it. Absent when none.
298
+ */
299
+ notStarted?: string[];
300
+ /** How each member of a group that ran ended, as its capability said (#4446), by component. Absent when no group ran. */
301
+ groupOutcomes?: Record<string, GroupMemberStatus>;
279
302
  }
280
303
 
281
304
  /**
@@ -299,7 +322,8 @@ export async function runFanOut(
299
322
 
300
323
  // Resume first, so the gate is decided against the work that is actually left
301
324
  // and a re-run does not re-approve. The digest is carried, not recomputed.
302
- const active = options.progress ? remainingFanOut(plan, components, options.progress) : plan;
325
+ // A group keeps what is left of it (#4465).
326
+ const active = options.progress ? narrowGroups(remainingFanOut(plan, components, options.progress), components, plan.groups) : plan;
303
327
 
304
328
  if (options.gate && options.waveGate) {
305
329
  throw new Error("a fan-out takes one gate over the set or one gate per wave, not both");
@@ -307,6 +331,9 @@ export async function runFanOut(
307
331
  if (options.waveGate && options.plans) {
308
332
  throw new Error("a gated-wave fan-out plans each wave itself, so it takes no plans made beforehand");
309
333
  }
334
+ if (options.waveGate && plan.groups?.length) {
335
+ throw new Error("a gated-wave fan-out plans component by component, so it takes no groups");
336
+ }
310
337
  if (options.waveGate) {
311
338
  return runGatedWaves(plan, active, components, registry, options, options.waveGate, gates, componentOutputs);
312
339
  }
@@ -348,44 +375,80 @@ export async function runFanOut(
348
375
  const results: DriverComponentResult[] = [];
349
376
  const completed: string[] = [];
350
377
  const failed: string[] = [];
378
+ const notStarted: string[] = [];
379
+ const groupOutcomes: Record<string, GroupMemberStatus> = {};
351
380
  const blockedBy = new Map<string, string>();
352
381
  const held = new Map<string, FanOutHeld>();
353
382
  let gatedFact: PendingGateRecord | undefined;
354
383
 
355
384
  const inPlan = new Set(active.order);
356
385
  const waveOf = waveNumbers(active.waves);
357
- /** Hold `root` and everything downstream of it that has not settled otherwise. */
358
- const markHeld = (root: string, reason: string): void => {
359
- held.set(root, { component: root, reason });
386
+ // ws-107: a group applies as one unit, so it is held and blocked as one.
387
+ const groupOf = groupsByComponent(active);
388
+ /** Hold `root` and everything downstream of it that has not settled otherwise. With `heldBy`, `root` is held for that component's reason. */
389
+ const markHeld = (root: string, reason: string, heldBy?: string): void => {
390
+ held.set(root, { component: root, reason, ...(heldBy ? { heldBy } : {}) });
360
391
  emit?.({ type: "component-settle", wave: waveOf.get(root)!, component: root, status: "waiting" });
361
392
  for (const [component] of downstreamWithin(components, inPlan, [root])) {
362
393
  if (held.has(component) || blockedBy.has(component) || failed.includes(component)) continue;
363
- held.set(component, { component, reason, heldBy: root });
394
+ held.set(component, { component, reason, heldBy: heldBy ?? root });
364
395
  emit?.({ type: "component-settle", wave: waveOf.get(component)!, component, status: "waiting" });
365
396
  }
366
397
  };
367
- /** Skip everything downstream of `root`, blaming `root` rather than the nearest edge. */
368
- const markBlocked = (root: string): void => {
369
- for (const [component, by] of downstreamWithin(components, inPlan, [root])) {
370
- if (blockedBy.has(component) || failed.includes(component)) continue;
398
+ /** Skip everything downstream of `roots`, blaming a root rather than the nearest edge. */
399
+ const markBlocked = (...roots: string[]): void => {
400
+ for (const [component, by] of downstreamWithin(components, inPlan, roots)) {
401
+ if (blockedBy.has(component) || failed.includes(component) || completed.includes(component) || notStarted.includes(component)) continue;
371
402
  blockedBy.set(component, by);
372
403
  }
373
404
  };
374
405
 
375
406
  for (const [waveIndex, wave] of active.waves.entries()) {
376
407
  const waveNum = waveIndex + 1;
408
+ // A member blocked blocks its whole group, by the same failure.
409
+ for (const name of wave) {
410
+ const by = blockedBy.get(name);
411
+ if (by === undefined) continue;
412
+ for (const mate of groupOf.get(name)?.components ?? []) if (!blockedBy.has(mate) && mate !== by) blockedBy.set(mate, by);
413
+ }
377
414
  // A component whose upstream failed in an earlier wave is not dispatched.
378
415
  const candidates = wave.filter((name) => inPlan.has(name) && !blockedBy.has(name) && !held.has(name));
379
416
  for (const name of candidates) {
417
+ if (held.has(name)) continue;
380
418
  const reason = options.hold?.(name, componentOutputs);
381
- if (reason !== undefined) markHeld(name, reason);
419
+ if (reason === undefined) continue;
420
+ markHeld(name, reason);
421
+ for (const mate of groupOf.get(name)?.components ?? []) if (!held.has(mate)) markHeld(mate, reason, name);
382
422
  }
383
423
  const runnable = candidates.filter((name) => !held.has(name));
384
424
  if (runnable.length === 0) continue;
385
425
 
386
426
  emit?.({ type: "wave-start", wave: waveNum, components: runnable });
427
+ const groupsHere = [...new Set(runnable.map((name) => groupOf.get(name)).filter((g): g is FanOutGroup => g !== undefined))];
428
+ // Groups and the wave's other components run side by side, as a wave's components do.
429
+ const groupRuns = Promise.all(
430
+ groupsHere.map(async (group) => {
431
+ for (const name of group.components) emit?.({ type: "component-start", wave: waveNum, component: name });
432
+ const approved = options.groupRuns?.[group.components[0]!];
433
+ const each = {
434
+ group,
435
+ components: byName,
436
+ registry,
437
+ env: options.env,
438
+ ...(options.vars ? { vars: options.vars } : {}),
439
+ ctxOf: (name: string) => fanOutContext(options, name),
440
+ componentOutputs,
441
+ };
442
+ const ran = approved === undefined && options.planGroupsAtWave ? await planAndRunComponentGroup(each) : await runComponentGroup({ ...each, run: approved });
443
+ for (const result of ran.results) {
444
+ emit?.({ type: "component-settle", wave: waveNum, component: result.component, status: result.status === "fail" ? "failed" : result.status });
445
+ options.onComponentSettled?.(result, componentOutputs[result.component]);
446
+ }
447
+ return ran;
448
+ }),
449
+ );
387
450
  const waveResults = await Promise.all(
388
- runnable.map(async (name) => {
451
+ runnable.filter((name) => !groupOf.has(name)).map(async (name) => {
389
452
  emit?.({ type: "component-start", wave: waveNum, component: name });
390
453
  const planned = options.plans?.[name];
391
454
  const result = await runComponentDeploy(
@@ -407,6 +470,7 @@ export async function runFanOut(
407
470
  return result;
408
471
  }),
409
472
  );
473
+ const groupResults = await groupRuns;
410
474
  results.push(...waveResults);
411
475
 
412
476
  for (const result of waveResults) {
@@ -425,8 +489,22 @@ export async function runFanOut(
425
489
  gatedFact ??= result.gate;
426
490
  markBlocked(result.component);
427
491
  }
492
+ // A group's members settle together; what depends on a member that failed
493
+ // or never started, outside the group, is blocked by that member.
494
+ for (const ran of groupResults) {
495
+ results.push(...ran.results);
496
+ Object.assign(groupOutcomes, ran.outcomes);
497
+ const stopped: string[] = [];
498
+ for (const result of ran.results) {
499
+ if (result.status === "ok") completed.push(result.component);
500
+ else if (ran.notStarted.includes(result.component)) notStarted.push(result.component);
501
+ else failed.push(result.component);
502
+ if (result.status !== "ok") stopped.push(result.component);
503
+ }
504
+ if (stopped.length > 0) markBlocked(...stopped);
505
+ }
428
506
 
429
- const waveFailed = waveResults.some((r) => r.status === "fail");
507
+ const waveFailed = waveResults.some((r) => r.status === "fail") || groupResults.some((ran) => ran.results.some((r) => r.status === "fail"));
430
508
  emit?.({
431
509
  type: "wave-settle",
432
510
  wave: waveNum,
@@ -442,7 +520,7 @@ export async function runFanOut(
442
520
 
443
521
  // A failure outranks a gate: something is actually broken, and reporting the
444
522
  // run as "waiting on a human" would hide it. Same precedence the driver uses.
445
- const status: FanOutRunResult["status"] = failed.length > 0 ? "fail" : gatedFact ? "gated" : "ok";
523
+ const status: FanOutRunResult["status"] = failed.length > 0 || notStarted.length > 0 ? "fail" : gatedFact ? "gated" : "ok";
446
524
  emit?.({ type: "run-settle", status: status === "fail" ? "failed" : status, durationMs: null });
447
525
 
448
526
  return {
@@ -455,6 +533,8 @@ export async function runFanOut(
455
533
  componentOutputs,
456
534
  ...(gatedFact ? { gate: gatedFact } : {}),
457
535
  ...(held.size > 0 ? { held: active.order.filter((name) => held.has(name)).map((name) => held.get(name)!) } : {}),
536
+ ...(notStarted.length > 0 ? { notStarted: [...notStarted].sort() } : {}),
537
+ ...(Object.keys(groupOutcomes).length > 0 ? { groupOutcomes } : {}),
458
538
  };
459
539
  }
460
540
 
@@ -19,6 +19,7 @@ import { applyConfigDefaults } from "./config-defaults";
19
19
  import { buildCapabilityRegistry } from "./capability-plugin-loader";
20
20
  import { componentsForUnits, planFanOut, type ChangedUnits, type ComponentChangeSignal, type FanOutPlan } from "./fan-out";
21
21
  import type { CapabilityRegistry } from "./capability";
22
+ import { withDeclaredReads } from "./declared-reads";
22
23
  import type { DriverComponent } from "./driver";
23
24
  import type { ComponentEnvironmentDeclaration } from "./component";
24
25
  import type { BuildParamProvenance } from "../provenance";
@@ -36,6 +37,12 @@ export interface DeriveFanOutOptions {
36
37
  config?: ChantConfig;
37
38
  /** Components that form wave 1 (#3049). See `FanOutRequest.canary`. */
38
39
  canary?: string[];
40
+ /**
41
+ * The registry whose capabilities' `declaredReads` (#4466) are written into
42
+ * the components before the order is derived. Absent, the project's own
43
+ * (`fanOutRegistry`) is built for it.
44
+ */
45
+ registry?: CapabilityRegistry;
39
46
  }
40
47
 
41
48
  export interface DerivedFanOut {
@@ -78,7 +85,9 @@ export async function deriveFanOut(options: DeriveFanOutOptions): Promise<Derive
78
85
  }
79
86
 
80
87
  const { config } = options.config ? { config: options.config } : await loadChantConfig(options.path);
81
- const components = resolved.targets.map((component) => applyConfigDefaults(component, config));
88
+ const defaulted = resolved.targets.map((component) => applyConfigDefaults(component, config));
89
+ // #4466: the reads a capability finds in its steps' files, such as a template's Fn::ImportValue, order and hold like declared ones.
90
+ const components = await withDeclaredReads(defaulted, options.registry ?? (await fanOutRegistry(options.path, config)), options.path);
82
91
 
83
92
  const signal = componentsForUnits(components, options.units);
84
93
  const plan = planFanOut({