@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,1206 @@
1
+ /**
2
+ * One drift record across engines (#4450, #4451).
3
+ *
4
+ * Drift had four vocabularies for one question: the converge observers'
5
+ * `in-sync | drifted | unknown` (`./lifecycle/symptoms.ts`), component
6
+ * status's `reconciled | unrecorded | stale | drifted | unknown`
7
+ * (`./lifecycle/status.ts`), the graph overlay's tones, and the live diff's
8
+ * buckets (`./lifecycle/live-diff.ts`). A renderer colouring a terraform, a
9
+ * choudoufu and a chant member side by side had to know all four, and behold
10
+ * read choudoufu's documents itself. This module is the one shape they all
11
+ * become: a {@link DriftRecord} per resource instance, and a
12
+ * {@link DriftDocument} (`drift: "v1"`) that holds them. Its schema is
13
+ * `./workspace/drift.schema.json`, `$id` {@link DRIFT_SCHEMA_ID}.
14
+ *
15
+ * A record says which read answered (`read`) and what it found (`status`).
16
+ * The two travel together because they are different claims: a `state` read
17
+ * says what the last apply recorded and can never say the account matches,
18
+ * so {@link driftRecord} refuses a state read whose status is anything but
19
+ * `recorded`, `pending` or `not-observed`. Every record goes through that
20
+ * constructor, so the rule holds for every adapter, and for a document read
21
+ * back with {@link parseDriftDocument}.
22
+ *
23
+ * `not-observed` is never absence. It always carries a reason from the
24
+ * observation contract's closed list (`./observation.ts`), and
25
+ * `not-reached` is the reason a stream gives an address it expected and
26
+ * never heard about ({@link driftSettle}).
27
+ *
28
+ * ## Redaction
29
+ *
30
+ * An attribute the planner marked sensitive keeps its path and loses both
31
+ * values (`sensitive: true`), as the change set spells it
32
+ * (`./change-set.ts`). A second pass redacts any path with a segment
33
+ * {@link isSensitiveKey} names, whatever the planner said, so an engine
34
+ * that marks nothing still never ships a `password` value. Values are
35
+ * optional: a card needs only the names.
36
+ *
37
+ * ## Not digest-bound
38
+ *
39
+ * A drift read authorizes nothing and goes stale on the next read, so a
40
+ * record is ephemeral. {@link driftDocumentDigest} exists for one case, a
41
+ * WatchOp writing its document into its own run ledger as evidence, and
42
+ * leaves out `observedAt` and `evidence`, which change on every read.
43
+ *
44
+ * ## Adapters
45
+ *
46
+ * The chant ones are here: `lifecycle diff --live` ({@link liveDiffDriftRecords}),
47
+ * deep observe ({@link deepDiffDriftRecords}, {@link mergeDeepDrift}) and
48
+ * component status ({@link componentStatusDriftRecords}). The terraform and
49
+ * choudoufu ones live in the terraform lexicon's `drift-record` subpath,
50
+ * beside its readers, and the Pulumi one (`refresh --preview-only`, #4453)
51
+ * in the pulumi lexicon's.
52
+ *
53
+ * ## The graph and the rollup
54
+ *
55
+ * A record's `node` and `instanceOf` come from the change set's own rule
56
+ * (`graphNodeFor`, #4454), and a block's status from core's one rollup
57
+ * (`rollupInstances`) over {@link DRIFT_INSTANCE_ROLLUP}. Neither is
58
+ * written a second time here.
59
+ *
60
+ * ## The import graph
61
+ *
62
+ * This module ships in the change-set bundle (`change-set-bundle.test.ts`,
63
+ * #3421): at run time it imports the plan digest, the observation
64
+ * contract's reason list, the change-set graph's node rule, the instance
65
+ * rollup and the sensitive-key list, and types only otherwise.
66
+ */
67
+
68
+ import { graphNodeFor, type ChangeSetGraphNodes } from "./change-set-graph";
69
+ import type { ChangeSetPlanner } from "./change-set";
70
+ import { rollupInstances } from "./instance-nodes";
71
+ import { computePlanDigest } from "./lifecycle/plan-digest";
72
+ import type { DeepDiffResult } from "./lifecycle/deep-diff";
73
+ import type { LiveDiffResult } from "./lifecycle/live-diff";
74
+ import type { ComponentStatusRow } from "./lifecycle/status";
75
+ import { isUnobservedReason, type UnobservedReason } from "./observation";
76
+ import { isSensitiveKey } from "./sensitive-key";
77
+
78
+ /** The schema a drift document names in `$schema`. */
79
+ export const DRIFT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/drift/v1/drift.schema.json";
80
+
81
+ /** The document's version. */
82
+ export const DRIFT_VERSION = "v1";
83
+
84
+ /**
85
+ * Which engine read the resource: a change-set planner (`pulumi` among them
86
+ * since #4453), or plunge, which has no planner of its own. A plunge
87
+ * address is a Pulumi URN, so a plunge record is placed on the graph by the
88
+ * `pulumi` planner's rule ({@link driftPlanner}).
89
+ */
90
+ export type DriftEngine = ChangeSetPlanner | "plunge";
91
+
92
+ /** The planner whose node rule places an engine's records: plunge's URNs go by `pulumi`'s (#4453). */
93
+ export function driftPlanner(engine: DriftEngine): ChangeSetPlanner {
94
+ return engine === "plunge" ? "pulumi" : engine;
95
+ }
96
+
97
+ const DRIFT_ENGINE_WITNESS: Record<DriftEngine, true> = {
98
+ terraform: true,
99
+ tofu: true,
100
+ choudoufu: true,
101
+ chant: true,
102
+ warden: true,
103
+ sql: true,
104
+ pulumi: true,
105
+ plunge: true,
106
+ };
107
+
108
+ /** Every engine, in the order the schema lists them. */
109
+ export const DRIFT_ENGINES = Object.keys(DRIFT_ENGINE_WITNESS) as readonly DriftEngine[];
110
+
111
+ /**
112
+ * Which read answered.
113
+ *
114
+ * - `state` — the engine's state file: what the last apply recorded.
115
+ * - `plan` — a plan: what the next apply would do, after a refresh.
116
+ * - `refresh` — a refresh preview (Pulumi's `refresh --preview-only`).
117
+ * - `live-read` — chant's own read of the provider.
118
+ * - `ownership` — an ownership read (choudoufu's `live-plan` and `live-ls`),
119
+ * which says who holds each object and not what its attributes are.
120
+ */
121
+ export type DriftRead = "state" | "plan" | "refresh" | "live-read" | "ownership";
122
+
123
+ const DRIFT_READ_WITNESS: Record<DriftRead, true> = { state: true, plan: true, refresh: true, "live-read": true, ownership: true };
124
+
125
+ /** Every read, in the order the schema lists them. */
126
+ export const DRIFT_READS = Object.keys(DRIFT_READ_WITNESS) as readonly DriftRead[];
127
+
128
+ /**
129
+ * What the read found.
130
+ *
131
+ * - `in-sync` — present, and nothing the read compares differs.
132
+ * - `drifted` — present, and something differs; `attributes` says what.
133
+ * - `pending` — declared and confirmed absent, never deployed.
134
+ * - `missing` — recorded or deployed, and now gone.
135
+ * - `unclaimed` — something live sits at a declared identity and does not
136
+ * carry this member's marker: adoptable, or held by someone else.
137
+ * - `undeclared` — live or recorded, and the member does not declare it.
138
+ * - `runtime` — live, undeclared, and expected: a controller's child.
139
+ * - `recorded` — in the engine's state. The only presence a state read gives.
140
+ * - `not-observed` — the read could not say; `unobserved` says why.
141
+ */
142
+ export type DriftStatus =
143
+ | "in-sync"
144
+ | "drifted"
145
+ | "pending"
146
+ | "missing"
147
+ | "unclaimed"
148
+ | "undeclared"
149
+ | "runtime"
150
+ | "recorded"
151
+ | "not-observed";
152
+
153
+ const DRIFT_STATUS_WITNESS: Record<DriftStatus, true> = {
154
+ "in-sync": true,
155
+ drifted: true,
156
+ pending: true,
157
+ missing: true,
158
+ unclaimed: true,
159
+ undeclared: true,
160
+ runtime: true,
161
+ recorded: true,
162
+ "not-observed": true,
163
+ };
164
+
165
+ /** Every status, in the order the schema lists them. */
166
+ export const DRIFT_STATUSES = Object.keys(DRIFT_STATUS_WITNESS) as readonly DriftStatus[];
167
+
168
+ /** The only statuses a `state` read may give. It never says in-sync or drifted. */
169
+ export const STATE_READ_STATUSES: readonly DriftStatus[] = ["recorded", "pending", "not-observed"];
170
+
171
+ /** Who an ownership facet says holds the object. */
172
+ export type DriftOwnershipVerdict = "owned" | "foreign" | "adoptable" | "unknown";
173
+
174
+ const OWNERSHIP_VERDICT_WITNESS: Record<DriftOwnershipVerdict, true> = { owned: true, foreign: true, adoptable: true, unknown: true };
175
+
176
+ /** Every ownership verdict. */
177
+ export const DRIFT_OWNERSHIP_VERDICTS = Object.keys(OWNERSHIP_VERDICT_WITNESS) as readonly DriftOwnershipVerdict[];
178
+
179
+ /** What the ownership verdict rests on. */
180
+ export type DriftOwnershipBy = "marker" | "derived" | "record" | "cache" | "state" | "label";
181
+
182
+ const OWNERSHIP_BY_WITNESS: Record<DriftOwnershipBy, true> = { marker: true, derived: true, record: true, cache: true, state: true, label: true };
183
+
184
+ /** Every ownership basis. */
185
+ export const DRIFT_OWNERSHIP_BY = Object.keys(OWNERSHIP_BY_WITNESS) as readonly DriftOwnershipBy[];
186
+
187
+ /**
188
+ * One attribute that differs. `before` is what was expected (the record, the
189
+ * snapshot, the declaration) and `after` what the read found. A path in the
190
+ * plan-observation grammar: keys joined with `.`, indexes and keys that are
191
+ * not identifiers in brackets (`tags["kubernetes.io/role"]`).
192
+ */
193
+ export interface DriftAttribute {
194
+ path: string;
195
+ before?: unknown;
196
+ after?: unknown;
197
+ /** The planner marked it sensitive, or its name looks secret-bearing, so neither value is carried. */
198
+ sensitive?: true;
199
+ }
200
+
201
+ /** Why a record is `not-observed`. */
202
+ export interface DriftUnobserved {
203
+ reason: UnobservedReason;
204
+ detail?: string;
205
+ /** The address the read was issued against, when the engine reported one. */
206
+ queried?: string;
207
+ }
208
+
209
+ /** Who holds the object, from an ownership read. */
210
+ export interface DriftOwnership {
211
+ verdict: DriftOwnershipVerdict;
212
+ by?: DriftOwnershipBy;
213
+ /** The marker was read off the object itself, not derived or remembered. */
214
+ markerOnObject?: boolean;
215
+ /** The estate, stack or controller that does hold it, when not this member. */
216
+ ownedBy?: string;
217
+ /** The address an adoption would write onto the object. */
218
+ adopt?: string;
219
+ }
220
+
221
+ /** What produced the record. */
222
+ export interface DriftEvidence {
223
+ tool: string;
224
+ version?: string;
225
+ command?: string;
226
+ /** The engine's own token for the verdict (`UNOWNED`, `NEEDS_DISCOVERY`, `ABSENT`), so a pane can show it. */
227
+ engineCode?: string;
228
+ }
229
+
230
+ export interface DriftRecord {
231
+ member: string;
232
+ engine: DriftEngine;
233
+ /** The engine's own address, at instance level: `aws_eip.pool[0]`, a Pulumi URN, a chant entity name. */
234
+ address: string;
235
+ /** The resource type, when the read reports one. Not `type`, which a stream line uses for its kind. */
236
+ resourceType?: string;
237
+ /**
238
+ * The graph node, by the change set's mapping rules (`graphNodeFor` in
239
+ * `./change-set-graph.ts`, {@link mapDriftToGraph}): for an instance of a
240
+ * `count` or `for_each` block the instance node, `<blockNode>[0]` or
241
+ * `<blockNode>["a"]`, and the block node otherwise. Null when the graph
242
+ * holds none. A Pulumi URN's node is `<member>/<key>` by the URN rule
243
+ * (#4456), and a plunge URN's by the same rule (#4453).
244
+ */
245
+ node: string | null;
246
+ /** On a record whose `node` is an instance node (#4454): the block node it is an instance of, the rollup. */
247
+ instanceOf?: string;
248
+ /** RFC 3339. Per record, because a stream's rows finish at different times. */
249
+ observedAt: string;
250
+ read: DriftRead;
251
+ /**
252
+ * The read that was asked for and failed, when this one answered instead:
253
+ * what `RootRead.depth` holds (`"plan"`, `"live+plan"`) on a root whose
254
+ * `RootRead.fellBackTo` is set (`./observation.ts`).
255
+ */
256
+ fellBackFrom?: string;
257
+ status: DriftStatus;
258
+ /** Set exactly when `status` is `not-observed`. */
259
+ unobserved?: DriftUnobserved;
260
+ /** What differs. Only on `drifted`. */
261
+ attributes?: DriftAttribute[];
262
+ /**
263
+ * Presence is known and the attributes are not: a deep read that failed
264
+ * for an entity the thin read found. On `in-sync` and `drifted` only.
265
+ */
266
+ attributesUnobserved?: DriftUnobserved;
267
+ ownership?: DriftOwnership;
268
+ evidence: DriftEvidence;
269
+ }
270
+
271
+ /** Counts per status, every status present. */
272
+ export type DriftCounts = Record<DriftStatus, number>;
273
+
274
+ export interface DriftDocument {
275
+ $schema: typeof DRIFT_SCHEMA_ID;
276
+ drift: typeof DRIFT_VERSION;
277
+ /** The chant that wrote it, when known. */
278
+ chant?: string;
279
+ /** Sorted by member, address and read. One per `(member, address, read)`. */
280
+ records: DriftRecord[];
281
+ counts: DriftCounts;
282
+ }
283
+
284
+ /** Which rule a refused record broke. */
285
+ export type DriftRecordRule =
286
+ | "shape"
287
+ | "state-read-status"
288
+ | "unobserved-reason"
289
+ | "unobserved-on-observed"
290
+ | "attributes-status"
291
+ | "attributes-unobserved-status";
292
+
293
+ /** A record {@link driftRecord} refused, naming the record and the rule. */
294
+ export class DriftRecordError extends Error {
295
+ readonly rule: DriftRecordRule;
296
+ constructor(rule: DriftRecordRule, message: string) {
297
+ super(message);
298
+ this.name = "DriftRecordError";
299
+ this.rule = rule;
300
+ }
301
+ }
302
+
303
+ const byString = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
304
+ const isRecordObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
305
+ const nonEmpty = (v: unknown): v is string => typeof v === "string" && v.length > 0;
306
+
307
+ /** RFC 3339 date-time, as JSON Schema's `date-time` format reads it. */
308
+ const RFC3339 = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
309
+
310
+ /**
311
+ * The key segments of an attribute path, indexes left out:
312
+ * `spec.env[0]["db_password"]` is `spec`, `env`, `db_password`. A keyed
313
+ * element (`Tags[#api-token-owner].Value`, the deep read's address for an
314
+ * array it can key) is a key too: it names the element the way a map key
315
+ * does, so an environment list keyed by `DB_PASSWORD` redacts like a map's.
316
+ */
317
+ export function attributePathKeys(path: string): string[] {
318
+ const keys: string[] = [];
319
+ let current = "";
320
+ for (let i = 0; i < path.length; i++) {
321
+ const c = path[i];
322
+ if (c === ".") {
323
+ if (current) keys.push(current);
324
+ current = "";
325
+ } else if (c === "[") {
326
+ if (current) keys.push(current);
327
+ current = "";
328
+ if (path[i + 1] === '"') {
329
+ let key = "";
330
+ let j = i + 2;
331
+ for (; j < path.length && path[j] !== '"'; j++) {
332
+ if (path[j] === "\\" && j + 1 < path.length) j++;
333
+ key += path[j];
334
+ }
335
+ keys.push(key);
336
+ i = path.indexOf("]", j);
337
+ if (i < 0) break;
338
+ } else if (path[i + 1] === "#") {
339
+ const close = path.indexOf("]", i);
340
+ if (close < 0) break;
341
+ keys.push(path.slice(i + 2, close));
342
+ i = close;
343
+ } else {
344
+ const close = path.indexOf("]", i);
345
+ if (close < 0) break;
346
+ i = close; // a numeric index: not a key
347
+ }
348
+ } else current += c;
349
+ }
350
+ if (current) keys.push(current);
351
+ return keys;
352
+ }
353
+
354
+ /**
355
+ * One attribute, redacted: the planner's mark first, then any key segment
356
+ * {@link isSensitiveKey} names. A redacted attribute is `{ path, sensitive:
357
+ * true }` and nothing else.
358
+ */
359
+ export function redactDriftAttribute(attribute: DriftAttribute): DriftAttribute {
360
+ if (attribute.sensitive === true || attributePathKeys(attribute.path).some(isSensitiveKey)) {
361
+ return { path: attribute.path, sensitive: true };
362
+ }
363
+ return {
364
+ path: attribute.path,
365
+ ...(attribute.before !== undefined ? { before: attribute.before } : {}),
366
+ ...(attribute.after !== undefined ? { after: attribute.after } : {}),
367
+ };
368
+ }
369
+
370
+ function checkUnobserved(value: unknown, where: string, field: string): DriftUnobserved {
371
+ if (!isRecordObject(value)) throw new DriftRecordError("unobserved-reason", `${where}: ${field} needs a reason`);
372
+ if (!isUnobservedReason(value.reason)) {
373
+ throw new DriftRecordError("unobserved-reason", `${where}: ${field}.reason ${JSON.stringify(value.reason)} is not an unobserved reason`);
374
+ }
375
+ return {
376
+ reason: value.reason,
377
+ ...(nonEmpty(value.detail) ? { detail: value.detail } : {}),
378
+ ...(nonEmpty(value.queried) ? { queried: value.queried } : {}),
379
+ };
380
+ }
381
+
382
+ function checkOwnership(value: unknown, where: string): DriftOwnership {
383
+ if (!isRecordObject(value) || !(DRIFT_OWNERSHIP_VERDICTS as readonly unknown[]).includes(value.verdict)) {
384
+ throw new DriftRecordError("shape", `${where}: ownership.verdict must be one of ${DRIFT_OWNERSHIP_VERDICTS.join(", ")}`);
385
+ }
386
+ if (value.by !== undefined && !(DRIFT_OWNERSHIP_BY as readonly unknown[]).includes(value.by)) {
387
+ throw new DriftRecordError("shape", `${where}: ownership.by ${JSON.stringify(value.by)} is not one of ${DRIFT_OWNERSHIP_BY.join(", ")}`);
388
+ }
389
+ if (value.markerOnObject !== undefined && typeof value.markerOnObject !== "boolean") {
390
+ throw new DriftRecordError("shape", `${where}: ownership.markerOnObject must be a boolean`);
391
+ }
392
+ return {
393
+ verdict: value.verdict as DriftOwnershipVerdict,
394
+ ...(value.by !== undefined ? { by: value.by as DriftOwnershipBy } : {}),
395
+ ...(typeof value.markerOnObject === "boolean" ? { markerOnObject: value.markerOnObject } : {}),
396
+ ...(nonEmpty(value.ownedBy) ? { ownedBy: value.ownedBy } : {}),
397
+ ...(nonEmpty(value.adopt) ? { adopt: value.adopt } : {}),
398
+ };
399
+ }
400
+
401
+ /**
402
+ * Build one record, or refuse it. Every adapter and every reader goes
403
+ * through here, so these hold for every record that exists:
404
+ *
405
+ * - a `state` read's status is `recorded`, `pending` or `not-observed`;
406
+ * - `not-observed` carries `unobserved` with a legal reason, and no other
407
+ * status carries it;
408
+ * - `attributes` appear only on `drifted`, sorted by path, one per path,
409
+ * and redacted ({@link redactDriftAttribute});
410
+ * - `attributesUnobserved` appears only on `in-sync` or `drifted`.
411
+ *
412
+ * A refusal is a {@link DriftRecordError} naming the member, the address,
413
+ * the read and the status.
414
+ */
415
+ export function driftRecord(input: DriftRecord): DriftRecord {
416
+ const raw = input as unknown as Record<string, unknown>;
417
+ if (!isRecordObject(raw)) throw new DriftRecordError("shape", "a drift record must be an object");
418
+ const where = `drift record ${JSON.stringify(raw.address)} in member ${JSON.stringify(raw.member)} (${String(raw.read)} read, ${String(raw.status)})`;
419
+ if (!nonEmpty(raw.member)) throw new DriftRecordError("shape", `${where}: member must be a non-empty string`);
420
+ if (!nonEmpty(raw.address)) throw new DriftRecordError("shape", `${where}: address must be a non-empty string`);
421
+ if (!(DRIFT_ENGINES as readonly unknown[]).includes(raw.engine)) {
422
+ throw new DriftRecordError("shape", `${where}: engine ${JSON.stringify(raw.engine)} is not one of ${DRIFT_ENGINES.join(", ")}`);
423
+ }
424
+ if (!(DRIFT_READS as readonly unknown[]).includes(raw.read)) {
425
+ throw new DriftRecordError("shape", `${where}: read ${JSON.stringify(raw.read)} is not one of ${DRIFT_READS.join(", ")}`);
426
+ }
427
+ if (!(DRIFT_STATUSES as readonly unknown[]).includes(raw.status)) {
428
+ throw new DriftRecordError("shape", `${where}: status ${JSON.stringify(raw.status)} is not one of ${DRIFT_STATUSES.join(", ")}`);
429
+ }
430
+ if (raw.node !== null && !nonEmpty(raw.node)) throw new DriftRecordError("shape", `${where}: node must be a graph id or null`);
431
+ if (typeof raw.observedAt !== "string" || !RFC3339.test(raw.observedAt)) {
432
+ throw new DriftRecordError("shape", `${where}: observedAt ${JSON.stringify(raw.observedAt)} is not an RFC 3339 date-time`);
433
+ }
434
+ if (raw.instanceOf !== undefined && (!nonEmpty(raw.instanceOf) || raw.node === null)) {
435
+ throw new DriftRecordError("shape", `${where}: instanceOf must be a block node id, beside a node`);
436
+ }
437
+ if (raw.fellBackFrom !== undefined && !nonEmpty(raw.fellBackFrom)) throw new DriftRecordError("shape", `${where}: fellBackFrom must be a non-empty string`);
438
+ if (raw.resourceType !== undefined && !nonEmpty(raw.resourceType)) throw new DriftRecordError("shape", `${where}: resourceType must be a non-empty string`);
439
+ if (!isRecordObject(raw.evidence) || !nonEmpty(raw.evidence.tool)) throw new DriftRecordError("shape", `${where}: evidence.tool must be a non-empty string`);
440
+
441
+ const read = raw.read as DriftRead;
442
+ const status = raw.status as DriftStatus;
443
+
444
+ if (read === "state" && !STATE_READ_STATUSES.includes(status)) {
445
+ throw new DriftRecordError(
446
+ "state-read-status",
447
+ `${where}: a state read says what the last apply recorded, so it can only be ${STATE_READ_STATUSES.join(", ")}, never ${status}`,
448
+ );
449
+ }
450
+
451
+ let unobserved: DriftUnobserved | undefined;
452
+ if (status === "not-observed") {
453
+ if (raw.unobserved === undefined) throw new DriftRecordError("unobserved-reason", `${where}: not-observed needs unobserved.reason`);
454
+ unobserved = checkUnobserved(raw.unobserved, where, "unobserved");
455
+ } else if (raw.unobserved !== undefined) {
456
+ throw new DriftRecordError("unobserved-on-observed", `${where}: only not-observed carries unobserved`);
457
+ }
458
+
459
+ let attributes: DriftAttribute[] | undefined;
460
+ if (raw.attributes !== undefined) {
461
+ if (status !== "drifted") throw new DriftRecordError("attributes-status", `${where}: only drifted carries attributes`);
462
+ if (!Array.isArray(raw.attributes)) throw new DriftRecordError("shape", `${where}: attributes must be an array`);
463
+ const byPath = new Map<string, DriftAttribute>();
464
+ for (const [i, a] of raw.attributes.entries()) {
465
+ if (!isRecordObject(a) || !nonEmpty(a.path)) throw new DriftRecordError("shape", `${where}: attributes[${i}] needs a path`);
466
+ if (a.sensitive !== undefined && a.sensitive !== true) throw new DriftRecordError("shape", `${where}: attributes[${i}].sensitive must be true when set`);
467
+ const redacted = redactDriftAttribute(a as unknown as DriftAttribute);
468
+ const earlier = byPath.get(redacted.path);
469
+ // Two rows at one path (a block's instances share none, so this is one
470
+ // instance reported twice): a redaction on either redacts the path.
471
+ byPath.set(redacted.path, earlier?.sensitive === true ? earlier : redacted);
472
+ }
473
+ attributes = [...byPath.values()].sort((a, b) => byString(a.path, b.path));
474
+ }
475
+
476
+ let attributesUnobserved: DriftUnobserved | undefined;
477
+ if (raw.attributesUnobserved !== undefined) {
478
+ if (status !== "in-sync" && status !== "drifted") {
479
+ throw new DriftRecordError("attributes-unobserved-status", `${where}: only in-sync or drifted carries attributesUnobserved`);
480
+ }
481
+ attributesUnobserved = checkUnobserved(raw.attributesUnobserved, where, "attributesUnobserved");
482
+ }
483
+
484
+ const ev = raw.evidence;
485
+ return {
486
+ member: raw.member,
487
+ engine: raw.engine as DriftEngine,
488
+ address: raw.address,
489
+ ...(raw.resourceType !== undefined ? { resourceType: raw.resourceType as string } : {}),
490
+ node: raw.node as string | null,
491
+ ...(raw.instanceOf !== undefined ? { instanceOf: raw.instanceOf as string } : {}),
492
+ observedAt: raw.observedAt,
493
+ read,
494
+ ...(raw.fellBackFrom !== undefined ? { fellBackFrom: raw.fellBackFrom as string } : {}),
495
+ status,
496
+ ...(unobserved ? { unobserved } : {}),
497
+ ...(attributes ? { attributes } : {}),
498
+ ...(attributesUnobserved ? { attributesUnobserved } : {}),
499
+ ...(raw.ownership !== undefined ? { ownership: checkOwnership(raw.ownership, where) } : {}),
500
+ evidence: {
501
+ tool: ev.tool as string,
502
+ ...(nonEmpty(ev.version) ? { version: ev.version } : {}),
503
+ ...(nonEmpty(ev.command) ? { command: ev.command } : {}),
504
+ ...(nonEmpty(ev.engineCode) ? { engineCode: ev.engineCode } : {}),
505
+ },
506
+ };
507
+ }
508
+
509
+ /** Read one record from parsed JSON. The same checks as {@link driftRecord}. */
510
+ export function parseDriftRecord(value: unknown): DriftRecord {
511
+ if (!isRecordObject(value)) throw new DriftRecordError("shape", "a drift record must be an object");
512
+ return driftRecord(value as unknown as DriftRecord);
513
+ }
514
+
515
+ function recordKey(r: Pick<DriftRecord, "member" | "address" | "read">): string {
516
+ return `${r.member}\u0000${r.address}\u0000${r.read}`;
517
+ }
518
+
519
+ /** Counts per status, zero included. */
520
+ export function countDriftStatuses(records: Iterable<Pick<DriftRecord, "status">>): DriftCounts {
521
+ const counts = Object.fromEntries(DRIFT_STATUSES.map((s) => [s, 0])) as DriftCounts;
522
+ for (const r of records) counts[r.status]++;
523
+ return counts;
524
+ }
525
+
526
+ export interface DriftDocumentOptions {
527
+ /** The chant version to record in `chant`. */
528
+ chant?: string;
529
+ }
530
+
531
+ /**
532
+ * Join records into one document. A later record for the same `(member,
533
+ * address, read)` replaces the earlier one, as a stream's later line does.
534
+ * Records are sorted by member, address and read. Every record is checked
535
+ * with {@link driftRecord}.
536
+ */
537
+ export function driftDocument(records: Iterable<DriftRecord>, options: DriftDocumentOptions = {}): DriftDocument {
538
+ const latest = new Map<string, DriftRecord>();
539
+ for (const r of records) {
540
+ const checked = driftRecord(r);
541
+ latest.set(recordKey(checked), checked);
542
+ }
543
+ const sorted = [...latest.entries()].sort(([a], [b]) => byString(a, b)).map(([, r]) => r);
544
+ return {
545
+ $schema: DRIFT_SCHEMA_ID,
546
+ drift: DRIFT_VERSION,
547
+ ...(options.chant !== undefined ? { chant: options.chant } : {}),
548
+ records: sorted,
549
+ counts: countDriftStatuses(sorted),
550
+ };
551
+ }
552
+
553
+ /** Read a document from parsed JSON, checking every record. Counts are recomputed. */
554
+ export function parseDriftDocument(value: unknown): DriftDocument {
555
+ if (!isRecordObject(value)) throw new DriftRecordError("shape", "a drift document must be an object");
556
+ if (value.drift !== DRIFT_VERSION) throw new DriftRecordError("shape", `a drift document says drift ${JSON.stringify(value.drift)}, not "${DRIFT_VERSION}"`);
557
+ if (!Array.isArray(value.records)) throw new DriftRecordError("shape", "a drift document needs a records array");
558
+ return driftDocument(value.records.map(parseDriftRecord), nonEmpty(value.chant) ? { chant: value.chant } : {});
559
+ }
560
+
561
+ /**
562
+ * The document's digest, for a WatchOp that keeps its document as evidence
563
+ * in its own run ledger: `computePlanDigest("drift-document", …)` over the
564
+ * records sorted as {@link driftDocument} sorts them, without `observedAt`
565
+ * and `evidence`, which change on every read. No gate binds it.
566
+ */
567
+ export function driftDocumentDigest(doc: Pick<DriftDocument, "records">): string {
568
+ const records = [...doc.records]
569
+ .sort((a, b) => byString(recordKey(a), recordKey(b)))
570
+ .map(({ observedAt: _at, evidence: _evidence, ...r }) => r);
571
+ return computePlanDigest("drift-document", records);
572
+ }
573
+
574
+ // ── the block rollup ───────────────────────────────────────────────────────────
575
+
576
+ /**
577
+ * The precedence a block's instance records roll up in, most significant
578
+ * first, for core's one rollup (`rollupInstances` in `./instance-nodes.ts`,
579
+ * #4454). Partial knowledge is not knowledge, so one not-observed instance
580
+ * makes the block not-observed, as `TERRAFORM_INSTANCE_ROLLUP` puts
581
+ * `unobserved` first. A difference outranks an absence, an absence
582
+ * outranks agreement, and `runtime` is last because a controller's children
583
+ * never decide a block.
584
+ */
585
+ export const DRIFT_INSTANCE_ROLLUP: readonly DriftStatus[] = [
586
+ "not-observed",
587
+ "drifted",
588
+ "missing",
589
+ "unclaimed",
590
+ "undeclared",
591
+ "pending",
592
+ "in-sync",
593
+ "recorded",
594
+ "runtime",
595
+ ];
596
+
597
+ /** One block node's rolled-up status and the records it came from. */
598
+ export interface DriftBlockRollup {
599
+ /** The block node: the records' `instanceOf`, or their `node` when they are on the block itself. */
600
+ node: string;
601
+ status: DriftStatus;
602
+ /** Every read that answered for the block's records, sorted. */
603
+ reads: DriftRead[];
604
+ records: DriftRecord[];
605
+ }
606
+
607
+ /**
608
+ * Records grouped by block node (`instanceOf`, else `node`) and rolled up
609
+ * with `rollupInstances` over {@link DRIFT_INSTANCE_ROLLUP}. A record with a
610
+ * null `node` is on no block and is left out. Sorted by node.
611
+ */
612
+ export function rollupDriftByBlock(records: Iterable<DriftRecord>): DriftBlockRollup[] {
613
+ const groups = new Map<string, DriftRecord[]>();
614
+ for (const r of records) {
615
+ const block = r.instanceOf ?? r.node;
616
+ if (block === null) continue;
617
+ const list = groups.get(block) ?? [];
618
+ list.push(r);
619
+ groups.set(block, list);
620
+ }
621
+ return [...groups.entries()]
622
+ .sort(([a], [b]) => byString(a, b))
623
+ .map(([node, list]) => ({
624
+ node,
625
+ status: rollupInstances(list.map((r) => ({ kind: r.status })), DRIFT_INSTANCE_ROLLUP)!.kind,
626
+ reads: [...new Set(list.map((r) => r.read))].sort(byString),
627
+ records: list,
628
+ }));
629
+ }
630
+
631
+ /**
632
+ * The document with each record's `node` and `instanceOf` set from `graph`
633
+ * by `graphNodeFor` (`./change-set-graph.ts`), the rule a change-set entry
634
+ * uses, so a drift row and a change-set entry for one instance name the
635
+ * same node: an instance of a `count` or `for_each` block is on
636
+ * `<blockNode>[0]` with `instanceOf` the block node, and an instance under
637
+ * an expanded module call is on `<root>/module.app[0]/<block>[1]` with the
638
+ * same `instanceOf` (#4486). A Pulumi URN is on
639
+ * `<member>/<key>` by the URN rule (`./pulumi-urn.ts`, #4456), through the
640
+ * `pulumi` planner's case, and so is a plunge URN ({@link driftPlanner}).
641
+ * A null `graph` gives every record `node: null`.
642
+ */
643
+ export function mapDriftToGraph(doc: DriftDocument, graph: ChangeSetGraphNodes | null): DriftDocument {
644
+ return {
645
+ ...doc,
646
+ records: doc.records.map(({ instanceOf: _i, ...r }) => {
647
+ if (graph === null) return { ...r, node: null };
648
+ const placed = graphNodeFor({ member: r.member, planner: driftPlanner(r.engine), address: r.address }, graph);
649
+ return { ...r, node: placed.node, ...(placed.instanceOf !== undefined ? { instanceOf: placed.instanceOf } : {}) };
650
+ }),
651
+ };
652
+ }
653
+
654
+ // ── streaming ──────────────────────────────────────────────────────────────────
655
+
656
+ /**
657
+ * The NDJSON stream, one JSON object per line, built like the run-event
658
+ * stream and in the layout choudoufu's `live-plan -resource` writes
659
+ * (choudoufu `internal/command/views/live_drift.go`): every line has a
660
+ * `type`, `drift-start`, `drift-record` or `drift-settle`, and a record
661
+ * line is the record itself with `type` beside its fields.
662
+ */
663
+
664
+ /** One address a stream expects to hear about, with the node and type a not-reached record should carry. */
665
+ export interface DriftExpected {
666
+ address: string;
667
+ node?: string | null;
668
+ instanceOf?: string;
669
+ resourceType?: string;
670
+ }
671
+
672
+ /** Opens a member's read: which member, engine and read, and every address it expects. */
673
+ export interface DriftStartLine {
674
+ type: "drift-start";
675
+ drift: typeof DRIFT_VERSION;
676
+ member: string;
677
+ engine: DriftEngine;
678
+ read: DriftRead;
679
+ /** The one address the read was asked about, on a single-resource read (`live-plan -resource`). */
680
+ resource?: string;
681
+ fellBackFrom?: string;
682
+ /** RFC 3339, when the writer gives it. */
683
+ at?: string;
684
+ /** Bare addresses, or addresses with the node and type a not-reached record is given. */
685
+ expected: Array<string | DriftExpected>;
686
+ /** The evidence a not-reached record is given. Default `{ tool: <engine> }`. */
687
+ evidence?: DriftEvidence;
688
+ }
689
+
690
+ /** One record, as its read finishes: the record's own fields beside `type`. A later line for the same `(member, address, read)` replaces it. */
691
+ export type DriftRecordLine = DriftRecord & { type: "drift-record" };
692
+
693
+ /** Closes a member's read. */
694
+ export interface DriftSettleLine {
695
+ type: "drift-settle";
696
+ drift: typeof DRIFT_VERSION;
697
+ member: string;
698
+ engine: DriftEngine;
699
+ read: DriftRead;
700
+ /** Records per status. A writer may leave out a status it has none of. */
701
+ counts: Partial<DriftCounts>;
702
+ /** The engine's own account of what the read cost (choudoufu: `recordsRead`, `sweepCalls`, ...). Carried, never interpreted. */
703
+ reads?: Record<string, unknown>;
704
+ /** RFC 3339, when the writer gives it: every not-reached record's `observedAt`. */
705
+ at?: string;
706
+ /**
707
+ * Expected addresses with no record line, when the writer lists them. A
708
+ * reader treats every expected address with no record as not-reached
709
+ * whether or not this is given; a writer that emits its own not-reached
710
+ * records (choudoufu does) leaves it out.
711
+ */
712
+ notReached?: string[];
713
+ }
714
+
715
+ export type DriftLine = DriftStartLine | DriftRecordLine | DriftSettleLine;
716
+
717
+ /** A start line's expected entries, bare addresses made objects. */
718
+ export function driftExpected(start: Pick<DriftStartLine, "expected">): DriftExpected[] {
719
+ return start.expected.map((e) => (typeof e === "string" ? { address: e } : e));
720
+ }
721
+
722
+ /** A record as its stream line. */
723
+ export function driftRecordLine(record: DriftRecord): DriftRecordLine {
724
+ return { type: "drift-record", ...record };
725
+ }
726
+
727
+ /** One line of the stream, newline-terminated. */
728
+ export function encodeDriftLine(line: DriftLine): string {
729
+ return `${JSON.stringify(line)}\n`;
730
+ }
731
+
732
+ /** The whole stream for one or more reads, in order. */
733
+ export function encodeDriftLines(lines: Iterable<DriftLine>): string {
734
+ let out = "";
735
+ for (const line of lines) out += encodeDriftLine(line);
736
+ return out;
737
+ }
738
+
739
+ function checkStreamHead(v: Record<string, unknown>, type: string): void {
740
+ if (v.drift !== DRIFT_VERSION) throw new DriftRecordError("shape", `a ${type} line says drift ${JSON.stringify(v.drift)}, not "${DRIFT_VERSION}"`);
741
+ if (!nonEmpty(v.member)) throw new DriftRecordError("shape", `a ${type} line needs a member`);
742
+ if (!(DRIFT_ENGINES as readonly unknown[]).includes(v.engine)) throw new DriftRecordError("shape", `a ${type} line for ${v.member} names engine ${JSON.stringify(v.engine)}`);
743
+ if (!(DRIFT_READS as readonly unknown[]).includes(v.read)) throw new DriftRecordError("shape", `a ${type} line for ${v.member} names read ${JSON.stringify(v.read)}`);
744
+ if (v.at !== undefined && (typeof v.at !== "string" || !RFC3339.test(v.at))) throw new DriftRecordError("shape", `a ${type} line for ${v.member} has an at that is not RFC 3339`);
745
+ }
746
+
747
+ /** Read one line, checking it. A record line goes through {@link driftRecord}. Unknown fields are dropped. */
748
+ export function decodeDriftLine(text: string): DriftLine {
749
+ let v: unknown;
750
+ try {
751
+ v = JSON.parse(text);
752
+ } catch (err) {
753
+ throw new DriftRecordError("shape", `a drift line is not JSON: ${(err as Error).message}`);
754
+ }
755
+ if (!isRecordObject(v)) throw new DriftRecordError("shape", "a drift line must be an object");
756
+ switch (v.type) {
757
+ case "drift-start": {
758
+ checkStreamHead(v, "drift-start");
759
+ if (!Array.isArray(v.expected)) throw new DriftRecordError("shape", `the drift-start line for ${v.member} needs an expected array`);
760
+ const expected = v.expected.map((e, i): string | DriftExpected => {
761
+ if (nonEmpty(e)) return e;
762
+ if (!isRecordObject(e) || !nonEmpty(e.address)) throw new DriftRecordError("shape", `the drift-start line for ${v.member}: expected[${i}] needs an address`);
763
+ return {
764
+ address: e.address,
765
+ ...(e.node === null || nonEmpty(e.node) ? { node: e.node as string | null } : {}),
766
+ ...(nonEmpty(e.instanceOf) ? { instanceOf: e.instanceOf } : {}),
767
+ ...(nonEmpty(e.resourceType) ? { resourceType: e.resourceType } : {}),
768
+ };
769
+ });
770
+ if (v.evidence !== undefined && (!isRecordObject(v.evidence) || !nonEmpty(v.evidence.tool))) {
771
+ throw new DriftRecordError("shape", `the drift-start line for ${v.member} has evidence with no tool`);
772
+ }
773
+ return {
774
+ type: "drift-start",
775
+ drift: DRIFT_VERSION,
776
+ member: v.member as string,
777
+ engine: v.engine as DriftEngine,
778
+ read: v.read as DriftRead,
779
+ ...(nonEmpty(v.resource) ? { resource: v.resource } : {}),
780
+ ...(nonEmpty(v.fellBackFrom) ? { fellBackFrom: v.fellBackFrom } : {}),
781
+ ...(v.at !== undefined ? { at: v.at as string } : {}),
782
+ expected,
783
+ ...(v.evidence !== undefined ? { evidence: v.evidence as unknown as DriftEvidence } : {}),
784
+ };
785
+ }
786
+ case "drift-record": {
787
+ const { type: _type, ...record } = v;
788
+ return driftRecordLine(parseDriftRecord(record));
789
+ }
790
+ case "drift-settle": {
791
+ checkStreamHead(v, "drift-settle");
792
+ if (!isRecordObject(v.counts)) throw new DriftRecordError("shape", `the drift-settle line for ${v.member} needs counts`);
793
+ const counts: Partial<DriftCounts> = {};
794
+ for (const [s, n] of Object.entries(v.counts)) {
795
+ if (!(DRIFT_STATUSES as readonly string[]).includes(s)) throw new DriftRecordError("shape", `the drift-settle line for ${v.member} counts status ${JSON.stringify(s)}`);
796
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 0) throw new DriftRecordError("shape", `the drift-settle line for ${v.member}: counts.${s} must be a count`);
797
+ counts[s as DriftStatus] = n;
798
+ }
799
+ if (v.notReached !== undefined && (!Array.isArray(v.notReached) || !v.notReached.every(nonEmpty))) {
800
+ throw new DriftRecordError("shape", `the drift-settle line for ${v.member} has a notReached that is not a list of addresses`);
801
+ }
802
+ if (v.reads !== undefined && !isRecordObject(v.reads)) throw new DriftRecordError("shape", `the drift-settle line for ${v.member} has reads that is not an object`);
803
+ return {
804
+ type: "drift-settle",
805
+ drift: DRIFT_VERSION,
806
+ member: v.member as string,
807
+ engine: v.engine as DriftEngine,
808
+ read: v.read as DriftRead,
809
+ counts,
810
+ ...(v.reads !== undefined ? { reads: v.reads as Record<string, unknown> } : {}),
811
+ ...(v.at !== undefined ? { at: v.at as string } : {}),
812
+ ...(v.notReached !== undefined ? { notReached: [...(v.notReached as string[])].sort(byString) } : {}),
813
+ };
814
+ }
815
+ default:
816
+ throw new DriftRecordError("shape", `a drift line has type ${JSON.stringify(v.type)}, not drift-start, drift-record or drift-settle`);
817
+ }
818
+ }
819
+
820
+ /** Every non-blank line of an NDJSON stream, decoded. */
821
+ export function decodeDriftLines(text: string): DriftLine[] {
822
+ return text
823
+ .split("\n")
824
+ .map((l) => l.trim())
825
+ .filter((l) => l.length > 0)
826
+ .map(decodeDriftLine);
827
+ }
828
+
829
+ /** Open a read: the start line for a member's expected addresses, each once. */
830
+ export function driftStart(input: Omit<DriftStartLine, "type" | "drift">): DriftStartLine {
831
+ const seen = new Set<string>();
832
+ const expected = input.expected.filter((e) => {
833
+ const address = typeof e === "string" ? e : e.address;
834
+ return !seen.has(address) && seen.add(address);
835
+ });
836
+ return { type: "drift-start", drift: DRIFT_VERSION, ...input, expected };
837
+ }
838
+
839
+ type StreamHead = Pick<DriftStartLine, "member" | "engine" | "read" | "fellBackFrom" | "evidence">;
840
+
841
+ function notReachedRecord(head: StreamHead, expected: DriftExpected, at: string): DriftRecord {
842
+ return driftRecord({
843
+ member: head.member,
844
+ engine: head.engine,
845
+ address: expected.address,
846
+ ...(expected.resourceType !== undefined ? { resourceType: expected.resourceType } : {}),
847
+ node: expected.node ?? null,
848
+ ...(expected.instanceOf !== undefined && expected.node ? { instanceOf: expected.instanceOf } : {}),
849
+ observedAt: at,
850
+ read: head.read,
851
+ ...(head.fellBackFrom !== undefined ? { fellBackFrom: head.fellBackFrom } : {}),
852
+ status: "not-observed",
853
+ unobserved: { reason: "not-reached", detail: `the ${head.read} read settled without reporting ${expected.address}` },
854
+ evidence: head.evidence ?? { tool: head.engine },
855
+ });
856
+ }
857
+
858
+ /**
859
+ * Close a read. `records` are the records sent since `start`, in order; a
860
+ * later one for an address and read replaces an earlier one, and records
861
+ * for another member are ignored. A record for an expected address answers
862
+ * it whatever its read, since one read can answer an address with another
863
+ * (a plan stream's unclaimed row is an `ownership` record). Every expected
864
+ * address with no record becomes `not-observed / not-reached` at `at`.
865
+ * Returns the settle line, which lists them in `notReached`, and the
866
+ * member's final records.
867
+ */
868
+ export function driftSettle(start: DriftStartLine, records: Iterable<DriftRecord>, at: string): { line: DriftSettleLine; records: DriftRecord[] } {
869
+ const mine = new Map<string, DriftRecord>();
870
+ const answered = new Set<string>();
871
+ for (const r of records) {
872
+ if (r.member !== start.member) continue;
873
+ const checked = driftRecord(r);
874
+ mine.set(recordKey(checked), checked);
875
+ answered.add(checked.address);
876
+ }
877
+ const notReached: string[] = [];
878
+ for (const e of driftExpected(start)) {
879
+ if (answered.has(e.address)) continue;
880
+ notReached.push(e.address);
881
+ const r = notReachedRecord(start, e, at);
882
+ mine.set(recordKey(r), r);
883
+ }
884
+ const final = [...mine.entries()].sort(([a], [b]) => byString(a, b)).map(([, r]) => r);
885
+ return {
886
+ line: {
887
+ type: "drift-settle",
888
+ drift: DRIFT_VERSION,
889
+ member: start.member,
890
+ engine: start.engine,
891
+ read: start.read,
892
+ counts: countDriftStatuses(final),
893
+ at,
894
+ notReached: notReached.sort(byString),
895
+ },
896
+ records: final,
897
+ };
898
+ }
899
+
900
+ /** An address a started read expects and has not reported yet. Not a status: a settle always resolves it. */
901
+ export interface DriftReading {
902
+ member: string;
903
+ read: DriftRead;
904
+ address: string;
905
+ node: string | null;
906
+ }
907
+
908
+ /**
909
+ * Fold a stream into a document. Record lines replace earlier ones for the
910
+ * same `(member, address, read)`. At a settle line, every address its start
911
+ * expected that no record answered (by member and address, whatever the
912
+ * read) becomes `not-observed / not-reached`, with the node and type the
913
+ * start gave, at the settle's `at`, else the start's, else the latest
914
+ * record's `observedAt` in the stream; `notReached` addresses the start did
915
+ * not list are added the same way. A start with no settle yet leaves its
916
+ * unanswered addresses in `reading`, which a renderer shows as "reading"
917
+ * and nothing else.
918
+ */
919
+ export function foldDriftLines(lines: Iterable<DriftLine>, options: DriftDocumentOptions = {}): { document: DriftDocument; reading: DriftReading[] } {
920
+ const records: DriftRecord[] = [];
921
+ const answered = new Set<string>();
922
+ const open = new Map<string, DriftStartLine>();
923
+ let latest: string | undefined;
924
+ const memberKey = (member: string, x: string) => `${member}\u0000${x}`;
925
+ for (const line of lines) {
926
+ if (line.type === "drift-start") {
927
+ open.set(memberKey(line.member, line.read), line);
928
+ } else if (line.type === "drift-record") {
929
+ const { type: _type, ...record } = line;
930
+ records.push(record);
931
+ answered.add(memberKey(record.member, record.address));
932
+ if (latest === undefined || record.observedAt > latest) latest = record.observedAt;
933
+ } else {
934
+ const start = open.get(memberKey(line.member, line.read));
935
+ open.delete(memberKey(line.member, line.read));
936
+ const expected = new Map((start ? driftExpected(start) : []).map((e) => [e.address, e]));
937
+ for (const address of line.notReached ?? []) if (!expected.has(address)) expected.set(address, { address });
938
+ const at = line.at ?? start?.at ?? latest ?? new Date(0).toISOString();
939
+ const head: StreamHead = start ?? { member: line.member, engine: line.engine, read: line.read };
940
+ for (const e of expected.values()) {
941
+ if (answered.has(memberKey(line.member, e.address))) continue;
942
+ records.push(notReachedRecord(head, e, at));
943
+ answered.add(memberKey(line.member, e.address));
944
+ }
945
+ }
946
+ }
947
+ const reading: DriftReading[] = [];
948
+ for (const start of open.values()) {
949
+ for (const e of driftExpected(start)) {
950
+ if (!answered.has(memberKey(start.member, e.address))) {
951
+ reading.push({ member: start.member, read: start.read, address: e.address, node: e.node ?? null });
952
+ }
953
+ }
954
+ }
955
+ reading.sort((a, b) => byString(recordKey(a), recordKey(b)));
956
+ return { document: driftDocument(records, options), reading };
957
+ }
958
+
959
+ // ── chant adapters ─────────────────────────────────────────────────────────────
960
+
961
+ /** What every adapter takes beside its source document. */
962
+ export interface DriftAdapterBase {
963
+ member: string;
964
+ /** RFC 3339, given to every record. */
965
+ observedAt: string;
966
+ /**
967
+ * The member's graph, to place each record with `graphNodeFor` as it is
968
+ * made. Omit to leave `node` null, for {@link mapDriftToGraph} to fill.
969
+ */
970
+ graph?: ChangeSetGraphNodes | null;
971
+ /** The read that was asked for and failed, when this one answered instead. */
972
+ fellBackFrom?: string;
973
+ /** The chant or engine version, for `evidence.version`. */
974
+ version?: string;
975
+ }
976
+
977
+ /**
978
+ * Where an adapter's record for `address` sits: `graphNodeFor` on the
979
+ * input's graph, plunge's by the `pulumi` rule ({@link driftPlanner}). Exported
980
+ * for the lexicons' adapters.
981
+ */
982
+ export function placeDriftRecord(
983
+ input: Pick<DriftAdapterBase, "member" | "graph">,
984
+ engine: DriftEngine,
985
+ address: string,
986
+ ): { node: string | null; instanceOf?: string } {
987
+ if (!input.graph) return { node: null };
988
+ const placed = graphNodeFor({ member: input.member, planner: driftPlanner(engine), address }, input.graph);
989
+ return { node: placed.node, ...(placed.instanceOf !== undefined ? { instanceOf: placed.instanceOf } : {}) };
990
+ }
991
+
992
+ function base(input: DriftAdapterBase, engine: DriftEngine, read: DriftRead, address: string) {
993
+ return {
994
+ member: input.member,
995
+ engine,
996
+ address,
997
+ ...placeDriftRecord(input, engine, address),
998
+ observedAt: input.observedAt,
999
+ read,
1000
+ ...(input.fellBackFrom !== undefined ? { fellBackFrom: input.fellBackFrom } : {}),
1001
+ };
1002
+ }
1003
+
1004
+ function chantEvidence(input: DriftAdapterBase, command: string): DriftEvidence {
1005
+ return { tool: "chant", ...(input.version ? { version: input.version } : {}), command };
1006
+ }
1007
+
1008
+ export interface LiveDiffDriftInput extends DriftAdapterBase {
1009
+ /** `chant lifecycle diff --live --json`, parsed. */
1010
+ diff: LiveDiffResult;
1011
+ /** Entity types by name, for the records whose diff row carries none. */
1012
+ types?: Record<string, string>;
1013
+ }
1014
+
1015
+ /**
1016
+ * A chant member's records, from `lifecycle diff --live`: `unchanged` and
1017
+ * `newlyObserved` are in-sync, `driftedSinceSnapshot` drifted (before is the
1018
+ * snapshot, after the read), `missing` pending, `disappeared` missing,
1019
+ * `orphan` undeclared, `runtimeChildren` runtime, `unobserved` not-observed.
1020
+ * An entity in both `missing` and `disappeared` was deployed and is gone,
1021
+ * so it is missing.
1022
+ */
1023
+ export function liveDiffDriftRecords(input: LiveDiffDriftInput): DriftRecord[] {
1024
+ const { diff } = input;
1025
+ const evidence = chantEvidence(input, "chant lifecycle diff --live");
1026
+ const out = new Map<string, DriftRecord>();
1027
+ const put = (name: string, rest: Pick<DriftRecord, "status"> & Partial<DriftRecord>, type?: string) => {
1028
+ if (out.has(name)) return;
1029
+ const t = type ?? input.types?.[name];
1030
+ out.set(name, driftRecord({ ...base(input, "chant", "live-read", name), ...(t ? { resourceType: t } : {}), evidence, ...rest }));
1031
+ };
1032
+ for (const u of diff.unobserved ?? []) {
1033
+ put(u.name, {
1034
+ status: "not-observed",
1035
+ unobserved: { reason: u.reason, ...(u.detail ? { detail: u.detail } : {}), ...(u.queried ? { queried: u.queried } : {}) },
1036
+ }, u.type);
1037
+ }
1038
+ for (const d of diff.driftedSinceSnapshot ?? []) {
1039
+ put(d.name, {
1040
+ status: "drifted",
1041
+ attributes: d.changes.map((c) => ({ path: c.path, ...(c.oldValue !== undefined ? { before: c.oldValue } : {}), ...(c.newValue !== undefined ? { after: c.newValue } : {}) })),
1042
+ }, d.type);
1043
+ }
1044
+ for (const name of diff.disappeared ?? []) put(name, { status: "missing" });
1045
+ for (const name of diff.missing ?? []) put(name, { status: "pending" });
1046
+ for (const name of diff.orphan ?? []) put(name, { status: "undeclared" });
1047
+ for (const r of diff.runtimeChildren ?? []) put(r.name, { status: "runtime", ownership: { verdict: "foreign", ownedBy: r.owner } }, r.type);
1048
+ for (const name of [...(diff.unchanged ?? []), ...(diff.newlyObserved ?? [])]) put(name, { status: "in-sync" });
1049
+ return [...out.values()].sort((a, b) => byString(a.address, b.address));
1050
+ }
1051
+
1052
+ export interface DeepDiffDriftInput extends DriftAdapterBase {
1053
+ /** `chant lifecycle diff --live --deep --json`'s deep result, parsed. */
1054
+ diff: DeepDiffResult;
1055
+ /**
1056
+ * The command for `evidence.command`, when the deep result came from
1057
+ * somewhere other than a live read: `chant lifecycle diff --index <ref>`
1058
+ * (#4526) diffs against an index version. Default
1059
+ * `chant lifecycle diff --live --deep`.
1060
+ */
1061
+ command?: string;
1062
+ }
1063
+
1064
+ /**
1065
+ * A chant member's records from the deep diff alone: `drifted` with paths
1066
+ * (before is the declared value, after the live one; an `absent` path has
1067
+ * no after), `unchanged` in-sync, `unobserved` not-observed and
1068
+ * `undeclaredEntities` undeclared. Accepted and held paths are not drift and
1069
+ * are left out. To lay these over the thin read's records, so a deep hole
1070
+ * stays on the attribute surface, use {@link mergeDeepDrift}.
1071
+ */
1072
+ export function deepDiffDriftRecords(input: DeepDiffDriftInput): DriftRecord[] {
1073
+ const { diff } = input;
1074
+ const evidence = chantEvidence(input, input.command ?? "chant lifecycle diff --live --deep");
1075
+ const out = new Map<string, DriftRecord>();
1076
+ const put = (name: string, rest: Pick<DriftRecord, "status"> & Partial<DriftRecord>) => {
1077
+ if (!out.has(name)) out.set(name, driftRecord({ ...base(input, "chant", "live-read", name), evidence, ...rest }));
1078
+ };
1079
+ for (const u of diff.unobserved ?? []) {
1080
+ put(u.name, {
1081
+ ...(u.type ? { resourceType: u.type } : {}),
1082
+ status: "not-observed",
1083
+ unobserved: { reason: u.reason, ...(u.detail ? { detail: u.detail } : {}), ...(u.queried ? { queried: u.queried } : {}) },
1084
+ });
1085
+ }
1086
+ for (const d of diff.drifted ?? []) {
1087
+ put(d.name, {
1088
+ resourceType: d.type,
1089
+ status: "drifted",
1090
+ attributes: d.changes.map((c) => ({
1091
+ path: c.path,
1092
+ ...(c.declared !== undefined ? { before: c.declared } : {}),
1093
+ ...(c.kind === "changed" && c.live !== undefined ? { after: c.live } : {}),
1094
+ })),
1095
+ });
1096
+ }
1097
+ for (const name of diff.undeclaredEntities ?? []) put(name, { status: "undeclared" });
1098
+ for (const name of diff.unchanged ?? []) put(name, { status: "in-sync" });
1099
+ return [...out.values()].sort((a, b) => byString(a.address, b.address));
1100
+ }
1101
+
1102
+ /**
1103
+ * The thin read's records with the deep read laid over them, by member and
1104
+ * address. A thin in-sync record the deep read found drifted becomes
1105
+ * drifted with the deep paths. A deep not-observed record never changes the
1106
+ * thin status: presence is known, so it becomes `attributesUnobserved` on an
1107
+ * in-sync or drifted record and is dropped elsewhere. Deep records with no
1108
+ * thin record are added as they are.
1109
+ */
1110
+ export function mergeDeepDrift(thin: Iterable<DriftRecord>, deep: Iterable<DriftRecord>): DriftRecord[] {
1111
+ const key = (r: DriftRecord) => `${r.member}\u0000${r.address}`;
1112
+ const out = new Map<string, DriftRecord>();
1113
+ for (const r of thin) out.set(key(r), r);
1114
+ for (const d of deep) {
1115
+ const t = out.get(key(d));
1116
+ if (!t) {
1117
+ out.set(key(d), d);
1118
+ continue;
1119
+ }
1120
+ if (d.status === "not-observed") {
1121
+ if (t.status === "in-sync" || t.status === "drifted") out.set(key(d), driftRecord({ ...t, attributesUnobserved: d.unobserved! }));
1122
+ } else if (d.status === "drifted" && (t.status === "in-sync" || t.status === "drifted")) {
1123
+ out.set(key(d), driftRecord({ ...t, status: "drifted", attributes: [...(t.attributes ?? []), ...(d.attributes ?? [])] }));
1124
+ }
1125
+ }
1126
+ return [...out.values()].sort((a, b) => byString(key(a), key(b)));
1127
+ }
1128
+
1129
+ export interface ComponentStatusDriftInput extends DriftAdapterBase {
1130
+ /** `chant components status [env] --json`'s rows. */
1131
+ rows: readonly ComponentStatusRow[];
1132
+ }
1133
+
1134
+ /**
1135
+ * A member's records from component status, one per component, addressed
1136
+ * by component name: `reconciled` in-sync, `drifted` drifted, `stale`
1137
+ * missing (recorded and no longer observed live), `unknown` not-observed
1138
+ * with the row's reason, or `not-reached` when live status was never asked
1139
+ * for. `unrecorded` is drifted when the component is live, since something
1140
+ * runs that no release accounts for, pending when nothing is, and
1141
+ * not-observed when live status was not asked for.
1142
+ */
1143
+ export function componentStatusDriftRecords(input: ComponentStatusDriftInput): DriftRecord[] {
1144
+ const evidence = chantEvidence(input, "chant components status --live");
1145
+ return input.rows
1146
+ .map((row) => {
1147
+ const at = base(input, "chant", "live-read", row.component);
1148
+ const common = { ...at, resourceType: "component", evidence };
1149
+ switch (row.reconciliation) {
1150
+ case "reconciled":
1151
+ return driftRecord({ ...common, status: "in-sync" });
1152
+ case "drifted":
1153
+ return driftRecord({ ...common, status: "drifted" });
1154
+ case "stale":
1155
+ return driftRecord({ ...common, status: "missing" });
1156
+ case "unrecorded":
1157
+ if (row.live === true) return driftRecord({ ...common, status: "drifted" });
1158
+ if (row.live === false) return driftRecord({ ...common, status: "pending" });
1159
+ return driftRecord({ ...common, status: "not-observed", unobserved: { reason: "not-reached", detail: row.detail } });
1160
+ case "unknown":
1161
+ return driftRecord({
1162
+ ...common,
1163
+ status: "not-observed",
1164
+ unobserved: row.unobserved
1165
+ ? { reason: row.unobserved.reason, ...(row.unobserved.detail ? { detail: row.unobserved.detail } : {}) }
1166
+ : { reason: "not-reached", detail: row.detail },
1167
+ });
1168
+ }
1169
+ })
1170
+ .sort((a, b) => byString(a.address, b.address));
1171
+ }
1172
+
1173
+ // ── a lexicon's own drift read ─────────────────────────────────────────────────
1174
+
1175
+ /**
1176
+ * What `chant lifecycle drift` asks a lexicon for (`LexiconPlugin.readDrift`,
1177
+ * behold#995). `chant workspace drift` runs that command in a member's reader
1178
+ * project, so a lexicon that reads members of a package kind (the terraform
1179
+ * lexicon's `terraform` and `choudoufu` kinds) answers with its own engine,
1180
+ * and chant core never imports the lexicon.
1181
+ */
1182
+ export interface ReadDriftOptions {
1183
+ /** The workspace member the records are for. Every line the lexicon returns names it. */
1184
+ member: string;
1185
+ /** The directory holding the project's `chant.config.*`. */
1186
+ projectRoot: string;
1187
+ /** The project's resolved config, for the lexicon's own namespace. */
1188
+ config: Record<string, unknown>;
1189
+ /** The read asked for. Unset, the lexicon's default read for what it finds. */
1190
+ read?: DriftRead;
1191
+ /** One address to read, on an engine that can read one resource on its own. */
1192
+ resource?: string;
1193
+ /** RFC 3339, the `observedAt` of every record the lexicon builds itself. */
1194
+ observedAt: string;
1195
+ }
1196
+
1197
+ /** Why a lexicon will not answer a drift read, in a code a caller can branch on. */
1198
+ export type ReadDriftRefusalCode = "read-unsupported" | "resource-unsupported" | "no-drift-read";
1199
+
1200
+ /**
1201
+ * A lexicon's answer: the stream lines of each read it ran, in order (a
1202
+ * `drift-start`, its records, a `drift-settle`, per root), or a refusal. A
1203
+ * read that ran and failed is not a refusal: its addresses are records
1204
+ * with status `not-observed`.
1205
+ */
1206
+ export type ReadDriftResult = { lines: DriftLine[] } | { refused: { code: ReadDriftRefusalCode; message: string } };