@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,64 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ canonicalScope,
4
+ compactObservedAt,
5
+ encodeIndexSegment,
6
+ indexScopeKey,
7
+ indexVersionName,
8
+ isIndexVersionName,
9
+ scopeDigest,
10
+ validateIndexDocument,
11
+ type IndexDocument,
12
+ type IndexScope,
13
+ } from "./index-document";
14
+
15
+ // The store layout ruled on INTENTIUS/behold#1025 (2026-10-26), as #4519 builds it.
16
+ describe("index keys", () => {
17
+ const cloud: IndexScope = { kind: "cloud", substrate: "aws", scopeId: "123456789012", locations: ["us-west-2", "us-east-1", "us-east-1"], types: "*" };
18
+
19
+ test("two reads with the same flags share a scope path, in any order", () => {
20
+ const reordered: IndexScope = { ...cloud, locations: ["us-east-1", "us-west-2"] };
21
+ expect(scopeDigest(cloud)).toBe(scopeDigest(reordered));
22
+ expect(indexScopeKey(cloud)).toBe(indexScopeKey(reordered));
23
+ expect(canonicalScope(cloud)).toEqual({ kind: "cloud", substrate: "aws", scopeId: "123456789012", locations: ["us-east-1", "us-west-2"], types: "*" });
24
+ });
25
+
26
+ test("other regions or types write under another scope path", () => {
27
+ expect(indexScopeKey({ ...cloud, locations: ["us-east-1"] })).not.toBe(indexScopeKey(cloud));
28
+ expect(indexScopeKey({ ...cloud, types: ["AWS::S3::Bucket"] })).not.toBe(indexScopeKey(cloud));
29
+ });
30
+
31
+ test("index/<source>/<scopeId>/<scope digest>, each segment percent-encoded", () => {
32
+ expect(indexScopeKey(cloud)).toMatch(/^cloud\/aws\/123456789012\/[0-9a-f]{12}$/);
33
+ const gcp: IndexScope = { kind: "cloud", substrate: "gcp", scopeId: "projects/p:1", locations: "*", types: "*" };
34
+ expect(indexScopeKey(gcp)).toMatch(/^cloud\/gcp\/projects%2Fp%3A1\/[0-9a-f]{12}$/);
35
+ expect(indexScopeKey({ kind: "engine", engine: "plunge", project: "web", stack: "prod" })).toMatch(/^engine\/plunge\/web\/prod\/[0-9a-f]{12}$/);
36
+ expect(indexScopeKey({ kind: "chant", member: ".", env: "prod", lexicon: "aws" })).toMatch(/^chant\/%2E\/prod\/-\/aws\/[0-9a-f]{12}$/);
37
+ expect(encodeIndexSegment("..")).toBe("%2E%2E");
38
+ expect(() => encodeIndexSegment("")).toThrow();
39
+ });
40
+
41
+ test("a version is <observedAt>-<digest12>.json and names sort in time order", () => {
42
+ const digest = "a".repeat(64);
43
+ expect(compactObservedAt("2026-10-26T09:14:03.120Z")).toBe("20261026T091403Z");
44
+ expect(indexVersionName("2026-10-26T09:14:03.120Z", digest)).toBe("20261026T091403Z-aaaaaaaaaaaa.json");
45
+ expect(indexVersionName("2026-10-26T09:14:03Z", `sha256:${"b".repeat(64)}`)).toBe("20261026T091403Z-bbbbbbbbbbbb.json");
46
+ expect(isIndexVersionName("20261026T091403Z-aaaaaaaaaaaa.json")).toBe(true);
47
+ expect(isIndexVersionName("scope.json")).toBe(false);
48
+ expect(indexVersionName("2026-10-26T09:00:00Z", digest) < indexVersionName("2026-10-26T10:00:00Z", digest)).toBe(true);
49
+ expect(() => indexVersionName("yesterday", digest)).toThrow();
50
+ expect(() => indexVersionName("2026-10-26T09:00:00Z", "nothex")).toThrow();
51
+ });
52
+
53
+ test("a stamp that disagrees with itself is refused", () => {
54
+ const doc = {
55
+ index: "v1",
56
+ stamp: { source: { kind: "cloud", name: "behold-lister" }, scope: cloud, scopeKey: indexScopeKey(cloud), observedAt: "2026-10-26T00:00:00Z", digest: "c".repeat(64) },
57
+ coverage: [],
58
+ graph: { version: 1, nodes: [], edges: [] },
59
+ } as unknown as IndexDocument;
60
+ expect(() => validateIndexDocument(doc)).not.toThrow();
61
+ expect(() => validateIndexDocument({ ...doc, stamp: { ...doc.stamp, scopeKey: "cloud/aws/x" } })).toThrow(/scopeKey/);
62
+ expect(() => validateIndexDocument({ ...doc, stamp: { ...doc.stamp, source: { kind: "engine", name: "plunge" } } })).toThrow(/source kind/);
63
+ });
64
+ });
@@ -0,0 +1,299 @@
1
+ /**
2
+ * An index document and where the index store keeps it (#4519, layout ruled
3
+ * on INTENTIUS/behold#1025, 2026-10-26).
4
+ *
5
+ * An index is an observed graph: a chant snapshot, a cloud listing typed
6
+ * through the lexicons (behold#1029, #4521), or a graph an engine prints
7
+ * (choudoufu, plunge). Every index is derived, stamped, never read for a
8
+ * decision and safe to lose. A document is a `GraphIR` inside a stamped
9
+ * envelope, never a second graph format.
10
+ *
11
+ * This module is pure: types, the canonical scope digest and the store key.
12
+ * ./snapshot-store.ts maps the keys onto a medium.
13
+ *
14
+ * Store key of an index version, relative to the store's `index` family:
15
+ *
16
+ * <source path>/<scope path>/<observedAt>-<digest12>.json
17
+ *
18
+ * - source path: `cloud/<substrate>`, `engine/<engine>`, or `chant`;
19
+ * - scope path: the scope's identity segments, then the first 12 hex of the
20
+ * scope digest: `<scopeId>/<scope digest>` for a cloud read,
21
+ * `<estate>/<scope digest>` or `<project>/<stack>/<scope digest>` for an
22
+ * engine, `<member>/<env>/<stack or ->/<lexicon>/<scope digest>` for chant.
23
+ * Each segment is percent-encoded, so `/` and `:` in a name survive a git
24
+ * path and an S3 key;
25
+ * - version: the read's start as `yyyymmddThhmmssZ` and the first 12 hex of
26
+ * the document digest. A version is written once and never overwritten.
27
+ *
28
+ * Two reads with the same scope write under one scope path, and a read with
29
+ * other regions, types or filters writes under another, so a reader refuses a
30
+ * scope mismatch by comparing one string (`stamp.scopeKey`).
31
+ */
32
+ import { createHash } from "node:crypto";
33
+ import type { GraphIR } from "../graph-ir";
34
+ import type { UnobservedReason } from "../observation";
35
+ import { NEXT_RELEASE } from "../release-floors";
36
+
37
+ /** The first chant that writes an index document to the store. */
38
+ export const INDEX_DOCUMENT_FLOOR = NEXT_RELEASE;
39
+
40
+ /** Who produced an index. */
41
+ export type IndexSourceKind = "cloud" | "engine" | "chant";
42
+
43
+ /** A direct cloud read (behold#1029). Lists are sorted and deduplicated by {@link canonicalScope}. */
44
+ export interface CloudIndexScope {
45
+ kind: "cloud";
46
+ substrate: "aws" | "azure" | "gcp" | "kubernetes";
47
+ /** The account, subscription, project or cluster. */
48
+ scopeId: string;
49
+ /** Regions or locations, or `"*"` for every one. */
50
+ locations: readonly string[] | "*";
51
+ /** Listed types, or `"*"` for every one. */
52
+ types: readonly string[] | "*";
53
+ /** A tag or label filter, when the read had one. */
54
+ tagFilter?: Readonly<Record<string, string>>;
55
+ }
56
+
57
+ /** A graph an engine printed (choudoufu, plunge). */
58
+ export interface EngineIndexScope {
59
+ kind: "engine";
60
+ engine: string;
61
+ /** choudoufu's estate. */
62
+ estate?: string;
63
+ /** plunge's project and stack. */
64
+ project?: string;
65
+ stack?: string;
66
+ /** The backend URL, with any credentials stripped before it reaches here. */
67
+ backend?: string;
68
+ }
69
+
70
+ /** A chant source: a snapshot or `chant graph --format ir`. Today's snapshot key, unchanged. */
71
+ export interface ChantIndexScope {
72
+ kind: "chant";
73
+ /** `.` for a project outside a workspace. */
74
+ member: string;
75
+ env: string;
76
+ stack?: string;
77
+ lexicon: string;
78
+ }
79
+
80
+ export type IndexScope = CloudIndexScope | EngineIndexScope | ChantIndexScope;
81
+
82
+ export interface IndexSource {
83
+ kind: IndexSourceKind;
84
+ /** `behold-lister`, `choudoufu`, `plunge`, `chant`. */
85
+ name: string;
86
+ version?: string;
87
+ }
88
+
89
+ export interface IndexStamp {
90
+ source: IndexSource;
91
+ scope: IndexScope;
92
+ /** The store path the scope maps to ({@link indexScopeKey}): a reader refuses a mismatch by comparing it. */
93
+ scopeKey: string;
94
+ /** When the read started, ISO UTC. */
95
+ observedAt: string;
96
+ /** When the last call returned, ISO UTC. */
97
+ completedAt?: string;
98
+ /** The source repository commit, when there is one. */
99
+ commit?: string;
100
+ /** sha256 hex of the canonical `graph` and `coverage` ({@link indexDocumentDigest}). */
101
+ digest: string;
102
+ }
103
+
104
+ /**
105
+ * What a read looked at, per type and location. A node can be absent only
106
+ * where this says `listed`. `"*"` in `listedType` or `location` means every
107
+ * type or every location (behold#1029's lister writes it for a run with
108
+ * `types` or `locations` of `"*"`); {@link indexCoverageFor} honours it.
109
+ */
110
+ export interface CoverageEntry {
111
+ listedType: string;
112
+ location: string;
113
+ verdict: "listed" | UnobservedReason;
114
+ detail?: string;
115
+ }
116
+
117
+ /** A type with no lister, a lexicon with no catalog, a disabled API. */
118
+ export interface IndexNote {
119
+ kind: string;
120
+ message: string;
121
+ listedType?: string;
122
+ location?: string;
123
+ /** A command the person can run themselves, never run by the source (a GCP API to enable). */
124
+ command?: string;
125
+ }
126
+
127
+ /**
128
+ * The coverage entry that speaks for one (type, location), or undefined when
129
+ * none does (not observed). An exact entry wins over a wildcard one, and a
130
+ * wildcard on one side over a wildcard on both.
131
+ */
132
+ export function indexCoverageFor<E extends Pick<CoverageEntry, "listedType" | "location"> = CoverageEntry>(coverage: readonly E[], listedType: string, location: string): E | undefined {
133
+ let best: E | undefined;
134
+ let bestScore = -1;
135
+ for (const e of coverage) {
136
+ const typeHit = e.listedType === listedType ? 2 : e.listedType === "*" ? 0 : -1;
137
+ const locHit = e.location === location ? 1 : e.location === "*" ? 0 : -1;
138
+ if (typeHit < 0 || locHit < 0) continue;
139
+ const score = typeHit + locHit;
140
+ if (score > bestScore) {
141
+ best = e;
142
+ bestScore = score;
143
+ }
144
+ }
145
+ return best;
146
+ }
147
+
148
+ /** One index version. */
149
+ export interface IndexDocument {
150
+ index: "v1";
151
+ stamp: IndexStamp;
152
+ coverage: CoverageEntry[];
153
+ graph: GraphIR;
154
+ notes?: IndexNote[];
155
+ }
156
+
157
+ const sortUnique = (xs: readonly string[]): string[] => [...new Set(xs)].sort();
158
+
159
+ /**
160
+ * The scope with every list sorted and deduplicated and every absent field
161
+ * left out, so two reads with the same flags give the same digest.
162
+ */
163
+ export function canonicalScope(scope: IndexScope): IndexScope {
164
+ switch (scope.kind) {
165
+ case "cloud":
166
+ return {
167
+ kind: "cloud",
168
+ substrate: scope.substrate,
169
+ scopeId: scope.scopeId,
170
+ locations: scope.locations === "*" ? "*" : sortUnique(scope.locations),
171
+ types: scope.types === "*" ? "*" : sortUnique(scope.types),
172
+ ...(scope.tagFilter && Object.keys(scope.tagFilter).length > 0
173
+ ? { tagFilter: Object.fromEntries(Object.entries(scope.tagFilter).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) }
174
+ : {}),
175
+ };
176
+ case "engine":
177
+ return {
178
+ kind: "engine",
179
+ engine: scope.engine,
180
+ ...(scope.estate !== undefined ? { estate: scope.estate } : {}),
181
+ ...(scope.project !== undefined ? { project: scope.project } : {}),
182
+ ...(scope.stack !== undefined ? { stack: scope.stack } : {}),
183
+ ...(scope.backend !== undefined ? { backend: scope.backend } : {}),
184
+ };
185
+ case "chant":
186
+ return {
187
+ kind: "chant",
188
+ member: scope.member,
189
+ env: scope.env,
190
+ ...(scope.stack !== undefined ? { stack: scope.stack } : {}),
191
+ lexicon: scope.lexicon,
192
+ };
193
+ }
194
+ }
195
+
196
+ /** JSON with object keys sorted at every level. Arrays keep their order. */
197
+ export function canonicalIndexJson(value: unknown): string {
198
+ return JSON.stringify(value, (_k, v: unknown) => {
199
+ if (v && typeof v === "object" && !Array.isArray(v)) {
200
+ return Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
201
+ }
202
+ return v;
203
+ });
204
+ }
205
+
206
+ const sha256 = (s: string): string => createHash("sha256").update(s, "utf8").digest("hex");
207
+
208
+ /** The first 12 hex of sha256 over the canonical scope. */
209
+ export function scopeDigest(scope: IndexScope): string {
210
+ return sha256(canonicalIndexJson(canonicalScope(scope))).slice(0, 12);
211
+ }
212
+
213
+ /** sha256 hex over the canonical `graph` and `coverage`, so an unchanged re-read is cheap to recognise. */
214
+ export function indexDocumentDigest(doc: Pick<IndexDocument, "graph" | "coverage">): string {
215
+ return sha256(canonicalIndexJson({ coverage: doc.coverage, graph: doc.graph }));
216
+ }
217
+
218
+ /** One path segment, percent-encoded so `/`, `:` and `%` in a name survive a git path and an S3 key. */
219
+ export function encodeIndexSegment(segment: string): string {
220
+ if (segment === "") throw new Error("an index key segment is never empty");
221
+ const encoded = encodeURIComponent(segment).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
222
+ // `.` and `..` are valid components to encodeURIComponent and meaningful to a filesystem.
223
+ return encoded === "." ? "%2E" : encoded === ".." ? "%2E%2E" : encoded;
224
+ }
225
+
226
+ /** `cloud/<substrate>`, `engine/<engine>` or `chant`. */
227
+ export function indexSourcePath(scope: IndexScope): string {
228
+ switch (scope.kind) {
229
+ case "cloud":
230
+ return `cloud/${encodeIndexSegment(scope.substrate)}`;
231
+ case "engine":
232
+ return `engine/${encodeIndexSegment(scope.engine)}`;
233
+ case "chant":
234
+ return "chant";
235
+ }
236
+ }
237
+
238
+ /** The scope's identity segments, then its digest. */
239
+ export function indexScopePath(scope: IndexScope): string {
240
+ const digest = scopeDigest(scope);
241
+ let segments: string[];
242
+ switch (scope.kind) {
243
+ case "cloud":
244
+ segments = [scope.scopeId];
245
+ break;
246
+ case "engine":
247
+ segments = scope.estate !== undefined ? [scope.estate] : [scope.project ?? "-", scope.stack ?? "-"];
248
+ break;
249
+ case "chant":
250
+ segments = [scope.member, scope.env, scope.stack ?? "-", scope.lexicon];
251
+ break;
252
+ }
253
+ return [...segments.map(encodeIndexSegment), digest].join("/");
254
+ }
255
+
256
+ /** `<source path>/<scope path>`: what `stamp.scopeKey` holds and what the store lists under. */
257
+ export function indexScopeKey(scope: IndexScope): string {
258
+ return `${indexSourcePath(scope)}/${indexScopePath(scope)}`;
259
+ }
260
+
261
+ /** `2026-10-26T09:14:03.120Z` as `20261026T091403Z`. */
262
+ export function compactObservedAt(observedAt: string): string {
263
+ const t = new Date(observedAt);
264
+ if (Number.isNaN(t.getTime())) throw new Error(`observedAt "${observedAt}" is not a time`);
265
+ return t.toISOString().replace(/[-:]|\.\d{3}/g, "");
266
+ }
267
+
268
+ /** `<observedAt>-<digest12>.json`. Names sort in time order. */
269
+ export function indexVersionName(observedAt: string, digest: string): string {
270
+ const hex = digest.replace(/^sha256:/, "");
271
+ if (!/^[0-9a-f]{12,}$/.test(hex)) throw new Error(`digest "${digest}" is not sha256 hex`);
272
+ return `${compactObservedAt(observedAt)}-${hex.slice(0, 12)}.json`;
273
+ }
274
+
275
+ /** Whether a name in a scope path is a version (and not `scope.json`). */
276
+ export function isIndexVersionName(name: string): boolean {
277
+ return /^\d{8}T\d{6}Z-[0-9a-f]{12}\.json$/.test(name);
278
+ }
279
+
280
+ /** The file beside the versions that holds the scope in full, so a person or `list` can read what a digest means. */
281
+ export const INDEX_SCOPE_FILE = "scope.json";
282
+
283
+ /**
284
+ * Refuse a document whose stamp disagrees with itself: a `scopeKey` that is
285
+ * not its scope's, or a source kind that is not its scope's kind. The store
286
+ * writes only a document that passes, so a path always means its scope.
287
+ */
288
+ export function validateIndexDocument(doc: IndexDocument): void {
289
+ if (doc.index !== "v1") throw new Error(`index document version "${String(doc.index)}" is not v1`);
290
+ const { stamp } = doc;
291
+ if (stamp.source.kind !== stamp.scope.kind) {
292
+ throw new Error(`index stamp source kind "${stamp.source.kind}" does not match its scope kind "${stamp.scope.kind}"`);
293
+ }
294
+ const expected = indexScopeKey(stamp.scope);
295
+ if (stamp.scopeKey !== expected) {
296
+ throw new Error(`index stamp scopeKey "${stamp.scopeKey}" is not its scope's key "${expected}"`);
297
+ }
298
+ indexVersionName(stamp.observedAt, stamp.digest);
299
+ }
@@ -0,0 +1,191 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { parseDriftDocument, type DriftRecord } from "../drift-record";
3
+ import type { DeclaredEntities } from "./deep-observe";
4
+ import type { DeclaredIdentity, IndexIdentity } from "./identity-match";
5
+ import { diffDeclaredAgainstIndex, indexDriftDocument, INDEX_NOT_OBSERVED, type IndexDriftLexicon } from "./index-drift";
6
+ import { indexVersionName } from "./index-document";
7
+ import { acceptDeviations, emptyBaseline } from "./observation-baseline";
8
+ import { typeListing, type ListedResource, type ListingLexicon, type ListingResult } from "./type-listing";
9
+
10
+ // #4526: `chant lifecycle diff <env> --index <ref>`, the deep diff of declared
11
+ // source against an index version, over fixture indexes typed the way
12
+ // behold#1026 writes one (#4521's typeListing), joined as #4525 joins them.
13
+
14
+ const IDENTITY: Record<string, { parts?: string[]; serverAssigned?: boolean; global?: boolean }> = {
15
+ "Store::Bucket": { parts: ["BucketName"] },
16
+ "Iam::Role": { parts: ["RoleName"], global: true },
17
+ "Queue::Queue": { parts: ["QueueUrl"], serverAssigned: true },
18
+ "Kv::Table": { parts: ["TableName"] },
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} 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 typing: ListingLexicon[] = [{ name: "fake", listedTypes: { substrate: "aws", kindFor: (t) => (t in IDENTITY ? t : undefined) } }];
38
+ const lexicons: IndexDriftLexicon[] = [{ name: "fake", indexIdentity: identity }, { name: "other" }];
39
+
40
+ const r = (listedType: string, location: string, nativeId: string, properties: Record<string, unknown> = {}): 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
+ });
49
+
50
+ function listing(resources: ListedResource[], startedAt = "2026-10-26T09:00:00.000Z"): ListingResult {
51
+ return {
52
+ listing: "v1",
53
+ source: { name: "behold-lister", version: "0.0.0-test" },
54
+ // Kv::Table is outside the scope: the index never looked for one.
55
+ scope: { substrate: "aws", scopeId: "000000000000", locations: ["us-east-1"], types: ["Store::Bucket", "Iam::Role", "Queue::Queue"] },
56
+ startedAt,
57
+ completedAt: startedAt.replace(":00.000Z", ":04.000Z"),
58
+ resources,
59
+ coverage: [
60
+ { listedType: "Store::Bucket", location: "us-east-1", verdict: "listed" },
61
+ { listedType: "Iam::Role", location: "global", verdict: "listed" },
62
+ { listedType: "Queue::Queue", location: "us-east-1", verdict: "listed" },
63
+ ],
64
+ notes: [],
65
+ };
66
+ }
67
+
68
+ const bucket = (status: string) => ({ BucketName: "shop-assets", Arn: "arn:aws:s3:::shop-assets", VersioningConfiguration: { Status: status } });
69
+
70
+ /** The index `chant import --index` read: what source was written from. */
71
+ const atImport = typeListing(listing([r("Store::Bucket", "us-east-1", "shop-assets", bucket("Enabled")), r("Iam::Role", "global", "app-runner", { RoleName: "app-runner" })]), typing);
72
+
73
+ /** A later read, after somebody set versioning to Suspended in the console. */
74
+ const afterConsole = typeListing(
75
+ listing([r("Store::Bucket", "us-east-1", "shop-assets", bucket("Suspended")), r("Iam::Role", "global", "app-runner", { RoleName: "app-runner" })], "2026-10-27T10:00:00.000Z"),
76
+ typing,
77
+ );
78
+
79
+ const versionOf = (doc: typeof atImport) => indexVersionName(doc.stamp.observedAt, doc.stamp.digest);
80
+
81
+ /** Source as the import wrote it: no read-only Arn, every other property as indexed. */
82
+ function declared(extra: Array<[string, string, Record<string, unknown>]> = [], lexicon = "fake"): Map<string, DeclaredEntities> {
83
+ const fake: DeclaredEntities = new Map([
84
+ ["assets", { entityType: "Store::Bucket", props: { BucketName: "shop-assets", VersioningConfiguration: { Status: "Enabled" } } }],
85
+ ["runner", { entityType: "Iam::Role", props: { RoleName: "app-runner" } }],
86
+ ]);
87
+ const out = new Map<string, DeclaredEntities>([[lexicon, fake]]);
88
+ for (const [name, type, props] of extra) fake.set(name, { entityType: type, props });
89
+ return out;
90
+ }
91
+
92
+ const run = (doc: typeof atImport, entities = declared(), more: Partial<Parameters<typeof diffDeclaredAgainstIndex>[0]> = {}) =>
93
+ diffDeclaredAgainstIndex({ doc, version: versionOf(doc), entities, lexicons, member: "shop", region: "us-east-1", ...more });
94
+
95
+ const record = (records: DriftRecord[], address: string) => records.find((x) => x.address === address)!;
96
+
97
+ describe("diffDeclaredAgainstIndex", () => {
98
+ test("source imported from an index reads back with no drift against that index", () => {
99
+ const res = run(atImport);
100
+ expect(res.counts).toMatchObject({ declared: 2, bound: 2, properties: 0, drifted: 0, pending: 0, notObserved: 0 });
101
+ expect(res.records.map((x) => [x.address, x.status])).toEqual([
102
+ ["assets", "in-sync"],
103
+ ["runner", "in-sync"],
104
+ ]);
105
+ // The read-only Arn is indexed and never declared: somebody else's field, not drift.
106
+ expect(res.byLexicon.fake.deep.unclaimed).toEqual([expect.objectContaining({ name: "assets", fields: [expect.objectContaining({ path: "Arn" })] })]);
107
+ });
108
+
109
+ test("a property changed in the console between import and a later read is drift on that declared property", () => {
110
+ const res = run(afterConsole);
111
+ expect(res.counts).toMatchObject({ bound: 2, properties: 1, drifted: 1 });
112
+ const assets = record(res.records, "assets");
113
+ expect(assets).toMatchObject({
114
+ status: "drifted",
115
+ read: "live-read",
116
+ resourceType: "Store::Bucket",
117
+ observedAt: afterConsole.stamp.observedAt,
118
+ attributes: [{ path: "VersioningConfiguration.Status", before: "Enabled", after: "Suspended" }],
119
+ });
120
+ expect(assets.evidence.command).toBe(`chant lifecycle diff --index ${afterConsole.stamp.scopeKey}@${versionOf(afterConsole).replace(/\.json$/, "")}`);
121
+ expect(record(res.records, "runner").status).toBe("in-sync");
122
+ });
123
+
124
+ test("an accepted deviation is quiet until the indexed value moves again", () => {
125
+ const baseline = acceptDeviations(emptyBaseline("dev"), "fake", [{ entity: "assets", type: "Store::Bucket", path: "VersioningConfiguration.Status", value: "Suspended" }], {
126
+ now: "2026-10-27T11:00:00.000Z",
127
+ });
128
+ const quiet = run(afterConsole, declared(), { baseline });
129
+ expect(quiet.counts).toMatchObject({ properties: 0, drifted: 0, accepted: 1 });
130
+ expect(record(quiet.records, "assets").status).toBe("in-sync");
131
+ expect(quiet.byLexicon.fake.deep.accepted).toEqual([expect.objectContaining({ name: "assets" })]);
132
+
133
+ const movedAgain = typeListing(
134
+ listing([r("Store::Bucket", "us-east-1", "shop-assets", { ...bucket("Enabled"), VersioningConfiguration: { Status: "Disabled" } })], "2026-10-28T10:00:00.000Z"),
135
+ typing,
136
+ );
137
+ const loud = run(movedAgain, declared(), { baseline });
138
+ expect(loud.byLexicon.fake.deep.drifted[0]?.changes[0]).toMatchObject({ path: "VersioningConfiguration.Status", declared: "Enabled", live: "Disabled", baseline: "Suspended" });
139
+ expect(record(loud.records, "assets")).toMatchObject({ status: "drifted", attributes: [{ path: "VersioningConfiguration.Status", before: "Enabled", after: "Disabled" }] });
140
+ });
141
+
142
+ test("an entity whose resource the index did not cover is not observed, never diffed", () => {
143
+ const res = run(afterConsole, declared([["sessions", "Kv::Table", { TableName: "sessions", BillingMode: "PAY_PER_REQUEST" }]]));
144
+ const sessions = record(res.records, "sessions");
145
+ expect(sessions).toMatchObject({ status: INDEX_NOT_OBSERVED, unobserved: { reason: "filtered" }, evidence: { engineCode: "not-observed" } });
146
+ expect(sessions.attributes).toBeUndefined();
147
+ expect(res.byLexicon.fake.deep.drifted.map((d) => d.name)).toEqual(["assets"]);
148
+ expect(res.byLexicon.fake.deep.unchanged).not.toContain("sessions");
149
+ expect(res.counts.notObserved).toBe(1);
150
+ });
151
+
152
+ test("absent where the index listed the type and location is pending, with the verdict", () => {
153
+ const res = run(atImport, declared([["archive", "Store::Bucket", { BucketName: "never-made" }]]));
154
+ expect(record(res.records, "archive")).toMatchObject({ status: "pending", evidence: { engineCode: "absent" } });
155
+ expect(res.counts.pending).toBe(1);
156
+ });
157
+
158
+ test("a row with no identity to join on, or two candidates, is reported with its verdict and never diffed", () => {
159
+ const res = run(atImport, declared([["orders", "Queue::Queue", { QueueName: "orders" }]]));
160
+ expect(record(res.records, "orders")).toMatchObject({ status: "not-observed", unobserved: { reason: "no-binding" }, evidence: { engineCode: "no-binding" } });
161
+
162
+ const twice = typeListing(
163
+ {
164
+ ...listing([r("Store::Bucket", "us-east-1", "shop-assets", bucket("Enabled")), r("Store::Bucket", "us-west-2", "shop-assets", bucket("Enabled"))]),
165
+ scope: { substrate: "aws", scopeId: "000000000000", locations: ["us-east-1", "us-west-2"], types: ["Store::Bucket"] },
166
+ },
167
+ typing,
168
+ );
169
+ const amb = diffDeclaredAgainstIndex({ doc: twice, version: versionOf(twice), entities: declared(), lexicons, member: "shop" });
170
+ expect(record(amb.records, "assets")).toMatchObject({ status: "not-observed", unobserved: { reason: "no-binding", detail: expect.stringContaining("none is bound") }, evidence: { engineCode: "ambiguous" } });
171
+ expect(amb.byLexicon.fake.deep.drifted).toEqual([]);
172
+ });
173
+
174
+ test("a lexicon with no index match is not observed on every row", () => {
175
+ const res = run(atImport, declared([], "other"));
176
+ expect(res.records.every((x) => x.status === "not-observed" && x.unobserved?.reason === "unsupported-kind")).toBe(true);
177
+ expect(res.counts).toMatchObject({ declared: 2, bound: 0, notObserved: 2 });
178
+ });
179
+
180
+ test("a stack's records are addressed as search ids are, and the records are one drift document v1", () => {
181
+ const res = run(afterConsole, declared([["sessions", "Kv::Table", { TableName: "sessions" }]]), { stack: "east" });
182
+ expect(res.records.map((x) => x.address)).toEqual(["east::assets", "east::runner", "east::sessions"]);
183
+ const doc = indexDriftDocument([res], "0.0.0-test");
184
+ expect(doc).toMatchObject({ drift: "v1", chant: "0.0.0-test", counts: { drifted: 1, "in-sync": 1, "not-observed": 1 } });
185
+ expect(parseDriftDocument(JSON.parse(JSON.stringify(doc)))).toEqual(doc);
186
+ });
187
+
188
+ test("the same index, source and baseline give the same records", () => {
189
+ expect(JSON.stringify(run(afterConsole).records)).toBe(JSON.stringify(run(afterConsole).records));
190
+ });
191
+ });