@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,645 @@
1
+ /**
2
+ * Promote a release without rebuilding (#2530).
3
+ */
4
+
5
+ import { describe, test, expect } from "vitest";
6
+ import { CapabilityRegistry, type DeployContext } from "./capability";
7
+ import { describeGateMismatch, memoryGateLedgerPort, type GateLedgerPort } from "../op/gate";
8
+ import { componentPlanDigest } from "./gate-plan";
9
+ import type { GateResolutionRecord } from "../lifecycle/gate-ledger";
10
+ import type { DriverComponent } from "./driver";
11
+ import type { ReleaseRecord } from "../lifecycle/release-ledger";
12
+ import {
13
+ planPromotion,
14
+ planRollback,
15
+ planRedeploy,
16
+ rollbackRecord,
17
+ redeployRecord,
18
+ selectRelease,
19
+ withoutBuildSteps,
20
+ runPromotion,
21
+ promotionRecord,
22
+ gateApprover,
23
+ promoteArchivePaths,
24
+ hasPublishStep,
25
+ parseDigestPins,
26
+ type PromotionPlan,
27
+ } from "./promote";
28
+
29
+ const rec = (component: string, env: string, digest: string, timestamp: string, runId = `run-${timestamp}`): ReleaseRecord => ({
30
+ version: 1,
31
+ component,
32
+ env,
33
+ digest,
34
+ gitSha: "abc123",
35
+ runId,
36
+ timestamp,
37
+ actor: "ci",
38
+ });
39
+
40
+ /** Build, publish, apply: the shape the pilots and the adopt example use. */
41
+ const service = (name: string, extra: Partial<DriverComponent> = {}): DriverComponent => ({
42
+ name,
43
+ deploy: [
44
+ { phase: "Build", steps: [{ kind: "docker-build", context: ".", into: `${name}.tar` }] },
45
+ { phase: "Publish", steps: [{ kind: "publish-image", from: `archive:${name}.tar`, to: "$env.registry" }] },
46
+ { phase: "Apply", steps: [{ kind: "apply", imageRef: "@Publish.digest" }] },
47
+ ],
48
+ ...extra,
49
+ });
50
+
51
+ describe("selecting the source release", () => {
52
+ const records = [
53
+ rec("api", "staging", "sha256:old", "2026-01-01T00:00:00Z"),
54
+ rec("api", "staging", "sha256:new", "2026-01-02T00:00:00Z"),
55
+ rec("web", "staging", "sha256:web", "2026-01-01T00:00:00Z"),
56
+ ];
57
+
58
+ test("the latest release by default", () => {
59
+ const picked = selectRelease(records, "api", "staging");
60
+ expect("record" in picked && picked.record.digest).toBe("sha256:new");
61
+ });
62
+
63
+ test("a named digest that the ledger recorded", () => {
64
+ const picked = selectRelease(records, "api", "staging", "sha256:old");
65
+ expect("record" in picked && picked.record.timestamp).toBe("2026-01-01T00:00:00Z");
66
+ });
67
+
68
+ test("a digest missing from the source ledger fails and names what is recorded", () => {
69
+ const picked = selectRelease(records, "api", "staging", "sha256:nope");
70
+ expect("error" in picked && picked.error).toMatch(/sha256:nope is not recorded for "api" in "staging"/);
71
+ expect("error" in picked && picked.error).toMatch(/sha256:old, sha256:new/);
72
+ });
73
+
74
+ test("a component with no release in the source fails", () => {
75
+ const picked = selectRelease(records, "db", "staging");
76
+ expect("error" in picked && picked.error).toMatch(/no release of "db" is recorded in "staging"/);
77
+ });
78
+ });
79
+
80
+ describe("planning", () => {
81
+ const records = [
82
+ rec("api", "staging", "sha256:api", "2026-01-02T00:00:00Z"),
83
+ rec("gone", "staging", "sha256:gone", "2026-01-02T00:00:00Z"),
84
+ ];
85
+
86
+ test("every declared component with a source release, and the rest reported", () => {
87
+ const plan = planPromotion({ from: "staging", to: "prod", sourceRecords: records, declared: ["api", "infra"] });
88
+ expect("items" in plan).toBe(true);
89
+ const { items, notPromoted } = plan as PromotionPlan;
90
+ expect(items.map((i) => [i.component, i.digest])).toEqual([["api", "sha256:api"]]);
91
+ expect(notPromoted).toEqual([{ component: "gone", reason: "recorded but not declared in this checkout" }]);
92
+ });
93
+
94
+ test("refuses the same environment on both sides", () => {
95
+ const plan = planPromotion({ from: "prod", to: "prod", sourceRecords: records, declared: ["api"] });
96
+ expect("error" in plan && plan.error).toMatch(/same environment/);
97
+ });
98
+
99
+ test("--digest needs --component", () => {
100
+ const plan = planPromotion({ from: "staging", to: "prod", sourceRecords: records, declared: ["api"], digest: "sha256:api" });
101
+ expect("error" in plan && plan.error).toMatch(/needs --component/);
102
+ });
103
+
104
+ test("an empty source ledger is an error, not a no-op", () => {
105
+ const plan = planPromotion({ from: "staging", to: "prod", sourceRecords: [], declared: ["api"] });
106
+ expect("error" in plan && plan.error).toMatch(/nothing to promote/);
107
+ });
108
+ });
109
+
110
+ describe("pinning each component to a digest (#2602)", () => {
111
+ // api's pinned release is older than its latest one: another pipeline
112
+ // recorded sha256:newer while this one ran.
113
+ const records = [
114
+ rec("api", "staging", "sha256:built", "2026-01-02T00:00:00Z"),
115
+ rec("api", "staging", "sha256:newer", "2026-01-03T00:00:00Z"),
116
+ rec("web", "staging", "sha256:web", "2026-01-02T00:00:00Z"),
117
+ rec("infra", "staging", "sha256:infra", "2026-01-02T00:00:00Z"),
118
+ ];
119
+ const declared = ["api", "web", "infra"];
120
+
121
+ test("promotes exactly the pinned components at the pinned digests, not the latest", () => {
122
+ const plan = planPromotion({
123
+ from: "staging", to: "prod", sourceRecords: records, declared,
124
+ pins: { web: "sha256:web", api: "sha256:built" },
125
+ });
126
+ expect("items" in plan).toBe(true);
127
+ const { items, notPromoted } = plan as PromotionPlan;
128
+ expect(items.map((i) => [i.component, i.digest, i.source.timestamp])).toEqual([
129
+ ["api", "sha256:built", "2026-01-02T00:00:00Z"],
130
+ ["web", "sha256:web", "2026-01-02T00:00:00Z"],
131
+ ]);
132
+ expect(notPromoted).toEqual([]);
133
+ });
134
+
135
+ test("a pinned digest the source never recorded fails", () => {
136
+ const plan = planPromotion({ from: "staging", to: "prod", sourceRecords: records, declared, pins: { api: "sha256:nope" } });
137
+ expect("error" in plan && plan.error).toMatch(/sha256:nope is not recorded for "api" in "staging"/);
138
+ });
139
+
140
+ test("a pinned component this checkout does not declare fails", () => {
141
+ const plan = planPromotion({ from: "staging", to: "prod", sourceRecords: records, declared, pins: { gone: "sha256:x" } });
142
+ expect("error" in plan && plan.error).toMatch(/"gone" is not declared/);
143
+ });
144
+
145
+ test("pins and --component do not mix", () => {
146
+ const plan = planPromotion({ from: "staging", to: "prod", sourceRecords: records, declared, component: "api", pins: { api: "sha256:built" } });
147
+ expect("error" in plan && plan.error).toMatch(/does not take --component/);
148
+ });
149
+
150
+ test("parseDigestPins reads <component>=<digest> values", () => {
151
+ expect(parseDigestPins(["api=sha256:a", "web=sha256:w"])).toEqual({ pins: { api: "sha256:a", web: "sha256:w" } });
152
+ expect(parseDigestPins(["api=sha256:a", "api=sha256:a"])).toEqual({ pins: { api: "sha256:a" } });
153
+ });
154
+
155
+ test("parseDigestPins refuses an empty digest, a bare digest, and two digests for one component", () => {
156
+ const err = (values: string[]) => {
157
+ const parsed = parseDigestPins(values);
158
+ return "error" in parsed ? parsed.error : undefined;
159
+ };
160
+ expect(err(["api="])).toMatch(/names no digest; the run that deployed "api" recorded no release/);
161
+ expect(err(["sha256:a"])).toMatch(/needs --component/);
162
+ expect(err(["=sha256:a"])).toMatch(/names no component/);
163
+ expect(err(["api=sha256:a", "api=sha256:b"])).toMatch(/pins "api" twice/);
164
+ });
165
+
166
+ test("hasPublishStep finds a publish step, nested phases included", () => {
167
+ expect(hasPublishStep(service("api"))).toBe(true);
168
+ expect(hasPublishStep({ name: "infra", deploy: [{ phase: "Apply", steps: [{ kind: "apply" }] }] })).toBe(false);
169
+ expect(hasPublishStep({
170
+ name: "nested",
171
+ deploy: [{ phase: "Outer", steps: [{ phase: "Inner", steps: [{ kind: "publish-artifact" }] }] }],
172
+ })).toBe(true);
173
+ });
174
+ });
175
+
176
+ describe("taking the build out of a composition", () => {
177
+ test("build steps go, publish and apply stay", () => {
178
+ const prepared = withoutBuildSteps(service("api"));
179
+ expect("component" in prepared).toBe(true);
180
+ if (!("component" in prepared)) return;
181
+ expect(prepared.removed).toEqual(["docker-build"]);
182
+ expect(prepared.component.deploy.map((p) => p.phase)).toEqual(["Publish", "Apply"]);
183
+ });
184
+
185
+ test("gates survive, even in a phase that loses its build step", () => {
186
+ const c = service("api");
187
+ c.deploy[0].steps.unshift({ kind: "gate", gate: "release" });
188
+ const prepared = withoutBuildSteps(c);
189
+ if (!("component" in prepared)) throw new Error(prepared.error);
190
+ expect(prepared.component.deploy[0]).toEqual({ phase: "Build", steps: [{ kind: "gate", gate: "release" }] });
191
+ });
192
+
193
+ test("no publish step is refused", () => {
194
+ const c: DriverComponent = { name: "chart", deploy: [{ phase: "Apply", steps: [{ kind: "helm-upgrade" }] }] };
195
+ const prepared = withoutBuildSteps(c);
196
+ expect("error" in prepared && prepared.error).toMatch(/no publish step/);
197
+ });
198
+
199
+ test("two publish steps are refused", () => {
200
+ const c = service("api");
201
+ c.deploy[1].steps.push({ kind: "publish-artifact", from: "archive:x.jar" });
202
+ const prepared = withoutBuildSteps(c);
203
+ expect("error" in prepared && prepared.error).toMatch(/2 publish steps/);
204
+ });
205
+
206
+ test("a step reading a removed build step's output is refused", () => {
207
+ const c = service("api");
208
+ c.deploy[1].steps[0] = { kind: "publish-image", from: "@Build.archivePath", to: "$env.registry" };
209
+ const prepared = withoutBuildSteps(c);
210
+ expect("error" in prepared && prepared.error).toMatch(/reads "@Build.archivePath"/);
211
+ });
212
+ });
213
+
214
+ /** A registry of fakes. `publishes` is the digest publish-image returns per component. */
215
+ function fakeRegistry(publishes: Record<string, string>) {
216
+ const ran: string[] = [];
217
+ const registry = new CapabilityRegistry();
218
+ const step = (kind: string, run: (ctx: DeployContext, input: Record<string, unknown>) => unknown) =>
219
+ registry.register({ kind, async run(ctx: DeployContext, input: Record<string, unknown>) { ran.push(`${ctx.env}:${ctx.component}:${kind}`); return run(ctx, input); } } as never);
220
+ step("docker-build", () => ({ digest: "sha256:rebuilt" }));
221
+ step("publish-image", (ctx) => ({ digest: publishes[ctx.component], uri: `reg/${ctx.component}@${publishes[ctx.component]}` }));
222
+ step("apply", (_ctx, input) => ({ applied: input.imageRef }));
223
+ return { registry, ran };
224
+ }
225
+
226
+ function planFor(component: string, digest: string): PromotionPlan {
227
+ return {
228
+ from: "staging",
229
+ to: "prod",
230
+ items: [{ component, digest, source: rec(component, "staging", digest, "2026-01-02T00:00:00Z", "run-7") }],
231
+ notPromoted: [],
232
+ };
233
+ }
234
+
235
+ function prepared(c: DriverComponent): DriverComponent {
236
+ const p = withoutBuildSteps(c);
237
+ if ("error" in p) throw new Error(p.error);
238
+ return p.component;
239
+ }
240
+
241
+ describe("running a promotion", () => {
242
+ test("deploys the recorded digest to the target without building", async () => {
243
+ const { registry, ran } = fakeRegistry({ api: "sha256:api" });
244
+ const run = await runPromotion({
245
+ plan: planFor("api", "sha256:api"),
246
+ components: [prepared(service("api"))],
247
+ registry,
248
+ gates: memoryGateLedgerPort(),
249
+ });
250
+ expect(run.status).toBe("ok");
251
+ expect(ran).toEqual(["prod:api:publish-image", "prod:api:apply"]);
252
+ const apply = run.results[0].records.find((r) => r.kind === "apply");
253
+ expect(apply?.output).toEqual({ applied: "sha256:api" });
254
+ });
255
+
256
+ test("a publish that yields another digest fails before apply runs", async () => {
257
+ const { registry, ran } = fakeRegistry({ api: "sha256:different" });
258
+ const run = await runPromotion({
259
+ plan: planFor("api", "sha256:api"),
260
+ components: [prepared(service("api"))],
261
+ registry,
262
+ gates: memoryGateLedgerPort(),
263
+ });
264
+ expect(run.status).toBe("fail");
265
+ expect(ran).not.toContain("prod:api:apply");
266
+ const publish = run.results[0].records.find((r) => r.kind === "publish-image" && r.status === "fail");
267
+ expect(publish?.error).toMatch(/published sha256:different, but the release being promoted recorded sha256:api/);
268
+ });
269
+
270
+ test("the target's gate still applies, and an approval lets the same promote through", async () => {
271
+ const gated = service("api");
272
+ gated.deploy[2].steps.unshift({ kind: "gate", gate: "prod-release" });
273
+ const resolutions: GateResolutionRecord[] = [];
274
+ const memory = memoryGateLedgerPort();
275
+ const gates: GateLedgerPort = {
276
+ async read(op) {
277
+ const read = await memory.read(op);
278
+ return { ...read, resolutions: [...resolutions] };
279
+ },
280
+ appendPending: (input) => memory.appendPending(input),
281
+ };
282
+ const { registry, ran } = fakeRegistry({ api: "sha256:api" });
283
+ const opts = { plan: planFor("api", "sha256:api"), components: [prepared(gated)], registry, gates };
284
+
285
+ const first = await runPromotion({ ...opts, now: "2026-01-03T00:00:00Z" });
286
+ expect(first.status).toBe("gated");
287
+ expect(first.gate?.gate).toBe("prod-release");
288
+ expect(ran).not.toContain("prod:api:apply");
289
+
290
+ // `chant approve` copies the pending fact's environment and plan (#2574).
291
+ const bound = { planDigest: first.gate!.planDigest!, environment: first.gate!.environment! };
292
+ expect(bound.environment).toBe("prod");
293
+ resolutions.push({ version: 1, kind: "resolution", op: "api", gate: "prod-release", resolvedBy: "alice", timestamp: "2026-01-03T00:01:00Z", ...bound });
294
+ const second = await runPromotion({ ...opts, now: "2026-01-03T00:02:00Z" });
295
+ expect(second.status).toBe("ok");
296
+ expect(gateApprover(second.results[0])).toBe("alice");
297
+
298
+ // The same approval does not pass a promote of another release (#2574).
299
+ const other = await runPromotion({
300
+ ...opts,
301
+ plan: planFor("api", "sha256:other"),
302
+ registry: fakeRegistry({ api: "sha256:other" }).registry,
303
+ now: "2026-01-03T00:03:00Z",
304
+ });
305
+ expect(other.status).toBe("gated");
306
+ expect(other.gateMismatch).toMatchObject({ approved: bound.planDigest, resolvedBy: "alice", environment: "prod" });
307
+ expect(other.gate?.planDigest).not.toBe(bound.planDigest);
308
+ });
309
+
310
+ test("dependencies outside the promotion do not block it", async () => {
311
+ const { registry } = fakeRegistry({ api: "sha256:api" });
312
+ const run = await runPromotion({
313
+ plan: planFor("api", "sha256:api"),
314
+ components: [prepared(service("api", { dependsOn: ["shared-alb"] }))],
315
+ registry,
316
+ gates: memoryGateLedgerPort(),
317
+ });
318
+ expect(run.status).toBe("ok");
319
+ });
320
+ });
321
+
322
+ describe("the target record", () => {
323
+ test("carries the source release it promoted", () => {
324
+ const plan = planFor("api", "sha256:api");
325
+ plan.items[0].source.manifestDigest = "sha256:manifest";
326
+ const record = promotionRecord(plan.items[0], "prod", {
327
+ runId: "run-9",
328
+ actor: "bob",
329
+ timestamp: "2026-01-04T00:00:00Z",
330
+ approver: "alice",
331
+ });
332
+ expect(record).toEqual({
333
+ component: "api",
334
+ env: "prod",
335
+ digest: "sha256:api",
336
+ gitSha: "abc123",
337
+ runId: "run-9",
338
+ timestamp: "2026-01-04T00:00:00Z",
339
+ actor: "bob",
340
+ approver: "alice",
341
+ manifestDigest: "sha256:manifest",
342
+ promotedFrom: { env: "staging", runId: "run-7", timestamp: "2026-01-02T00:00:00Z" },
343
+ });
344
+ });
345
+ });
346
+
347
+ describe("choosing the release a rollback restores", () => {
348
+ const history = [
349
+ rec("api", "prod", "sha256:a", "2026-01-01T00:00:00Z"),
350
+ rec("api", "prod", "sha256:b", "2026-01-02T00:00:00Z"),
351
+ rec("api", "prod", "sha256:c", "2026-01-03T00:00:00Z"),
352
+ ];
353
+ const plan = (input: Partial<Parameters<typeof planRollback>[0]>) =>
354
+ planRollback({ env: "prod", records: history, declared: ["api"], component: "api", ...input });
355
+
356
+ test("the release before the current one by default", () => {
357
+ const p = plan({});
358
+ expect("items" in p && p.items[0].digest).toBe("sha256:b");
359
+ expect("items" in p && p.from).toBe("prod");
360
+ });
361
+
362
+ test("a redeploy of the current digest does not count as an earlier release", () => {
363
+ const p = plan({ records: [...history, rec("api", "prod", "sha256:c", "2026-01-04T00:00:00Z")] });
364
+ expect("items" in p && p.items[0].digest).toBe("sha256:b");
365
+ });
366
+
367
+ test("a chosen digest", () => {
368
+ const p = plan({ digest: "sha256:a" });
369
+ expect("items" in p && p.items[0].source.timestamp).toBe("2026-01-01T00:00:00Z");
370
+ });
371
+
372
+ test("a digest the environment never recorded is refused", () => {
373
+ const p = plan({ digest: "sha256:z" });
374
+ expect("error" in p && p.error).toMatch(/sha256:z is not recorded for "api" in "prod"/);
375
+ });
376
+
377
+ test("the current release is refused", () => {
378
+ const p = plan({ digest: "sha256:c" });
379
+ expect("error" in p && p.error).toMatch(/already the current release/);
380
+ });
381
+
382
+ test("a single release has nothing earlier", () => {
383
+ const p = plan({ records: [history[0]] });
384
+ expect("error" in p && p.error).toMatch(/no earlier release/);
385
+ });
386
+ });
387
+
388
+ describe("choosing the release a redeploy puts back (#2604)", () => {
389
+ const history = [
390
+ rec("api", "prod", "sha256:a", "2026-01-01T00:00:00Z"),
391
+ rec("api", "prod", "sha256:b", "2026-01-02T00:00:00Z", "run-2"),
392
+ ];
393
+ const plan = (input: Partial<Parameters<typeof planRedeploy>[0]>) =>
394
+ planRedeploy({ env: "prod", records: history, declared: ["api"], component: "api", ...input });
395
+
396
+ test("the release the ledger records as current", () => {
397
+ const p = plan({});
398
+ if ("error" in p) throw new Error(p.error);
399
+ expect(p.from).toBe("prod");
400
+ expect(p.to).toBe("prod");
401
+ expect(p.items).toEqual([{ component: "api", digest: "sha256:b", source: history[1] }]);
402
+ });
403
+
404
+ test("a --digest naming the current release is accepted", () => {
405
+ const p = plan({ digest: "sha256:b" });
406
+ expect("items" in p && p.items[0].digest).toBe("sha256:b");
407
+ });
408
+
409
+ test("a --digest naming any other release is refused and points at rollback", () => {
410
+ const p = plan({ digest: "sha256:a" });
411
+ expect("error" in p && p.error).toMatch(/sha256:a is not the current release of "api" in "prod" \(that is sha256:b\)/);
412
+ expect("error" in p && p.error).toMatch(/chant components rollback prod --component api --digest sha256:a/);
413
+ });
414
+
415
+ test("a component with no release in the environment is refused", () => {
416
+ const p = plan({ records: [] });
417
+ expect("error" in p && p.error).toMatch(/no release of "api" is recorded in "prod"/);
418
+ });
419
+
420
+ test("a component this checkout does not declare is refused", () => {
421
+ const p = plan({ declared: ["web"] });
422
+ expect("error" in p && p.error).toMatch(/not declared in this checkout/);
423
+ });
424
+
425
+ test("the record names the release it put back", () => {
426
+ const p = plan({});
427
+ if ("error" in p) throw new Error(p.error);
428
+ const record = redeployRecord(p.items[0], "prod", { runId: "run-5", actor: "bob", timestamp: "2026-01-05T00:00:00Z" });
429
+ expect(record).toMatchObject({
430
+ env: "prod",
431
+ digest: "sha256:b",
432
+ redeploys: { env: "prod", runId: "run-2", timestamp: "2026-01-02T00:00:00Z" },
433
+ });
434
+ expect(record).not.toHaveProperty("restores");
435
+ expect(record).not.toHaveProperty("promotedFrom");
436
+ });
437
+
438
+ test("a refusal from the pinned composition names the redeploy", () => {
439
+ const c = service("api");
440
+ c.deploy[2].steps[0] = { kind: "apply", image: "@Publish.uri" };
441
+ const p = withoutBuildSteps(c, { pinDigest: "sha256:b", verb: "a redeploy" });
442
+ expect("error" in p && p.error).toMatch(/but a redeploy knows only the recorded digest/);
443
+ });
444
+ });
445
+
446
+ describe("a redeploy's gate approval is bound like a rollback's (#2574, #2604)", () => {
447
+ /** A gated service, pinned the way `chant components redeploy` pins it. */
448
+ const gatedService = (): DriverComponent => {
449
+ const c = service("api");
450
+ c.deploy[2].steps.unshift({ kind: "gate", gate: "prod-release" });
451
+ return c;
452
+ };
453
+ const pinned = (digest: string): DriverComponent => {
454
+ const p = withoutBuildSteps(gatedService(), { pinDigest: digest, verb: "a redeploy" });
455
+ if ("error" in p) throw new Error(p.error);
456
+ return p.component;
457
+ };
458
+ const redeployOf = (records: ReleaseRecord[]): PromotionPlan => {
459
+ const p = planRedeploy({ env: "prod", records, declared: ["api"], component: "api" });
460
+ if ("error" in p) throw new Error(p.error);
461
+ return p;
462
+ };
463
+ /** A ledger port whose resolutions the test writes, as `chant approve` would. */
464
+ const ledger = () => {
465
+ const resolutions: GateResolutionRecord[] = [];
466
+ const memory = memoryGateLedgerPort();
467
+ const gates: GateLedgerPort = {
468
+ async read(op) {
469
+ const read = await memory.read(op);
470
+ return { ...read, resolutions: [...resolutions] };
471
+ },
472
+ appendPending: (input) => memory.appendPending(input),
473
+ };
474
+ return { resolutions, gates };
475
+ };
476
+ const approval = (bound: Partial<GateResolutionRecord>, timestamp: string): GateResolutionRecord => ({
477
+ version: 1,
478
+ kind: "resolution",
479
+ op: "api",
480
+ gate: "prod-release",
481
+ resolvedBy: "alice",
482
+ timestamp,
483
+ ...bound,
484
+ });
485
+
486
+ test("the pending fact records prod and the redeploy's plan, and an approval of it lets the same redeploy through", async () => {
487
+ const { resolutions, gates } = ledger();
488
+ const { registry, ran } = fakeRegistry({ api: "sha256:unused" });
489
+ const plan = redeployOf([rec("api", "prod", "sha256:b", "2026-01-02T00:00:00Z", "run-2")]);
490
+ const opts = { plan, components: [pinned("sha256:b")], registry, gates };
491
+
492
+ const first = await runPromotion({ ...opts, now: "2026-01-05T00:00:00Z" });
493
+ expect(first.status).toBe("gated");
494
+ expect(first.gate?.environment).toBe("prod");
495
+ expect(first.gate?.planDigest).toMatch(/^sha256:[0-9a-f]{64}$/);
496
+ expect(ran).toEqual([]);
497
+
498
+ resolutions.push(approval({ planDigest: first.gate!.planDigest!, environment: "prod" }, "2026-01-05T00:01:00Z"));
499
+ const second = await runPromotion({ ...opts, now: "2026-01-05T00:02:00Z" });
500
+ expect(second.status).toBe("ok");
501
+ expect(gateApprover(second.results[0])).toBe("alice");
502
+ expect(ran).toEqual(["prod:api:apply"]);
503
+ });
504
+
505
+ test("an approval for another environment, for a plain deploy, or from before #2574 does not pass it", async () => {
506
+ const { resolutions, gates } = ledger();
507
+ const { registry, ran } = fakeRegistry({ api: "sha256:unused" });
508
+ const plan = redeployOf([rec("api", "prod", "sha256:b", "2026-01-02T00:00:00Z", "run-2")]);
509
+ const opts = { plan, components: [pinned("sha256:b")], registry, gates };
510
+ const first = await runPromotion({ ...opts, now: "2026-01-05T00:00:00Z" });
511
+ const planned = first.gate!.planDigest!;
512
+
513
+ // The same plan approved for staging is not read at all in prod.
514
+ resolutions.push(approval({ planDigest: planned, environment: "staging" }, "2026-01-05T00:01:00Z"));
515
+ const staging = await runPromotion({ ...opts, now: "2026-01-05T00:02:00Z" });
516
+ expect(staging.status).toBe("gated");
517
+ expect(staging.gateMismatch).toBeUndefined();
518
+
519
+ // An approval of a plain deploy to prod is for another plan.
520
+ const deployPlan = componentPlanDigest({ environment: "prod", component: gatedService() });
521
+ resolutions.push(approval({ planDigest: deployPlan, environment: "prod" }, "2026-01-05T00:03:00Z"));
522
+ const plain = await runPromotion({ ...opts, now: "2026-01-05T00:04:00Z" });
523
+ expect(plain.status).toBe("gated");
524
+ expect(plain.gateMismatch).toMatchObject({ approved: deployPlan, planned, environment: "prod" });
525
+
526
+ // One recorded before #2574 names neither, and the refusal says how to approve again.
527
+ resolutions.push(approval({}, "2026-01-05T00:05:00Z"));
528
+ const old = await runPromotion({ ...opts, now: "2026-01-05T00:06:00Z" });
529
+ expect(old.status).toBe("gated");
530
+ expect(old.gateMismatch).toMatchObject({ planned, environment: "prod" });
531
+ expect(old.gateMismatch?.approved).toBeUndefined();
532
+ expect(describeGateMismatch("api", "prod-release", old.gateMismatch!)).toMatch(
533
+ /predates environment- and plan-bound component gates \(#2574\).*chant approve api prod-release --env prod$/,
534
+ );
535
+ expect(ran).toEqual([]);
536
+ });
537
+
538
+ test("an approval of one redeploy does not pass a redeploy of a release recorded since", async () => {
539
+ const { resolutions, gates } = ledger();
540
+ const { registry } = fakeRegistry({ api: "sha256:unused" });
541
+ const b = rec("api", "prod", "sha256:b", "2026-01-02T00:00:00Z", "run-2");
542
+ const first = await runPromotion({ plan: redeployOf([b]), components: [pinned("sha256:b")], registry, gates, now: "2026-01-05T00:00:00Z" });
543
+ const approved = first.gate!.planDigest!;
544
+ resolutions.push(approval({ planDigest: approved, environment: "prod" }, "2026-01-05T00:01:00Z"));
545
+
546
+ const c = rec("api", "prod", "sha256:c", "2026-01-06T00:00:00Z", "run-3");
547
+ const next = await runPromotion({ plan: redeployOf([b, c]), components: [pinned("sha256:c")], registry, gates, now: "2026-01-06T00:01:00Z" });
548
+ expect(next.status).toBe("gated");
549
+ expect(next.gateMismatch).toMatchObject({ approved, resolvedBy: "alice", environment: "prod" });
550
+ expect(next.gate?.planDigest).not.toBe(approved);
551
+ });
552
+
553
+ test("a current release recorded with a legacy digest (#2514) binds the approval to that digest as recorded", async () => {
554
+ const legacy = `sha256:${"0a1b2c3d".repeat(8)}`;
555
+ const { resolutions, gates } = ledger();
556
+ const { registry, ran } = fakeRegistry({ api: "sha256:unused" });
557
+ const opts = {
558
+ plan: redeployOf([rec("api", "prod", legacy, "2026-01-02T00:00:00Z", "run-2")]),
559
+ components: [pinned(legacy)],
560
+ registry,
561
+ gates,
562
+ };
563
+ const first = await runPromotion({ ...opts, now: "2026-01-05T00:00:00Z" });
564
+ expect(first.status).toBe("gated");
565
+ const planned = first.gate!.planDigest!;
566
+ expect(planned).toBe(componentPlanDigest({ environment: "prod", component: pinned(legacy), release: legacy }));
567
+ expect(planned).not.toBe(legacy);
568
+
569
+ resolutions.push(approval({ planDigest: planned, environment: "prod" }, "2026-01-05T00:01:00Z"));
570
+ const second = await runPromotion({ ...opts, now: "2026-01-05T00:02:00Z" });
571
+ expect(second.status).toBe("ok");
572
+ expect(second.results[0].records.find((r) => r.kind === "apply")?.output).toEqual({ applied: legacy });
573
+ expect(ran).toEqual(["prod:api:apply"]);
574
+ });
575
+ });
576
+
577
+ describe("pinning the publish step for a rollback", () => {
578
+ test("the publish step is replaced by the recorded digest", () => {
579
+ const p = withoutBuildSteps(service("api"), { pinDigest: "sha256:b" });
580
+ if ("error" in p) throw new Error(p.error);
581
+ expect(p.removed).toEqual(["docker-build", "publish-image"]);
582
+ expect(p.component.deploy[0].steps[0]).toMatchObject({ kind: "recorded-digest", digest: "sha256:b", replaces: "publish-image" });
583
+ });
584
+
585
+ test("a step reading another publish output is refused", () => {
586
+ const c = service("api");
587
+ c.deploy[2].steps[0] = { kind: "apply", image: "@Publish.uri" };
588
+ const p = withoutBuildSteps(c, { pinDigest: "sha256:b" });
589
+ expect("error" in p && p.error).toMatch(/reads "@Publish.uri", but a rollback knows only the recorded digest/);
590
+ });
591
+
592
+ test("a rollback redeploys the recorded digest and publishes nothing", async () => {
593
+ const { registry, ran } = fakeRegistry({ api: "sha256:new" });
594
+ const p = withoutBuildSteps(service("api"), { pinDigest: "sha256:b" });
595
+ if ("error" in p) throw new Error(p.error);
596
+ const plan: PromotionPlan = {
597
+ from: "prod",
598
+ to: "prod",
599
+ items: [{ component: "api", digest: "sha256:b", source: rec("api", "prod", "sha256:b", "2026-01-02T00:00:00Z", "run-2") }],
600
+ notPromoted: [],
601
+ };
602
+ const run = await runPromotion({ plan, components: [p.component], registry, gates: memoryGateLedgerPort() });
603
+ expect(run.status).toBe("ok");
604
+ expect(ran).toEqual(["prod:api:apply"]);
605
+ expect(run.results[0].records.find((r) => r.kind === "apply")?.output).toEqual({ applied: "sha256:b" });
606
+
607
+ expect(rollbackRecord(plan.items[0], "prod", { runId: "run-5", actor: "bob", timestamp: "2026-01-05T00:00:00Z" })).toMatchObject({
608
+ env: "prod",
609
+ digest: "sha256:b",
610
+ restores: { env: "prod", runId: "run-2", timestamp: "2026-01-02T00:00:00Z" },
611
+ });
612
+ });
613
+ });
614
+
615
+ describe("the archive paths a generated promote job carries (#2575)", () => {
616
+ test("every build step's into, nested phases included, once each and in order", () => {
617
+ const component: DriverComponent = {
618
+ name: "api",
619
+ deploy: [
620
+ { phase: "Build", steps: [
621
+ { kind: "docker-build", context: ".", into: "dist/api.tar" },
622
+ { kind: "generate-sbom", artifactType: "image", path: "dist/api.tar", into: "dist/api.sbom.json" },
623
+ { phase: "Lambdas", parallel: true, steps: [{ kind: "zip-package", source: "fn", into: "dist/fn.zip" }] },
624
+ ] },
625
+ { phase: "Again", steps: [{ kind: "docker-build", context: ".", into: "dist/api.tar" }] },
626
+ { phase: "Publish", steps: [{ kind: "publish-image", from: "archive:dist/api.tar" }] },
627
+ ],
628
+ };
629
+ expect(promoteArchivePaths(component)).toEqual(["dist/api.tar", "dist/fn.zip"]);
630
+ });
631
+
632
+ test("a component that builds nothing carries nothing", () => {
633
+ expect(promoteArchivePaths({ name: "infra", deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy" }] }] })).toEqual([]);
634
+ });
635
+
636
+ test("an into that is only known at run time is refused by name", () => {
637
+ for (const into of ["@Params.path", "$env.archive", undefined]) {
638
+ const component: DriverComponent = {
639
+ name: "api",
640
+ deploy: [{ phase: "Build", steps: [{ kind: "jvm-build", into }] }],
641
+ };
642
+ expect(() => promoteArchivePaths(component)).toThrow(/component "api": the jvm-build step in phase "Build".*not a literal path/);
643
+ }
644
+ });
645
+ });