@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,596 @@
1
+ /**
2
+ * The drift record (#4451): the constructor's rules, redaction, the
3
+ * document and its digest, the block rollup, the NDJSON stream, and the
4
+ * chant adapters over `__fixtures__/drift-record/`.
5
+ */
6
+
7
+ import { readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { describe, expect, test } from "vitest";
10
+ import {
11
+ attributePathKeys,
12
+ componentStatusDriftRecords,
13
+ decodeDriftLine,
14
+ decodeDriftLines,
15
+ deepDiffDriftRecords,
16
+ DRIFT_READS,
17
+ DRIFT_SCHEMA_ID,
18
+ DRIFT_STATUSES,
19
+ driftDocument,
20
+ driftDocumentDigest,
21
+ DriftRecordError,
22
+ driftRecord,
23
+ driftSettle,
24
+ driftRecordLine,
25
+ driftStart,
26
+ encodeDriftLine,
27
+ encodeDriftLines,
28
+ foldDriftLines,
29
+ liveDiffDriftRecords,
30
+ mapDriftToGraph,
31
+ mergeDeepDrift,
32
+ parseDriftDocument,
33
+ DRIFT_INSTANCE_ROLLUP,
34
+ rollupDriftByBlock,
35
+ STATE_READ_STATUSES,
36
+ type DriftLine,
37
+ type DriftRecord,
38
+ type DriftSettleLine,
39
+ type DriftStatus,
40
+ } from "./drift-record";
41
+ import type { DeepDiffResult } from "./lifecycle/deep-diff";
42
+ import type { LiveDiffResult } from "./lifecycle/live-diff";
43
+ import type { ComponentStatusRow } from "./lifecycle/status";
44
+ import { UNOBSERVED_REASONS } from "./observation";
45
+ import { contract, validSchema } from "./workspace/__fixtures__/contract-repo";
46
+ import schema from "./workspace/drift.schema.json";
47
+
48
+ const DIR = join(import.meta.dirname, "__fixtures__", "drift-record");
49
+ const fixture = <T>(name: string): T => JSON.parse(readFileSync(join(DIR, name), "utf-8")) as T;
50
+ const AT = "2026-10-25T12:00:00Z";
51
+ const { expectValid } = contract(schema);
52
+ /** One `$defs` entry of the schema as its own schema, without the `$id` the whole schema already registered. */
53
+ const part = (def: keyof typeof schema.$defs) => contract({ $schema: schema.$schema, $defs: schema.$defs, $ref: `#/$defs/${def}` });
54
+ const recordSchema = part("record");
55
+ const lineSchema = part("line");
56
+
57
+ function rec(over: Partial<DriftRecord> = {}): DriftRecord {
58
+ return {
59
+ member: "estate",
60
+ engine: "terraform",
61
+ address: "aws_eip.pool[0]",
62
+ node: null,
63
+ observedAt: AT,
64
+ read: "plan",
65
+ status: "in-sync",
66
+ evidence: { tool: "terraform" },
67
+ ...over,
68
+ } as DriftRecord;
69
+ }
70
+
71
+ /** JSON out and back through the document reader: what a consumer in another process sees. */
72
+ function roundTrip(records: DriftRecord[]) {
73
+ const doc = driftDocument(records, { chant: "0.124.34" });
74
+ expectValid(doc);
75
+ const back = parseDriftDocument(JSON.parse(JSON.stringify(doc)));
76
+ expect(back).toEqual(doc);
77
+ return doc;
78
+ }
79
+
80
+ const statusOf = (records: DriftRecord[]) => Object.fromEntries(records.map((r) => [r.address, r.status]));
81
+
82
+ describe("the schema", () => {
83
+ test("is a valid schema and names the module's id", () => {
84
+ expect(validSchema(schema)).toBe(true);
85
+ expect(schema.$id).toBe(DRIFT_SCHEMA_ID);
86
+ expect(schema.$defs.record.properties.status.enum).toEqual([...DRIFT_STATUSES]);
87
+ expect(schema.$defs.record.properties.read.enum).toEqual([...DRIFT_READS]);
88
+ expect([...schema.$defs.unobserved.properties.reason.enum].sort()).toEqual([...UNOBSERVED_REASONS].sort());
89
+ });
90
+ });
91
+
92
+ describe("a state read", () => {
93
+ test("never yields in-sync or drifted", () => {
94
+ for (const status of ["in-sync", "drifted"] as const) {
95
+ let err: unknown;
96
+ try {
97
+ driftRecord(rec({ read: "state", status, ...(status === "drifted" ? { attributes: [{ path: "tags.env" }] } : {}) }));
98
+ } catch (e) {
99
+ err = e;
100
+ }
101
+ expect(err).toBeInstanceOf(DriftRecordError);
102
+ expect((err as DriftRecordError).rule).toBe("state-read-status");
103
+ expect((err as Error).message).toContain("aws_eip.pool[0]");
104
+ expect((err as Error).message).toContain(status);
105
+ }
106
+ });
107
+
108
+ test("yields only recorded, pending or not-observed, for every status", () => {
109
+ for (const status of DRIFT_STATUSES) {
110
+ const make = () =>
111
+ driftRecord(rec({ read: "state", status, ...(status === "not-observed" ? { unobserved: { reason: "read-failed" } } : {}) }));
112
+ if (STATE_READ_STATUSES.includes(status)) expect(make().status).toBe(status);
113
+ else expect(make, status).toThrow(DriftRecordError);
114
+ }
115
+ });
116
+
117
+ test("the schema refuses it too", () => {
118
+ expect(recordSchema.validate(rec({ read: "state", status: "in-sync" }))).toBe(false);
119
+ expect(recordSchema.validate(rec({ read: "state", status: "recorded" }))).toBe(true);
120
+ expect(recordSchema.validate(rec({ status: "not-observed" }))).toBe(false);
121
+ });
122
+ });
123
+
124
+ describe("the constructor", () => {
125
+ test("not-observed always carries a legal reason", () => {
126
+ expect(() => driftRecord(rec({ status: "not-observed" }))).toThrow(/not-observed needs unobserved.reason/);
127
+ expect(() => driftRecord(rec({ status: "not-observed", unobserved: { reason: "gone" as never } }))).toThrow(DriftRecordError);
128
+ for (const reason of UNOBSERVED_REASONS) {
129
+ expect(driftRecord(rec({ status: "not-observed", unobserved: { reason } })).unobserved).toEqual({ reason });
130
+ }
131
+ });
132
+
133
+ test("only not-observed carries unobserved, only drifted carries attributes", () => {
134
+ expect(() => driftRecord(rec({ status: "in-sync", unobserved: { reason: "read-failed" } }))).toThrow(/only not-observed/);
135
+ expect(() => driftRecord(rec({ status: "in-sync", attributes: [{ path: "a" }] }))).toThrow(/only drifted/);
136
+ expect(() => driftRecord(rec({ status: "pending", attributesUnobserved: { reason: "read-failed" } }))).toThrow(/attributesUnobserved/);
137
+ });
138
+
139
+ test("refuses a record with no tool, a bad engine or a bad time, naming it", () => {
140
+ expect(() => driftRecord(rec({ evidence: { tool: "" } }))).toThrow(/evidence.tool/);
141
+ expect(() => driftRecord(rec({ engine: "cdk" as never }))).toThrow(/engine "cdk"/);
142
+ expect(() => driftRecord(rec({ observedAt: "yesterday" }))).toThrow(/RFC 3339/);
143
+ });
144
+ });
145
+
146
+ describe("redaction", () => {
147
+ test("a planner's mark drops both values", () => {
148
+ const r = driftRecord(rec({ status: "drifted", attributes: [{ path: "input.token_value", before: "a", after: "b", sensitive: true }] }));
149
+ expect(r.attributes).toEqual([{ path: "input.token_value", sensitive: true }]);
150
+ });
151
+
152
+ test("a secret-bearing key name drops both values even unmarked", () => {
153
+ const r = driftRecord(
154
+ rec({
155
+ status: "drifted",
156
+ attributes: [
157
+ { path: "tags.env", before: "prod", after: "dev" },
158
+ { path: "settings[0].db_password", before: "x", after: "y" },
159
+ { path: 'env["API_TOKEN"]', before: "x", after: "y" },
160
+ { path: "Tags[0].Key", before: "k", after: "l" },
161
+ ],
162
+ }),
163
+ );
164
+ expect(r.attributes).toEqual([
165
+ { path: "Tags[0].Key", before: "k", after: "l" },
166
+ { path: 'env["API_TOKEN"]', sensitive: true },
167
+ { path: "settings[0].db_password", sensitive: true },
168
+ { path: "tags.env", before: "prod", after: "dev" },
169
+ ]);
170
+ });
171
+
172
+ test("path keys skip indexes and read quoted keys", () => {
173
+ expect(attributePathKeys('spec.env[0]["db.password"].value')).toEqual(["spec", "env", "db.password", "value"]);
174
+ expect(attributePathKeys("a")).toEqual(["a"]);
175
+ expect(attributePathKeys("Environment[#DB_PASSWORD].Value")).toEqual(["Environment", "DB_PASSWORD", "Value"]);
176
+ });
177
+ });
178
+
179
+ describe("the document", () => {
180
+ test("keeps the later record for one (member, address, read), sorted, with counts", () => {
181
+ const doc = driftDocument([
182
+ rec({ address: "b", status: "pending" }),
183
+ rec({ address: "a" }),
184
+ rec({ address: "b", status: "in-sync" }),
185
+ rec({ address: "b", read: "ownership", status: "unclaimed" }),
186
+ ]);
187
+ expect(doc.$schema).toBe(DRIFT_SCHEMA_ID);
188
+ expect(doc.drift).toBe("v1");
189
+ expect(doc.records.map((r) => [r.address, r.read, r.status])).toEqual([
190
+ ["a", "plan", "in-sync"],
191
+ ["b", "ownership", "unclaimed"],
192
+ ["b", "plan", "in-sync"],
193
+ ]);
194
+ expect(doc.counts["in-sync"]).toBe(2);
195
+ expect(doc.counts.unclaimed).toBe(1);
196
+ expect(doc.counts.pending).toBe(0);
197
+ });
198
+
199
+ test("its digest leaves out observedAt and evidence and nothing else", () => {
200
+ const a = driftDocument([rec()]);
201
+ const later = driftDocument([rec({ observedAt: "2026-10-26T00:00:00Z", evidence: { tool: "tofu", version: "1.9" } })]);
202
+ const other = driftDocument([rec({ status: "pending" })]);
203
+ expect(driftDocumentDigest(later)).toBe(driftDocumentDigest(a));
204
+ expect(driftDocumentDigest(other)).not.toBe(driftDocumentDigest(a));
205
+ expect(driftDocumentDigest(a)).toMatch(/^jcs1-sha256:[0-9a-f]{64}$/);
206
+ });
207
+
208
+ test("a document read back refuses a state read painted green", () => {
209
+ const doc = JSON.parse(JSON.stringify(driftDocument([rec({ read: "state", status: "recorded" })])));
210
+ doc.records[0].status = "in-sync";
211
+ expect(() => parseDriftDocument(doc)).toThrow(/state read/);
212
+ });
213
+ });
214
+
215
+ describe("the block rollup", () => {
216
+ const block = (...statuses: DriftStatus[]) =>
217
+ rollupDriftByBlock(
218
+ statuses.map((status, i) =>
219
+ rec({
220
+ address: `aws_eip.pool[${i}]`,
221
+ node: `estate/aws_eip.pool[${i}]`,
222
+ instanceOf: "estate/aws_eip.pool",
223
+ status,
224
+ ...(status === "not-observed" ? { unobserved: { reason: "read-failed" } } : {}),
225
+ }),
226
+ ),
227
+ )[0]?.status;
228
+
229
+ test("not-observed wins, then a difference, then an absence, then agreement", () => {
230
+ expect(block("in-sync", "not-observed", "drifted")).toBe("not-observed");
231
+ expect(block("in-sync", "drifted")).toBe("drifted");
232
+ expect(block("in-sync", "pending")).toBe("pending");
233
+ expect(block("in-sync", "runtime")).toBe("in-sync");
234
+ expect(block()).toBeUndefined();
235
+ expect([...DRIFT_INSTANCE_ROLLUP].sort()).toEqual([...DRIFT_STATUSES].sort());
236
+ });
237
+
238
+ test("groups instances under their block node and leaves out records on no node", () => {
239
+ const rolled = rollupDriftByBlock([
240
+ rec({ address: "aws_eip.pool[0]", node: "estate/aws_eip.pool[0]", instanceOf: "estate/aws_eip.pool" }),
241
+ rec({ address: "aws_eip.pool[1]", node: "estate/aws_eip.pool[1]", instanceOf: "estate/aws_eip.pool", status: "drifted" }),
242
+ rec({ address: "aws_vpc.main", node: "estate/aws_vpc.main" }),
243
+ rec({ address: "aws_s3_bucket.x", node: null }),
244
+ ]);
245
+ expect(rolled.map((r) => [r.node, r.status, r.records.length])).toEqual([
246
+ ["estate/aws_eip.pool", "drifted", 2],
247
+ ["estate/aws_vpc.main", "in-sync", 1],
248
+ ]);
249
+ });
250
+ });
251
+
252
+ describe("mapping onto a graph", () => {
253
+ test("uses graphNodeFor, the change set's rule, and leaves a Pulumi URN the graph lacks null", () => {
254
+ const doc = driftDocument([
255
+ rec({ address: "aws_eip.pool[0]" }),
256
+ rec({ address: "module.app[0].aws_eip.pool[1]" }),
257
+ rec({ engine: "chant", member: "dns", address: "zone", evidence: { tool: "chant" } }),
258
+ rec({ engine: "pulumi", member: "site", address: "urn:pulumi:dev::site::aws:s3/bucket:Bucket::assets", evidence: { tool: "pulumi" } }),
259
+ ]);
260
+ const mapped = mapDriftToGraph(doc, { member: "infra", at: null, ids: ["estate/aws_eip.pool", "estate/module.app/aws_eip.pool", "zone"] });
261
+ const at = (address: string) => mapped.records.find((r) => r.address === address)!;
262
+ expect(at("aws_eip.pool[0]")).toMatchObject({ node: "infra/estate/aws_eip.pool[0]", instanceOf: "infra/estate/aws_eip.pool" });
263
+ // Under an expanded module call the module key sits on the call's segment (#4486).
264
+ expect(at("module.app[0].aws_eip.pool[1]")).toMatchObject({
265
+ node: "infra/estate/module.app[0]/aws_eip.pool[1]",
266
+ instanceOf: "infra/estate/module.app/aws_eip.pool",
267
+ });
268
+ expect(at("zone").node).toBe("infra/zone");
269
+ expect(at("urn:pulumi:dev::site::aws:s3/bucket:Bucket::assets").node).toBeNull();
270
+ expect(mapDriftToGraph(doc, null).records.every((r) => r.node === null && r.instanceOf === undefined)).toBe(true);
271
+ roundTrip(mapped.records);
272
+ });
273
+
274
+ test("puts a Pulumi URN on <member>/<key> by the URN rule (#4453, #4456), and a provider on none", () => {
275
+ const doc = driftDocument([
276
+ rec({ engine: "pulumi", member: "site", address: "urn:pulumi:dev::site::aws:s3/bucket:Bucket::assets", evidence: { tool: "pulumi" } }),
277
+ rec({ engine: "pulumi", member: "site", address: "urn:pulumi:dev::site::pulumi:providers:aws::default", evidence: { tool: "pulumi" } }),
278
+ ]);
279
+ const mapped = mapDriftToGraph(doc, { member: null, at: null, ids: ["site/aws:s3/bucket:Bucket::assets"] });
280
+ const at = (address: string) => mapped.records.find((r) => r.address === address)!;
281
+ expect(at("urn:pulumi:dev::site::aws:s3/bucket:Bucket::assets").node).toBe("site/aws:s3/bucket:Bucket::assets");
282
+ expect(at("urn:pulumi:dev::site::pulumi:providers:aws::default").node).toBeNull();
283
+ });
284
+
285
+ test("puts a plunge URN on the same node, by the pulumi rule (#4453)", () => {
286
+ const doc = driftDocument([
287
+ rec({ engine: "plunge", member: "site", address: "urn:pulumi:dev::site::aws:s3/bucket:Bucket::assets", read: "refresh", evidence: { tool: "plunge" } }),
288
+ ]);
289
+ const mapped = mapDriftToGraph(doc, { member: null, at: null, ids: ["site/aws:s3/bucket:Bucket::assets"] });
290
+ expect(mapped.records[0].node).toBe("site/aws:s3/bucket:Bucket::assets");
291
+ });
292
+ });
293
+
294
+ describe("the stream", () => {
295
+ const start = driftStart({
296
+ member: "estate",
297
+ engine: "choudoufu",
298
+ read: "ownership",
299
+ at: AT,
300
+ expected: [
301
+ { address: "aws_vpc.main", node: "estate/aws_vpc.main" },
302
+ { address: "aws_eip.pool[0]", node: "estate/aws_eip.pool[0]", instanceOf: "estate/aws_eip.pool", resourceType: "aws_eip" },
303
+ { address: "aws_eip.pool[1]", node: "estate/aws_eip.pool[1]", instanceOf: "estate/aws_eip.pool", resourceType: "aws_eip" },
304
+ ],
305
+ evidence: { tool: "choudoufu", command: "choudoufu live-plan -json" },
306
+ });
307
+ const sent = [
308
+ rec({ engine: "choudoufu", read: "ownership", address: "aws_vpc.main", status: "pending", evidence: { tool: "choudoufu" } }),
309
+ rec({ engine: "choudoufu", read: "ownership", address: "aws_eip.pool[0]", evidence: { tool: "choudoufu" } }),
310
+ rec({ engine: "choudoufu", read: "ownership", address: "aws_vpc.main", evidence: { tool: "choudoufu" } }),
311
+ ];
312
+
313
+ test("settle turns every unreported expected address into not-observed / not-reached", () => {
314
+ const { line, records } = driftSettle(start, sent, "2026-10-25T12:00:05Z");
315
+ expect(line.notReached).toEqual(["aws_eip.pool[1]"]);
316
+ expect(line.counts["in-sync"]).toBe(2);
317
+ expect(line.counts["not-observed"]).toBe(1);
318
+ const missing = records.find((r) => r.address === "aws_eip.pool[1]")!;
319
+ expect(missing.status).toBe("not-observed");
320
+ expect(missing.unobserved?.reason).toBe("not-reached");
321
+ expect(missing.node).toBe("estate/aws_eip.pool[1]");
322
+ expect(missing.instanceOf).toBe("estate/aws_eip.pool");
323
+ expect(missing.resourceType).toBe("aws_eip");
324
+ // The later line for aws_vpc.main replaced the earlier one.
325
+ expect(records.find((r) => r.address === "aws_vpc.main")!.status).toBe("in-sync");
326
+ });
327
+
328
+ test("round-trips through NDJSON and folds to the same document settle gives", () => {
329
+ const { line: settle, records } = driftSettle(start, sent, "2026-10-25T12:00:05Z");
330
+ const lines: DriftLine[] = [start, ...sent.map(driftRecordLine), settle];
331
+ const text = encodeDriftLines(lines);
332
+ expect(text.split("\n").filter(Boolean)).toHaveLength(lines.length);
333
+ const decoded = decodeDriftLines(text);
334
+ expect(decoded).toEqual(lines.map((l) => decodeDriftLine(encodeDriftLine(l))));
335
+ const { document, reading } = foldDriftLines(decoded);
336
+ expect(reading).toEqual([]);
337
+ expect(document).toEqual(driftDocument(records));
338
+ for (const l of decoded) lineSchema.expectValid(l);
339
+ });
340
+
341
+ test("a start with no settle leaves its unreported addresses reading, not a status", () => {
342
+ const { document, reading } = foldDriftLines([start, driftRecordLine(sent[1])]);
343
+ expect(document.records.map((r) => r.address)).toEqual(["aws_eip.pool[0]"]);
344
+ expect(reading.map((r) => r.address)).toEqual(["aws_eip.pool[1]", "aws_vpc.main"]);
345
+ });
346
+
347
+ test("a malformed line is refused by name", () => {
348
+ expect(() => decodeDriftLine('{"type":"drift-finish"}')).toThrow(/drift-finish/);
349
+ expect(() => decodeDriftLine("not json")).toThrow(/not JSON/);
350
+ expect(() => decodeDriftLine(JSON.stringify(driftRecordLine({ ...rec(), read: "state", status: "drifted" })))).toThrow(/state read/);
351
+ });
352
+
353
+ test("an answer under another read answers the expected address", () => {
354
+ const { line } = driftSettle(start, [rec({ engine: "choudoufu", read: "plan", address: "aws_eip.pool[1]", evidence: { tool: "choudoufu" } })], AT);
355
+ expect(line.notReached).toEqual(["aws_eip.pool[0]", "aws_vpc.main"]);
356
+ });
357
+ });
358
+
359
+ describe("choudoufu's live-plan -resource stream, as it writes it", () => {
360
+ // Recorded (#4475, __fixtures__/drift-record/README.md): choudoufu's estate-block fixture on floci, its log group's
361
+ // retention changed out of band (choudoufu-resource.ndjson), and the subnet's cone, the VPC with it (choudoufu-cone.ndjson).
362
+ const text = readFileSync(join(DIR, "choudoufu-resource.ndjson"), "utf-8");
363
+ const cone = readFileSync(join(DIR, "choudoufu-cone.ndjson"), "utf-8");
364
+
365
+ test("decodes line for line, the settle's reads carried as given", () => {
366
+ const lines = decodeDriftLines(text);
367
+ expect(lines.map((l) => l.type)).toEqual(["drift-start", "drift-record", "drift-settle"]);
368
+ for (const l of [...lines, ...decodeDriftLines(cone)]) lineSchema.expectValid(JSON.parse(encodeDriftLine(l)));
369
+ for (const raw of [...text.split("\n"), ...cone.split("\n")].filter(Boolean)) lineSchema.expectValid(JSON.parse(raw));
370
+ const settle = lines[2] as DriftSettleLine;
371
+ expect(settle.reads).toEqual({ recordsRead: 6, recordKeys: 1, recordListCalls: 0, sweepCalls: 0, scanTypes: [], scanListCalls: 0, tagIndexCalls: 0 });
372
+ expect(settle.counts).toEqual({ drifted: 1 });
373
+ });
374
+
375
+ test("folds to one record per expected address; choudoufu names a changed path and no values", () => {
376
+ const one = foldDriftLines(decodeDriftLines(text));
377
+ expect(one.reading).toEqual([]);
378
+ expect(one.document.records.map((r) => [r.address, r.read, r.status, r.ownership?.by])).toEqual([["aws_cloudwatch_log_group.app", "plan", "drifted", "derived"]]);
379
+ expect(one.document.records[0].attributes).toEqual([{ path: "retention_in_days" }]);
380
+ const { document, reading } = foldDriftLines(decodeDriftLines(cone));
381
+ expect(reading).toEqual([]);
382
+ expect(document.records.map((r) => [r.address, r.read, r.status, r.ownership?.by])).toEqual([
383
+ ["aws_subnet.app", "plan", "in-sync", "marker"],
384
+ ["aws_vpc.main", "plan", "in-sync", "marker"],
385
+ ]);
386
+ roundTrip([...one.document.records, ...document.records]);
387
+ });
388
+
389
+ test("a settle with an expected address nobody answered folds it to not-reached", () => {
390
+ const short = cone.split("\n").filter(Boolean).filter((l) => !l.includes('"address":"aws_vpc.main"')).join("\n");
391
+ const { document } = foldDriftLines(decodeDriftLines(short));
392
+ const silent = document.records.find((r) => r.address === "aws_vpc.main")!;
393
+ expect(silent.status).toBe("not-observed");
394
+ expect(silent.unobserved?.reason).toBe("not-reached");
395
+ });
396
+ });
397
+
398
+ // The recorded reads (#4475): `lifecycle diff --live --json` and `components
399
+ // status --json` (with and without --live) run against a chant AWS project on
400
+ // floci. __fixtures__/drift-record/README.md says how. Shapes floci cannot
401
+ // produce (a runtime child, an orphan, an unchanged thin entity, a thin
402
+ // attribute change, a failed deep read, a digest drift, missing credentials)
403
+ // stay below as constructed inputs, labelled as such.
404
+ const recordedDiff = fixture<{ lexicons: Record<string, { resources: LiveDiffResult; observed: Record<string, { type: string }>; deep: DeepDiffResult }> }>(
405
+ "lifecycle-diff-live.json",
406
+ ).lexicons.aws;
407
+ const recordedTypes = Object.fromEntries(Object.entries(recordedDiff.observed).map(([name, meta]) => [name, meta.type]));
408
+
409
+ describe("lifecycle diff --live, recorded", () => {
410
+ const records = liveDiffDriftRecords({ member: "app", observedAt: AT, diff: fixture<LiveDiffResult>("live-diff.json"), types: recordedTypes });
411
+
412
+ test("the slice is the command's own resources", () => {
413
+ expect(fixture<LiveDiffResult>("live-diff.json")).toEqual(recordedDiff.resources);
414
+ });
415
+
416
+ test("maps every bucket the run produced", () => {
417
+ expect(statusOf(records)).toEqual({
418
+ alertsTopic: "in-sync",
419
+ appRole: "drifted",
420
+ cacheTopic: "pending",
421
+ dataBucket: "drifted",
422
+ dataBucketPolicy: "drifted",
423
+ eventsTopic: "drifted",
424
+ quietSg: "in-sync",
425
+ strayTopic: "missing",
426
+ taskQueue: "drifted",
427
+ webSg: "in-sync",
428
+ });
429
+ expect(records.every((r) => r.read === "live-read" && r.engine === "chant")).toBe(true);
430
+ expect(records.find((r) => r.address === "webSg")!.resourceType).toBe("AWS::EC2::SecurityGroup");
431
+ });
432
+
433
+ test("a redeploy since the snapshot reads as thin drift on its timestamps", () => {
434
+ expect(records.find((r) => r.address === "eventsTopic")!.attributes?.map((a) => a.path)).toEqual(["lastUpdated", "status"]);
435
+ });
436
+
437
+ test("round-trips", () => {
438
+ roundTrip(records);
439
+ });
440
+ });
441
+
442
+ describe("lifecycle diff --live, constructed shapes", () => {
443
+ // Not recorded: floci gives no runtime children, no ownership markers on a
444
+ // stack (so no orphan), and no thin attribute change beyond timestamps.
445
+ const constructed = {"missing": ["queue"], "orphan": ["stray-bucket"], "runtimeChildren": [{"name": "web-7d9f-abcde", "type": "K8s::Core::Pod", "owner": "web"}], "disappeared": ["cache"], "newlyObserved": ["topic"], "driftedSinceSnapshot": [{"name": "bucket", "type": "AWS::S3::Bucket", "changes": [{"path": "attributes.tags.env", "oldValue": "prod", "newValue": "staging"}, {"path": "attributes.dbPassword", "oldValue": "hunter2", "newValue": "hunter3"}]}], "unchanged": ["role"], "unobserved": [{"name": "widget", "type": "K8s::X::Widget", "reason": "unsupported-kind", "detail": "no reader for this CRD", "queried": "/apis/x/v1/widgets/widget"}]} as LiveDiffResult;
446
+ const records = liveDiffDriftRecords({ member: "app", observedAt: AT, diff: constructed });
447
+
448
+ test("maps every bucket", () => {
449
+ expect(statusOf(records)).toEqual({
450
+ bucket: "drifted",
451
+ cache: "missing",
452
+ queue: "pending",
453
+ role: "in-sync",
454
+ "stray-bucket": "undeclared",
455
+ topic: "in-sync",
456
+ "web-7d9f-abcde": "runtime",
457
+ widget: "not-observed",
458
+ });
459
+ });
460
+
461
+ test("carries the paths, redacted, and the hole's reason and address", () => {
462
+ const bucket = records.find((r) => r.address === "bucket")!;
463
+ expect(bucket.attributes).toEqual([
464
+ { path: "attributes.dbPassword", sensitive: true },
465
+ { path: "attributes.tags.env", before: "prod", after: "staging" },
466
+ ]);
467
+ expect(records.find((r) => r.address === "widget")!.unobserved).toEqual({
468
+ reason: "unsupported-kind",
469
+ detail: "no reader for this CRD",
470
+ queried: "/apis/x/v1/widgets/widget",
471
+ });
472
+ expect(records.find((r) => r.address === "web-7d9f-abcde")!.ownership).toEqual({ verdict: "foreign", ownedBy: "web" });
473
+ });
474
+
475
+ test("round-trips", () => {
476
+ roundTrip(records);
477
+ });
478
+ });
479
+
480
+ describe("deep observe, recorded", () => {
481
+ const deep = deepDiffDriftRecords({ member: "app", observedAt: AT, diff: fixture<DeepDiffResult>("deep-diff.json") });
482
+
483
+ test("the slice is the command's own deep result", () => {
484
+ expect(fixture<DeepDiffResult>("deep-diff.json")).toEqual(recordedDiff.deep);
485
+ });
486
+
487
+ test("a changed property carries declared before and live after", () => {
488
+ expect(deep.find((r) => r.address === "webSg")!.attributes).toContainEqual({ path: "GroupDescription", before: "rx web tier, v2", after: "rx web tier" });
489
+ });
490
+
491
+ test("a property the live read does not return carries only the declared value", () => {
492
+ expect(deep.find((r) => r.address === "appRole")!.attributes).toContainEqual({ path: "MaxSessionDuration", before: 3600 });
493
+ });
494
+
495
+ test("a keyed element whose key looks secret-bearing is redacted", () => {
496
+ const role = deep.find((r) => r.address === "appRole")!.attributes!;
497
+ expect(role.filter((a) => a.path.startsWith("Tags[#api-token-owner]"))).toEqual([
498
+ { path: "Tags[#api-token-owner].Key", sensitive: true },
499
+ { path: "Tags[#api-token-owner].Value", sensitive: true },
500
+ ]);
501
+ });
502
+
503
+ test("a type with no deep reader is a hole with its reason", () => {
504
+ expect(statusOf(deep)).toEqual({
505
+ alertsTopic: "not-observed",
506
+ appRole: "drifted",
507
+ dataBucket: "drifted",
508
+ dataBucketPolicy: "not-observed",
509
+ eventsTopic: "not-observed",
510
+ quietSg: "drifted",
511
+ taskQueue: "not-observed",
512
+ webSg: "drifted",
513
+ });
514
+ expect(deep.find((r) => r.address === "taskQueue")!.unobserved?.reason).toBe("unsupported-kind");
515
+ roundTrip(deep);
516
+ });
517
+
518
+ test("laid over the thin read, an unsupported deep kind stays on the attribute surface", () => {
519
+ const thin = liveDiffDriftRecords({ member: "app", observedAt: AT, diff: recordedDiff.resources, types: recordedTypes });
520
+ const merged = mergeDeepDrift(thin, deep);
521
+ expect(merged.find((r) => r.address === "alertsTopic")!.status).toBe("in-sync");
522
+ expect(merged.find((r) => r.address === "alertsTopic")!.attributesUnobserved?.reason).toBe("unsupported-kind");
523
+ expect(merged.find((r) => r.address === "webSg")!.status).toBe("drifted");
524
+ roundTrip(merged);
525
+ });
526
+ });
527
+
528
+ describe("deep observe, constructed shapes", () => {
529
+ // Not recorded: floci's Cloud Control answers every read, so no read-failed
530
+ // hole, and it returns too little of any type for a deep unchanged entity.
531
+ const constructed = {"drifted": [{"name": "role", "type": "AWS::IAM::Role", "changes": [{"path": "MaxSessionDuration", "kind": "changed", "declared": 3600, "live": 7200}, {"path": "Tags[0].Value", "kind": "absent", "declared": "prod"}, {"path": "Policies[0].secretArn", "kind": "changed", "declared": "arn:a", "live": "arn:b"}]}], "accepted": [], "unclaimed": [], "held": [], "unchanged": ["topic"], "unobserved": [{"name": "bucket", "type": "AWS::S3::Bucket", "reason": "read-failed", "detail": "GetBucketPolicy returned 500"}], "undeclaredEntities": []} as DeepDiffResult;
532
+ const deep = deepDiffDriftRecords({ member: "app", observedAt: AT, diff: constructed });
533
+ const thinConstructed = liveDiffDriftRecords({ member: "app", observedAt: AT, diff: {"missing": ["queue"], "orphan": ["stray-bucket"], "runtimeChildren": [{"name": "web-7d9f-abcde", "type": "K8s::Core::Pod", "owner": "web"}], "disappeared": ["cache"], "newlyObserved": ["topic"], "driftedSinceSnapshot": [{"name": "bucket", "type": "AWS::S3::Bucket", "changes": [{"path": "attributes.tags.env", "oldValue": "prod", "newValue": "staging"}, {"path": "attributes.dbPassword", "oldValue": "hunter2", "newValue": "hunter3"}]}], "unchanged": ["role"], "unobserved": [{"name": "widget", "type": "K8s::X::Widget", "reason": "unsupported-kind", "detail": "no reader for this CRD", "queried": "/apis/x/v1/widgets/widget"}]} as LiveDiffResult });
534
+
535
+ test("drifted carries the paths, declared before and live after", () => {
536
+ expect(deep.find((r) => r.address === "role")!.attributes).toEqual([
537
+ { path: "MaxSessionDuration", before: 3600, after: 7200 },
538
+ { path: "Policies[0].secretArn", sensitive: true },
539
+ { path: "Tags[0].Value", before: "prod" },
540
+ ]);
541
+ expect(statusOf(deep)).toEqual({ bucket: "not-observed", role: "drifted", topic: "in-sync" });
542
+ roundTrip(deep);
543
+ });
544
+
545
+ test("laid over the thin read, a deep hole stays on the attribute surface", () => {
546
+ const merged = mergeDeepDrift(thinConstructed, deep);
547
+ const bucket = merged.find((r) => r.address === "bucket")!;
548
+ expect(bucket.status).toBe("drifted");
549
+ expect(bucket.attributesUnobserved).toEqual({ reason: "read-failed", detail: "GetBucketPolicy returned 500" });
550
+ const role = merged.find((r) => r.address === "role")!;
551
+ expect(role.status).toBe("drifted");
552
+ expect(role.attributes?.map((a) => a.path)).toEqual(["MaxSessionDuration", "Policies[0].secretArn", "Tags[0].Value"]);
553
+ expect(merged.find((r) => r.address === "topic")!.status).toBe("in-sync");
554
+ roundTrip(merged);
555
+ });
556
+ });
557
+
558
+ describe("component status, recorded", () => {
559
+ const live = componentStatusDriftRecords({ member: "app", observedAt: AT, rows: fixture<ComponentStatusRow[]>("component-status.json") });
560
+ const ledger = componentStatusDriftRecords({ member: "app", observedAt: AT, rows: fixture<ComponentStatusRow[]>("component-status-ledger.json") });
561
+
562
+ test("--live maps every verdict the run produced", () => {
563
+ expect(statusOf(live)).toEqual({ api: "in-sync", "demo-infra": "in-sync", docs: "pending", edge: "drifted", jobs: "missing" });
564
+ roundTrip(live);
565
+ });
566
+
567
+ test("without --live every row is a hole the read never reached", () => {
568
+ expect(new Set(ledger.map((r) => r.status))).toEqual(new Set(["not-observed"]));
569
+ expect(ledger.find((r) => r.address === "api")!.unobserved?.reason).toBe("not-reached");
570
+ roundTrip(ledger);
571
+ });
572
+ });
573
+
574
+ describe("component status, constructed shapes", () => {
575
+ // Not recorded: floci gives no live digest to differ from a record, and
576
+ // credentials it never checks cannot be missing.
577
+ const records = componentStatusDriftRecords({ member: "app", observedAt: AT, rows: [{"component": "api", "env": "prod", "reconciliation": "reconciled", "detail": "recorded 2026-10-01T00:00:00Z (digest sha256:aa), live and consistent", "live": true}, {"component": "web", "env": "prod", "reconciliation": "drifted", "detail": "recorded 2026-10-01T00:00:00Z (digest sha256:bb), but the live digest is sha256:cc", "live": true}, {"component": "jobs", "env": "prod", "reconciliation": "stale", "detail": "recorded 2026-10-01T00:00:00Z (digest sha256:dd), but nothing observed live now", "live": false}, {"component": "edge", "env": "prod", "reconciliation": "unrecorded", "detail": "live and chant-owned, but no release record exists \u2014 deployed outside the recorded path", "live": true}, {"component": "docs", "env": "prod", "reconciliation": "unrecorded", "detail": "no release record and nothing observed live", "live": false}, {"component": "search", "env": "prod", "reconciliation": "unknown", "detail": "recorded, but live state could not be observed \u2014 no credentials", "unobserved": {"reason": "no-credentials", "detail": "AWS_PROFILE unset"}}, {"component": "mail", "env": "prod", "reconciliation": "unknown", "detail": "recorded; live status not queried (pass --live to reconcile)"}] as ComponentStatusRow[] });
578
+
579
+ test("maps every reconciliation verdict", () => {
580
+ expect(statusOf(records)).toEqual({
581
+ api: "in-sync",
582
+ docs: "pending",
583
+ edge: "drifted",
584
+ jobs: "missing",
585
+ mail: "not-observed",
586
+ search: "not-observed",
587
+ web: "drifted",
588
+ });
589
+ expect(records.find((r) => r.address === "search")!.unobserved).toEqual({ reason: "no-credentials", detail: "AWS_PROFILE unset" });
590
+ expect(records.find((r) => r.address === "mail")!.unobserved?.reason).toBe("not-reached");
591
+ });
592
+
593
+ test("round-trips", () => {
594
+ roundTrip(records);
595
+ });
596
+ });