@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
@@ -39,13 +39,5 @@
39
39
  { "kind": "health-gate", "path": "/healthz" }
40
40
  ]
41
41
  }
42
- ],
43
- "rollback": [
44
- {
45
- "phase": "Rollback",
46
- "steps": [
47
- { "kind": "rollback-previous", "service": "search", "cluster": "$env.cluster" }
48
- ]
49
- }
50
42
  ]
51
43
  }
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import { CapabilityRegistry } from "./capability";
25
+ import { lexiconModulePath } from "../lexicon-module";
25
26
  import type { CapabilityPlugin } from "./capability-plugin";
26
27
  import { isCapabilityPlugin } from "./capability-plugin";
27
28
  import { starterCapabilityPlugin } from "./starter-plugin";
@@ -113,7 +114,8 @@ export async function loadCapabilityPlugins(names: string[]): Promise<Capability
113
114
  * where the package's whole reason to exist is the plugin.
114
115
  */
115
116
  export async function loadCapabilityPluginFromLexicon(name: string): Promise<CapabilityPlugin | null> {
116
- const packageName = `@intentius/chant-lexicon-${name}`;
117
+ // chant #2520 — a lexicon declared by module path is imported from that path.
118
+ const packageName = lexiconModulePath(name) ?? `@intentius/chant-lexicon-${name}`;
117
119
  let mod: Record<string, unknown>;
118
120
  try {
119
121
  mod = (await import(packageName)) as Record<string, unknown>;
@@ -22,6 +22,7 @@
22
22
  * executor, the CLI entrypoint the driver (#556) never had.
23
23
  */
24
24
 
25
+ import { lexiconModulePath, lexiconNames } from "../lexicon-module";
25
26
  import { discoverComponents } from "./discover";
26
27
  import type { BuildParamProvenance } from "../provenance";
27
28
  import { projectToJson, type Archetype } from "./component";
@@ -38,7 +39,7 @@ import {
38
39
  type DriverRunResult,
39
40
  } from "./driver";
40
41
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
41
- import type { GateLedgerPort } from "../op/gate";
42
+ import type { GateDigestMismatch, GateLedgerPort } from "../op/gate";
42
43
  import { gateName } from "../op/gate-name";
43
44
  import { isLexiconPlugin, type LexiconPlugin, type ComponentPipelineOptions } from "../lexicon";
44
45
  import type { RunProgressEvent } from "./run-progress";
@@ -237,7 +238,7 @@ export interface GenerateComponentsResult {
237
238
  async function loadLexiconPlugin(name: string): Promise<LexiconPlugin | null> {
238
239
  let mod: Record<string, unknown>;
239
240
  try {
240
- mod = (await import(`@intentius/chant-lexicon-${name}`)) as Record<string, unknown>;
241
+ mod = (await import(lexiconModulePath(name) ?? `@intentius/chant-lexicon-${name}`)) as Record<string, unknown>;
241
242
  } catch {
242
243
  return null;
243
244
  }
@@ -431,6 +432,8 @@ export interface RunComponentsResult {
431
432
  /** Whether this run's own append reached the remote (#2310); absent when the pending fact was already standing. */
432
433
  pushed?: boolean;
433
434
  pushWarning?: string;
435
+ /** Set when an approval stands for this gate, but for another plan or none (#2574). */
436
+ mismatch?: GateDigestMismatch;
434
437
  };
435
438
  /** This run's resolved build-time parameters (chant #1108) — the component-driver counterpart of `../cli/commands/build.ts`'s `BuildResult.buildParams`. Present only once the run actually reached dispatch (mirrors `BuildResult.buildParams`, which is likewise absent on an early-error return). */
436
439
  buildParams?: BuildParamProvenance[];
@@ -550,7 +553,7 @@ export async function runComponents(
550
553
  options.registry ??
551
554
  (await buildCapabilityRegistry({
552
555
  plugins: options.capabilityPlugins ?? config.capabilities,
553
- lexicons: config.lexicons,
556
+ lexicons: lexiconNames(config.lexicons),
554
557
  }));
555
558
  const env = options.env ?? "local";
556
559
  const selected = targets.map((c) => c.name);
@@ -583,6 +586,7 @@ export async function runComponents(
583
586
  gate: run.gate,
584
587
  ...(run.gatePushed !== undefined ? { pushed: run.gatePushed } : {}),
585
588
  ...(run.gatePushWarning ? { pushWarning: run.gatePushWarning } : {}),
589
+ ...(run.gateMismatch ? { mismatch: run.gateMismatch } : {}),
586
590
  },
587
591
  }
588
592
  : {}),
@@ -631,6 +635,7 @@ export async function runComponents(
631
635
  gate: componentResult.gate,
632
636
  ...(componentResult.gatePushed !== undefined ? { gatePushed: componentResult.gatePushed } : {}),
633
637
  ...(componentResult.gatePushWarning ? { gatePushWarning: componentResult.gatePushWarning } : {}),
638
+ ...(componentResult.gateMismatch ? { gateMismatch: componentResult.gateMismatch } : {}),
634
639
  }
635
640
  : {}),
636
641
  componentOutputs,
@@ -647,6 +652,7 @@ export async function runComponents(
647
652
  gate: componentResult.gate,
648
653
  ...(componentResult.gatePushed !== undefined ? { pushed: componentResult.gatePushed } : {}),
649
654
  ...(componentResult.gatePushWarning ? { pushWarning: componentResult.gatePushWarning } : {}),
655
+ ...(componentResult.gateMismatch ? { mismatch: componentResult.gateMismatch } : {}),
650
656
  },
651
657
  }
652
658
  : {}),
@@ -280,76 +280,46 @@ describe("discoverComponents", () => {
280
280
  expect(result.errors.some((e) => e.type === "import")).toBe(true);
281
281
  });
282
282
 
283
- test("the first chant.config.ts encountered becomes the source root and is still scanned", async () => {
284
- // Matches findInfraFiles's own rule: the *first* config directory found
285
- // becomes `sourceRoot` and is descended into, same as any other
286
- // directory — the skip only applies to a *second* nested config found
287
- // after that (see the next test).
288
- const childDir = join(testDir, "child-project");
289
- await mkdir(childDir, { recursive: true });
290
- await writeFile(join(childDir, "chant.config.ts"), `export default {};`);
291
- await writeFile(
292
- join(childDir, "child.component.ts"),
293
- `
294
- export const childSvc = {
295
- name: "child-svc",
296
- dependsOn: [],
297
- deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
298
- };
299
- `,
300
- );
301
- await writeFile(
302
- join(testDir, "root.component.ts"),
303
- `
304
- export const rootSvc = {
305
- name: "root-svc",
283
+ const svc = (exportName: string, name: string) => `
284
+ export const ${exportName} = {
285
+ name: "${name}",
306
286
  dependsOn: [],
307
287
  deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
308
288
  };
309
- `,
310
- );
289
+ `;
311
290
 
312
- const result = await discoverComponents(testDir);
313
-
314
- expect(result.components.has("root-svc")).toBe(true);
315
- expect(result.components.has("child-svc")).toBe(true);
316
- });
317
-
318
- test("a second nested chant.config.ts (a true child project) is not descended into", async () => {
291
+ test("from outside every project, every child project is read (#2527)", async () => {
292
+ // The source-root quirk is gone: no child config is special for being
293
+ // found first. The scan root has no config at or above it, so every
294
+ // child project's components are discovered, nested ones included.
319
295
  const firstConfigDir = join(testDir, "first-config");
320
296
  const nestedChildProject = join(firstConfigDir, "nested-child-project");
321
297
  await mkdir(nestedChildProject, { recursive: true });
322
298
  await writeFile(join(firstConfigDir, "chant.config.ts"), `export default {};`);
323
299
  await writeFile(join(nestedChildProject, "chant.config.ts"), `export default {};`);
324
- await writeFile(
325
- join(firstConfigDir, "first.component.ts"),
326
- `
327
- export const firstSvc = {
328
- name: "first-svc",
329
- dependsOn: [],
330
- deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
331
- };
332
- `,
333
- );
334
- await writeFile(
335
- join(nestedChildProject, "nested.component.ts"),
336
- `
337
- export const nestedSvc = {
338
- name: "nested-svc",
339
- dependsOn: [],
340
- deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
341
- };
342
- `,
343
- );
300
+ await writeFile(join(testDir, "root.component.ts"), svc("rootSvc", "root-svc"));
301
+ await writeFile(join(firstConfigDir, "first.component.ts"), svc("firstSvc", "first-svc"));
302
+ await writeFile(join(nestedChildProject, "nested.component.ts"), svc("nestedSvc", "nested-svc"));
344
303
 
345
304
  const result = await discoverComponents(testDir);
346
305
 
347
- // first-config/ has no config above it, so it becomes the source root
348
- // and is scanned; nested-child-project/'s own chant.config.ts is the
349
- // *second* config encountered, so it is treated as a separate project
350
- // scope and not descended into.
306
+ expect(result.components.has("root-svc")).toBe(true);
351
307
  expect(result.components.has("first-svc")).toBe(true);
352
- expect(result.components.has("nested-svc")).toBe(false);
308
+ expect(result.components.has("nested-svc")).toBe(true);
309
+ });
310
+
311
+ test("from inside a project, a child project is a boundary (#2527)", async () => {
312
+ const childDir = join(testDir, "child-project");
313
+ await mkdir(childDir, { recursive: true });
314
+ await writeFile(join(testDir, "chant.config.ts"), `export default {};`);
315
+ await writeFile(join(childDir, "chant.config.ts"), `export default {};`);
316
+ await writeFile(join(testDir, "root.component.ts"), svc("rootSvc", "root-svc"));
317
+ await writeFile(join(childDir, "child.component.ts"), svc("childSvc", "child-svc"));
318
+
319
+ const result = await discoverComponents(testDir);
320
+
321
+ expect(result.components.has("root-svc")).toBe(true);
322
+ expect(result.components.has("child-svc")).toBe(false);
353
323
  });
354
324
 
355
325
  // ── chant #1108 — build-time parameters populated before import ────────────
@@ -40,15 +40,16 @@
40
40
  * `./component.ts`, already proves the plain-JSON round-trip).
41
41
  */
42
42
 
43
- import { readdir } from "node:fs/promises";
44
- import { join, resolve } from "node:path";
43
+ import { resolve } from "node:path";
45
44
  import { pathToFileURL } from "node:url";
46
- import { existsSync } from "node:fs";
47
45
  import { DiscoveryError } from "../errors";
48
46
  import { isComponent, type Component } from "./component";
49
47
  import type { BuildParamProvenance } from "../provenance";
50
48
  import { buildParamValues } from "../build-params";
51
49
  import { setBuildParams } from "../params";
50
+ import { hasDiscoveryMarkerSync } from "../discovery/files";
51
+ import { resolveDiscoveryGlobs } from "../config";
52
+ import { walkDiscovery, workspaceMemberDirs } from "../discovery/walk";
52
53
 
53
54
  /** One discovered component, paired with the file it was exported from. */
54
55
  export interface DiscoveredComponent {
@@ -115,56 +116,24 @@ export interface ImportedComponentModule {
115
116
  }
116
117
 
117
118
  /**
118
- * Recursively find every `*.component.ts` file under `path`, the same
119
- * child-project boundary rule `findInfraFiles` (`../discovery/files.ts`)
120
- * uses: a subdirectory with its own `chant.config.ts` is a separate scope
121
- * once the outer project's own source root has been established.
119
+ * Every `*.component.ts` file under `path`, through the one discovery walk
120
+ * (`../discovery/walk.ts`, #2527): the same dot-directory, `dist`,
121
+ * git-ignore and child-project rules as source discovery, the project's
122
+ * `exclude`/`include` globs and the skip marker (#2519). Scanned from outside
123
+ * any project, every child project is read.
122
124
  */
123
125
  async function findComponentFiles(path: string): Promise<string[]> {
124
- const files: string[] = [];
125
- let sourceRoot: string | null = null;
126
-
127
- async function scanDirectory(dir: string): Promise<void> {
128
- let entries;
129
- try {
130
- entries = await readdir(dir, { withFileTypes: true });
131
- } catch {
132
- return;
133
- }
134
-
135
- for (const entry of entries) {
136
- const fullPath = join(dir, entry.name);
137
-
138
- if (entry.isDirectory()) {
139
- // Skip dependency and tool/VCS directories: component sources never live
140
- // in them, and descending in is both wasted work and a correctness bug —
141
- // e.g. a CI runner like gitlab-ci-local stages a copy of the project
142
- // under `.gitlab-ci-local/`, which discovery would otherwise pick up as
143
- // duplicate component names alongside the originals.
144
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
145
-
146
- const configPath = join(fullPath, "chant.config.ts");
147
- if (existsSync(configPath)) {
148
- if (sourceRoot === null) {
149
- sourceRoot = fullPath;
150
- } else {
151
- continue;
152
- }
153
- }
154
- await scanDirectory(fullPath);
155
- } else if (
156
- entry.isFile() &&
157
- entry.name.endsWith(".component.ts") &&
158
- !entry.name.endsWith(".test.component.ts") &&
159
- !entry.name.endsWith(".spec.component.ts")
160
- ) {
161
- files.push(fullPath);
162
- }
163
- }
164
- }
165
-
166
- await scanDirectory(path);
167
- return files;
126
+ return walkDiscovery({
127
+ walker: "components",
128
+ root: path,
129
+ globs: await resolveDiscoveryGlobs(path),
130
+ excludeDirs: await workspaceMemberDirs(path),
131
+ accept: (name, full) =>
132
+ name.endsWith(".component.ts") &&
133
+ !name.endsWith(".test.component.ts") &&
134
+ !name.endsWith(".spec.component.ts") &&
135
+ !hasDiscoveryMarkerSync(full),
136
+ });
168
137
  }
169
138
 
170
139
  /**
@@ -13,7 +13,8 @@
13
13
  import { describe, expect, it } from "vitest";
14
14
  import { CapabilityRegistry, type DeployContext } from "./capability";
15
15
  import { stubCapability } from "./verbs/stub";
16
- import { memoryGateLedgerPort } from "../op/gate";
16
+ import { describeGateMismatch, memoryGateLedgerPort } from "../op/gate";
17
+ import { componentPlanDigest } from "./gate-plan";
17
18
  import type { PendingGateInput } from "../lifecycle/gate-ledger";
18
19
  import {
19
20
  DependencyCycleError,
@@ -311,14 +312,17 @@ describe("runComponentDeploy — gate as fact (#2119)", () => {
311
312
 
312
313
  it("a resolution newer than the pending fact passes the gate and carries the approver", async () => {
313
314
  const { registry, calls } = registryWithCalls();
315
+ const planDigest = componentPlanDigest({ environment: "dev", component: gatedComponent() });
314
316
  const port = memoryGateLedgerPort({
315
317
  pending: [{
316
318
  version: 1, kind: "pending", op: "neo4j-cluster", gate: "approve-node-1",
317
319
  timestamp: "2026-09-05T10:00:00.000Z", expiresAt: "2026-09-07T10:00:00.000Z",
320
+ planDigest, environment: "dev",
318
321
  }],
319
322
  resolutions: [{
320
323
  version: 1, op: "neo4j-cluster", gate: "approve-node-1",
321
324
  resolvedBy: "alex", timestamp: "2026-09-05T11:00:00.000Z",
325
+ planDigest, environment: "dev",
322
326
  }],
323
327
  });
324
328
  const result = await runComponentDeploy(
@@ -332,6 +336,79 @@ describe("runComponentDeploy — gate as fact (#2119)", () => {
332
336
  .toEqual({ gate: "approve-node-1", resolvedBy: "alex", timestamp: "2026-09-05T11:00:00.000Z" });
333
337
  });
334
338
 
339
+ // #2574: a component gate approval is bound to the environment and the plan.
340
+ describe("approvals bound to environment and plan (#2574)", () => {
341
+ const run = (port: ReturnType<typeof memoryGateLedgerPort>, env: string, component = gatedComponent()) =>
342
+ runComponentDeploy(component, { env, component: "neo4j-cluster" }, registryWithCalls().registry, {}, undefined, { port, now: NOW });
343
+
344
+ it("the pending fact records the environment and the plan", async () => {
345
+ const port = memoryGateLedgerPort();
346
+ const result = await run(port, "staging");
347
+ expect(result.gate).toMatchObject({
348
+ environment: "staging",
349
+ planDigest: componentPlanDigest({ environment: "staging", component: gatedComponent() }),
350
+ });
351
+ });
352
+
353
+ it("an approval given for another environment does not pass the gate", async () => {
354
+ const port = memoryGateLedgerPort();
355
+ const staging = await run(port, "staging");
356
+ const approval = {
357
+ version: 1 as const, op: "neo4j-cluster", gate: "approve-node-1", resolvedBy: "alex",
358
+ timestamp: "2026-09-05T12:30:00.000Z",
359
+ planDigest: staging.gate!.planDigest!, environment: "staging",
360
+ };
361
+ const withApproval = memoryGateLedgerPort({ pending: [staging.gate!], resolutions: [approval] });
362
+
363
+ expect((await run(withApproval, "staging")).status).toBe("ok");
364
+ const prod = await run(withApproval, "prod");
365
+ expect(prod.status).toBe("gated");
366
+ expect(prod.gate).toMatchObject({ environment: "prod" });
367
+ // The staging approval is not this run's business, so it is not reported as a mismatch either.
368
+ expect(prod.gateMismatch).toBeUndefined();
369
+ });
370
+
371
+ it("an approval given for another plan in the same environment is refused and named", async () => {
372
+ const port = memoryGateLedgerPort();
373
+ const before = await run(port, "prod");
374
+ const approval = {
375
+ version: 1 as const, op: "neo4j-cluster", gate: "approve-node-1", resolvedBy: "alex",
376
+ timestamp: "2026-09-05T12:30:00.000Z",
377
+ planDigest: before.gate!.planDigest!, environment: "prod",
378
+ };
379
+ const changed = gatedComponent();
380
+ changed.deploy[1]!.steps.push({ kind: "cfn-deploy", stack: "extra" });
381
+ const result = await run(memoryGateLedgerPort({ pending: [before.gate!], resolutions: [approval] }), "prod", changed);
382
+
383
+ expect(result.status).toBe("gated");
384
+ expect(result.gateMismatch).toMatchObject({
385
+ approved: before.gate!.planDigest, planned: result.gate!.planDigest, environment: "prod", resolvedBy: "alex",
386
+ });
387
+ expect(result.gate!.planDigest).not.toBe(before.gate!.planDigest);
388
+ });
389
+
390
+ it("an approval recorded before the binding is refused, with the command that replaces it", async () => {
391
+ const legacyPending = {
392
+ version: 1 as const, kind: "pending" as const, op: "neo4j-cluster", gate: "approve-node-1",
393
+ timestamp: "2026-09-05T10:00:00.000Z", expiresAt: "2026-09-07T10:00:00.000Z",
394
+ };
395
+ const legacy = {
396
+ version: 1 as const, op: "neo4j-cluster", gate: "approve-node-1",
397
+ resolvedBy: "alex", timestamp: "2026-09-05T11:00:00.000Z",
398
+ };
399
+ const port = memoryGateLedgerPort({ pending: [legacyPending], resolutions: [legacy] });
400
+ const result = await run(port, "prod");
401
+
402
+ expect(result.status).toBe("gated");
403
+ expect(port.appended).toHaveLength(1);
404
+ expect(result.gateMismatch).toMatchObject({ resolvedBy: "alex", environment: "prod" });
405
+ expect(result.gateMismatch!.approved).toBeUndefined();
406
+ const message = describeGateMismatch("neo4j-cluster", "approve-node-1", result.gateMismatch!);
407
+ expect(message).toContain("predates environment- and plan-bound component gates (#2574)");
408
+ expect(message).toContain("chant approve neo4j-cluster approve-node-1 --env prod");
409
+ });
410
+ });
411
+
335
412
  it("the neo4j pilot's gated Node 1 phase ends the run pending approval", async () => {
336
413
  const registry = new CapabilityRegistry();
337
414
  for (const kind of ["cfn-deploy", "code-deploy", "wait-cluster-healthy"]) {
@@ -450,7 +527,7 @@ describe("runComponentDeploy — onFailure saga rollback", () => {
450
527
  );
451
528
  });
452
529
 
453
- it("runs a component-level rollback (schema `rollback` field) after step rollback, matching the ALB/ECS pilot's explicit compensation", async () => {
530
+ it("runs a component-level rollback (schema `rollback` field) after step rollback", async () => {
454
531
  const registry = new CapabilityRegistry();
455
532
  registry.register(fakeCapability("docker-build", { run: () => ({ digest: "sha256:abc" }) }).capability);
456
533
  registry.register(
@@ -458,26 +535,29 @@ describe("runComponentDeploy — onFailure saga rollback", () => {
458
535
  .capability,
459
536
  );
460
537
  registry.register(fakeCapability("cfn-deploy", { run: () => ({ stackStatus: "UPDATE_COMPLETE", outputs: {} }) }).capability);
461
- // ecs-update-service fails, and (per the pilot's comment) has no native
462
- // rollback — the component's own `rollback` phase is what should fire.
538
+ // ecs-update-service fails; the component's own `rollback` phase is what
539
+ // should fire after the executed steps unwind.
463
540
  registry.register(fakeCapability("ecs-update-service", { failRun: true }).capability);
464
- const { capability: rollbackPreviousCap, calls: rollbackPreviousCalls } = fakeCapability("rollback-previous", {
541
+ const { capability: restoreCap, calls: restoreCalls } = fakeCapability("restore-service", {
465
542
  run: () => ({ restored: true }),
466
543
  });
467
- registry.register(rollbackPreviousCap);
544
+ registry.register(restoreCap);
468
545
  // Verify/rollback phases never reached since Apply fails first, but register
469
546
  // stubs anyway so a hypothetical reordering wouldn't crash the test on an
470
547
  // unregistered kind.
471
548
  registry.register(fakeCapability("wait-steady-state", { run: () => ({ runningCount: 1 }) }).capability);
472
549
  registry.register(fakeCapability("health-gate", { run: () => ({ healthy: true }) }).capability);
473
550
 
474
- const json = projectToJson(searchService) as unknown as DriverComponent;
551
+ const json: DriverComponent = {
552
+ ...(projectToJson(searchService) as unknown as DriverComponent),
553
+ rollback: [{ phase: "Rollback", steps: [{ kind: "restore-service", service: "search" }] }],
554
+ };
475
555
  const result = await runComponentDeploy(json, { env: "dev", component: "search-service" }, registry, {});
476
556
 
477
557
  expect(result.ok).toBe(false);
478
- expect(rollbackPreviousCalls).toHaveLength(1);
479
- expect(rollbackPreviousCalls[0]!.input).toMatchObject({ service: "search" });
480
- const rollbackRecord = result.records.find((r) => r.phase === "Rollback" && r.kind === "rollback-previous");
558
+ expect(restoreCalls).toHaveLength(1);
559
+ expect(restoreCalls[0]!.input).toMatchObject({ service: "search" });
560
+ const rollbackRecord = result.records.find((r) => r.phase === "Rollback" && r.kind === "restore-service");
481
561
  expect(rollbackRecord?.status).toBe("ok");
482
562
  });
483
563
  });
@@ -630,7 +710,6 @@ describe("runInterpretDriver — end to end", () => {
630
710
  "ecs-update-service",
631
711
  "wait-steady-state",
632
712
  "health-gate",
633
- "rollback-previous",
634
713
  "wait-for-stack",
635
714
  "run-migration",
636
715
  ];
@@ -750,7 +829,6 @@ describe("runInterpretDriver — end to end", () => {
750
829
  "ecs-update-service",
751
830
  "wait-steady-state",
752
831
  "health-gate",
753
- "rollback-previous",
754
832
  "wait-for-stack",
755
833
  "run-migration",
756
834
  "lambda-deploy",
@@ -47,8 +47,9 @@
47
47
  */
48
48
 
49
49
  import { topoSort } from "../codegen/topo-sort";
50
- import { evaluateGate, gitGateLedgerPort, type GateLedgerPort } from "../op/gate";
50
+ import { evaluateGate, gitGateLedgerPort, type GateDigestMismatch, type GateLedgerPort } from "../op/gate";
51
51
  import { gateName } from "../op/gate-name";
52
+ import { componentPlanDigest } from "./gate-plan";
52
53
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
53
54
  import type { CapabilityRegistry, DeployContext } from "./capability";
54
55
  import type { RunProgressEvent, RunProgressStatus } from "./run-progress";
@@ -167,6 +168,8 @@ export interface DriverComponentResult {
167
168
  gatePushed?: boolean;
168
169
  /** Set when `gatePushed` is false: why, in one line. */
169
170
  gatePushWarning?: string;
171
+ /** Present when `status === "gated"` because an approval stands for this gate, but for another plan or none (#2574). */
172
+ gateMismatch?: GateDigestMismatch;
170
173
  }
171
174
 
172
175
  export interface DriverRunResult {
@@ -188,6 +191,8 @@ export interface DriverRunResult {
188
191
  gatePushed?: boolean;
189
192
  /** Set when `gatePushed` is false: why, in one line. */
190
193
  gatePushWarning?: string;
194
+ /** Present when `status === "gated"` because an approval stands for another plan or none (#2574). */
195
+ gateMismatch?: GateDigestMismatch;
191
196
  /**
192
197
  * The accumulated cross-component/cross-stack outputs after the run — each
193
198
  * component's `publish` output and, for an applied stack, its `cfn-deploy`
@@ -370,6 +375,8 @@ class GateStop extends Error {
370
375
  /** Whether this run's own append reached the remote — see {@link DriverComponentResult.gatePushed} (#2310). */
371
376
  public readonly pushed?: boolean,
372
377
  public readonly pushWarning?: string,
378
+ /** Set when an approval stands for this gate but not for this plan (#2574). */
379
+ public readonly mismatch?: GateDigestMismatch,
373
380
  ) {
374
381
  super(`gate "${pending.gate}" is pending approval`);
375
382
  this.name = "GateStop";
@@ -385,6 +392,14 @@ function progressStatus(status: DriverComponentResult["status"]): RunProgressSta
385
392
  export interface GateContext {
386
393
  port: GateLedgerPort;
387
394
  now?: string;
395
+ /**
396
+ * The recorded digest each component is deployed at, for a promote or a
397
+ * rollback (#2574). It goes into the plan digest a gate approval is bound
398
+ * to, keyed by component name.
399
+ */
400
+ releases?: ReadonlyMap<string, string>;
401
+ /** This component's plan digest. Set by {@link runComponentDeploy}, never by a caller. */
402
+ planDigest?: string;
388
403
  }
389
404
 
390
405
  /** Run a single capability step. Never throws for a capability-run failure; returns a fail record instead. */
@@ -467,6 +482,9 @@ async function runPhase(
467
482
  const check = await evaluateGate(gates.port, {
468
483
  op: ctx.component,
469
484
  gate: gateName(gate),
485
+ // #2574: an approval counts only for this environment and this plan.
486
+ environment: ctx.env,
487
+ ...(gates.planDigest !== undefined ? { planDigest: gates.planDigest } : {}),
470
488
  ...(gate.description ? { description: gate.description } : {}),
471
489
  ...(gate.timeout ? { timeout: gate.timeout } : {}),
472
490
  ...(gates.now ? { now: gates.now } : {}),
@@ -477,7 +495,7 @@ async function runPhase(
477
495
  for (const skipped of phaseDef.steps.filter((s): s is DriverStep | DriverPhase => !isGateStep(s))) {
478
496
  gateRecords.push(skippedRecord(skipped));
479
497
  }
480
- throw new GateStop(gateRecords, [], check.pending, check.pushed, check.pushWarning);
498
+ throw new GateStop(gateRecords, [], check.pending, check.pushed, check.pushWarning, check.mismatch);
481
499
  }
482
500
  gateRecords.push({
483
501
  ...base,
@@ -505,6 +523,7 @@ async function runPhase(
505
523
  pending?: PendingGateRecord;
506
524
  pushed?: boolean;
507
525
  pushWarning?: string;
526
+ mismatch?: GateDigestMismatch;
508
527
  }> => {
509
528
  if (isPhaseStep(entry)) {
510
529
  try {
@@ -522,6 +541,7 @@ async function runPhase(
522
541
  pending: err.pending,
523
542
  pushed: err.pushed,
524
543
  pushWarning: err.pushWarning,
544
+ mismatch: err.mismatch,
525
545
  };
526
546
  }
527
547
  throw err;
@@ -570,7 +590,7 @@ async function runPhase(
570
590
  const executed = results.flatMap((r) => r.executed);
571
591
  const withPending = results.find((r) => r.pending);
572
592
  if (withPending?.pending) {
573
- throw new GateStop(records, executed, withPending.pending, withPending.pushed, withPending.pushWarning);
593
+ throw new GateStop(records, executed, withPending.pending, withPending.pushed, withPending.pushWarning, withPending.mismatch);
574
594
  }
575
595
  if (results.some((r) => r.failed)) throw new StepFailure(records, executed);
576
596
  return { records, executed };
@@ -584,7 +604,7 @@ async function runPhase(
584
604
  executed.push(...result.executed);
585
605
  if (result.pending) {
586
606
  for (const skipped of entries.slice(i + 1)) records.push(skippedRecord(skipped));
587
- throw new GateStop(records, executed, result.pending, result.pushed, result.pushWarning);
607
+ throw new GateStop(records, executed, result.pending, result.pushed, result.pushWarning, result.mismatch);
588
608
  }
589
609
  if (result.failed) {
590
610
  for (const skipped of entries.slice(i + 1)) records.push(skippedRecord(skipped));
@@ -693,9 +713,22 @@ export async function runComponentDeploy(
693
713
  registry: CapabilityRegistry,
694
714
  componentOutputs: Record<string, Record<string, unknown>>,
695
715
  onProgress?: (event: RunProgressEvent) => void,
696
- gates: GateContext = { port: gitGateLedgerPort() },
716
+ gateContext: GateContext = { port: gitGateLedgerPort() },
697
717
  ): Promise<DriverComponentResult> {
698
718
  const phaseOutputs: Record<string, Record<string, unknown>> = {};
719
+ // #2574: every gate this component reaches binds its approval to this
720
+ // environment and this composition, so an approval for staging or for last
721
+ // week's deploy cannot pass it.
722
+ const release = gateContext.releases?.get(component.name);
723
+ const gates: GateContext = {
724
+ ...gateContext,
725
+ planDigest: componentPlanDigest({
726
+ environment: ctx.env,
727
+ component,
728
+ ...(ctx.vars ? { vars: ctx.vars } : {}),
729
+ ...(release !== undefined ? { release } : {}),
730
+ }),
731
+ };
699
732
  const records: DriverStepRecord[] = [];
700
733
  const allExecuted: ExecutedStep[] = [];
701
734
 
@@ -720,6 +753,7 @@ export async function runComponentDeploy(
720
753
  gate: err.pending,
721
754
  ...(err.pushed !== undefined ? { gatePushed: err.pushed } : {}),
722
755
  ...(err.pushWarning ? { gatePushWarning: err.pushWarning } : {}),
756
+ ...(err.mismatch ? { gateMismatch: err.mismatch } : {}),
723
757
  };
724
758
  }
725
759
 
@@ -873,6 +907,8 @@ export interface InterpretRunOptions {
873
907
  gates?: GateLedgerPort;
874
908
  /** ISO-8601 "now" for gate decisions, so a test is deterministic. */
875
909
  now?: string;
910
+ /** The recorded digest a promote or rollback deploys, per component (#2574). Part of the plan a gate approval is bound to. */
911
+ releases?: ReadonlyMap<string, string>;
876
912
  }
877
913
 
878
914
  /**
@@ -906,6 +942,7 @@ export async function runInterpretDriver(
906
942
  const gates: GateContext = {
907
943
  port: options.gates ?? gitGateLedgerPort(),
908
944
  ...(options.now ? { now: options.now } : {}),
945
+ ...(options.releases ? { releases: options.releases } : {}),
909
946
  };
910
947
 
911
948
  onProgress?.({ type: "run-start", waves });
@@ -957,6 +994,7 @@ export async function runInterpretDriver(
957
994
  gate: gated.gate,
958
995
  ...(gated.gatePushed !== undefined ? { gatePushed: gated.gatePushed } : {}),
959
996
  ...(gated.gatePushWarning ? { gatePushWarning: gated.gatePushWarning } : {}),
997
+ ...(gated.gateMismatch ? { gateMismatch: gated.gateMismatch } : {}),
960
998
  }
961
999
  : {}),
962
1000
  };
@@ -13,6 +13,7 @@
13
13
  * over.
14
14
  */
15
15
 
16
+ import { lexiconNames } from "../lexicon-module";
16
17
  import { resolveComponentTargets } from "./cli-support";
17
18
  import { applyConfigDefaults } from "./config-defaults";
18
19
  import { buildCapabilityRegistry } from "./capability-plugin-loader";
@@ -91,5 +92,5 @@ export async function deriveFanOut(options: DeriveFanOutOptions): Promise<Derive
91
92
  */
92
93
  export async function fanOutRegistry(path: string, config?: ChantConfig): Promise<CapabilityRegistry> {
93
94
  const resolved = config ?? (await loadChantConfig(path)).config;
94
- return buildCapabilityRegistry({ plugins: resolved.capabilities, lexicons: resolved.lexicons });
95
+ return buildCapabilityRegistry({ plugins: resolved.capabilities, lexicons: lexiconNames(resolved.lexicons) });
95
96
  }