@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,308 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { reconstructIdentityEdges, type IndexedNodeScope } from "../graph-refs";
3
+ import type { IRNode } from "../graph-ir";
4
+ import { deriveEdges, indexedNodeScope, type EdgeLexicon } from "./derive-edges";
5
+ import { indexCoverageVerdict, matchDeclaredToIndex, type DeclaredIdentity, type IndexIdentity } from "./identity-match";
6
+ import type { IndexDocument } from "./index-document";
7
+ import { typeListing, type ListedResource, type ListingLexicon, type ListingResult } from "./type-listing";
8
+
9
+ // #4523: the tier-2 rule from INTENTIUS/behold#1025's design comment, with
10
+ // the join rules ruled on INTENTIUS/behold#1023 (2026-10-26).
11
+
12
+ /** A fake lexicon's identity table: type -> its identifying props, or why it has none. */
13
+ const IDENTITY: Record<string, { parts?: string[]; serverAssigned?: boolean; global?: boolean }> = {
14
+ "Store::Bucket": { parts: ["BucketName"] },
15
+ "Iam::Role": { parts: ["RoleName"], global: true },
16
+ "Queue::Queue": { parts: ["QueueUrl"], serverAssigned: true },
17
+ "Kv::Table": { parts: ["TableName"] },
18
+ "Net::Route": { parts: ["RouteTableId", "DestinationCidrBlock"] },
19
+ };
20
+
21
+ const identity: IndexIdentity = {
22
+ substrate: "aws",
23
+ declaredIdentity(entityType, props): DeclaredIdentity {
24
+ const row = IDENTITY[entityType];
25
+ if (!row?.parts) return { kind: "unmatchable", reason: "unsupported-kind", detail: `${entityType} has no primary identifier on record` };
26
+ if (row.serverAssigned) return { kind: "unmatchable", reason: "no-binding", detail: `${entityType} ${row.parts.join(", ")} is assigned by the service` };
27
+ const values: string[] = [];
28
+ for (const p of row.parts) {
29
+ const v = props[p];
30
+ if (typeof v !== "string" || v === "") return { kind: "unmatchable", reason: "no-binding", detail: "identity is not in the declaration" };
31
+ values.push(v);
32
+ }
33
+ return { kind: "identifier", listedType: entityType, identifier: values.join("|"), global: row.global === true };
34
+ },
35
+ };
36
+
37
+ const KINDS = [...Object.keys(IDENTITY), "Store::BucketPolicy", "Fn::Function", "Logs::Group"];
38
+ const typing: ListingLexicon[] = [{ name: "fake", listedTypes: { substrate: "aws", kindFor: (t) => (KINDS.includes(t) ? t : undefined) } }];
39
+
40
+ const r = (listedType: string, location: string, nativeId: string, properties: Record<string, unknown> = {}, extra: Partial<ListedResource> = {}): ListedResource => ({
41
+ listedType,
42
+ location,
43
+ nativeId,
44
+ cloudIds: [{ kind: "import-id", value: nativeId }],
45
+ tags: {},
46
+ properties,
47
+ observedAt: "2026-10-26T09:00:01.000Z",
48
+ ...extra,
49
+ });
50
+
51
+ function listing(resources: ListedResource[], extra: Partial<ListingResult> = {}): ListingResult {
52
+ return {
53
+ listing: "v1",
54
+ source: { name: "behold-lister", version: "0.0.0-test" },
55
+ scope: { substrate: "aws", scopeId: "000000000000", locations: ["us-east-1", "us-west-2"], types: ["Store::Bucket", "Iam::Role", "Queue::Queue", "Net::Route"] },
56
+ startedAt: "2026-10-26T09:00:00.000Z",
57
+ completedAt: "2026-10-26T09:00:04.000Z",
58
+ resources,
59
+ coverage: [
60
+ { listedType: "Store::Bucket", location: "us-east-1", verdict: "listed" },
61
+ { listedType: "Store::Bucket", location: "us-west-2", verdict: "listed" },
62
+ { listedType: "Iam::Role", location: "global", verdict: "listed" },
63
+ { listedType: "Queue::Queue", location: "us-east-1", verdict: "listed" },
64
+ { listedType: "Net::Route", location: "us-east-1", verdict: "read-failed", detail: "ListResources AWS::EC2::Route needs a RouteTableId" },
65
+ ],
66
+ notes: [],
67
+ ...extra,
68
+ };
69
+ }
70
+
71
+ const RESOURCES: ListedResource[] = [
72
+ r("Store::Bucket", "us-east-1", "shop-assets", { BucketName: "shop-assets" }),
73
+ // The same name in another region (a second listing of a global-namespace type, or a replica).
74
+ r("Store::Bucket", "us-west-2", "shop-assets", { BucketName: "shop-assets" }),
75
+ r("Store::Bucket", "us-east-1", "shop-logs", { BucketName: "shop-logs" }),
76
+ r("Iam::Role", "global", "app-runner", { RoleName: "app-runner" }, { cloudIds: [{ kind: "arn", value: "arn:aws:iam::000000000000:role/app-runner" }] }),
77
+ r("Queue::Queue", "us-east-1", "https://sqs.us-east-1.amazonaws.com/000000000000/orders", { QueueName: "orders" }),
78
+ ];
79
+
80
+ const doc = typeListing(listing(RESOURCES), typing);
81
+ const id = (location: string, type: string, nativeId: string) => `aws/000000000000/${location}/${type}/${nativeId}`;
82
+ const declared = (name: string, entityType: string, props: Record<string, unknown>, region?: string) => ({ name, entityType, props, ...(region ? { region } : {}) });
83
+
84
+ describe("matchDeclaredToIndex", () => {
85
+ const { matches } = matchDeclaredToIndex(
86
+ [
87
+ declared("assets", "Store::Bucket", { BucketName: "shop-assets" }),
88
+ declared("runner", "Iam::Role", { RoleName: "app-runner" }),
89
+ declared("missing", "Store::Bucket", { BucketName: "shop-never-made" }),
90
+ declared("orders", "Queue::Queue", { QueueName: "orders" }),
91
+ declared("table", "Kv::Table", { TableName: "carts" }),
92
+ declared("route", "Net::Route", { RouteTableId: "rtb-1", DestinationCidrBlock: "0.0.0.0/0" }),
93
+ declared("byRef", "Store::Bucket", { BucketName: { Ref: "Name" } }),
94
+ declared("unknown", "Made::Up", { Name: "x" }),
95
+ ],
96
+ [doc],
97
+ identity,
98
+ { region: "us-east-1" },
99
+ );
100
+
101
+ test("one node binds, by the identifier the declaration spells", () => {
102
+ expect(matches.assets).toMatchObject({ verdict: "bound", node: id("us-east-1", "Store::Bucket", "shop-assets"), nativeId: "shop-assets", location: "us-east-1" });
103
+ });
104
+
105
+ test("a global type binds at location global, whatever the entity's region", () => {
106
+ expect(matches.runner).toMatchObject({ verdict: "bound", node: id("global", "Iam::Role", "app-runner"), location: "global" });
107
+ });
108
+
109
+ test("no node where the index listed the type in that region is absent", () => {
110
+ expect(matches.missing).toEqual({
111
+ entity: "missing",
112
+ entityType: "Store::Bucket",
113
+ verdict: "absent",
114
+ identifier: "shop-never-made",
115
+ listedType: "Store::Bucket",
116
+ location: "us-east-1",
117
+ });
118
+ });
119
+
120
+ test("a server-assigned identity cannot match: not observed, no-binding", () => {
121
+ expect(matches.orders).toMatchObject({ verdict: "not-observed", reason: "no-binding" });
122
+ expect(matches.byRef).toMatchObject({ verdict: "not-observed", reason: "no-binding", detail: "identity is not in the declaration" });
123
+ });
124
+
125
+ test("a type outside the index's scope is not observed (filtered), never absent", () => {
126
+ expect(matches.table).toMatchObject({ verdict: "not-observed", reason: "filtered", listedType: "Kv::Table", location: "us-east-1" });
127
+ expect(matches.table.verdict).not.toBe("absent");
128
+ });
129
+
130
+ test("coverage that did not list carries its own reason and detail", () => {
131
+ expect(matches.route).toMatchObject({
132
+ verdict: "not-observed",
133
+ reason: "read-failed",
134
+ detail: "ListResources AWS::EC2::Route needs a RouteTableId",
135
+ identifier: "rtb-1|0.0.0.0/0",
136
+ });
137
+ });
138
+
139
+ test("a type with no identity on record is unsupported", () => {
140
+ expect(matches.unknown).toMatchObject({ verdict: "not-observed", reason: "unsupported-kind" });
141
+ });
142
+
143
+ test("two candidates are refused, naming both, and nothing binds", () => {
144
+ const { matches: m } = matchDeclaredToIndex([declared("assets", "Store::Bucket", { BucketName: "shop-assets" })], [doc], identity);
145
+ expect(m.assets).toMatchObject({
146
+ verdict: "refused",
147
+ candidates: [id("us-east-1", "Store::Bucket", "shop-assets"), id("us-west-2", "Store::Bucket", "shop-assets")],
148
+ });
149
+ expect(m.assets.verdict === "refused" && m.assets.detail).toContain(id("us-west-2", "Store::Bucket", "shop-assets"));
150
+ });
151
+
152
+ test("the entity's own region wins over the option", () => {
153
+ const { matches: m } = matchDeclaredToIndex([declared("west", "Store::Bucket", { BucketName: "shop-assets" }, "us-west-2")], [doc], identity, { region: "us-east-1" });
154
+ expect(m.west).toMatchObject({ verdict: "bound", node: id("us-west-2", "Store::Bucket", "shop-assets") });
155
+ });
156
+
157
+ test("with no region a miss cannot be scoped, so it is not observed", () => {
158
+ const { matches: m } = matchDeclaredToIndex([declared("missing", "Store::Bucket", { BucketName: "shop-never-made" })], [doc], identity);
159
+ expect(m.missing).toMatchObject({ verdict: "not-observed", reason: "no-binding" });
160
+ });
161
+
162
+ test("a region the index never read is not observed (filtered)", () => {
163
+ const { matches: m } = matchDeclaredToIndex([declared("eu", "Store::Bucket", { BucketName: "shop-never-made" }, "eu-west-1")], [doc], identity);
164
+ expect(m.eu).toMatchObject({ verdict: "not-observed", reason: "filtered", location: "eu-west-1" });
165
+ });
166
+
167
+ test("the verdict does not depend on node order", () => {
168
+ const shuffled: IndexDocument = { ...doc, graph: { ...doc.graph, nodes: [...doc.graph.nodes].reverse() } };
169
+ const ents = [declared("assets", "Store::Bucket", { BucketName: "shop-assets" }), declared("missing", "Store::Bucket", { BucketName: "x-never" })];
170
+ expect(matchDeclaredToIndex(ents, [shuffled], identity)).toEqual(matchDeclaredToIndex(ents, [doc], identity));
171
+ });
172
+
173
+ test("documents from two accounts or two reads are refused", () => {
174
+ const other = typeListing(listing(RESOURCES, { scope: { substrate: "aws", scopeId: "111111111111", locations: ["us-east-1"], types: "*" } }), typing);
175
+ expect(() => matchDeclaredToIndex([], [doc, other], identity)).toThrow(/one account/);
176
+ const later = typeListing(listing(RESOURCES, { startedAt: "2026-10-26T10:00:00.000Z" }), typing);
177
+ expect(() => matchDeclaredToIndex([], [doc, later], identity)).toThrow(/one read/);
178
+ });
179
+ });
180
+
181
+ describe("indexCoverageVerdict", () => {
182
+ test('"*" in coverage matches every type and location', () => {
183
+ const wild = typeListing(listing([], { scope: { substrate: "aws", scopeId: "000000000000", locations: "*", types: "*" }, coverage: [{ listedType: "*", location: "*", verdict: "listed" }] }), typing);
184
+ expect(indexCoverageVerdict([wild], "Kv::Table", "ap-south-1")).toEqual({ listed: true });
185
+ });
186
+
187
+ test("an exact refusal beats a wildcard listing", () => {
188
+ const mixed = typeListing(
189
+ listing([], {
190
+ scope: { substrate: "aws", scopeId: "000000000000", locations: "*", types: "*" },
191
+ coverage: [
192
+ { listedType: "*", location: "*", verdict: "listed" },
193
+ { listedType: "Kv::Table", location: "us-east-1", verdict: "no-credentials", detail: "AccessDenied on dynamodb:ListTables" },
194
+ ],
195
+ }),
196
+ typing,
197
+ );
198
+ expect(indexCoverageVerdict([mixed], "Kv::Table", "us-east-1")).toEqual({ listed: false, reason: "no-credentials", detail: "AccessDenied on dynamodb:ListTables" });
199
+ });
200
+
201
+ test("a tag-filtered read cannot say absent", () => {
202
+ const tagged = typeListing(listing([], { scope: { substrate: "aws", scopeId: "000000000000", locations: ["us-east-1"], types: "*", tagFilter: { team: "shop" } }, coverage: [{ listedType: "*", location: "*", verdict: "listed" }] }), typing);
203
+ expect(indexCoverageVerdict([tagged], "Store::Bucket", "us-east-1")).toMatchObject({ listed: false, reason: "filtered" });
204
+ });
205
+
206
+ test("in scope with no coverage entry is not reached", () => {
207
+ const bare = typeListing(listing([], { coverage: [] }), typing);
208
+ expect(indexCoverageVerdict([bare], "Store::Bucket", "us-east-1")).toMatchObject({ listed: false, reason: "not-reached" });
209
+ });
210
+ });
211
+
212
+ describe("the identity-match pass", () => {
213
+ const holders: ListedResource[] = [
214
+ r("Store::Bucket", "us-east-1", "shop-assets", { BucketName: "shop-assets" }),
215
+ r("Store::Bucket", "us-west-2", "shop-assets", { BucketName: "shop-assets" }),
216
+ r("Logs::Group", "us-east-1", "shop-assets", { LogGroupName: "shop-assets" }),
217
+ r("Iam::Role", "global", "app-runner", { RoleName: "app-runner" }, { cloudIds: [{ kind: "arn", value: "arn:aws:iam::000000000000:role/app-runner" }] }),
218
+ r("Fn::Function", "us-east-1", "resize", {
219
+ FunctionName: "resize",
220
+ Role: "arn:aws:iam::000000000000:role/app-runner",
221
+ Environment: { Variables: { LOG_BUCKET: "shop-logs" } },
222
+ Tags: [{ Key: "bucket", Value: "shop-logs" }],
223
+ }),
224
+ r("Store::Bucket", "us-east-1", "shop-logs", { BucketName: "shop-logs" }),
225
+ r("Store::BucketPolicy", "us-east-1", "shop-logs-policy", { Bucket: "shop-logs" }),
226
+ // Names the bucket that exists in two regions, and a log group of the same name in its own: ambiguous.
227
+ r("Fn::Function", "us-east-1", "thumbs", { FunctionName: "thumbs", Environment: { Variables: { ASSETS: "shop-assets" } } }),
228
+ // In us-west-2, the bucket name reaches only the us-west-2 bucket.
229
+ r("Fn::Function", "us-west-2", "mirror", { FunctionName: "mirror", Environment: { Variables: { ASSETS: "shop-assets" } } }),
230
+ ];
231
+ const typed = typeListing(
232
+ listing(holders, { scope: { substrate: "aws", scopeId: "000000000000", locations: ["us-east-1", "us-west-2"], types: "*" }, coverage: [{ listedType: "*", location: "*", verdict: "listed" }] }),
233
+ typing,
234
+ );
235
+ const lexicons: EdgeLexicon[] = [
236
+ {
237
+ name: "fake",
238
+ referenceCatalog: { identities: [{ kind: "Store::Bucket", ids: ["BucketName"] }], refs: [{ from: "Store::BucketPolicy", path: "Bucket", targetKind: "Store::Bucket", relation: "reference" }] },
239
+ indexIdentity: identity,
240
+ },
241
+ ];
242
+ const out = deriveEdges(typed, lexicons);
243
+ const identityEdges = out.graph.edges.filter((e) => e.rule?.startsWith("identity-match:"));
244
+
245
+ test("an attribute equal to another node's identity draws an identity-match edge", () => {
246
+ expect(identityEdges).toContainEqual({
247
+ from: id("us-east-1", "Fn::Function", "resize"),
248
+ to: id("global", "Iam::Role", "app-runner"),
249
+ kind: "ref",
250
+ viaAttr: "Role",
251
+ grain: "property",
252
+ origin: "derived",
253
+ rule: "identity-match:fake:Iam::Role:arn",
254
+ relation: "reference",
255
+ });
256
+ expect(identityEdges).toContainEqual(
257
+ expect.objectContaining({ from: id("us-east-1", "Fn::Function", "resize"), to: id("us-east-1", "Store::Bucket", "shop-logs"), viaAttr: "Environment", rule: "identity-match:fake:Store::Bucket:native-id" }),
258
+ );
259
+ });
260
+
261
+ test("the join key is scoped to the holder's region", () => {
262
+ expect(identityEdges).toContainEqual(
263
+ expect.objectContaining({ from: id("us-west-2", "Fn::Function", "mirror"), to: id("us-west-2", "Store::Bucket", "shop-assets") }),
264
+ );
265
+ expect(identityEdges.some((e) => e.from === id("us-west-2", "Fn::Function", "mirror") && e.to.includes("/us-east-1/"))).toBe(false);
266
+ });
267
+
268
+ test("a value naming two nodes draws no edge and the note names every candidate", () => {
269
+ const thumbs = id("us-east-1", "Fn::Function", "thumbs");
270
+ expect(out.graph.edges.some((e) => e.from === thumbs)).toBe(false);
271
+ const note = out.notes?.find((n) => n.kind === "ambiguous-identity" && n.message.startsWith(thumbs));
272
+ expect(note?.message).toContain(id("us-east-1", "Store::Bucket", "shop-assets"));
273
+ expect(note?.message).toContain(id("us-east-1", "Logs::Group", "shop-assets"));
274
+ });
275
+
276
+ test("a pair the catalog already joined gets no identity-match edge", () => {
277
+ const policy = id("us-east-1", "Store::BucketPolicy", "shop-logs-policy");
278
+ const fromPolicy = out.graph.edges.filter((e) => e.from === policy);
279
+ expect(fromPolicy).toHaveLength(1);
280
+ expect(fromPolicy[0].rule).toBe("fake:Store::BucketPolicy:Bucket");
281
+ });
282
+
283
+ test("tags are never read by the identity pass", () => {
284
+ const resize = id("us-east-1", "Fn::Function", "resize");
285
+ expect(identityEdges.some((e) => e.from === resize && e.viaAttr === "Tags")).toBe(false);
286
+ });
287
+
288
+ test("a node's own identity is not a reference to itself", () => {
289
+ for (const e of identityEdges) expect(e.from).not.toBe(e.to);
290
+ });
291
+
292
+ test("re-deriving gives the same document", () => {
293
+ expect(deriveEdges(out, lexicons)).toEqual(out);
294
+ });
295
+
296
+ test("node order does not change the edges or the ambiguities", () => {
297
+ const scopeOf = (n: IRNode): IndexedNodeScope => indexedNodeScope(typed, n);
298
+ const nodes = typed.graph.nodes;
299
+ const forward = reconstructIdentityEdges(nodes, [{ lexicon: "fake" }], scopeOf);
300
+ const backward = reconstructIdentityEdges([...nodes].reverse(), [{ lexicon: "fake" }], scopeOf);
301
+ expect(backward).toEqual(forward);
302
+ });
303
+
304
+ test("without an indexIdentity the pass draws nothing", () => {
305
+ const plain = deriveEdges(typed, [{ name: "fake", referenceCatalog: lexicons[0].referenceCatalog }]);
306
+ expect(plain.graph.edges.some((e) => e.rule?.startsWith("identity-match:"))).toBe(false);
307
+ });
308
+ });
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Declared entities matched to indexed resources by identity (#4523, tier 2
3
+ * of INTENTIUS/behold#1025, rule written out on behold#1025's design comment,
4
+ * join rules ruled on behold#1023 2026-10-26).
5
+ *
6
+ * A lexicon whose live read needs a stack chant deployed (aws: the
7
+ * CloudFormation stack named after the environment) cannot see source that
8
+ * was never deployed. Its identity fallback (#1647, lexicons/aws/src/
9
+ * identity-observe.ts) asks Cloud Control `GetResource` for the identifier
10
+ * the declared props spell. {@link matchDeclaredToIndex} is that rule run
11
+ * against an index version instead of a live call:
12
+ *
13
+ * A declared entity of type T with props P matches the indexed node whose
14
+ * `listedType` is T, whose `location` is the entity's region (or `global`
15
+ * for a global type), and whose `nativeId` equals the identifier P spells.
16
+ *
17
+ * - one node binds;
18
+ * - two or more are refused, naming every candidate. A refusal never reads
19
+ * as absent or as bound;
20
+ * - no node is absent only when the index's coverage lists (T, location) as
21
+ * `listed` ({@link indexCoverageFor}, where `"*"` matches every type or
22
+ * location). Otherwise the entity is not observed, with the coverage's
23
+ * reason: `filtered` when the scope left the type or location out (or the
24
+ * read had a tag filter, which cannot tell an untagged object from a
25
+ * missing one), `not-reached` when nothing covered it;
26
+ * - an identity the declaration does not spell (a server-assigned id such as
27
+ * an instance id or a queue URL, a Ref, an intrinsic) cannot match: not
28
+ * observed, reason `no-binding`.
29
+ *
30
+ * The lexicon says how its props spell an identifier ({@link IndexIdentity},
31
+ * a plugin's `indexIdentity`); this module holds the rule. Pure.
32
+ */
33
+ import type { IRNode } from "../graph-ir";
34
+ import type { NodeIdentityValue } from "../graph-refs";
35
+ import type { UnobservedReason } from "../observation";
36
+ import { indexCoverageFor, type CloudIndexScope, type IndexDocument } from "./index-document";
37
+
38
+ /** What a declared entity's props say about its identity. */
39
+ export type DeclaredIdentity =
40
+ | {
41
+ kind: "identifier";
42
+ /** The inventory API's type name the identifier belongs to (the CloudFormation type for aws). */
43
+ listedType: string;
44
+ /** The identifier as the index's `nativeId` spells it (Cloud Control's, `|`-joined for a multi-part one). */
45
+ identifier: string;
46
+ /** A global type is indexed once per account at location `global`. */
47
+ global: boolean;
48
+ }
49
+ | {
50
+ kind: "unmatchable";
51
+ /** `no-binding` for an identity the declaration does not spell; `unsupported-kind` for a type with none on record. */
52
+ reason: UnobservedReason;
53
+ detail: string;
54
+ };
55
+
56
+ /**
57
+ * How a lexicon matches its declared entities to its substrate's indexed
58
+ * nodes (a plugin's `indexIdentity`). Data plus pure functions.
59
+ */
60
+ export interface IndexIdentity {
61
+ substrate: CloudIndexScope["substrate"];
62
+ /** The identifier a declared entity's props spell, or why there is none. */
63
+ declaredIdentity(entityType: string, props: Record<string, unknown>): DeclaredIdentity;
64
+ /**
65
+ * The values that name an indexed node of this lexicon, for the
66
+ * identity-match edge pass (../graph-refs.ts `reconstructIdentityEdges`).
67
+ * Absent: its native id and its ARN cloud ids.
68
+ */
69
+ nodeIdentities?(node: IRNode): NodeIdentityValue[];
70
+ }
71
+
72
+ /** One declared entity, as the build output names it. */
73
+ export interface DeclaredEntity {
74
+ name: string;
75
+ entityType: string;
76
+ props: Record<string, unknown>;
77
+ /** The entity's region; falls back to the match's `region` option. */
78
+ region?: string;
79
+ }
80
+
81
+ interface MatchBase {
82
+ entity: string;
83
+ entityType: string;
84
+ }
85
+
86
+ /** The verdict for one declared entity. */
87
+ export type IdentityMatch =
88
+ | (MatchBase & { verdict: "bound"; identifier: string; listedType: string; location: string; node: string; nativeId: string })
89
+ | (MatchBase & { verdict: "refused"; identifier: string; listedType: string; candidates: string[]; detail: string })
90
+ | (MatchBase & { verdict: "absent"; identifier: string; listedType: string; location: string })
91
+ | (MatchBase & { verdict: "not-observed"; reason: UnobservedReason; detail: string; identifier?: string; listedType?: string; location?: string });
92
+
93
+ export interface IdentityMatchResult {
94
+ /** Keyed by entity name, in name order. */
95
+ matches: Record<string, IdentityMatch>;
96
+ }
97
+
98
+ export interface MatchOptions {
99
+ /** The region of an entity that names none. */
100
+ region?: string;
101
+ }
102
+
103
+ const byString = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
104
+
105
+ /** The documents must be cloud reads of one substrate, one account and one read, or a candidate from another read would bind. */
106
+ function cloudScopes(docs: readonly IndexDocument[], substrate: CloudIndexScope["substrate"]): CloudIndexScope[] {
107
+ const scopes: CloudIndexScope[] = [];
108
+ for (const d of docs) {
109
+ const s = d.stamp.scope;
110
+ if (s.kind !== "cloud") throw new Error(`identity matching reads cloud index documents; ${d.stamp.scopeKey} is a ${s.kind} source`);
111
+ if (s.substrate !== substrate) throw new Error(`identity matching for ${substrate} was given a ${s.substrate} index (${d.stamp.scopeKey})`);
112
+ scopes.push(s);
113
+ }
114
+ for (const d of docs) {
115
+ const s = d.stamp.scope as CloudIndexScope;
116
+ if (s.scopeId !== scopes[0].scopeId) throw new Error(`identity matching runs over one account: ${scopes[0].scopeId} and ${s.scopeId} are two`);
117
+ if (d.stamp.observedAt !== docs[0].stamp.observedAt) {
118
+ throw new Error(`identity matching runs over one read: ${docs[0].stamp.observedAt} and ${d.stamp.observedAt} are two`);
119
+ }
120
+ }
121
+ return scopes;
122
+ }
123
+
124
+ function scopeIncludes(list: readonly string[] | "*", value: string): boolean {
125
+ return list === "*" || list.includes(value);
126
+ }
127
+
128
+ /**
129
+ * Whether the index looked at (type, location), from its coverage. `listed`
130
+ * means a missing node is absent; anything else is a reason it is not
131
+ * observed.
132
+ */
133
+ export function indexCoverageVerdict(
134
+ docs: readonly IndexDocument[],
135
+ listedType: string,
136
+ location: string,
137
+ ): { listed: true } | { listed: false; reason: UnobservedReason; detail: string } {
138
+ let firstRefusal: { reason: UnobservedReason; detail: string } | undefined;
139
+ let tagFiltered: string | undefined;
140
+ let scoped = false;
141
+ for (const d of docs) {
142
+ const scope = d.stamp.scope;
143
+ if (scope.kind !== "cloud") continue;
144
+ const entry = indexCoverageFor(d.coverage, listedType, location);
145
+ if (entry?.verdict === "listed") {
146
+ if (scope.tagFilter && Object.keys(scope.tagFilter).length > 0) {
147
+ tagFiltered ??= Object.entries(scope.tagFilter).map(([k, v]) => `${k}=${v}`).join(",");
148
+ continue;
149
+ }
150
+ return { listed: true };
151
+ }
152
+ if (entry && !firstRefusal) {
153
+ firstRefusal = {
154
+ reason: entry.verdict as UnobservedReason,
155
+ detail: entry.detail ?? `the index read ${listedType} in ${location} as ${entry.verdict}`,
156
+ };
157
+ }
158
+ // A global type sits at location `global`, which a region list never names.
159
+ if (scopeIncludes(scope.types, listedType) && (location === "global" || scopeIncludes(scope.locations, location))) scoped = true;
160
+ }
161
+ if (firstRefusal) return { listed: false, ...firstRefusal };
162
+ if (tagFiltered !== undefined) {
163
+ return {
164
+ listed: false,
165
+ reason: "filtered",
166
+ detail: `the index listed ${listedType} in ${location} under the tag filter ${tagFiltered}, so an untagged object and a missing one look the same`,
167
+ };
168
+ }
169
+ if (!scoped) return { listed: false, reason: "filtered", detail: `the index scope left out ${listedType} in ${location}` };
170
+ return { listed: false, reason: "not-reached", detail: `the index has no coverage for ${listedType} in ${location}` };
171
+ }
172
+
173
+ /** The node's listed type: `listedType` on an indexed node, else its kind. */
174
+ const listedTypeOf = (n: IRNode): string => n.listedType ?? n.kind;
175
+
176
+ /**
177
+ * Match each declared entity to the index by identity. `docs` are the
178
+ * documents of one read of one account (one `behold index` run, or its
179
+ * regional and account-level documents); `identity` is the lexicon's.
180
+ */
181
+ export function matchDeclaredToIndex(
182
+ entities: readonly DeclaredEntity[],
183
+ docs: readonly IndexDocument[],
184
+ identity: IndexIdentity,
185
+ options: MatchOptions = {},
186
+ ): IdentityMatchResult {
187
+ cloudScopes(docs, identity.substrate);
188
+
189
+ // listedType -> nativeId -> nodes
190
+ const byIdentity = new Map<string, Map<string, IRNode[]>>();
191
+ for (const d of docs) {
192
+ for (const n of d.graph.nodes) {
193
+ if (n.nativeId === undefined) continue;
194
+ const t = listedTypeOf(n);
195
+ const m = byIdentity.get(t) ?? byIdentity.set(t, new Map()).get(t)!;
196
+ (m.get(n.nativeId) ?? m.set(n.nativeId, []).get(n.nativeId)!).push(n);
197
+ }
198
+ }
199
+
200
+ const matches: Record<string, IdentityMatch> = {};
201
+ for (const entity of [...entities].sort((a, b) => byString(a.name, b.name))) {
202
+ const base = { entity: entity.name, entityType: entity.entityType };
203
+ const declared = identity.declaredIdentity(entity.entityType, entity.props ?? {});
204
+ if (declared.kind === "unmatchable") {
205
+ matches[entity.name] = { ...base, verdict: "not-observed", reason: declared.reason, detail: declared.detail };
206
+ continue;
207
+ }
208
+ const { listedType, identifier } = declared;
209
+ const region = entity.region ?? options.region;
210
+ const location = declared.global ? "global" : region;
211
+ const all = byIdentity.get(listedType)?.get(identifier) ?? [];
212
+ const inScope = location === undefined ? all : all.filter((n) => n.location === location);
213
+ const candidates = [...new Set(inScope.map((n) => n.id))].sort(byString);
214
+
215
+ if (candidates.length > 1) {
216
+ matches[entity.name] = {
217
+ ...base,
218
+ verdict: "refused",
219
+ identifier,
220
+ listedType,
221
+ candidates,
222
+ detail: `${listedType} ${identifier} names ${candidates.length} indexed resources (${candidates.join(", ")}); none is bound`,
223
+ };
224
+ continue;
225
+ }
226
+ if (candidates.length === 1) {
227
+ const node = inScope.find((n) => n.id === candidates[0])!;
228
+ matches[entity.name] = {
229
+ ...base,
230
+ verdict: "bound",
231
+ identifier,
232
+ listedType,
233
+ location: node.location ?? location ?? "",
234
+ node: node.id,
235
+ nativeId: node.nativeId!,
236
+ };
237
+ continue;
238
+ }
239
+ if (location === undefined) {
240
+ // No region, so there is no (type, location) whose coverage could make absence true.
241
+ matches[entity.name] = {
242
+ ...base,
243
+ verdict: "not-observed",
244
+ reason: "no-binding",
245
+ detail: `${entity.name} names no region and no match option gives one, so absence of ${listedType} ${identifier} cannot be scoped`,
246
+ identifier,
247
+ listedType,
248
+ };
249
+ continue;
250
+ }
251
+ const coverage = indexCoverageVerdict(docs, listedType, location);
252
+ matches[entity.name] = coverage.listed
253
+ ? { ...base, verdict: "absent", identifier, listedType, location }
254
+ : { ...base, verdict: "not-observed", reason: coverage.reason, detail: coverage.detail, identifier, listedType, location };
255
+ }
256
+ return { matches };
257
+ }