@intentius/chant 0.79.0 → 0.81.0

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 (462) hide show
  1. package/dist/audit/core.d.ts.map +1 -1
  2. package/dist/audit/discover.d.ts +30 -0
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/report-html.d.ts +5 -0
  5. package/dist/audit/report-html.d.ts.map +1 -1
  6. package/dist/audit/report-model.d.ts +13 -0
  7. package/dist/audit/report-model.d.ts.map +1 -1
  8. package/dist/audit/terraform-state.d.ts +13 -0
  9. package/dist/audit/terraform-state.d.ts.map +1 -1
  10. package/dist/cli/command-group.d.ts +6 -0
  11. package/dist/cli/command-group.d.ts.map +1 -1
  12. package/dist/cli/commands/audit.d.ts +44 -3
  13. package/dist/cli/commands/audit.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  15. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/doctor.d.ts.map +1 -1
  18. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  19. package/dist/cli/commands/init.d.ts +6 -0
  20. package/dist/cli/commands/init.d.ts.map +1 -1
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/onboard.d.ts.map +1 -1
  23. package/dist/cli/commands/update.d.ts +6 -0
  24. package/dist/cli/commands/update.d.ts.map +1 -1
  25. package/dist/cli/commands/vendor.d.ts +5 -0
  26. package/dist/cli/commands/vendor.d.ts.map +1 -1
  27. package/dist/cli/conflict-check.d.ts +6 -0
  28. package/dist/cli/conflict-check.d.ts.map +1 -1
  29. package/dist/cli/handlers/build.d.ts.map +1 -1
  30. package/dist/cli/handlers/components.d.ts.map +1 -1
  31. package/dist/cli/handlers/dev.d.ts.map +1 -1
  32. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  33. package/dist/cli/handlers/init.d.ts.map +1 -1
  34. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  35. package/dist/cli/handlers/misc.d.ts.map +1 -1
  36. package/dist/cli/handlers/operator.d.ts +7 -0
  37. package/dist/cli/handlers/operator.d.ts.map +1 -1
  38. package/dist/cli/handlers/promote.d.ts +20 -0
  39. package/dist/cli/handlers/promote.d.ts.map +1 -0
  40. package/dist/cli/handlers/run-generate.d.ts +39 -0
  41. package/dist/cli/handlers/run-generate.d.ts.map +1 -0
  42. package/dist/cli/handlers/run.d.ts +4 -1
  43. package/dist/cli/handlers/run.d.ts.map +1 -1
  44. package/dist/cli/handlers/vendor.d.ts +8 -3
  45. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  46. package/dist/cli/main.d.ts +12 -0
  47. package/dist/cli/main.d.ts.map +1 -1
  48. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  49. package/dist/cli/plugins.d.ts +14 -0
  50. package/dist/cli/plugins.d.ts.map +1 -1
  51. package/dist/cli/registry.d.ts +46 -1
  52. package/dist/cli/registry.d.ts.map +1 -1
  53. package/dist/codegen/fetch.d.ts +10 -0
  54. package/dist/codegen/fetch.d.ts.map +1 -1
  55. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  56. package/dist/components/cli-support.d.ts +3 -1
  57. package/dist/components/cli-support.d.ts.map +1 -1
  58. package/dist/components/discover.d.ts.map +1 -1
  59. package/dist/components/driver.d.ts +16 -2
  60. package/dist/components/driver.d.ts.map +1 -1
  61. package/dist/components/fan-out-support.d.ts.map +1 -1
  62. package/dist/components/gate-plan.d.ts +40 -0
  63. package/dist/components/gate-plan.d.ts.map +1 -0
  64. package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
  65. package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
  66. package/dist/components/presets/ecs-fargate.d.ts +10 -4
  67. package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
  68. package/dist/components/promote.d.ts +264 -0
  69. package/dist/components/promote.d.ts.map +1 -0
  70. package/dist/components/sandbox/driver.d.ts.map +1 -1
  71. package/dist/components/sandbox/run.d.ts.map +1 -1
  72. package/dist/components/verbs/build-archive.d.ts +22 -11
  73. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  74. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  75. package/dist/config-static.d.ts +59 -0
  76. package/dist/config-static.d.ts.map +1 -0
  77. package/dist/config.d.ts +104 -5
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/content-digest.d.ts +17 -0
  80. package/dist/content-digest.d.ts.map +1 -0
  81. package/dist/discovery/files.d.ts +48 -4
  82. package/dist/discovery/files.d.ts.map +1 -1
  83. package/dist/discovery/fold-import.d.ts +32 -1
  84. package/dist/discovery/fold-import.d.ts.map +1 -1
  85. package/dist/discovery/index.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/driver.d.ts +20 -0
  87. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  88. package/dist/discovery/sandbox/fork.d.ts +20 -0
  89. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  90. package/dist/discovery/walk.d.ts +119 -0
  91. package/dist/discovery/walk.d.ts.map +1 -0
  92. package/dist/graph-ir.d.ts +17 -1
  93. package/dist/graph-ir.d.ts.map +1 -1
  94. package/dist/join-key.d.ts +31 -0
  95. package/dist/join-key.d.ts.map +1 -0
  96. package/dist/lexicon-module.d.ts +110 -0
  97. package/dist/lexicon-module.d.ts.map +1 -0
  98. package/dist/lexicon.d.ts +82 -1
  99. package/dist/lexicon.d.ts.map +1 -1
  100. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  101. package/dist/lifecycle/digest.d.ts +5 -1
  102. package/dist/lifecycle/digest.d.ts.map +1 -1
  103. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +64 -0
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/lease.d.ts +8 -1
  108. package/dist/lifecycle/lease.d.ts.map +1 -1
  109. package/dist/lifecycle/legacy-digest.d.ts +59 -0
  110. package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
  111. package/dist/lifecycle/member-ledger.d.ts +67 -0
  112. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  113. package/dist/lifecycle/plan-digest.d.ts +4 -3
  114. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +42 -1
  116. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  117. package/dist/lifecycle/status.d.ts +90 -5
  118. package/dist/lifecycle/status.d.ts.map +1 -1
  119. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
  120. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
  121. package/dist/op/activities/activity-contracts.d.ts +32 -0
  122. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  123. package/dist/op/activities/index.d.ts +2 -0
  124. package/dist/op/activities/index.d.ts.map +1 -1
  125. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  126. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  127. package/dist/op/activity-contract-registry.d.ts.map +1 -1
  128. package/dist/op/activity-registry.d.ts.map +1 -1
  129. package/dist/op/discover.d.ts.map +1 -1
  130. package/dist/op/gate-approval.d.ts.map +1 -1
  131. package/dist/op/gate-name.d.ts +7 -0
  132. package/dist/op/gate-name.d.ts.map +1 -1
  133. package/dist/op/gate-summary.d.ts +16 -0
  134. package/dist/op/gate-summary.d.ts.map +1 -1
  135. package/dist/op/gate.d.ts +16 -2
  136. package/dist/op/gate.d.ts.map +1 -1
  137. package/dist/op/generate-pipeline.d.ts.map +1 -1
  138. package/dist/op/runtimes/local.d.ts.map +1 -1
  139. package/dist/project-root.d.ts +26 -0
  140. package/dist/project-root.d.ts.map +1 -1
  141. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  142. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  143. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  144. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  145. package/dist/workspace/checks/generated.d.ts +156 -0
  146. package/dist/workspace/checks/generated.d.ts.map +1 -0
  147. package/dist/workspace/checks/ledgers.d.ts +78 -0
  148. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  149. package/dist/workspace/checks/links.d.ts +25 -0
  150. package/dist/workspace/checks/links.d.ts.map +1 -0
  151. package/dist/workspace/checks/pipelines.d.ts +76 -0
  152. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  153. package/dist/workspace/checks.d.ts +161 -0
  154. package/dist/workspace/checks.d.ts.map +1 -0
  155. package/dist/workspace/compose-graph.d.ts +115 -0
  156. package/dist/workspace/compose-graph.d.ts.map +1 -0
  157. package/dist/workspace/compose-reports.d.ts +68 -0
  158. package/dist/workspace/compose-reports.d.ts.map +1 -0
  159. package/dist/workspace/declaration.d.ts +220 -0
  160. package/dist/workspace/declaration.d.ts.map +1 -0
  161. package/dist/workspace/declaration.schema.json +434 -0
  162. package/dist/workspace/generated-files.d.ts +65 -0
  163. package/dist/workspace/generated-files.d.ts.map +1 -0
  164. package/dist/workspace/glob.d.ts +13 -0
  165. package/dist/workspace/glob.d.ts.map +1 -0
  166. package/dist/workspace/graph-cli.d.ts +75 -0
  167. package/dist/workspace/graph-cli.d.ts.map +1 -0
  168. package/dist/workspace/init.d.ts +96 -0
  169. package/dist/workspace/init.d.ts.map +1 -0
  170. package/dist/workspace/jsonc.d.ts +38 -0
  171. package/dist/workspace/jsonc.d.ts.map +1 -0
  172. package/dist/workspace/kinds.d.ts +192 -0
  173. package/dist/workspace/kinds.d.ts.map +1 -0
  174. package/dist/workspace/lineage-check.d.ts +84 -0
  175. package/dist/workspace/lineage-check.d.ts.map +1 -0
  176. package/dist/workspace/lineage-cli.d.ts +31 -0
  177. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  178. package/dist/workspace/lineage-init.d.ts +101 -0
  179. package/dist/workspace/lineage-init.d.ts.map +1 -0
  180. package/dist/workspace/lineage-lock.d.ts +258 -0
  181. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  182. package/dist/workspace/lineage-merge.d.ts +14 -0
  183. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  184. package/dist/workspace/lineage-migrations.d.ts +178 -0
  185. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  186. package/dist/workspace/lineage-update.d.ts +59 -0
  187. package/dist/workspace/lineage-update.d.ts.map +1 -0
  188. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  189. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  190. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  191. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  192. package/dist/workspace/lineage-vendor.d.ts +63 -0
  193. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  194. package/dist/workspace/lineage-version.d.ts +32 -0
  195. package/dist/workspace/lineage-version.d.ts.map +1 -0
  196. package/dist/workspace/links.d.ts +150 -0
  197. package/dist/workspace/links.d.ts.map +1 -0
  198. package/dist/workspace/ls.d.ts +99 -0
  199. package/dist/workspace/ls.d.ts.map +1 -0
  200. package/dist/workspace/member-commands.d.ts +139 -0
  201. package/dist/workspace/member-commands.d.ts.map +1 -0
  202. package/dist/workspace/member-handles.d.ts +54 -0
  203. package/dist/workspace/member-handles.d.ts.map +1 -0
  204. package/dist/workspace/member-pipeline.d.ts +143 -0
  205. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  206. package/dist/workspace/member-run.d.ts +67 -0
  207. package/dist/workspace/member-run.d.ts.map +1 -0
  208. package/dist/workspace/reason-codes.d.ts +65 -0
  209. package/dist/workspace/reason-codes.d.ts.map +1 -0
  210. package/dist/workspace/record-source.d.ts +22 -0
  211. package/dist/workspace/record-source.d.ts.map +1 -0
  212. package/dist/workspace/records-cli.d.ts +81 -0
  213. package/dist/workspace/records-cli.d.ts.map +1 -0
  214. package/dist/workspace/records.d.ts +118 -0
  215. package/dist/workspace/records.d.ts.map +1 -0
  216. package/dist/workspace/root-refusal.d.ts +53 -0
  217. package/dist/workspace/root-refusal.d.ts.map +1 -0
  218. package/dist/workspace/status.d.ts +150 -0
  219. package/dist/workspace/status.d.ts.map +1 -0
  220. package/dist/workspace/template-manifest.d.ts +90 -0
  221. package/dist/workspace/template-manifest.d.ts.map +1 -0
  222. package/dist/workspace/tree.d.ts +36 -0
  223. package/dist/workspace/tree.d.ts.map +1 -0
  224. package/dist/workspace/trust/attestor.d.ts +67 -0
  225. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  226. package/dist/workspace/trust/policy.d.ts +111 -0
  227. package/dist/workspace/trust/policy.d.ts.map +1 -0
  228. package/dist/workspace/trust/provenance.d.ts +67 -0
  229. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  230. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  231. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  232. package/dist/workspace/trust/test-repo.d.ts +42 -0
  233. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  234. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  235. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  236. package/dist/workspace/trust/verify.d.ts +66 -0
  237. package/dist/workspace/trust/verify.d.ts.map +1 -0
  238. package/dist/workspace/which-chant.d.ts +58 -0
  239. package/dist/workspace/which-chant.d.ts.map +1 -0
  240. package/dist/workspace/workspace-kinds.schema.json +133 -0
  241. package/package.json +4 -3
  242. package/src/audit/core.ts +13 -2
  243. package/src/audit/discover.ts +102 -36
  244. package/src/audit/report-html.ts +3 -1
  245. package/src/audit/report-model.ts +14 -1
  246. package/src/audit/terraform-state.test.ts +81 -1
  247. package/src/audit/terraform-state.ts +25 -0
  248. package/src/cli/command-group.ts +7 -0
  249. package/src/cli/commands/audit-truncation.test.ts +167 -0
  250. package/src/cli/commands/audit.ts +132 -29
  251. package/src/cli/commands/check-lexicon-examples.ts +2 -1
  252. package/src/cli/commands/check-lexicon.ts +24 -0
  253. package/src/cli/commands/doctor.test.ts +30 -0
  254. package/src/cli/commands/doctor.ts +21 -0
  255. package/src/cli/commands/import-agents.ts +15 -4
  256. package/src/cli/commands/init.ts +76 -25
  257. package/src/cli/commands/lint.test.ts +52 -1
  258. package/src/cli/commands/lint.ts +41 -71
  259. package/src/cli/commands/onboard.ts +15 -0
  260. package/src/cli/commands/update.ts +18 -5
  261. package/src/cli/commands/vendor.ts +11 -6
  262. package/src/cli/conflict-check.test.ts +18 -1
  263. package/src/cli/conflict-check.ts +13 -0
  264. package/src/cli/discovery-skip.test.ts +168 -0
  265. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  266. package/src/cli/handlers/build-member.test.ts +127 -0
  267. package/src/cli/handlers/build.test.ts +11 -0
  268. package/src/cli/handlers/build.ts +35 -1
  269. package/src/cli/handlers/components.test.ts +127 -0
  270. package/src/cli/handlers/components.ts +70 -8
  271. package/src/cli/handlers/dev.ts +6 -1
  272. package/src/cli/handlers/fan-out.ts +9 -2
  273. package/src/cli/handlers/graph.test.ts +20 -0
  274. package/src/cli/handlers/graph.ts +4 -2
  275. package/src/cli/handlers/init.ts +54 -1
  276. package/src/cli/handlers/lifecycle.ts +2 -1
  277. package/src/cli/handlers/misc.ts +16 -0
  278. package/src/cli/handlers/operator.test.ts +68 -0
  279. package/src/cli/handlers/operator.ts +39 -10
  280. package/src/cli/handlers/promote.test.ts +377 -0
  281. package/src/cli/handlers/promote.ts +391 -0
  282. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  283. package/src/cli/handlers/run-generate.test.ts +158 -0
  284. package/src/cli/handlers/run-generate.ts +168 -0
  285. package/src/cli/handlers/run.test.ts +47 -0
  286. package/src/cli/handlers/run.ts +41 -6
  287. package/src/cli/handlers/vendor.ts +147 -43
  288. package/src/cli/main.test.ts +137 -1
  289. package/src/cli/main.ts +290 -24
  290. package/src/cli/mcp/op-tools.ts +2 -1
  291. package/src/cli/path-lexicon-messages.test.ts +152 -0
  292. package/src/cli/plugins.ts +43 -5
  293. package/src/cli/registry.ts +46 -1
  294. package/src/cli/static-config-read.test.ts +208 -0
  295. package/src/codegen/fetch.test.ts +63 -1
  296. package/src/codegen/fetch.ts +30 -1
  297. package/src/components/__fixtures__/alb-ecs-service.json +0 -8
  298. package/src/components/capability-plugin-loader.ts +3 -1
  299. package/src/components/cli-support.ts +9 -3
  300. package/src/components/discover.test.ts +27 -57
  301. package/src/components/discover.ts +20 -51
  302. package/src/components/driver.test.ts +90 -12
  303. package/src/components/driver.ts +43 -5
  304. package/src/components/fan-out-support.ts +2 -1
  305. package/src/components/gate-plan.ts +53 -0
  306. package/src/components/pilots/README.md +1 -1
  307. package/src/components/pilots/alb-ecs.pilot.ts +7 -7
  308. package/src/components/presets/ecs-fargate.ts +10 -5
  309. package/src/components/promote.test.ts +645 -0
  310. package/src/components/promote.ts +641 -0
  311. package/src/components/sandbox/driver.ts +8 -8
  312. package/src/components/sandbox/run.ts +4 -1
  313. package/src/components/verbs/build-archive.ts +27 -21
  314. package/src/components/verbs/run-agent.ts +3 -5
  315. package/src/config-static.test.ts +136 -0
  316. package/src/config-static.ts +232 -0
  317. package/src/config.test.ts +40 -0
  318. package/src/config.ts +161 -11
  319. package/src/content-digest.test.ts +120 -0
  320. package/src/content-digest.ts +21 -0
  321. package/src/discovery/files.test.ts +127 -1
  322. package/src/discovery/files.ts +91 -59
  323. package/src/discovery/fold-import.ts +127 -9
  324. package/src/discovery/index.ts +1 -0
  325. package/src/discovery/sandbox/driver.ts +73 -35
  326. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  327. package/src/discovery/sandbox/fork.ts +83 -43
  328. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  329. package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
  330. package/src/discovery/walk.test.ts +263 -0
  331. package/src/discovery/walk.ts +334 -0
  332. package/src/effect-receipt.ts +2 -2
  333. package/src/graph-ir.ts +18 -1
  334. package/src/join-key.test.ts +25 -0
  335. package/src/join-key.ts +39 -0
  336. package/src/lexicon-module.test.ts +203 -0
  337. package/src/lexicon-module.ts +215 -0
  338. package/src/lexicon.ts +105 -1
  339. package/src/lifecycle/build-ledger-store.ts +13 -2
  340. package/src/lifecycle/digest.ts +9 -5
  341. package/src/lifecycle/gate-ledger.test.ts +20 -0
  342. package/src/lifecycle/gate-ledger.ts +28 -6
  343. package/src/lifecycle/git.test.ts +13 -1
  344. package/src/lifecycle/git.ts +221 -176
  345. package/src/lifecycle/lease.ts +25 -10
  346. package/src/lifecycle/legacy-digest.test.ts +206 -0
  347. package/src/lifecycle/legacy-digest.ts +103 -0
  348. package/src/lifecycle/member-ledger.test.ts +319 -0
  349. package/src/lifecycle/member-ledger.ts +145 -0
  350. package/src/lifecycle/plan-digest.ts +7 -7
  351. package/src/lifecycle/release-ledger.test.ts +20 -0
  352. package/src/lifecycle/release-ledger.ts +53 -3
  353. package/src/lifecycle/status.test.ts +151 -1
  354. package/src/lifecycle/status.ts +228 -7
  355. package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
  356. package/src/op/activities/activity-contracts.ts +37 -0
  357. package/src/op/activities/index.ts +10 -0
  358. package/src/op/activities/propose-upgrade.ts +206 -0
  359. package/src/op/activity-contract-registry.ts +18 -4
  360. package/src/op/activity-registry.ts +26 -0
  361. package/src/op/discover.test.ts +14 -4
  362. package/src/op/discover.ts +19 -23
  363. package/src/op/gate-approval.ts +3 -1
  364. package/src/op/gate-name.ts +8 -0
  365. package/src/op/gate-summary.test.ts +14 -0
  366. package/src/op/gate-summary.ts +25 -2
  367. package/src/op/gate.ts +36 -7
  368. package/src/op/generate-pipeline.test.ts +14 -5
  369. package/src/op/generate-pipeline.ts +2 -1
  370. package/src/op/runtimes/local.ts +2 -1
  371. package/src/project-root.test.ts +66 -1
  372. package/src/project-root.ts +48 -0
  373. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  374. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  375. package/src/workspace/behold-kinds.test.ts +162 -0
  376. package/src/workspace/check-contract.test.ts +127 -0
  377. package/src/workspace/check.schema.json +347 -0
  378. package/src/workspace/checks/generated.test.ts +287 -0
  379. package/src/workspace/checks/generated.ts +475 -0
  380. package/src/workspace/checks/ledgers.test.ts +139 -0
  381. package/src/workspace/checks/ledgers.ts +246 -0
  382. package/src/workspace/checks/links.ts +183 -0
  383. package/src/workspace/checks/pipelines.test.ts +133 -0
  384. package/src/workspace/checks/pipelines.ts +225 -0
  385. package/src/workspace/checks-repo.test.ts +71 -0
  386. package/src/workspace/checks.test.ts +450 -0
  387. package/src/workspace/checks.ts +461 -0
  388. package/src/workspace/compose-graph.test.ts +146 -0
  389. package/src/workspace/compose-graph.ts +228 -0
  390. package/src/workspace/compose-reports.test.ts +109 -0
  391. package/src/workspace/compose-reports.ts +186 -0
  392. package/src/workspace/declaration.schema.json +434 -0
  393. package/src/workspace/declaration.test.ts +347 -0
  394. package/src/workspace/declaration.ts +623 -0
  395. package/src/workspace/generated-files.test.ts +114 -0
  396. package/src/workspace/generated-files.ts +94 -0
  397. package/src/workspace/glob.ts +49 -0
  398. package/src/workspace/graph-cli.ts +220 -0
  399. package/src/workspace/graph-contract.test.ts +204 -0
  400. package/src/workspace/graph.schema.json +662 -0
  401. package/src/workspace/init.test.ts +223 -0
  402. package/src/workspace/init.ts +585 -0
  403. package/src/workspace/jsonc.test.ts +65 -0
  404. package/src/workspace/jsonc.ts +231 -0
  405. package/src/workspace/kinds.test.ts +288 -0
  406. package/src/workspace/kinds.ts +506 -0
  407. package/src/workspace/lineage-check.ts +264 -0
  408. package/src/workspace/lineage-cli.ts +114 -0
  409. package/src/workspace/lineage-init.test.ts +231 -0
  410. package/src/workspace/lineage-init.ts +365 -0
  411. package/src/workspace/lineage-lock.test.ts +224 -0
  412. package/src/workspace/lineage-lock.ts +357 -0
  413. package/src/workspace/lineage-merge.ts +44 -0
  414. package/src/workspace/lineage-migrations.test.ts +178 -0
  415. package/src/workspace/lineage-migrations.ts +343 -0
  416. package/src/workspace/lineage-update.ts +160 -0
  417. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  418. package/src/workspace/lineage-upgrade.test.ts +474 -0
  419. package/src/workspace/lineage-upgrade.ts +624 -0
  420. package/src/workspace/lineage-vendor.test.ts +126 -0
  421. package/src/workspace/lineage-vendor.ts +215 -0
  422. package/src/workspace/lineage-version.ts +127 -0
  423. package/src/workspace/links.test.ts +344 -0
  424. package/src/workspace/links.ts +331 -0
  425. package/src/workspace/ls-contract.test.ts +200 -0
  426. package/src/workspace/ls.schema.json +190 -0
  427. package/src/workspace/ls.ts +254 -0
  428. package/src/workspace/member-commands.e2e.test.ts +244 -0
  429. package/src/workspace/member-commands.test.ts +217 -0
  430. package/src/workspace/member-commands.ts +540 -0
  431. package/src/workspace/member-handles.test.ts +76 -0
  432. package/src/workspace/member-handles.ts +228 -0
  433. package/src/workspace/member-pipeline.test.ts +135 -0
  434. package/src/workspace/member-pipeline.ts +317 -0
  435. package/src/workspace/member-run.ts +146 -0
  436. package/src/workspace/read-contract.test.ts +129 -0
  437. package/src/workspace/reason-codes.test.ts +113 -0
  438. package/src/workspace/reason-codes.ts +81 -0
  439. package/src/workspace/record-source.ts +117 -0
  440. package/src/workspace/records-cli.ts +198 -0
  441. package/src/workspace/records-contract.test.ts +93 -0
  442. package/src/workspace/records.schema.json +150 -0
  443. package/src/workspace/records.test.ts +243 -0
  444. package/src/workspace/records.ts +384 -0
  445. package/src/workspace/root-refusal.ts +97 -0
  446. package/src/workspace/status-contract.test.ts +308 -0
  447. package/src/workspace/status.schema.json +210 -0
  448. package/src/workspace/status.ts +412 -0
  449. package/src/workspace/template-manifest.test.ts +79 -0
  450. package/src/workspace/template-manifest.ts +221 -0
  451. package/src/workspace/tree.ts +132 -0
  452. package/src/workspace/trust/attestor.ts +93 -0
  453. package/src/workspace/trust/policy.ts +306 -0
  454. package/src/workspace/trust/provenance.ts +181 -0
  455. package/src/workspace/trust/ssh-commit.ts +138 -0
  456. package/src/workspace/trust/test-repo.ts +138 -0
  457. package/src/workspace/trust/trust.test.ts +397 -0
  458. package/src/workspace/trust/verify-cli.ts +56 -0
  459. package/src/workspace/trust/verify.ts +194 -0
  460. package/src/workspace/which-chant.test.ts +117 -0
  461. package/src/workspace/which-chant.ts +151 -0
  462. package/src/workspace/workspace-kinds.schema.json +133 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `chant build --components --generate` inside a workspace member (#2542).
3
+ * Component discovery is stubbed with a fixed graph; the github generator is
4
+ * the real one, so the files checked here are what a user gets. A project
5
+ * outside any workspace is held to its old output byte for byte, and to a
6
+ * snapshot, since level 0 must not move (#2525).
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
10
+ import { execFileSync } from "node:child_process";
11
+ import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import type { DriverComponent } from "../../components/driver";
15
+ import type { ComponentPipelineOptions } from "../../lexicon";
16
+ import type { ParsedArgs } from "../registry";
17
+ import { generateGithubPipeline } from "@intentius/chant-lexicon-github/components/generate-pipeline";
18
+ import { twoMemberWorkspace } from "../../workspace/__fixtures__/two-member-workspace";
19
+
20
+ const components: DriverComponent[] = [
21
+ { name: "shared-alb", dependsOn: [], deploy: [] },
22
+ { name: "api", dependsOn: ["shared-alb"], deploy: [] },
23
+ ];
24
+
25
+ const received: (ComponentPipelineOptions | undefined)[] = [];
26
+ vi.mock("../../components/cli-support", () => ({
27
+ generateComponentsPipeline: async (_path: string, _lexicon: string, options?: ComponentPipelineOptions) => {
28
+ received.push(options);
29
+ const { yaml, stages, jobs, env } = generateGithubPipeline(components, options);
30
+ return { success: true, yaml, stages, jobs, env };
31
+ },
32
+ }));
33
+ vi.mock("../../config", async () => {
34
+ const actual = await vi.importActual<typeof import("../../config")>("../../config");
35
+ return { ...actual, loadChantConfigUpward: async () => ({ config: {} }) };
36
+ });
37
+
38
+ const { runBuild } = await import("./build");
39
+
40
+ function args(overrides: Partial<ParsedArgs>): ParsedArgs {
41
+ return {
42
+ command: "build",
43
+ path: ".",
44
+ format: "",
45
+ fix: false,
46
+ watch: false,
47
+ verbose: false,
48
+ help: false,
49
+ live: false,
50
+ components: true,
51
+ generate: "github",
52
+ ...overrides,
53
+ };
54
+ }
55
+
56
+ let repo: string;
57
+ let logs: string[];
58
+ beforeEach(() => {
59
+ received.length = 0;
60
+ logs = [];
61
+ vi.spyOn(console, "error").mockImplementation(() => {});
62
+ vi.spyOn(console, "log").mockImplementation((s: string) => {
63
+ logs.push(s);
64
+ });
65
+ });
66
+ afterEach(() => {
67
+ vi.restoreAllMocks();
68
+ rmSync(repo, { recursive: true, force: true });
69
+ });
70
+
71
+ describe("a project inside a workspace member", () => {
72
+ beforeEach(() => {
73
+ repo = twoMemberWorkspace();
74
+ });
75
+
76
+ test("the pipeline is written to the member's file at the repository root, filtered and recorded", async () => {
77
+ expect(await runBuild({ args: args({ path: join(repo, "services/api"), env: "staging" }), plugins: [], serializers: [] })).toBe(0);
78
+
79
+ expect(received[0]?.member).toEqual({ name: "api", dir: "services/api", file: ".github/workflows/chant-api-staging.yml" });
80
+ const file = join(repo, ".github/workflows/chant-api-staging.yml");
81
+ const text = readFileSync(file, "utf-8");
82
+ const [header, ...rest] = text.split("\n");
83
+ expect(header).toContain("Regenerate with: chant build --components --generate github --env staging");
84
+ expect(rest.join("\n")).toBe(generateGithubPipeline(components, { env: "staging", member: received[0]!.member }).yaml);
85
+ expect(text).toContain("working-directory: services/api");
86
+
87
+ const record = JSON.parse(readFileSync(join(repo, "services/api/.chant/generated.json"), "utf-8"));
88
+ expect(record.files).toEqual([{ path: ".github/workflows/chant-api-staging.yml", command: "chant build --components --generate github --env staging", env: "staging" }]);
89
+ });
90
+
91
+ test("two members write two files and never collide", async () => {
92
+ await runBuild({ args: args({ path: join(repo, "services/api"), env: "prod" }), plugins: [], serializers: [] });
93
+ await runBuild({ args: args({ path: join(repo, "apps/web"), env: "prod" }), plugins: [], serializers: [] });
94
+ expect(existsSync(join(repo, ".github/workflows/chant-api-prod.yml"))).toBe(true);
95
+ expect(existsSync(join(repo, ".github/workflows/chant-web-prod.yml"))).toBe(true);
96
+ });
97
+
98
+ test("--format json names the member and the file and writes nothing", async () => {
99
+ expect(await runBuild({ args: args({ path: join(repo, "apps/web"), env: "prod", format: "json" }), plugins: [], serializers: [] })).toBe(0);
100
+ const out = JSON.parse(logs.join("\n"));
101
+ expect(out).toMatchObject({ member: "web", file: ".github/workflows/chant-web-prod.yml", env: "prod" });
102
+ expect(existsSync(join(repo, ".github/workflows/chant-web-prod.yml"))).toBe(false);
103
+ });
104
+
105
+ test("an example project in a group is not a member, and gets the plain pipeline", async () => {
106
+ expect(await runBuild({ args: args({ path: join(repo, "examples/demo") }), plugins: [], serializers: [] })).toBe(0);
107
+ expect(received[0]).toEqual({ env: undefined });
108
+ expect(logs.join("\n")).toBe(generateGithubPipeline(components, {}).yaml);
109
+ });
110
+ });
111
+
112
+ describe("a project outside any workspace (level 0)", () => {
113
+ beforeEach(() => {
114
+ repo = realpathSync(mkdtempSync(join(tmpdir(), "chant-build-level0-")));
115
+ execFileSync("git", ["init", "-q"], { cwd: repo });
116
+ });
117
+
118
+ test("the pipeline is the generator's output, unchanged", async () => {
119
+ const output = join(repo, "pipeline.yml");
120
+ expect(await runBuild({ args: args({ path: repo, env: "staging", output }), plugins: [], serializers: [] })).toBe(0);
121
+ expect(received[0]).toEqual({ env: "staging" });
122
+ const text = readFileSync(output, "utf-8");
123
+ expect(text).toBe(generateGithubPipeline(components, { env: "staging" }).yaml);
124
+ expect(text).toMatchSnapshot();
125
+ expect(existsSync(join(repo, ".chant"))).toBe(false);
126
+ });
127
+ });
@@ -70,6 +70,17 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
70
70
  vi.restoreAllMocks();
71
71
  });
72
72
 
73
+ test("--promote-to is forwarded to the generator as promoteTo (#2575)", async () => {
74
+ generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
75
+ vi.spyOn(console, "log").mockImplementation(() => {});
76
+
77
+ const exit = await runBuild({ args: makeArgs({ env: "staging", promoteTo: "prod" }), plugins: [], serializers: [] });
78
+
79
+ expect(exit).toBe(0);
80
+ expect(generateComponentsPipelineMock).toHaveBeenCalledWith(".", "gitlab", { env: "staging", promoteTo: "prod" }, undefined, []);
81
+ vi.restoreAllMocks();
82
+ });
83
+
73
84
  test("chant.config.ts's declared buildParams resolve, log, and are forwarded to generateComponentsPipeline", async () => {
74
85
  loadChantConfigUpwardMock.mockResolvedValue({
75
86
  config: { buildParams: { tier: { type: "string", default: "light" } } },
@@ -6,6 +6,7 @@ import type { CommandContext } from "../registry";
6
6
  import { generateComponentsPipeline } from "../../components/cli-support";
7
7
  import { loadChantConfigUpward, type ChantConfig } from "../../config";
8
8
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
9
+ import { findWorkspaceRoot } from "../../project-root";
9
10
 
10
11
  /**
11
12
  * `chant build --components --generate <lexicon>` — generate mode (#563,
@@ -44,6 +45,33 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
44
45
  return 1;
45
46
  }
46
47
 
48
+ // A project inside a workspace member gets that member's pipeline (#2542):
49
+ // path-filtered, run in the member's directory, written to a file named
50
+ // after the member and recorded as the member's generated file. The
51
+ // workspace module loads only when a declaration was found, so a project
52
+ // outside any workspace generates exactly what it did before.
53
+ const found = findWorkspaceRoot(resolve(args.path));
54
+ let plan: import("../../workspace/member-pipeline").MemberComponentPlan | undefined;
55
+ let memberPipeline: typeof import("../../workspace/member-pipeline") | undefined;
56
+ if (found) {
57
+ memberPipeline = await import("../../workspace/member-pipeline");
58
+ try {
59
+ plan = memberPipeline.planMemberComponentPipeline({
60
+ projectDir: resolve(args.path),
61
+ found,
62
+ lexicon,
63
+ env: args.env,
64
+ promoteTo: args.promoteTo,
65
+ output: args.output,
66
+ params: args.param,
67
+ paramsFile: args.paramsFile,
68
+ });
69
+ } catch (err) {
70
+ console.error(formatError({ message: memberPipeline.describeError(err) }));
71
+ return 1;
72
+ }
73
+ }
74
+
47
75
  // Which lexicons support generate mode is a property of the loaded lexicon
48
76
  // plugins (those implementing `generateComponentPipeline`, #688), not a
49
77
  // hard-coded core list — `generateComponentsPipeline` returns a descriptive
@@ -51,7 +79,7 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
51
79
  const result = await generateComponentsPipeline(
52
80
  args.path,
53
81
  lexicon,
54
- { env: args.env },
82
+ { env: args.env, ...(args.promoteTo ? { promoteTo: args.promoteTo } : {}), ...(plan ? { member: plan.member } : {}) },
55
83
  args.sandbox,
56
84
  paramsResolution.provenance,
57
85
  );
@@ -73,11 +101,17 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
73
101
  // generator's own resolution, forwarded rather than left for a
74
102
  // consumer to re-derive by parsing the YAML back (#2060).
75
103
  ...(result.env ? { env: result.env } : {}),
104
+ // A workspace member's pipeline (#2542): the member and the file
105
+ // the pipeline belongs in, relative to the repository root.
106
+ ...(plan ? { member: plan.member.name, file: plan.file } : {}),
76
107
  },
77
108
  null,
78
109
  2,
79
110
  ),
80
111
  );
112
+ } else if (plan && memberPipeline) {
113
+ memberPipeline.writeMemberComponentPipeline(plan, yaml);
114
+ console.error(formatSuccess(`wrote ${formatBold(plan.file)} for member ${formatBold(plan.member.name)}`));
81
115
  } else if (args.output) {
82
116
  const outputPath = resolve(args.output);
83
117
  mkdirSync(dirname(outputPath), { recursive: true });
@@ -23,6 +23,7 @@ const awsEmulatorStub = {
23
23
  const getHeadCommitMock = vi.fn();
24
24
  const fetchLifecycleMock = vi.fn();
25
25
  const pushLifecycleMock = vi.fn();
26
+ const readSnapshotMock = vi.fn();
26
27
 
27
28
  const appendReleaseRecordMock = vi.fn();
28
29
  const readReleaseLedgerMock = vi.fn();
@@ -38,6 +39,8 @@ vi.mock("../../lifecycle/git", () => ({
38
39
  getHeadCommit: (...args: unknown[]) => getHeadCommitMock(...args),
39
40
  fetchLifecycle: (...args: unknown[]) => fetchLifecycleMock(...args),
40
41
  pushLifecycle: (...args: unknown[]) => pushLifecycleMock(...args),
42
+ readSnapshot: (...args: unknown[]) => readSnapshotMock(...args),
43
+ snapshotStorageKey: (lexicon: string, stack?: string) => (stack ? `${stack}__${lexicon}` : lexicon),
41
44
  StaleLifecycleBranchError: class StaleLifecycleBranchError extends Error {},
42
45
  }));
43
46
 
@@ -125,6 +128,7 @@ describe("components handlers", () => {
125
128
  getHeadCommitMock.mockReset().mockResolvedValue("abc123headsha");
126
129
  fetchLifecycleMock.mockReset().mockResolvedValue(true);
127
130
  pushLifecycleMock.mockReset().mockResolvedValue(true);
131
+ readSnapshotMock.mockReset().mockResolvedValue(null);
128
132
  appendReleaseRecordMock.mockReset();
129
133
  readReleaseLedgerMock.mockReset().mockResolvedValue({ records: [], malformed: 0 });
130
134
  listReleaseEnvironmentsMock.mockReset().mockResolvedValue([]);
@@ -567,6 +571,129 @@ describe("components handlers", () => {
567
571
  });
568
572
  });
569
573
 
574
+ // #2513: `--live` could never report `drifted` — the change set was built
575
+ // with no prior observation, so a live, declared entity was always `noop`,
576
+ // and nothing compared the live identity with the recorded release. These
577
+ // go through the handler end to end with a fake lexicon.
578
+ describe("drifted (#2513)", () => {
579
+ const released = {
580
+ version: 1,
581
+ component: "svc",
582
+ env: "prod",
583
+ digest: "sha256:released",
584
+ gitSha: "1111111aaaaaaa",
585
+ runId: "run-1",
586
+ timestamp: "2026-02-01T00:00:00.000Z",
587
+ actor: "alice",
588
+ };
589
+
590
+ async function statusRow(
591
+ resources: Record<string, ResourceMetadata>,
592
+ opts: { snapshot?: { timestamp: string; resources: Record<string, ResourceMetadata> }; failRead?: boolean } = {},
593
+ ) {
594
+ readReleaseLedgerMock.mockResolvedValue({ records: [released], malformed: 0 });
595
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["svc"] }));
596
+ readSnapshotMock.mockImplementation(async (env: string, key: string) =>
597
+ opts.snapshot && env === "prod" && key === "aws"
598
+ ? JSON.stringify({ lexicon: "aws", environment: "prod", commit: "c", ...opts.snapshot })
599
+ : null,
600
+ );
601
+ const plugins: LexiconPlugin[] = [
602
+ createMockPlugin({
603
+ name: "aws",
604
+ emulator: awsEmulatorStub,
605
+ describeResources: opts.failRead
606
+ ? async () => { throw new Error("credentials expired"); }
607
+ : staticDescribeResources(resources),
608
+ }),
609
+ ];
610
+ stdoutBuf.length = 0;
611
+ const exit = await runComponentsStatus({
612
+ args: makeArgs({ extraPositional: "prod", live: true, json: true }),
613
+ plugins,
614
+ serializers: plugins.map((p) => p.serializer),
615
+ });
616
+ expect(exit).toBe(0);
617
+ return JSON.parse(stdoutBuf.join(""))[0];
618
+ }
619
+
620
+ test("reconciled: live identity matches the recorded digest and git sha", async () => {
621
+ const row = await statusRow({ svc: meta({ attributes: { digest: "sha256:released", gitSha: "1111111" } }) });
622
+ expect(row).toMatchObject({ component: "svc", reconciliation: "reconciled", live: true });
623
+ });
624
+
625
+ test("drifted: live resource reports a digest other than the recorded one", async () => {
626
+ const row = await statusRow({ svc: meta({ attributes: { digest: "sha256:foreign" } }) });
627
+ expect(row).toMatchObject({ component: "svc", reconciliation: "drifted", live: true });
628
+ expect(row.detail).toContain("sha256:foreign");
629
+ });
630
+
631
+ test("drifted: live resource reports a different git sha", async () => {
632
+ const row = await statusRow({ svc: meta({ attributes: { gitSha: "2222222bbbbbbb" } }) });
633
+ expect(row).toMatchObject({ reconciliation: "drifted" });
634
+ expect(row.detail).toContain("2222222bbbbbbb");
635
+ });
636
+
637
+ test("drifted: live attributes changed since a snapshot taken after the release", async () => {
638
+ const row = await statusRow(
639
+ { svc: meta({ attributes: { revision: "7" } }) },
640
+ { snapshot: { timestamp: "2026-02-01T00:05:00.000Z", resources: { svc: meta({ attributes: { revision: "6" } }) } } },
641
+ );
642
+ expect(row).toMatchObject({ reconciliation: "drifted" });
643
+ expect(row.detail).toContain("attributes.revision");
644
+ });
645
+
646
+ test("drifted: chant's ownership marker disappeared since a post-release snapshot", async () => {
647
+ const row = await statusRow(
648
+ { svc: meta({ ownership: "foreign" }) },
649
+ { snapshot: { timestamp: "2026-02-02T00:00:00.000Z", resources: { svc: meta({ ownership: "owned" }) } } },
650
+ );
651
+ expect(row).toMatchObject({ reconciliation: "drifted" });
652
+ expect(row.detail).toContain("ownership marker");
653
+ });
654
+
655
+ test("reconciled: a snapshot older than the release does not attribute the release's own change to drift", async () => {
656
+ const row = await statusRow(
657
+ { svc: meta({ attributes: { revision: "7" } }) },
658
+ { snapshot: { timestamp: "2026-01-15T00:00:00.000Z", resources: { svc: meta({ attributes: { revision: "6" } }) } } },
659
+ );
660
+ expect(row).toMatchObject({ reconciliation: "reconciled" });
661
+ });
662
+
663
+ test("reconciled: unchanged since the snapshot, and no identity reported (no identity is not drift)", async () => {
664
+ const row = await statusRow(
665
+ { svc: meta() },
666
+ { snapshot: { timestamp: "2026-02-01T00:05:00.000Z", resources: { svc: meta() } } },
667
+ );
668
+ expect(row).toMatchObject({ reconciliation: "reconciled" });
669
+ });
670
+
671
+ test("stale: recorded, and the provider confirmed nothing live", async () => {
672
+ const row = await statusRow(
673
+ {},
674
+ { snapshot: { timestamp: "2026-02-01T00:05:00.000Z", resources: { svc: meta({ attributes: { digest: "sha256:foreign" } }) } } },
675
+ );
676
+ expect(row).toMatchObject({ reconciliation: "stale", live: false });
677
+ });
678
+
679
+ test("unknown: a failed read is never drifted, whatever the snapshot says", async () => {
680
+ const row = await statusRow(
681
+ {},
682
+ {
683
+ failRead: true,
684
+ snapshot: { timestamp: "2026-02-01T00:05:00.000Z", resources: { svc: meta({ attributes: { digest: "sha256:foreign" } }) } },
685
+ },
686
+ );
687
+ expect(row).toMatchObject({ reconciliation: "unknown" });
688
+ expect(row).not.toHaveProperty("live");
689
+ });
690
+
691
+ test("reads the same unstacked snapshot `lifecycle plan` reads", async () => {
692
+ await statusRow({ svc: meta() });
693
+ expect(readSnapshotMock).toHaveBeenCalledWith("prod", "aws");
694
+ });
695
+ });
696
+
570
697
  test("reports malformed ledger lines as a warning without failing", async () => {
571
698
  readReleaseLedgerMock.mockResolvedValue({ records: [], malformed: 2 });
572
699
  const ctx = { args: makeArgs({ extraPositional: "prod" }), plugins: [], serializers: [] };
@@ -28,7 +28,8 @@
28
28
  */
29
29
  import { resolve, join, dirname } from "node:path";
30
30
  import { mkdir, readFile, writeFile } from "node:fs/promises";
31
- import { getHeadCommit, fetchLifecycle, pushLifecycle, StaleLifecycleBranchError } from "../../lifecycle/git";
31
+ import { getHeadCommit, fetchLifecycle, pushLifecycle, readSnapshot, snapshotStorageKey, StaleLifecycleBranchError } from "../../lifecycle/git";
32
+ import type { LifecycleSnapshot } from "../../lifecycle/types";
32
33
  import {
33
34
  appendReleaseRecord,
34
35
  readReleaseLedger,
@@ -37,7 +38,7 @@ import {
37
38
  resolveRunId,
38
39
  InvalidReleaseRecordError,
39
40
  } from "../../lifecycle/release-ledger";
40
- import { reconcileStatus, liveEvidenceFromChangeSet, compareAcrossEnvironments, mergeLiveEvidence, type LiveComponentEvidence } from "../../lifecycle/status";
41
+ import { reconcileStatus, liveEvidenceFromChangeSet, compareAcrossEnvironments, mergeLiveEvidence, type LiveComponentEvidence, type EntityLiveRead } from "../../lifecycle/status";
41
42
  import { commandBuildParams } from "../build-params-cli";
42
43
  import { buildChangeSet } from "../../lifecycle/change-set";
43
44
  import { buildLedgerEntries, componentBomSummary, type BuildLedgerEntry } from "../../lifecycle/build-ledger";
@@ -51,11 +52,12 @@ import { build } from "../../build";
51
52
  import { discoverComponents } from "../../components/discover";
52
53
  import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
53
54
  import type { CommandContext } from "../registry";
54
- import type { LexiconPlugin } from "../../lexicon";
55
+ import type { LexiconPlugin, ResourceMetadata } from "../../lexicon";
55
56
  import { normalizeObservation, mergeObservations, unobservedAll, type NormalizedObservation } from "../../observation";
56
57
  import type { Phase, Component } from "../../components/component";
57
58
  import { deployUnits } from "../../components/deploy-units";
58
59
  import { sortedJsonReplacer } from "../../utils";
60
+ import { withoutReadFlags } from "../../lifecycle/legacy-digest";
59
61
 
60
62
  /**
61
63
  * chant components release <env> --component <name> --digest <sha256:...>
@@ -276,7 +278,8 @@ export async function runComponentsExport(ctx: CommandContext): Promise<number>
276
278
  }
277
279
  }
278
280
 
279
- await writeFile(join(targetDir, "manifest.json"), JSON.stringify(manifest, sortedJsonReplacer, 2));
281
+ // The exported manifest is the recorded one; read-side flags (#2514) stay out of it.
282
+ await writeFile(join(targetDir, "manifest.json"), JSON.stringify(withoutReadFlags(manifest), sortedJsonReplacer, 2));
280
283
 
281
284
  const missing = results.filter((r) => r.status === "missing");
282
285
 
@@ -284,6 +287,7 @@ export async function runComponentsExport(ctx: CommandContext): Promise<number>
284
287
  console.log(JSON.stringify({
285
288
  component: manifest.component,
286
289
  manifestDigest: manifest.manifestDigest,
290
+ ...(manifest.flags ? { flags: manifest.flags } : {}),
287
291
  outDir: targetDir,
288
292
  entries: results,
289
293
  }, null, 2));
@@ -318,6 +322,7 @@ interface StatusJsonRow {
318
322
  runId: string;
319
323
  timestamp: string;
320
324
  actor: string;
325
+ flags?: string[];
321
326
  } | null;
322
327
  build: {
323
328
  manifestDigest: string;
@@ -464,6 +469,42 @@ async function observeComponentStacks(
464
469
  return evidence;
465
470
  }
466
471
 
472
+ /**
473
+ * The last lifecycle snapshot(s) for one lexicon, as `components status --live`
474
+ * reads them for its drift baseline (#2513). Reads the stack-keyed snapshot for
475
+ * each stack the status read targets and the unstacked one `lifecycle plan`
476
+ * reads, merging resources (a stack's own snapshot wins) and remembering, per
477
+ * entity, which snapshot's timestamp it came from. A missing or unreadable
478
+ * snapshot is no baseline, never an error: with none, nothing can have changed
479
+ * since, and the status falls back to identity and presence.
480
+ */
481
+ async function readStatusBaseline(
482
+ environment: string,
483
+ lexicon: string,
484
+ stacks: Array<string | undefined>,
485
+ ): Promise<{ resources: Record<string, ResourceMetadata> | undefined; timestamps: Map<string, string> }> {
486
+ const keys = [...new Set([...stacks.filter((s): s is string => !!s).map((s) => snapshotStorageKey(lexicon, s)), snapshotStorageKey(lexicon)])];
487
+ let resources: Record<string, ResourceMetadata> | undefined;
488
+ const timestamps = new Map<string, string>();
489
+ for (const key of keys) {
490
+ let snap: LifecycleSnapshot | undefined;
491
+ try {
492
+ const content = await readSnapshot(environment, key);
493
+ snap = content ? (JSON.parse(content) as LifecycleSnapshot) : undefined;
494
+ } catch {
495
+ snap = undefined;
496
+ }
497
+ if (!snap?.resources) continue;
498
+ resources ??= {};
499
+ for (const [name, meta] of Object.entries(snap.resources)) {
500
+ if (Object.prototype.hasOwnProperty.call(resources, name)) continue;
501
+ resources[name] = meta;
502
+ if (snap.timestamp) timestamps.set(name, snap.timestamp);
503
+ }
504
+ }
505
+ return { resources, timestamps };
506
+ }
507
+
467
508
  export async function runComponentsStatus(ctx: CommandContext): Promise<number> {
468
509
  const { args, plugins, serializers } = ctx;
469
510
  const requestedEnv = args.extraPositional;
@@ -554,6 +595,7 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
554
595
  for (const stack of config.stacks ?? []) componentStackNames.add(stack.name);
555
596
  const readTargets: Array<string | undefined> = componentStackNames.size ? [...componentStackNames] : [undefined];
556
597
  const merged: { env: string; entries: import("../../lifecycle/change-set").ChangeSetEntry[] } = { env: environment, entries: [] };
598
+ const observations = new Map<string, EntityLiveRead>();
557
599
  for (const plugin of plugins) {
558
600
  if (!plugin.describeResources) continue;
559
601
  const declared = new Set<string>();
@@ -593,15 +635,29 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
593
635
  console.error(formatWarning({ message: `${plugin.name}: describeResources failed — ${message} (components in this lexicon report unknown, not stale)` }));
594
636
  observed = { resources: {}, unobserved: unobservedAll(declared, "read-failed", message, entities), queried: {}, sources: {}, notes: [] };
595
637
  }
638
+ // The last lifecycle snapshot is the prior observation drift is
639
+ // measured against (#2513) — the same one `lifecycle plan` reads, so
640
+ // `drifted` here means what `update` means there. Without it every
641
+ // live, declared entity classified `noop` and `drifted` could not be
642
+ // reached. Each entity remembers its snapshot's timestamp so the
643
+ // status join can ignore a change made before the release.
644
+ const baseline = await readStatusBaseline(environment, plugin.name, readTargets);
596
645
  const cs = buildChangeSet(environment, {
597
646
  declared,
598
647
  observedNow: observed.resources,
599
- observedThen: undefined,
648
+ observedThen: baseline.resources,
600
649
  unobserved: observed.unobserved,
601
650
  }, { lexicon: plugin.name });
602
651
  merged.entries.push(...cs.entries);
652
+ for (const entry of cs.entries) {
653
+ observations.set(entry.name, {
654
+ now: observed.resources[entry.name],
655
+ then: baseline.resources?.[entry.name],
656
+ thenAt: baseline.timestamps.get(entry.name),
657
+ });
658
+ }
603
659
  }
604
- liveEvidence = liveEvidenceFromChangeSet(merged, liveNameMapping);
660
+ liveEvidence = liveEvidenceFromChangeSet(merged, liveNameMapping, { observations });
605
661
 
606
662
  // Multi-stack component projects (each component owns its own stack) are
607
663
  // invisible to the entity-keyed, single-stack `describeResources` above —
@@ -652,6 +708,7 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
652
708
  runId: row.recorded.runId,
653
709
  timestamp: row.recorded.timestamp,
654
710
  actor: row.recorded.actor,
711
+ ...(row.recorded.flags ? { flags: row.recorded.flags } : {}),
655
712
  }
656
713
  : null,
657
714
  build: row.build
@@ -729,14 +786,19 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
729
786
  row.env.padEnd(12) +
730
787
  digestShort.padEnd(20) +
731
788
  row.reconciliation.padEnd(14) +
732
- row.detail,
789
+ row.detail +
790
+ (row.recorded?.flags?.length ? ` [${row.recorded.flags.join(", ")}]` : ""),
733
791
  );
734
792
  }
735
793
 
736
794
  if (comparisons) {
737
795
  console.log(`\n${formatBold(`Cross-check: ${requestedEnv} vs ${args.compareTo}`)}`);
738
796
  for (const c of comparisons) {
739
- const verdict = c.same ? "same build" : "DIFFERENT builds";
797
+ const verdict = c.same
798
+ ? "same build"
799
+ : c.mixedDigestForms
800
+ ? "different digests, one recorded before real SHA-256 (legacy-digest), so this cannot tell"
801
+ : "DIFFERENT builds";
740
802
  console.log(` ${c.component}: ${c.digestA ?? "(none)"} vs ${c.digestB ?? "(none)"} — ${verdict}`);
741
803
  }
742
804
  }
@@ -1,5 +1,5 @@
1
1
  import { resolve } from "node:path";
2
- import { formatError, formatSuccess } from "../format";
2
+ import { formatError, formatSuccess, formatWarning } from "../format";
3
3
  import type { CommandContext } from "../registry";
4
4
 
5
5
  export async function runDevGenerate(ctx: CommandContext): Promise<number> {
@@ -33,6 +33,11 @@ export async function runDevOnboard(ctx: CommandContext): Promise<number> {
33
33
  }
34
34
 
35
35
  const { onboardCommand, printOnboardResult } = await import("../commands/onboard");
36
+ // chant#2578 — so a lexicon the project declares by path is recognised.
37
+ // chant#2591 — read without running chant.config.ts.
38
+ const { recordProjectLexicons, unknownPathLexiconsNotice } = await import("../plugins");
39
+ const notice = unknownPathLexiconsNotice(recordProjectLexicons(process.cwd()));
40
+ if (notice !== undefined) console.error(formatWarning({ message: notice }));
36
41
  const result = onboardCommand({ name, verbose: ctx.args.verbose });
37
42
  await printOnboardResult(result, name);
38
43
  return result.success ? 0 : 1;
@@ -43,7 +43,8 @@ import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support";
43
43
  import { runFanOut } from "../../components/fan-out-run";
44
44
  import { renderFanOutHuman, renderFanOutJson, renderFanOutPlan } from "../../components/fan-out-output";
45
45
  import { ndjsonProgressSink } from "../../components/run-progress";
46
- import { writeGatedRunSummary } from "../../op/gate-summary";
46
+ import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary";
47
+ import { approveCommand } from "../../op/gate";
47
48
  import { remainingFanOut, type ChangedUnits, type FanOutProgress } from "../../components/fan-out";
48
49
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
49
50
  import { formatError, formatInfo, formatWarning } from "../format";
@@ -320,7 +321,11 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
320
321
 
321
322
  if (result.status === "gated" && result.gate) {
322
323
  const pending = result.gate;
323
- console.error(formatInfo(`approve : chant approve ${pending.op} ${pending.gate} --plan ${result.plan.digest}`));
324
+ // The pending fact's own plan: the fan-out's digest for the gate over the
325
+ // set, or a component's plan and environment for a gate inside one (#2574).
326
+ console.error(formatInfo(
327
+ `approve : ${approveCommand(pending.op, pending.gate, pending.environment)} --plan ${pending.planDigest ?? result.plan.digest}`,
328
+ ));
324
329
  writeGatedRunSummary({
325
330
  op: pending.op,
326
331
  gate: pending.gate,
@@ -328,6 +333,8 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
328
333
  expiresAt: pending.expiresAt,
329
334
  ...(pending.url ? { url: pending.url } : {}),
330
335
  ...(pending.planDigest ? { planDigest: pending.planDigest } : {}),
336
+ ...(await summaryLedgerPrefix()),
337
+ ...(pending.environment ? { environment: pending.environment } : {}),
331
338
  });
332
339
  return GATED_EXIT_CODE;
333
340
  }
@@ -2,6 +2,7 @@ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import type { ParsedArgs } from "../registry";
3
3
  import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
4
4
  import { AttrRef } from "../../attrref";
5
+ import { GRAPH_IR_VERSION } from "../../graph-ir";
5
6
 
6
7
  /**
7
8
  * The aws emulator capability, as the real plugin declares it. `--live`
@@ -226,6 +227,25 @@ describe("runGraph", () => {
226
227
  expect(ir.edges).toContainEqual({ from: "subnet", to: "vpc", kind: "ref", viaAttr: "network" });
227
228
  });
228
229
 
230
+ // #2529 — the IR carries its version, as the first key, so a reader can
231
+ // decide how to read the rest before it reads anything else.
232
+ test("--format ir leads with the IR version", async () => {
233
+ lintClean(); discovered();
234
+ const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
235
+ expect(exit).toBe(0);
236
+ const ir = JSON.parse(stdoutBuf.join("\n"));
237
+ expect(ir.version).toBe(GRAPH_IR_VERSION);
238
+ expect(GRAPH_IR_VERSION).toBe(1);
239
+ expect(Object.keys(ir)[0]).toBe("version");
240
+ });
241
+
242
+ test("the other view formats carry no version field", async () => {
243
+ lintClean(); discovered();
244
+ const exit = await runGraph({ args: makeArgs({ format: "mermaid" }), plugins: [], serializers: [] });
245
+ expect(exit).toBe(0);
246
+ expect(stdoutBuf.join("\n")).not.toContain("version");
247
+ });
248
+
229
249
  // #1675 — discovery scans sourceDir, but ops live beside it (project root,
230
250
  // `ops/`) and are found by discoverOps from the git root. The IR joins them.
231
251
  describe("ops outside sourceDir reach the IR (#1675)", () => {
@@ -2,7 +2,7 @@ import { dirname, resolve } from "node:path";
2
2
  import { discoverOps } from "../../op/discover";
3
3
  import { discover } from "../../discovery/index";
4
4
  import { partitionByLexicon, computeStackGraph, build, mergeBuildRootEntities } from "../../build";
5
- import { buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, type GraphIR, type IRPipeline, type LiveObservation } from "../../graph-ir";
5
+ import { buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, GRAPH_IR_VERSION, type GraphIR, type IRPipeline, type LiveObservation } from "../../graph-ir";
6
6
  import { behaviourOverlay } from "../../behaviour-overlay";
7
7
  import type { SerializerResult } from "../../serializer";
8
8
  import { applyBehaviourOverlay, behaviourRequestFromIr } from "../../behaviour-graph";
@@ -886,7 +886,9 @@ async function emitIr(
886
886
  }
887
887
  case "ir":
888
888
  default:
889
- console.log(JSON.stringify(ir, null, 2));
889
+ // The version leads the document (#2529), so a reader sees it before
890
+ // anything it might not understand.
891
+ console.log(JSON.stringify({ version: GRAPH_IR_VERSION, ...ir }, null, 2));
890
892
  return 0;
891
893
  }
892
894
  }