@intentius/chant 0.124.52 → 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 (584) 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 +15 -2
  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 +215 -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/declared-reads.d.ts +38 -0
  120. package/dist/components/declared-reads.d.ts.map +1 -0
  121. package/dist/components/declared-reads.js +131 -0
  122. package/dist/components/deploy-units.d.ts.map +1 -1
  123. package/dist/components/deploy-units.js +4 -0
  124. package/dist/components/driver.d.ts +34 -1
  125. package/dist/components/driver.d.ts.map +1 -1
  126. package/dist/components/driver.js +66 -3
  127. package/dist/components/fan-out-run.d.ts +23 -1
  128. package/dist/components/fan-out-run.d.ts.map +1 -1
  129. package/dist/components/fan-out-run.js +82 -14
  130. package/dist/components/fan-out-support.d.ts +6 -0
  131. package/dist/components/fan-out-support.d.ts.map +1 -1
  132. package/dist/components/fan-out-support.js +4 -1
  133. package/dist/components/fan-out.d.ts +28 -0
  134. package/dist/components/fan-out.d.ts.map +1 -1
  135. package/dist/components/groups.d.ts +229 -0
  136. package/dist/components/groups.d.ts.map +1 -0
  137. package/dist/components/groups.js +760 -0
  138. package/dist/components/index.d.ts +6 -2
  139. package/dist/components/index.d.ts.map +1 -1
  140. package/dist/components/index.js +4 -0
  141. package/dist/components/plan-shape-conformance.d.ts +48 -0
  142. package/dist/components/plan-shape-conformance.d.ts.map +1 -0
  143. package/dist/components/plan-shape-conformance.js +122 -0
  144. package/dist/components/pr-run.d.ts +41 -4
  145. package/dist/components/pr-run.d.ts.map +1 -1
  146. package/dist/components/pr-run.js +209 -39
  147. package/dist/components/promote-rounds.d.ts +9 -0
  148. package/dist/components/promote-rounds.d.ts.map +1 -1
  149. package/dist/components/promote-rounds.js +23 -2
  150. package/dist/components/promote.d.ts +17 -0
  151. package/dist/components/promote.d.ts.map +1 -1
  152. package/dist/components/promote.js +36 -6
  153. package/dist/components/release-gate.d.ts +23 -1
  154. package/dist/components/release-gate.d.ts.map +1 -1
  155. package/dist/components/release-gate.js +97 -33
  156. package/dist/components/run-groups.d.ts +58 -0
  157. package/dist/components/run-groups.d.ts.map +1 -0
  158. package/dist/components/run-groups.js +78 -0
  159. package/dist/components/starter-plugin.d.ts +1 -0
  160. package/dist/components/starter-plugin.d.ts.map +1 -1
  161. package/dist/components/starter-plugin.js +6 -1
  162. package/dist/components/verbs/index.d.ts +1 -0
  163. package/dist/components/verbs/index.d.ts.map +1 -1
  164. package/dist/components/verbs/index.js +1 -0
  165. package/dist/components/verbs/member-outputs.d.ts +187 -0
  166. package/dist/components/verbs/member-outputs.d.ts.map +1 -0
  167. package/dist/components/verbs/member-outputs.js +218 -0
  168. package/dist/config.d.ts +60 -0
  169. package/dist/config.d.ts.map +1 -1
  170. package/dist/config.js +27 -0
  171. package/dist/deep-observation.d.ts +1 -13
  172. package/dist/deep-observation.d.ts.map +1 -1
  173. package/dist/deep-observation.js +5 -22
  174. package/dist/drift-record.d.ts +542 -0
  175. package/dist/drift-record.d.ts.map +1 -0
  176. package/dist/drift-record.js +886 -0
  177. package/dist/engine-facts.d.ts +144 -0
  178. package/dist/engine-facts.d.ts.map +1 -0
  179. package/dist/engine-facts.js +14 -0
  180. package/dist/graph-ir.d.ts +153 -2
  181. package/dist/graph-ir.d.ts.map +1 -1
  182. package/dist/graph-ir.js +224 -6
  183. package/dist/graph-refs.d.ts +145 -0
  184. package/dist/graph-refs.d.ts.map +1 -1
  185. package/dist/graph-refs.js +305 -0
  186. package/dist/graph-schema/fixtures/adopt.json +15 -0
  187. package/dist/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  188. package/dist/graph-schema/fixtures/index.json +68 -0
  189. package/dist/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  190. package/dist/graph-schema/fixtures.d.ts +23 -0
  191. package/dist/graph-schema/fixtures.d.ts.map +1 -0
  192. package/dist/graph-schema/fixtures.js +246 -0
  193. package/dist/graph-schema/generate.d.ts +15 -0
  194. package/dist/graph-schema/generate.d.ts.map +1 -0
  195. package/dist/graph-schema/generate.js +267 -0
  196. package/dist/graph-schema/graph.schema.json +1387 -0
  197. package/dist/health.schema.json +2 -1
  198. package/dist/identity.d.ts.map +1 -1
  199. package/dist/identity.js +2 -0
  200. package/dist/index.d.ts +1 -0
  201. package/dist/index.d.ts.map +1 -1
  202. package/dist/index.js +2 -0
  203. package/dist/instance-nodes.d.ts +159 -0
  204. package/dist/instance-nodes.d.ts.map +1 -0
  205. package/dist/instance-nodes.js +231 -0
  206. package/dist/lexicon.d.ts +72 -0
  207. package/dist/lexicon.d.ts.map +1 -1
  208. package/dist/lifecycle/adoption-file.d.ts +56 -0
  209. package/dist/lifecycle/adoption-file.d.ts.map +1 -0
  210. package/dist/lifecycle/adoption-file.js +123 -0
  211. package/dist/lifecycle/cloud-index.d.ts +186 -0
  212. package/dist/lifecycle/cloud-index.d.ts.map +1 -0
  213. package/dist/lifecycle/cloud-index.js +618 -0
  214. package/dist/lifecycle/derive-edges.d.ts +59 -0
  215. package/dist/lifecycle/derive-edges.d.ts.map +1 -0
  216. package/dist/lifecycle/derive-edges.js +126 -0
  217. package/dist/lifecycle/git.d.ts +10 -9
  218. package/dist/lifecycle/git.d.ts.map +1 -1
  219. package/dist/lifecycle/git.js +7 -1
  220. package/dist/lifecycle/identity-match.d.ts +133 -0
  221. package/dist/lifecycle/identity-match.d.ts.map +1 -0
  222. package/dist/lifecycle/identity-match.js +147 -0
  223. package/dist/lifecycle/index-diff.d.ts +224 -0
  224. package/dist/lifecycle/index-diff.d.ts.map +1 -0
  225. package/dist/lifecycle/index-diff.js +327 -0
  226. package/dist/lifecycle/index-document.d.ts +130 -0
  227. package/dist/lifecycle/index-document.d.ts.map +1 -0
  228. package/dist/lifecycle/index-document.js +189 -0
  229. package/dist/lifecycle/index-drift.d.ts +121 -0
  230. package/dist/lifecycle/index-drift.d.ts.map +1 -0
  231. package/dist/lifecycle/index-drift.js +178 -0
  232. package/dist/lifecycle/index-import.d.ts +168 -0
  233. package/dist/lifecycle/index-import.d.ts.map +1 -0
  234. package/dist/lifecycle/index-import.js +515 -0
  235. package/dist/lifecycle/index-join.d.ts +129 -0
  236. package/dist/lifecycle/index-join.d.ts.map +1 -0
  237. package/dist/lifecycle/index-join.js +248 -0
  238. package/dist/lifecycle/index.d.ts +4 -0
  239. package/dist/lifecycle/index.d.ts.map +1 -1
  240. package/dist/lifecycle/index.js +4 -0
  241. package/dist/lifecycle/observation-baseline.d.ts +61 -21
  242. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  243. package/dist/lifecycle/observation-baseline.js +144 -19
  244. package/dist/lifecycle/observe.d.ts.map +1 -1
  245. package/dist/lifecycle/observe.js +6 -1
  246. package/dist/lifecycle/query.d.ts +45 -4
  247. package/dist/lifecycle/query.d.ts.map +1 -1
  248. package/dist/lifecycle/query.js +89 -7
  249. package/dist/lifecycle/replay.d.ts +10 -3
  250. package/dist/lifecycle/replay.d.ts.map +1 -1
  251. package/dist/lifecycle/replay.js +10 -6
  252. package/dist/lifecycle/scenario.d.ts +4 -3
  253. package/dist/lifecycle/scenario.d.ts.map +1 -1
  254. package/dist/lifecycle/snapshot-store-fake.d.ts +9 -0
  255. package/dist/lifecycle/snapshot-store-fake.d.ts.map +1 -0
  256. package/dist/lifecycle/snapshot-store-fake.js +21 -0
  257. package/dist/lifecycle/snapshot-store-s3.d.ts +102 -0
  258. package/dist/lifecycle/snapshot-store-s3.d.ts.map +1 -0
  259. package/dist/lifecycle/snapshot-store-s3.js +254 -0
  260. package/dist/lifecycle/snapshot-store.d.ts +170 -0
  261. package/dist/lifecycle/snapshot-store.d.ts.map +1 -0
  262. package/dist/lifecycle/snapshot-store.js +832 -0
  263. package/dist/lifecycle/snapshot.d.ts +11 -0
  264. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  265. package/dist/lifecycle/snapshot.js +18 -5
  266. package/dist/lifecycle/type-listing.d.ts +148 -0
  267. package/dist/lifecycle/type-listing.d.ts.map +1 -0
  268. package/dist/lifecycle/type-listing.js +166 -0
  269. package/dist/lint/component-checks.d.ts +15 -1
  270. package/dist/lint/component-checks.d.ts.map +1 -1
  271. package/dist/lint/component-checks.js +1 -0
  272. package/dist/lint/presets/relaxed.json +2 -1
  273. package/dist/lint/presets/strict.json +2 -1
  274. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts +4 -0
  275. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts.map +1 -0
  276. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.js +13 -0
  277. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts +4 -0
  278. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts.map +1 -0
  279. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.js +8 -0
  280. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts +33 -0
  281. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts.map +1 -0
  282. package/dist/lint/rules/comp/comp008-applies-signal-only-root.js +123 -0
  283. package/dist/lint/rules/comp/index.d.ts +2 -1
  284. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  285. package/dist/lint/rules/comp/index.js +4 -1
  286. package/dist/observation.d.ts +19 -2
  287. package/dist/observation.d.ts.map +1 -1
  288. package/dist/observation.js +18 -1
  289. package/dist/op/activities/activity-contracts.d.ts +1 -1
  290. package/dist/op/activities/activity-contracts.js +1 -1
  291. package/dist/op/activities/change-set.d.ts +6 -4
  292. package/dist/op/activities/change-set.d.ts.map +1 -1
  293. package/dist/op/activities/change-set.js +4 -2
  294. package/dist/op/activity-failure.d.ts +27 -0
  295. package/dist/op/activity-failure.d.ts.map +1 -1
  296. package/dist/op/activity-failure.js +31 -0
  297. package/dist/op/index.d.ts +2 -1
  298. package/dist/op/index.d.ts.map +1 -1
  299. package/dist/op/index.js +1 -1
  300. package/dist/op/local-executor.d.ts +5 -1
  301. package/dist/op/local-executor.d.ts.map +1 -1
  302. package/dist/op/local-executor.js +22 -8
  303. package/dist/pr-loop.d.ts +56 -1
  304. package/dist/pr-loop.d.ts.map +1 -1
  305. package/dist/pr-loop.js +51 -1
  306. package/dist/pulumi-urn.d.ts +101 -0
  307. package/dist/pulumi-urn.d.ts.map +1 -0
  308. package/dist/pulumi-urn.js +131 -0
  309. package/dist/release-floors.d.ts +23 -0
  310. package/dist/release-floors.d.ts.map +1 -0
  311. package/dist/release-floors.js +22 -0
  312. package/dist/sensitive-key.d.ts +23 -0
  313. package/dist/sensitive-key.d.ts.map +1 -0
  314. package/dist/sensitive-key.js +31 -0
  315. package/dist/wave-runtime.mjs +234 -234
  316. package/dist/workspace/adoption-report.d.ts +53 -0
  317. package/dist/workspace/adoption-report.d.ts.map +1 -0
  318. package/dist/workspace/adoption-report.js +76 -0
  319. package/dist/workspace/capabilities-contract.d.ts +7 -0
  320. package/dist/workspace/capabilities-contract.d.ts.map +1 -1
  321. package/dist/workspace/capabilities-contract.js +2 -1
  322. package/dist/workspace/capabilities.schema.json +98 -0
  323. package/dist/workspace/change-set.schema.json +42 -3
  324. package/dist/workspace/ci-cloud-identity.d.ts +13 -1
  325. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  326. package/dist/workspace/ci-cloud-identity.js +30 -1
  327. package/dist/workspace/compose-graph.d.ts +10 -1
  328. package/dist/workspace/compose-graph.d.ts.map +1 -1
  329. package/dist/workspace/compose-graph.js +6 -1
  330. package/dist/workspace/conformance/index.d.ts +1 -1
  331. package/dist/workspace/conformance/index.d.ts.map +1 -1
  332. package/dist/workspace/conformance/index.js +6 -1
  333. package/dist/workspace/drift-cli.d.ts +81 -0
  334. package/dist/workspace/drift-cli.d.ts.map +1 -0
  335. package/dist/workspace/drift-cli.js +317 -0
  336. package/dist/workspace/drift.schema.json +672 -0
  337. package/dist/workspace/graph-cli.d.ts +2 -0
  338. package/dist/workspace/graph-cli.d.ts.map +1 -1
  339. package/dist/workspace/graph-cli.js +3 -0
  340. package/dist/workspace/graph.schema.json +72 -0
  341. package/dist/workspace/init.d.ts +5 -1
  342. package/dist/workspace/init.d.ts.map +1 -1
  343. package/dist/workspace/init.js +55 -9
  344. package/dist/workspace/ledger.schema.json +1 -1
  345. package/dist/workspace/nested-graph.d.ts.map +1 -1
  346. package/dist/workspace/nested-graph.js +2 -0
  347. package/dist/workspace/pr-report.schema.json +51 -2
  348. package/dist/workspace/promote-waves.d.ts +112 -0
  349. package/dist/workspace/promote-waves.d.ts.map +1 -0
  350. package/dist/workspace/promote-waves.js +290 -0
  351. package/dist/workspace/promote.d.ts +84 -2
  352. package/dist/workspace/promote.d.ts.map +1 -1
  353. package/dist/workspace/promote.js +347 -61
  354. package/dist/workspace/reason-codes.d.ts +12 -0
  355. package/dist/workspace/reason-codes.d.ts.map +1 -1
  356. package/dist/workspace/reason-codes.js +14 -0
  357. package/dist/workspace/search-contract.d.ts +3 -3
  358. package/dist/workspace/search-contract.d.ts.map +1 -1
  359. package/dist/workspace/search-contract.js +2 -0
  360. package/dist/workspace/search.schema.json +103 -5
  361. package/package.json +14 -1
  362. package/src/__fixtures__/drift-record/README.md +58 -0
  363. package/src/__fixtures__/drift-record/choudoufu-cone.ndjson +4 -0
  364. package/src/__fixtures__/drift-record/choudoufu-resource.ndjson +3 -0
  365. package/src/__fixtures__/drift-record/component-status-ledger.json +65 -0
  366. package/src/__fixtures__/drift-record/component-status.json +91 -0
  367. package/src/__fixtures__/drift-record/deep-diff.json +259 -0
  368. package/src/__fixtures__/drift-record/lifecycle-diff-live.json +825 -0
  369. package/src/__fixtures__/drift-record/live-diff.json +125 -0
  370. package/src/audit/discover.e2e.test.ts +14 -0
  371. package/src/audit/discover.ts +3 -1
  372. package/src/behaviour.ts +1 -1
  373. package/src/capabilities/templates.json +145 -0
  374. package/src/capabilities/templates.test.ts +212 -0
  375. package/src/capabilities/templates.ts +204 -0
  376. package/src/change-set-bundle.test.ts +83 -0
  377. package/src/change-set-graph.ts +80 -9
  378. package/src/change-set.test.ts +24 -0
  379. package/src/change-set.ts +77 -14
  380. package/src/ci-identity-targets.test.ts +23 -0
  381. package/src/ci-identity.ts +10 -4
  382. package/src/cli/commands/import-index.test.ts +117 -0
  383. package/src/cli/commands/import-index.ts +265 -0
  384. package/src/cli/commands/lint.ts +4 -2
  385. package/src/cli/handlers/components.test.ts +9 -0
  386. package/src/cli/handlers/components.ts +12 -2
  387. package/src/cli/handlers/fan-out.ts +17 -3
  388. package/src/cli/handlers/graph.test.ts +42 -0
  389. package/src/cli/handlers/graph.ts +26 -4
  390. package/src/cli/handlers/index-cloud.ts +32 -0
  391. package/src/cli/handlers/index-diff.test.ts +68 -0
  392. package/src/cli/handlers/index-diff.ts +146 -0
  393. package/src/cli/handlers/ledger-root-member.test.ts +161 -0
  394. package/src/cli/handlers/lifecycle-drift.ts +68 -0
  395. package/src/cli/handlers/lifecycle.test.ts +33 -11
  396. package/src/cli/handlers/lifecycle.ts +205 -30
  397. package/src/cli/handlers/misc.ts +73 -0
  398. package/src/cli/handlers/pr-waves-blocked.test.ts +37 -2
  399. package/src/cli/handlers/pr.ts +14 -2
  400. package/src/cli/handlers/promote.test.ts +64 -0
  401. package/src/cli/handlers/promote.ts +81 -6
  402. package/src/cli/handlers/query.ts +27 -4
  403. package/src/cli/handlers/run-declared-reads-cli.test.ts +187 -0
  404. package/src/cli/handlers/run-groups-cli.test.ts +229 -0
  405. package/src/cli/handlers/scenario.test.ts +13 -0
  406. package/src/cli/handlers/scenario.ts +8 -7
  407. package/src/cli/handlers/search-index.test.ts +255 -0
  408. package/src/cli/handlers/search-json.test.ts +1 -1
  409. package/src/cli/handlers/search-ownership.test.ts +194 -0
  410. package/src/cli/handlers/search.ts +271 -38
  411. package/src/cli/main.test.ts +7 -0
  412. package/src/cli/main.ts +56 -1
  413. package/src/cli/mcp/lifecycle-tools.ts +3 -2
  414. package/src/cli/mcp/resource-handlers.ts +4 -3
  415. package/src/cli/mcp/workspace-tools.ts +1 -1
  416. package/src/cli/parse-args.ts +40 -2
  417. package/src/cli/registry.ts +19 -1
  418. package/src/cloud-lister/aws.test.ts +202 -0
  419. package/src/cloud-lister/aws.ts +226 -0
  420. package/src/cloud-lister/azure.test.ts +132 -0
  421. package/src/cloud-lister/azure.ts +142 -0
  422. package/src/cloud-lister/common.ts +115 -0
  423. package/src/cloud-lister/credentials.ts +110 -0
  424. package/src/cloud-lister/fakes.ts +66 -0
  425. package/src/cloud-lister/gcp.test.ts +121 -0
  426. package/src/cloud-lister/gcp.ts +159 -0
  427. package/src/cloud-lister/guard.test.ts +198 -0
  428. package/src/cloud-lister/guard.ts +111 -0
  429. package/src/cloud-lister/ids.ts +54 -0
  430. package/src/cloud-lister/index.ts +134 -0
  431. package/src/cloud-lister/kubernetes.test.ts +160 -0
  432. package/src/cloud-lister/kubernetes.ts +284 -0
  433. package/src/cloud-lister/markers.ts +74 -0
  434. package/src/cloud-lister/mask.ts +77 -0
  435. package/src/cloud-lister/shape.test.ts +81 -0
  436. package/src/cloud-lister/sigv4.ts +70 -0
  437. package/src/cloud-lister/transport.ts +118 -0
  438. package/src/cloud-lister/types.ts +135 -0
  439. package/src/components/capability-plugin-loader.ts +4 -0
  440. package/src/components/capability-plugin.ts +10 -0
  441. package/src/components/capability.ts +229 -0
  442. package/src/components/cli-support.ts +70 -14
  443. package/src/components/declared-reads.test.ts +103 -0
  444. package/src/components/declared-reads.ts +134 -0
  445. package/src/components/deploy-units.ts +4 -0
  446. package/src/components/driver.ts +89 -4
  447. package/src/components/fan-out-run.ts +95 -15
  448. package/src/components/fan-out-support.ts +10 -1
  449. package/src/components/fan-out.ts +30 -0
  450. package/src/components/groups.test.ts +320 -0
  451. package/src/components/groups.ts +880 -0
  452. package/src/components/index.ts +42 -0
  453. package/src/components/plan-shape-conformance.ts +130 -0
  454. package/src/components/pr-env-run-bundle.test.ts +2 -0
  455. package/src/components/pr-run.test.ts +516 -3
  456. package/src/components/pr-run.ts +231 -41
  457. package/src/components/promote-rounds.ts +25 -2
  458. package/src/components/promote.ts +55 -5
  459. package/src/components/registry.test.ts +1 -1
  460. package/src/components/release-gate.ts +115 -30
  461. package/src/components/release-groups.test.ts +362 -0
  462. package/src/components/run-groups.ts +113 -0
  463. package/src/components/starter-plugin.ts +6 -0
  464. package/src/components/verbs/index.ts +1 -0
  465. package/src/components/verbs/member-outputs.test.ts +205 -0
  466. package/src/components/verbs/member-outputs.ts +308 -0
  467. package/src/config.ts +44 -0
  468. package/src/deep-observation.ts +5 -23
  469. package/src/drift-record.test.ts +596 -0
  470. package/src/drift-record.ts +1206 -0
  471. package/src/engine-facts.guard.test.ts +111 -0
  472. package/src/engine-facts.test.ts +178 -0
  473. package/src/engine-facts.ts +154 -0
  474. package/src/gated-waves-bundle.test.ts +2 -0
  475. package/src/graph-ir-live.test.ts +92 -0
  476. package/src/graph-ir.ts +317 -7
  477. package/src/graph-refs.ts +416 -0
  478. package/src/graph-schema/fixtures/adopt.json +15 -0
  479. package/src/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  480. package/src/graph-schema/fixtures/index.json +68 -0
  481. package/src/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  482. package/src/graph-schema/fixtures.ts +267 -0
  483. package/src/graph-schema/generate.ts +263 -0
  484. package/src/graph-schema/graph-schema.test.ts +153 -0
  485. package/src/graph-schema/graph.schema.json +1387 -0
  486. package/src/health.schema.json +2 -1
  487. package/src/identity.ts +2 -0
  488. package/src/index.ts +2 -0
  489. package/src/instance-nodes.ts +288 -0
  490. package/src/lexicon.ts +76 -0
  491. package/src/lifecycle/__fixtures__/index-diff/20261026T120000Z-aaaaaaaaaaaa.json +149 -0
  492. package/src/lifecycle/__fixtures__/index-diff/20261026T130000Z-bbbbbbbbbbbb.json +127 -0
  493. package/src/lifecycle/adoption-file.test.ts +100 -0
  494. package/src/lifecycle/adoption-file.ts +154 -0
  495. package/src/lifecycle/cloud-index.test.ts +517 -0
  496. package/src/lifecycle/cloud-index.ts +736 -0
  497. package/src/lifecycle/derive-edges.test.ts +193 -0
  498. package/src/lifecycle/derive-edges.ts +182 -0
  499. package/src/lifecycle/git.ts +10 -4
  500. package/src/lifecycle/identity-match.test.ts +308 -0
  501. package/src/lifecycle/identity-match.ts +257 -0
  502. package/src/lifecycle/index-diff.test.ts +198 -0
  503. package/src/lifecycle/index-diff.ts +476 -0
  504. package/src/lifecycle/index-document.test.ts +64 -0
  505. package/src/lifecycle/index-document.ts +299 -0
  506. package/src/lifecycle/index-drift.test.ts +191 -0
  507. package/src/lifecycle/index-drift.ts +304 -0
  508. package/src/lifecycle/index-import.test.ts +255 -0
  509. package/src/lifecycle/index-import.ts +634 -0
  510. package/src/lifecycle/index-join.test.ts +250 -0
  511. package/src/lifecycle/index-join.ts +304 -0
  512. package/src/lifecycle/index-store-load-bearing.test.ts +187 -0
  513. package/src/lifecycle/index.ts +4 -0
  514. package/src/lifecycle/member-ledger.test.ts +25 -7
  515. package/src/lifecycle/observation-baseline.test.ts +72 -1
  516. package/src/lifecycle/observation-baseline.ts +190 -27
  517. package/src/lifecycle/observe.ts +6 -1
  518. package/src/lifecycle/query.test.ts +69 -1
  519. package/src/lifecycle/query.ts +103 -10
  520. package/src/lifecycle/replay.test.ts +9 -5
  521. package/src/lifecycle/replay.ts +9 -5
  522. package/src/lifecycle/scenario.ts +4 -3
  523. package/src/lifecycle/snapshot-store-fake.ts +29 -0
  524. package/src/lifecycle/snapshot-store-s3.ts +316 -0
  525. package/src/lifecycle/snapshot-store.test.ts +385 -0
  526. package/src/lifecycle/snapshot-store.ts +981 -0
  527. package/src/lifecycle/snapshot.test.ts +13 -0
  528. package/src/lifecycle/snapshot.ts +23 -6
  529. package/src/lifecycle/type-listing.test.ts +166 -0
  530. package/src/lifecycle/type-listing.ts +309 -0
  531. package/src/lint/component-checks.ts +17 -1
  532. package/src/lint/config.test.ts +3 -0
  533. package/src/lint/presets/relaxed.json +2 -1
  534. package/src/lint/presets/strict.json +2 -1
  535. package/src/lint/rules/__fixtures__/comp/comp008/fail/dns.component.ts +15 -0
  536. package/src/lint/rules/__fixtures__/comp/comp008/pass/dns.component.ts +10 -0
  537. package/src/lint/rules/comp/comp.test.ts +2 -1
  538. package/src/lint/rules/comp/comp008-applies-signal-only-root.ts +121 -0
  539. package/src/lint/rules/comp/comp008.test.ts +77 -0
  540. package/src/lint/rules/comp/index.ts +4 -1
  541. package/src/observation-edges.test.ts +33 -0
  542. package/src/observation.ts +36 -1
  543. package/src/op/activities/activity-contracts.ts +1 -1
  544. package/src/op/activities/change-set.ts +8 -5
  545. package/src/op/activity-failure.ts +36 -0
  546. package/src/op/index.ts +2 -1
  547. package/src/op/local-executor.test.ts +80 -0
  548. package/src/op/local-executor.ts +27 -9
  549. package/src/pr-loop-bundle.test.ts +5 -2
  550. package/src/pr-loop.test.ts +43 -0
  551. package/src/pr-loop.ts +111 -2
  552. package/src/pulumi-urn.ts +151 -0
  553. package/src/release-floors.ts +22 -0
  554. package/src/sensitive-key.ts +34 -0
  555. package/src/workspace/adoption-report.test.ts +61 -0
  556. package/src/workspace/adoption-report.ts +103 -0
  557. package/src/workspace/capabilities-contract.test.ts +4 -0
  558. package/src/workspace/capabilities-contract.ts +9 -1
  559. package/src/workspace/capabilities.schema.json +98 -0
  560. package/src/workspace/change-set.schema.json +42 -3
  561. package/src/workspace/ci-cloud-identity.ts +31 -1
  562. package/src/workspace/compose-graph.test.ts +12 -0
  563. package/src/workspace/compose-graph.ts +12 -1
  564. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  565. package/src/workspace/conformance/index.ts +6 -1
  566. package/src/workspace/drift-cli.test.ts +65 -0
  567. package/src/workspace/drift-cli.ts +365 -0
  568. package/src/workspace/drift.schema.json +672 -0
  569. package/src/workspace/graph-cli.ts +5 -0
  570. package/src/workspace/graph.schema.json +72 -0
  571. package/src/workspace/init.test.ts +38 -1
  572. package/src/workspace/init.ts +50 -8
  573. package/src/workspace/intent-gaps.test.ts +18 -2
  574. package/src/workspace/ledger.schema.json +1 -1
  575. package/src/workspace/nested-graph.ts +1 -0
  576. package/src/workspace/pr-report.schema.json +51 -2
  577. package/src/workspace/promote-waves.test.ts +296 -0
  578. package/src/workspace/promote-waves.ts +340 -0
  579. package/src/workspace/promote.test.ts +158 -4
  580. package/src/workspace/promote.ts +398 -57
  581. package/src/workspace/reason-codes.test.ts +4 -1
  582. package/src/workspace/reason-codes.ts +14 -0
  583. package/src/workspace/search-contract.ts +2 -0
  584. package/src/workspace/search.schema.json +103 -5
@@ -62,6 +62,29 @@ const DECL: CloudIdentityDeclaration = {
62
62
 
63
63
  const ROOTS = ["envs/prod/net", "envs/staging/net", "shared/dns", "envs/prod/app"];
64
64
 
65
+ describe("a target with several names takes the first rule any name matches (#4332)", () => {
66
+ // A Terraform root's key in `terraform.roots`, then its directory relative to the project.
67
+ const decl: CloudIdentityDeclaration = {
68
+ aws: {
69
+ rules: [
70
+ { targets: ["envs/prod/**"], plan: role("dir-plan"), apply: role("dir-apply") },
71
+ { targets: ["network"], plan: role("key-plan"), apply: role("key-apply") },
72
+ { plan: role("own-plan"), apply: role("own-apply") },
73
+ ],
74
+ },
75
+ };
76
+ const apply = (...names: string[]) => resolveTargetCloudIdentity(decl, "prod", undefined, names)!.apply.aws!.role;
77
+
78
+ test("the key matches", () => expect(apply("network", "infra/net")).toBe(role("key-apply")));
79
+ test("the directory matches", () => expect(apply("prod-net", "envs/prod/net")).toBe(role("dir-apply")));
80
+ test("both match different rules: the first declared rule wins", () => {
81
+ expect(apply("network", "envs/prod/net")).toBe(role("dir-apply"));
82
+ expect(apply("envs/prod/net", "network")).toBe(role("dir-apply"));
83
+ });
84
+ test("neither matches: the member's own rule", () => expect(apply("dns", "shared/dns")).toBe(role("own-apply")));
85
+ test("a single name resolves as before", () => expect(resolveTargetCloudIdentity(decl, "prod", undefined, "network")).toEqual(resolveTargetCloudIdentity(decl, "prod", undefined, ["network"])));
86
+ });
87
+
65
88
  describe("which identity a target takes (#4332)", () => {
66
89
  test("the first rule that covers the environment, the member and the target wins, per cloud", () => {
67
90
  const prod = resolveTargetCloudIdentity(DECL, "prod", "infra", "envs/prod/net")!;
@@ -207,14 +207,20 @@ function globBody(g: string, paths: boolean): string {
207
207
 
208
208
  const matches = (globs: readonly string[] | undefined, name: string | undefined): boolean => globs === undefined || (name !== undefined && globs.some((g) => nameGlobMatch(g, name)));
209
209
 
210
- const coversTarget = (r: CloudIdentityRule, target: string | undefined): boolean =>
211
- r.targets === undefined || (target !== undefined && r.targets.some((g) => targetGlobMatch(g, target)));
210
+ /** One target's names: a string, or the several names a tool knows it by, such as a Terraform root's key and its directory. */
211
+ export type TargetNames = string | readonly string[];
212
+
213
+ const coversTarget = (r: CloudIdentityRule, target: TargetNames | undefined): boolean =>
214
+ r.targets === undefined || (target !== undefined && (typeof target === "string" ? [target] : target).some((n) => r.targets!.some((g) => targetGlobMatch(g, n))));
212
215
 
213
216
  /**
214
217
  * The first rule that covers `env`, `member` and `target`, or undefined.
215
218
  * With no `target`, the first rule that names no targets: the pipeline's own.
219
+ * A `target` given as several names is covered by a rule when any one name
220
+ * matches a glob, and the first rule in declared order wins, whichever name
221
+ * it matched.
216
222
  */
217
- export function ruleFor(rules: readonly CloudIdentityRule[], env: string, member?: string, target?: string): CloudIdentityRule | undefined {
223
+ export function ruleFor(rules: readonly CloudIdentityRule[], env: string, member?: string, target?: TargetNames): CloudIdentityRule | undefined {
218
224
  return rules.find((r) => matches(r.environments, env) && matches(r.members, member) && coversTarget(r, target));
219
225
  }
220
226
 
@@ -289,7 +295,7 @@ export function resolveCloudIdentity(decl: CloudIdentityDeclaration | undefined,
289
295
  * any cloud covers it. Each trade names one identity per cloud and no
290
296
  * `targets`.
291
297
  */
292
- export function resolveTargetCloudIdentity(decl: CloudIdentityDeclaration | undefined, env: string, member: string | undefined, target: string): PipelineCloudIdentity | undefined {
298
+ export function resolveTargetCloudIdentity(decl: CloudIdentityDeclaration | undefined, env: string, member: string | undefined, target: TargetNames): PipelineCloudIdentity | undefined {
293
299
  if (!decl) return undefined;
294
300
  const plan: Record<string, unknown> = {};
295
301
  const apply: Record<string, unknown> = {};
@@ -0,0 +1,117 @@
1
+ /**
2
+ * #4524: `importFromIndex` end to end with a fake lexicon: it reads an index
3
+ * file, writes the generator's files and the report, gives byte-identical
4
+ * output for the same index and selection, and refuses an empty selection.
5
+ * No project is built (the declared entities are passed in) and nothing
6
+ * reaches a cloud.
7
+ */
8
+ import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
9
+ import { tmpdir } from "os";
10
+ import { join } from "path";
11
+ import { afterAll, describe, expect, test } from "vitest";
12
+ import { MASKED } from "../../deep-observation";
13
+ import type { LexiconPlugin } from "../../lexicon";
14
+ import { indexDocumentDigest, indexScopeKey, type CloudIndexScope, type IndexDocument } from "../../lifecycle/index-document";
15
+ import { adoptionFileProblemsAgainst, type AdoptionFile } from "../../lifecycle/adoption-file";
16
+ import { importFromIndex, INDEX_IMPORT_REPORT_FILE } from "./import-index";
17
+
18
+ const dirs: string[] = [];
19
+ const scratch = () => {
20
+ const d = mkdtempSync(join(tmpdir(), "chant-index-import-"));
21
+ dirs.push(d);
22
+ return d;
23
+ };
24
+ afterAll(() => {
25
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
26
+ });
27
+
28
+ const SCOPE: CloudIndexScope = { kind: "cloud", substrate: "aws", scopeId: "111", locations: ["r1"], types: "*" };
29
+ const nodes = [
30
+ { id: "aws/111/r1/Fake::Net/net-1", kind: "Fake::Net", lexicon: "fake", attrs: { Cidr: "10.0.0.0/16" }, listedType: "Fake::Net", location: "r1", nativeId: "net-1" },
31
+ { id: "aws/111/r1/Fake::Db/db-1", kind: "Fake::Db", lexicon: "fake", attrs: { NetId: "net-1", Password: MASKED }, listedType: "Fake::Db", location: "r1", nativeId: "db-1" },
32
+ ];
33
+ const edges = [{ from: nodes[1].id, to: nodes[0].id, kind: "ref" as const, origin: "derived" as const, rule: "fake:Fake::Db:NetId", viaAttr: "NetId" }];
34
+ const graph = { version: 1 as const, nodes, edges, groups: {} };
35
+ const DOC: IndexDocument = {
36
+ index: "v1",
37
+ stamp: { source: { kind: "cloud", name: "behold-lister" }, scope: SCOPE, scopeKey: indexScopeKey(SCOPE), observedAt: "2026-10-26T00:00:00.000Z", digest: indexDocumentDigest({ graph, coverage: [] }) },
38
+ coverage: [],
39
+ graph,
40
+ };
41
+
42
+ // The generator writes the IR as JSON, which is enough to see what it was given.
43
+ const plugin = {
44
+ name: "fake",
45
+ indexImport: {
46
+ resource: (n: { kind: string; attrs: Record<string, unknown> }) => ({ type: n.kind, properties: n.attrs }),
47
+ reference: (v: string, t: { logicalId: string; node: { nativeId?: string } }) => (v === t.node.nativeId ? { ref: t.logicalId } : undefined),
48
+ secretParameter: (name: string) => ({ name, type: "String" }),
49
+ parameterReference: (name: string) => ({ param: name }),
50
+ },
51
+ templateGenerator: () => ({
52
+ ownsLayout: true,
53
+ generate: (ir: unknown) => [{ path: "main.ts", content: `export const ir = ${JSON.stringify(ir, null, 2)};\n` }],
54
+ }),
55
+ } as unknown as LexiconPlugin;
56
+
57
+ function indexFile(): string {
58
+ const p = join(scratch(), "index.json");
59
+ writeFileSync(p, JSON.stringify(DOC));
60
+ return p;
61
+ }
62
+
63
+ describe("importFromIndex", () => {
64
+ test("writes the generator's files with the header, and the report beside them", async () => {
65
+ const out = join(scratch(), "out");
66
+ const result = await importFromIndex({ index: indexFile(), selection: { all: true }, output: out, plugins: [plugin], declaredEntities: new Map() });
67
+ expect(result.success).toBe(true);
68
+ expect(readdirSync(out).sort()).toEqual([INDEX_IMPORT_REPORT_FILE, "main.ts"]);
69
+ const main = readFileSync(join(out, "main.ts"), "utf-8");
70
+ expect(main.startsWith("// Generated by chant import --index. Nothing was applied.")).toBe(true);
71
+ expect(main).not.toContain(MASKED);
72
+ expect(main).toContain('"ref": "Net1"');
73
+ const report = JSON.parse(readFileSync(join(out, INDEX_IMPORT_REPORT_FILE), "utf-8"));
74
+ expect(report.report).toBe("chant-index-import/v1");
75
+ expect(report.resources.map((r: { status: string }) => r.status)).toEqual(["imported", "imported"]);
76
+ expect(report.parameters).toEqual([{ name: "Db1Password", node: nodes[1].id, path: "Password", group: "" }]);
77
+ });
78
+
79
+ test("the same index and selection give byte-identical files", async () => {
80
+ const index = indexFile();
81
+ const a = join(scratch(), "a");
82
+ const b = join(scratch(), "b");
83
+ await importFromIndex({ index, selection: { all: true }, output: a, plugins: [plugin], declaredEntities: new Map() });
84
+ await importFromIndex({ index, selection: { all: true }, output: b, plugins: [plugin], declaredEntities: new Map() });
85
+ for (const f of ["main.ts", INDEX_IMPORT_REPORT_FILE]) {
86
+ expect(readFileSync(join(b, f), "utf-8")).toBe(readFileSync(join(a, f), "utf-8"));
87
+ }
88
+ });
89
+
90
+ test("an empty selection is refused before the index is read", async () => {
91
+ const result = await importFromIndex({ index: "does-not-exist", selection: {}, output: join(scratch(), "o"), plugins: [plugin] });
92
+ expect(result.success).toBe(false);
93
+ expect(result.error).toMatch(/name what to import/);
94
+ });
95
+
96
+ test("a file that is not an index is refused", async () => {
97
+ const p = join(scratch(), "not-index.json");
98
+ writeFileSync(p, JSON.stringify({ hello: 1 }));
99
+ const result = await importFromIndex({ index: p, selection: { all: true }, output: join(scratch(), "o"), plugins: [plugin], declaredEntities: new Map() });
100
+ expect(result.success).toBe(false);
101
+ expect(result.error).toMatch(/does not hold index documents/);
102
+ });
103
+
104
+ test("--adopt-out writes the adoption file for what was imported, valid against the index (#4536)", async () => {
105
+ const out = join(scratch(), "out");
106
+ const adoptOut = join(scratch(), "adopt.json");
107
+ const result = await importFromIndex({ index: indexFile(), selection: { all: true }, output: out, adoptOut, plugins: [plugin], declaredEntities: new Map() });
108
+ expect(result.success).toBe(true);
109
+ const adopt = JSON.parse(readFileSync(adoptOut, "utf-8")) as AdoptionFile;
110
+ expect(adopt.adopt).toBe("v1");
111
+ expect(adopt.select).toEqual([
112
+ { node: nodes[1].id, as: "Db1", type: "Fake::Db" },
113
+ { node: nodes[0].id, as: "Net1", type: "Fake::Net" },
114
+ ]);
115
+ expect(adoptionFileProblemsAgainst(adopt, DOC)).toEqual([]);
116
+ });
117
+ });
@@ -0,0 +1,265 @@
1
+ /**
2
+ * `chant import --index <ref>` (#4524): typed source for indexed resources.
3
+ *
4
+ * Reads one index version (a file, or a version in the project's index
5
+ * store), selects nodes, and writes source through each lexicon's own
6
+ * template generator. Nothing is applied and nothing is written to the
7
+ * cloud. The pure part is ../../lifecycle/index-import.ts; this module reads
8
+ * the index, builds the project for the declared check, and writes files.
9
+ *
10
+ * The report (`chant-index-import.json` in the output directory, or
11
+ * `--report <path>`) names every selected resource's identity and cloud ids,
12
+ * what was imported, declared or unsupported, each reference and each
13
+ * parameter. The adoption step (INTENTIUS/choudoufu#2284) reads it.
14
+ * `--adopt-out <file>` also writes the adoption file (`adopt: v1`, #4536)
15
+ * that adopts every imported resource as its logical id.
16
+ */
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
18
+ import { dirname, join, relative, resolve } from "path";
19
+ import type { GeneratedFile } from "../../import/generator";
20
+ import type { LexiconPlugin } from "../../lexicon";
21
+ import type { DeclaredEntity } from "../../lifecycle/identity-match";
22
+ import { adoptionFileFromImportReport, adoptionFileJson } from "../../lifecycle/adoption-file";
23
+ import type { IndexDocument } from "../../lifecycle/index-document";
24
+ import {
25
+ declaredNodes,
26
+ indexImportHeader,
27
+ planIndexImport,
28
+ selectionIsEmpty,
29
+ type IndexImportPlan,
30
+ type IndexImportReport,
31
+ type IndexImportSelection,
32
+ } from "../../lifecycle/index-import";
33
+ import { generateOrganizedFiles, type ImportResult } from "./import";
34
+
35
+ export const INDEX_IMPORT_REPORT_FILE = "chant-index-import.json";
36
+
37
+ export interface IndexImportOptions {
38
+ /** A JSON file (one index document, or a list of the documents of one read), or `<scopeKey>[@<version>]` in the index store. */
39
+ index: string;
40
+ selection: IndexImportSelection;
41
+ /** Output directory (defaults to ./infra/). */
42
+ output?: string;
43
+ force?: boolean;
44
+ /** Where to write the report; defaults to `<output>/chant-index-import.json`. */
45
+ report?: string;
46
+ /** Where to write the adoption file for what was imported (`--adopt-out`, #4536). */
47
+ adoptOut?: string;
48
+ /** The project whose declared entities are checked (defaults to the current directory). */
49
+ projectDir?: string;
50
+ /** The project's source directory (`--src`; defaults to `sourceDir` from chant.config, then the project). */
51
+ src?: string;
52
+ /** Plugins to use instead of loading them (tests). */
53
+ plugins?: LexiconPlugin[];
54
+ /** Declared entities by lexicon instead of building the project (tests). */
55
+ declaredEntities?: ReadonlyMap<string, readonly DeclaredEntity[]>;
56
+ }
57
+
58
+ export interface IndexImportResult extends ImportResult {
59
+ report?: IndexImportReport;
60
+ }
61
+
62
+ function asDocuments(parsed: unknown, from: string): IndexDocument[] {
63
+ const list = Array.isArray(parsed)
64
+ ? parsed
65
+ : parsed && typeof parsed === "object" && Array.isArray((parsed as { documents?: unknown }).documents)
66
+ ? (parsed as { documents: unknown[] }).documents
67
+ : [parsed];
68
+ for (const d of list) {
69
+ if (!d || typeof d !== "object" || (d as { index?: unknown }).index !== "v1") {
70
+ throw new Error(`${from} does not hold index documents (index: "v1")`);
71
+ }
72
+ }
73
+ return list as IndexDocument[];
74
+ }
75
+
76
+ /**
77
+ * The documents an index ref names: a file path, or a store ref in the
78
+ * grammar `chant search --index` reads (`<scopeKey>[@latest|@<version>]`,
79
+ * ../../lifecycle/index-join.ts), from the store chant.config's `snapshots`
80
+ * picks (#4519).
81
+ */
82
+ export async function loadIndexDocuments(ref: string, cwd: string): Promise<IndexDocument[]> {
83
+ const asFile = resolve(cwd, ref);
84
+ if (existsSync(asFile)) return asDocuments(JSON.parse(readFileSync(asFile, "utf-8")), ref);
85
+ const { parseIndexRef, readIndexRef } = await import("../../lifecycle/index-join");
86
+ const parsed = parseIndexRef(ref);
87
+ if ("error" in parsed) throw new Error(`no file of that name, and ${parsed.error}; ${parsed.hint}`);
88
+ const { resolveSnapshotStore } = await import("../../lifecycle/snapshot-store");
89
+ const store = await resolveSnapshotStore({ cwd });
90
+ await store.fetch();
91
+ const read = await readIndexRef(store, parsed);
92
+ if ("error" in read) throw new Error(read.hint ? `${read.error}; ${read.hint}` : read.error);
93
+ return [read.doc];
94
+ }
95
+
96
+ /**
97
+ * The files an import plan writes, through each lexicon's template
98
+ * generator, with the plan's header on each group's first file. Pure apart
99
+ * from the generators, so the same plan gives byte-identical files.
100
+ */
101
+ export function renderIndexImport(
102
+ plan: IndexImportPlan,
103
+ plugins: readonly LexiconPlugin[],
104
+ ): { files: GeneratedFile[]; warnings: string[] } {
105
+ const files: GeneratedFile[] = [];
106
+ const warnings: string[] = [];
107
+ for (const group of plan.groups) {
108
+ const plugin = plugins.find((p) => p.name === group.lexicon);
109
+ if (!plugin?.templateGenerator) {
110
+ warnings.push(`the ${group.lexicon} lexicon has no template generator; ${group.ir.resources.length} resource(s) in ${group.dir || "."} were not written`);
111
+ continue;
112
+ }
113
+ const organized = generateOrganizedFiles(group.ir, plugin.templateGenerator());
114
+ warnings.push(...organized.warnings);
115
+ organized.files.forEach((f, i) => {
116
+ const path = group.dir ? `${group.dir}/${f.path}` : f.path;
117
+ files.push({ path, content: i === 0 ? indexImportHeader(plan, group) + f.content : f.content });
118
+ });
119
+ }
120
+ return { files, warnings };
121
+ }
122
+
123
+ /** The report as written: two-space JSON and a final newline. */
124
+ export function indexImportReportJson(report: IndexImportReport): string {
125
+ return JSON.stringify(report, null, 2) + "\n";
126
+ }
127
+
128
+ /** The project's declared entities by lexicon, or why there are none to check. */
129
+ async function projectEntities(
130
+ projectDir: string,
131
+ src: string | undefined,
132
+ plugins: readonly LexiconPlugin[],
133
+ ): Promise<{ entities: Map<string, DeclaredEntity[]>; warning?: string }> {
134
+ const entities = new Map<string, DeclaredEntity[]>();
135
+ try {
136
+ const { loadChantConfigUpward } = await import("../../config");
137
+ const { config, configPath } = await loadChantConfigUpward(projectDir);
138
+ if (!configPath) return { entities };
139
+ const projectPlugins = plugins.filter((p) => p.serializer);
140
+ if (projectPlugins.length === 0) return { entities };
141
+ const { build } = await import("../../build");
142
+ const { isObservableDeclarable } = await import("../../declarable");
143
+ const root = src ? resolve(src) : resolve(dirname(configPath), config.sourceDir ?? ".");
144
+ const result = await build(root, projectPlugins.map((p) => p.serializer), undefined, { config: config as unknown as Record<string, unknown> });
145
+ if (result.errors.length > 0) {
146
+ return { entities, warning: `the project at ${root} does not build, so no indexed resource was checked against its declarations` };
147
+ }
148
+ for (const [name, entity] of result.entities) {
149
+ if (!isObservableDeclarable(entity)) continue;
150
+ const list = entities.get(entity.lexicon) ?? entities.set(entity.lexicon, []).get(entity.lexicon)!;
151
+ list.push({ name, entityType: entity.entityType, props: (entity.props ?? {}) as Record<string, unknown> });
152
+ }
153
+ return { entities };
154
+ } catch (err) {
155
+ return { entities, warning: `the declared check did not run: ${err instanceof Error ? err.message : String(err)}` };
156
+ }
157
+ }
158
+
159
+ /** The plugins for the index's lexicons and the project's, each loaded once; one that fails to load is left out. */
160
+ async function pluginsFor(docs: readonly IndexDocument[], projectDir: string): Promise<LexiconPlugin[]> {
161
+ const { loadPlugin, resolveProjectLexicons } = await import("../plugins");
162
+ let names: string[] = [];
163
+ try {
164
+ names = await resolveProjectLexicons(projectDir);
165
+ } catch {
166
+ names = [];
167
+ }
168
+ for (const d of docs) for (const n of d.graph.nodes) if (!n.unmappedKind) names.push(n.lexicon);
169
+ const plugins: LexiconPlugin[] = [];
170
+ for (const name of [...new Set(names)].sort()) {
171
+ try {
172
+ const plugin = await loadPlugin(name);
173
+ await plugin.init?.();
174
+ plugins.push(plugin);
175
+ } catch {
176
+ // Not installed here: its nodes are reported as not loaded.
177
+ }
178
+ }
179
+ return plugins;
180
+ }
181
+
182
+ export async function importFromIndex(options: IndexImportOptions): Promise<IndexImportResult> {
183
+ const outputDir = resolve(options.output ?? "./infra/");
184
+ const projectDir = resolve(options.projectDir ?? (options.output ? dirname(outputDir) : "."));
185
+ const warnings: string[] = [];
186
+ const fail = (error: string): IndexImportResult => ({ success: false, generatedFiles: [], warnings, error });
187
+
188
+ if (selectionIsEmpty(options.selection)) {
189
+ return fail("name what to import: --type <type>, --name <native id>, --tag key=value, --select <file of node ids>, or --all");
190
+ }
191
+
192
+ let docs: IndexDocument[];
193
+ try {
194
+ docs = await loadIndexDocuments(options.index, process.cwd());
195
+ } catch (err) {
196
+ return fail(`could not read the index ${options.index}: ${err instanceof Error ? err.message : String(err)}`);
197
+ }
198
+
199
+ const plugins = options.plugins ?? (await pluginsFor(docs, projectDir));
200
+ let declared: Map<string, string> = new Map();
201
+ let entities: ReadonlyMap<string, readonly DeclaredEntity[]>;
202
+ if (options.declaredEntities) {
203
+ entities = options.declaredEntities;
204
+ } else {
205
+ const project = await projectEntities(projectDir, options.src, plugins);
206
+ if (project.warning) warnings.push(project.warning);
207
+ entities = project.entities;
208
+ }
209
+ try {
210
+ declared = declaredNodes(docs, entities, plugins);
211
+ } catch (err) {
212
+ warnings.push(`the declared check did not run: ${err instanceof Error ? err.message : String(err)}`);
213
+ }
214
+
215
+ let plan: IndexImportPlan;
216
+ try {
217
+ plan = planIndexImport(docs, options.selection, plugins, { declared });
218
+ } catch (err) {
219
+ return fail(err instanceof Error ? err.message : String(err));
220
+ }
221
+ if (plan.report.resources.length === 0) return fail("the selection matches no node in the index");
222
+
223
+ const { files, warnings: renderWarnings } = renderIndexImport(plan, plugins);
224
+ warnings.push(...renderWarnings);
225
+ for (const r of plan.report.resources) {
226
+ if (r.status === "unsupported") warnings.push(`${r.node}: not imported, ${r.reason}`);
227
+ }
228
+
229
+ const reportPath = resolve(options.report ?? join(outputDir, INDEX_IMPORT_REPORT_FILE));
230
+ const generatedFiles: string[] = [];
231
+ const write = (path: string, content: string, shown: string) => {
232
+ if (existsSync(path) && !options.force) {
233
+ warnings.push(`File ${shown} already exists, skipping`);
234
+ return;
235
+ }
236
+ mkdirSync(dirname(path), { recursive: true });
237
+ writeFileSync(path, content);
238
+ generatedFiles.push(shown);
239
+ };
240
+ for (const file of files) write(join(outputDir, file.path), file.content, file.path);
241
+ write(reportPath, indexImportReportJson(plan.report), relative(outputDir, reportPath) || reportPath);
242
+ if (options.adoptOut) {
243
+ const adoptPath = resolve(options.adoptOut);
244
+ try {
245
+ write(adoptPath, adoptionFileJson(adoptionFileFromImportReport(plan.report)), relative(outputDir, adoptPath) || adoptPath);
246
+ } catch (err) {
247
+ return { success: false, generatedFiles, warnings, error: `no adoption file written: ${err instanceof Error ? err.message : String(err)}`, report: plan.report };
248
+ }
249
+ }
250
+
251
+ const lexicons = [...new Set(plan.groups.map((g) => g.lexicon))];
252
+ return {
253
+ success: true,
254
+ generatedFiles,
255
+ warnings,
256
+ ...(lexicons.length === 1 ? { lexicon: lexicons[0] } : {}),
257
+ report: plan.report,
258
+ };
259
+ }
260
+
261
+ /** One line per status: what was imported, declared and left out. */
262
+ export function summarizeIndexImport(report: IndexImportReport): string {
263
+ const count = (s: string) => report.resources.filter((r) => r.status === s).length;
264
+ return `index import: ${count("imported")} imported, ${count("declared")} already declared, ${count("unsupported")} unsupported, ${report.parameters.length} parameter(s) to fill in`;
265
+ }
@@ -9,7 +9,7 @@ import { findInfraFiles, detectLexicons } from "../../index";
9
9
  import { formatStylish, formatJson, formatSarif } from "../reporters/stylish";
10
10
  import { loadLocalRules } from "../../lint/rule-loader";
11
11
  import { loadCoreRules } from "../../lint/rules/index";
12
- import { loadComponentChecks } from "../../lint/rules/comp/index";
12
+ import { loadComponentChecks, signalOnlyRootsOf } from "../../lint/rules/comp/index";
13
13
  import { runComponentChecks, type ComponentCheckDiagnostic } from "../../lint/component-checks";
14
14
  import { buildCapabilityRegistry } from "../../components/capability-plugin-loader";
15
15
  import type { RollbackPolicy } from "../../components/capability";
@@ -464,7 +464,9 @@ async function runComponentCheckDiagnostics(
464
464
  const allCheckIds = new Set(checks.map((c) => c.id));
465
465
 
466
466
  const registryContext = await resolveRegistryContext(infraPath);
467
- const raw = await runComponentChecks(infraPath, checks, registryContext, sandbox, buildParams);
467
+ // COMP008 (#4490) reads the project's signalOnly terraform roots.
468
+ const signalOnlyRoots = await signalOnlyRootsOf(findProjectRoot(infraPath));
469
+ const raw = await runComponentChecks(infraPath, checks, { ...registryContext, signalOnlyRoots }, sandbox, buildParams);
468
470
 
469
471
  const fileDirectivesCache = new Map<string, ReturnType<typeof parseDisableComments>>();
470
472
  const fileLevelDisable = (
@@ -44,6 +44,15 @@ vi.mock("../../lifecycle/git", () => ({
44
44
  StaleLifecycleBranchError: class StaleLifecycleBranchError extends Error {},
45
45
  }));
46
46
 
47
+ // The status baseline is read through the index store (#4519); this one hands the read to the mock above.
48
+ vi.mock("../../lifecycle/snapshot-store", async () => {
49
+ const { fakeSnapshotStore } = await import("../../lifecycle/snapshot-store-fake");
50
+ const store = fakeSnapshotStore({
51
+ readLatest: async (environment, key) => (await readSnapshotMock(environment, key)) as string | null,
52
+ });
53
+ return { resolveSnapshotStore: async () => store };
54
+ });
55
+
47
56
  vi.mock("../../lifecycle/build-ledger-store", () => ({
48
57
  findBuildManifestByArtifactDigest: (...args: unknown[]) => findBuildManifestByArtifactDigestMock(...args),
49
58
  readBuildManifest: (...args: unknown[]) => readBuildManifestMock(...args),
@@ -28,7 +28,8 @@
28
28
  */
29
29
  import { resolve, join, dirname } from "node:path";
30
30
  import { mkdir, readFile, writeFile } from "node:fs/promises";
31
- import { getHeadCommit, fetchLifecycle, pushLifecycle, readSnapshot, snapshotStorageKey, StaleLifecycleBranchError } from "../../lifecycle/git";
31
+ import { getHeadCommit, fetchLifecycle, pushLifecycle, snapshotStorageKey, StaleLifecycleBranchError } from "../../lifecycle/git";
32
+ import { resolveSnapshotStore } from "../../lifecycle/snapshot-store";
32
33
  import type { LifecycleSnapshot } from "../../lifecycle/types";
33
34
  import {
34
35
  appendReleaseRecord,
@@ -519,18 +520,27 @@ async function observeComponentStacks(
519
520
  * snapshot is no baseline, never an error: with none, nothing can have changed
520
521
  * since, and the status falls back to identity and presence.
521
522
  */
523
+ /** Stores already fetched in this process, so a status over many lexicons fetches once. */
524
+ const fetchedBaselineStores = new WeakSet<object>();
525
+
522
526
  async function readStatusBaseline(
523
527
  environment: string,
524
528
  lexicon: string,
525
529
  stacks: Array<string | undefined>,
526
530
  ): Promise<{ resources: Record<string, ResourceMetadata> | undefined; timestamps: Map<string, string> }> {
527
531
  const keys = [...new Set([...stacks.filter((s): s is string => !!s).map((s) => snapshotStorageKey(lexicon, s)), snapshotStorageKey(lexicon)])];
532
+ // Through the index store (#4519); a store that cannot be built is no baseline, like a missing snapshot.
533
+ const store = await resolveSnapshotStore().catch(() => null);
534
+ if (store && !fetchedBaselineStores.has(store)) {
535
+ fetchedBaselineStores.add(store);
536
+ await store.fetch();
537
+ }
528
538
  let resources: Record<string, ResourceMetadata> | undefined;
529
539
  const timestamps = new Map<string, string>();
530
540
  for (const key of keys) {
531
541
  let snap: LifecycleSnapshot | undefined;
532
542
  try {
533
- const content = await readSnapshot(environment, key);
543
+ const content = store ? await store.readLatest(environment, key) : null;
534
544
  snap = content ? (JSON.parse(content) as LifecycleSnapshot) : undefined;
535
545
  } catch {
536
546
  snap = undefined;
@@ -70,6 +70,8 @@ import { approveCommand, gateIsSealed } from "../../op/gate";
70
70
  import { getHeadCommit } from "../../lifecycle/git";
71
71
  import { remainingFanOut, type ChangedUnits, type FanOutProgress } from "../../components/fan-out";
72
72
  import { readDeployedOutputs } from "../../components/wave-plan";
73
+ import { formRunGroups } from "../../components/groups";
74
+ import { renderRunGroups, runGroupsReport } from "../../components/run-groups";
73
75
  import { deployContext, failedRunExitCode } from "../../components/driver";
74
76
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
75
77
  import { formatError, formatInfo, formatWarning } from "../format";
@@ -234,6 +236,8 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
234
236
  return 1;
235
237
  }
236
238
 
239
+ // #4516: one registry finds the declared reads (a template's Fn::ImportValue) that order the fan-out and runs it.
240
+ const registry = await fanOutRegistry(projectPath, config);
237
241
  let derived;
238
242
  try {
239
243
  derived = await deriveFanOut({
@@ -242,6 +246,7 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
242
246
  sandbox: args.sandbox,
243
247
  buildParams: paramsResolution.provenance,
244
248
  config,
249
+ registry,
245
250
  ...(args.canary?.length ? { canary: args.canary } : {}),
246
251
  });
247
252
  } catch (err) {
@@ -387,7 +392,6 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
387
392
  }
388
393
  }
389
394
 
390
- const registry = await fanOutRegistry(projectPath, config);
391
395
  const env = args.env ?? "local";
392
396
 
393
397
  // A completed component's recorded outputs win over a `--seed-outputs`
@@ -450,10 +454,15 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
450
454
  const release = autoReleaseRun(`fan-out-${Date.now()}`);
451
455
  const events = await openCliRunEvents(args, { kind: "component", name: `fan-out ${derived.plan.order.join(",")}`, runId: release.runId, cwd: projectPath });
452
456
 
457
+ // ws-107 (#4465): the components a capability takes as one plan in their
458
+ // wave and apply as one group; under --wave-gate none forms (ruling 1).
459
+ const runPlan = await formRunGroups({ components: derived.components, plan: derived.plan, registry, env, ...(waveGate ? { waveGate: true } : {}) });
460
+
453
461
  let result;
454
462
  try {
455
- result = await runFanOut(derived.plan, derived.components, registry, {
463
+ result = await runFanOut(runPlan, derived.components, registry, {
456
464
  env,
465
+ ...(runPlan.groups?.length ? { planGroupsAtWave: true } : {}),
457
466
  releaseIdentity: () => (revision ? { revision } : {}),
458
467
  componentOutputs,
459
468
  ...(targets ? { targets } : {}),
@@ -524,7 +533,12 @@ async function fanOut(ctx: CommandContext, projectPath: string, config: ChantCon
524
533
  recordAttempt();
525
534
 
526
535
  if (args.json) renderFanOutJson(result);
527
- else renderFanOutHuman(result, { ...(gate ? { gate } : {}), ...(waveGate ? { waveGate } : {}) });
536
+ else {
537
+ renderFanOutHuman(result, { ...(gate ? { gate } : {}), ...(waveGate ? { waveGate } : {}) });
538
+ // #4465: the groups applied as one, and how each member ended.
539
+ const report = runGroupsReport({ grouped: result.plan.groups?.length || result.plan.fallbacks?.length ? result.plan : runPlan, ...(result.groupOutcomes ? { outcomes: result.groupOutcomes } : {}) });
540
+ for (const line of renderRunGroups(report, "the same fan-out with --resume")) console.error(formatInfo(line));
541
+ }
528
542
 
529
543
  // A wave whose set moved after its approval stops with both digests named
530
544
  // (#3049), the #2300 refusal on a set. Printed on stderr even under --json,
@@ -963,6 +963,48 @@ describe("runGraph", () => {
963
963
  expect(node?.physicalId).toBeUndefined();
964
964
  });
965
965
 
966
+ // #4452 — which read answered each root reaches the IR as the lexicon's
967
+ // structured report, so behold reads `rootReads` instead of matching the
968
+ // stderr notes. Asked the way behold asks for a plan read: `--overlay
969
+ // --detail 3`, whose overlay and detail passes each build a fresh IR.
970
+ test("a plan read's IR carries rootReads, a fallback with its reason", async () => {
971
+ resolveLexMock.mockResolvedValue(["terraform"]);
972
+ loadPluginsMock.mockResolvedValue([
973
+ { name: "terraform", serializer: {}, describeResources: () => Promise.resolve({}) },
974
+ ]);
975
+ discoverMock.mockResolvedValue({ entities: new Map(), errors: [], sourceFiles: [] });
976
+ const rootReads = {
977
+ "terraform.roots.app": { depth: "plan" },
978
+ "terraform.roots.edge": { depth: "plan", fellBackTo: "state", reason: "no-credentials" },
979
+ };
980
+ observeMock.mockResolvedValue({
981
+ observations: [{ lexicon: "terraform", resources: { "app/aws_s3_bucket.logs": { type: "aws_s3_bucket", status: "OK" } } }],
982
+ errors: [],
983
+ warnings: [],
984
+ sources: { "app/aws_s3_bucket.logs": "plan" },
985
+ rootReads,
986
+ });
987
+ const exit = await runGraph({ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", detail: 3 }), plugins: [], serializers: [] });
988
+ expect(exit).toBe(0);
989
+ const ir = JSON.parse(stdoutBuf.join("\n")) as { version: number; rootReads?: unknown };
990
+ expect(ir.version).toBe(GRAPH_IR_VERSION);
991
+ expect(ir.rootReads).toEqual(rootReads);
992
+ });
993
+
994
+ test("an IR whose read reported no root reads has no rootReads", async () => {
995
+ resolveLexMock.mockResolvedValue(["aws"]);
996
+ loadPluginsMock.mockResolvedValue([
997
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
998
+ ]);
999
+ observeMock.mockResolvedValue({
1000
+ observations: [{ lexicon: "aws", resources: { "web-vpc": { type: "AWS::EC2::VPC", status: "OK" } } }],
1001
+ errors: [],
1002
+ warnings: [],
1003
+ });
1004
+ expect(await runGraph({ args: makeArgs({ format: "ir", live: true, env: "prod" }), plugins: [], serializers: [] })).toBe(0);
1005
+ expect(JSON.parse(stdoutBuf.join("\n"))).not.toHaveProperty("rootReads");
1006
+ });
1007
+
966
1008
  // #2377 — the overlay carries the prediction. behold's reader takes
967
1009
  // `attrs._behaviour` per node and `meta._behaviour` on the graph, and
968
1010
  // reads the absence of both as "not looked" rather than as a refusal, so