@intentius/chant 0.80.0 → 0.82.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 (415) hide show
  1. package/dist/audit/discover.d.ts +8 -16
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-html.d.ts +5 -0
  4. package/dist/audit/report-html.d.ts.map +1 -1
  5. package/dist/audit/report-model.d.ts +13 -0
  6. package/dist/audit/report-model.d.ts.map +1 -1
  7. package/dist/audit/terraform-state.d.ts +4 -5
  8. package/dist/audit/terraform-state.d.ts.map +1 -1
  9. package/dist/cli/commands/audit.d.ts +25 -9
  10. package/dist/cli/commands/audit.d.ts.map +1 -1
  11. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  12. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  13. package/dist/cli/commands/doctor.d.ts.map +1 -1
  14. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/lint.d.ts.map +1 -1
  17. package/dist/cli/commands/update.d.ts +6 -0
  18. package/dist/cli/commands/update.d.ts.map +1 -1
  19. package/dist/cli/commands/vendor.d.ts +5 -0
  20. package/dist/cli/commands/vendor.d.ts.map +1 -1
  21. package/dist/cli/handlers/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/components.d.ts.map +1 -1
  23. package/dist/cli/handlers/dev.d.ts.map +1 -1
  24. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  25. package/dist/cli/handlers/init.d.ts.map +1 -1
  26. package/dist/cli/handlers/operator.d.ts +7 -0
  27. package/dist/cli/handlers/operator.d.ts.map +1 -1
  28. package/dist/cli/handlers/promote.d.ts +7 -4
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/run-generate.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts +4 -1
  32. package/dist/cli/handlers/run.d.ts.map +1 -1
  33. package/dist/cli/handlers/vendor.d.ts +8 -3
  34. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  35. package/dist/cli/main.d.ts +12 -0
  36. package/dist/cli/main.d.ts.map +1 -1
  37. package/dist/cli/plugins.d.ts +12 -7
  38. package/dist/cli/plugins.d.ts.map +1 -1
  39. package/dist/cli/registry.d.ts +32 -0
  40. package/dist/cli/registry.d.ts.map +1 -1
  41. package/dist/codegen/fetch.d.ts +10 -0
  42. package/dist/codegen/fetch.d.ts.map +1 -1
  43. package/dist/components/cli-support.d.ts +3 -1
  44. package/dist/components/cli-support.d.ts.map +1 -1
  45. package/dist/components/discover.d.ts.map +1 -1
  46. package/dist/components/driver.d.ts +16 -2
  47. package/dist/components/driver.d.ts.map +1 -1
  48. package/dist/components/gate-plan.d.ts +40 -0
  49. package/dist/components/gate-plan.d.ts.map +1 -0
  50. package/dist/components/promote.d.ts +60 -7
  51. package/dist/components/promote.d.ts.map +1 -1
  52. package/dist/components/sandbox/driver.d.ts.map +1 -1
  53. package/dist/components/sandbox/run.d.ts.map +1 -1
  54. package/dist/components/verbs/build-archive.d.ts +22 -11
  55. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  56. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  57. package/dist/config-static.d.ts +59 -0
  58. package/dist/config-static.d.ts.map +1 -0
  59. package/dist/config.d.ts +32 -10
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/content-digest.d.ts +17 -0
  62. package/dist/content-digest.d.ts.map +1 -0
  63. package/dist/discovery/files.d.ts +10 -5
  64. package/dist/discovery/files.d.ts.map +1 -1
  65. package/dist/discovery/fold-import.d.ts +20 -4
  66. package/dist/discovery/fold-import.d.ts.map +1 -1
  67. package/dist/discovery/index.d.ts.map +1 -1
  68. package/dist/discovery/sandbox/driver.d.ts +20 -0
  69. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  70. package/dist/discovery/sandbox/fork.d.ts +20 -0
  71. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  72. package/dist/discovery/walk.d.ts +119 -0
  73. package/dist/discovery/walk.d.ts.map +1 -0
  74. package/dist/graph-ir.d.ts +4 -1
  75. package/dist/graph-ir.d.ts.map +1 -1
  76. package/dist/join-key.d.ts +31 -0
  77. package/dist/join-key.d.ts.map +1 -0
  78. package/dist/lexicon-module.d.ts +51 -6
  79. package/dist/lexicon-module.d.ts.map +1 -1
  80. package/dist/lexicon.d.ts +58 -1
  81. package/dist/lexicon.d.ts.map +1 -1
  82. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  83. package/dist/lifecycle/digest.d.ts +5 -1
  84. package/dist/lifecycle/digest.d.ts.map +1 -1
  85. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  86. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  87. package/dist/lifecycle/git.d.ts +64 -0
  88. package/dist/lifecycle/git.d.ts.map +1 -1
  89. package/dist/lifecycle/lease.d.ts +8 -1
  90. package/dist/lifecycle/lease.d.ts.map +1 -1
  91. package/dist/lifecycle/legacy-digest.d.ts +41 -8
  92. package/dist/lifecycle/legacy-digest.d.ts.map +1 -1
  93. package/dist/lifecycle/member-ledger.d.ts +67 -0
  94. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  95. package/dist/lifecycle/plan-digest.d.ts +4 -3
  96. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  97. package/dist/lifecycle/release-ledger.d.ts +18 -2
  98. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  99. package/dist/lifecycle/status.d.ts +90 -5
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/op/activities/activity-contracts.d.ts +32 -0
  102. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  103. package/dist/op/activities/index.d.ts +2 -0
  104. package/dist/op/activities/index.d.ts.map +1 -1
  105. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  106. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  107. package/dist/op/discover.d.ts.map +1 -1
  108. package/dist/op/gate-name.d.ts +7 -0
  109. package/dist/op/gate-name.d.ts.map +1 -1
  110. package/dist/op/gate-summary.d.ts +16 -0
  111. package/dist/op/gate-summary.d.ts.map +1 -1
  112. package/dist/op/gate.d.ts +16 -2
  113. package/dist/op/gate.d.ts.map +1 -1
  114. package/dist/project-root.d.ts +26 -0
  115. package/dist/project-root.d.ts.map +1 -1
  116. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  117. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  118. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  119. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  120. package/dist/workspace/checks/generated.d.ts +156 -0
  121. package/dist/workspace/checks/generated.d.ts.map +1 -0
  122. package/dist/workspace/checks/ledgers.d.ts +78 -0
  123. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  124. package/dist/workspace/checks/links.d.ts +25 -0
  125. package/dist/workspace/checks/links.d.ts.map +1 -0
  126. package/dist/workspace/checks/pipelines.d.ts +76 -0
  127. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  128. package/dist/workspace/checks.d.ts +161 -0
  129. package/dist/workspace/checks.d.ts.map +1 -0
  130. package/dist/workspace/compose-graph.d.ts +115 -0
  131. package/dist/workspace/compose-graph.d.ts.map +1 -0
  132. package/dist/workspace/compose-reports.d.ts +68 -0
  133. package/dist/workspace/compose-reports.d.ts.map +1 -0
  134. package/dist/workspace/declaration.d.ts +220 -0
  135. package/dist/workspace/declaration.d.ts.map +1 -0
  136. package/dist/workspace/declaration.schema.json +434 -0
  137. package/dist/workspace/generated-files.d.ts +65 -0
  138. package/dist/workspace/generated-files.d.ts.map +1 -0
  139. package/dist/workspace/glob.d.ts +13 -0
  140. package/dist/workspace/glob.d.ts.map +1 -0
  141. package/dist/workspace/graph-cli.d.ts +75 -0
  142. package/dist/workspace/graph-cli.d.ts.map +1 -0
  143. package/dist/workspace/init.d.ts +96 -0
  144. package/dist/workspace/init.d.ts.map +1 -0
  145. package/dist/workspace/jsonc.d.ts +38 -0
  146. package/dist/workspace/jsonc.d.ts.map +1 -0
  147. package/dist/workspace/kinds.d.ts +192 -0
  148. package/dist/workspace/kinds.d.ts.map +1 -0
  149. package/dist/workspace/lineage-check.d.ts +84 -0
  150. package/dist/workspace/lineage-check.d.ts.map +1 -0
  151. package/dist/workspace/lineage-cli.d.ts +35 -0
  152. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  153. package/dist/workspace/lineage-init.d.ts +144 -0
  154. package/dist/workspace/lineage-init.d.ts.map +1 -0
  155. package/dist/workspace/lineage-lock.d.ts +271 -0
  156. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  157. package/dist/workspace/lineage-merge.d.ts +14 -0
  158. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  159. package/dist/workspace/lineage-migrations.d.ts +178 -0
  160. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  161. package/dist/workspace/lineage-update.d.ts +59 -0
  162. package/dist/workspace/lineage-update.d.ts.map +1 -0
  163. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  164. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  165. package/dist/workspace/lineage-upgrade.d.ts +152 -0
  166. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  167. package/dist/workspace/lineage-vendor.d.ts +63 -0
  168. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  169. package/dist/workspace/lineage-version.d.ts +32 -0
  170. package/dist/workspace/lineage-version.d.ts.map +1 -0
  171. package/dist/workspace/links.d.ts +150 -0
  172. package/dist/workspace/links.d.ts.map +1 -0
  173. package/dist/workspace/ls.d.ts +99 -0
  174. package/dist/workspace/ls.d.ts.map +1 -0
  175. package/dist/workspace/member-commands.d.ts +139 -0
  176. package/dist/workspace/member-commands.d.ts.map +1 -0
  177. package/dist/workspace/member-handles.d.ts +54 -0
  178. package/dist/workspace/member-handles.d.ts.map +1 -0
  179. package/dist/workspace/member-pipeline.d.ts +143 -0
  180. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  181. package/dist/workspace/member-run.d.ts +67 -0
  182. package/dist/workspace/member-run.d.ts.map +1 -0
  183. package/dist/workspace/reason-codes.d.ts +65 -0
  184. package/dist/workspace/reason-codes.d.ts.map +1 -0
  185. package/dist/workspace/records-cli.d.ts +28 -4
  186. package/dist/workspace/records-cli.d.ts.map +1 -1
  187. package/dist/workspace/records.d.ts.map +1 -1
  188. package/dist/workspace/root-refusal.d.ts +53 -0
  189. package/dist/workspace/root-refusal.d.ts.map +1 -0
  190. package/dist/workspace/status.d.ts +150 -0
  191. package/dist/workspace/status.d.ts.map +1 -0
  192. package/dist/workspace/template-manifest.d.ts +90 -0
  193. package/dist/workspace/template-manifest.d.ts.map +1 -0
  194. package/dist/workspace/tree.d.ts +36 -0
  195. package/dist/workspace/tree.d.ts.map +1 -0
  196. package/dist/workspace/trust/attestor.d.ts +67 -0
  197. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  198. package/dist/workspace/trust/policy.d.ts +111 -0
  199. package/dist/workspace/trust/policy.d.ts.map +1 -0
  200. package/dist/workspace/trust/provenance.d.ts +67 -0
  201. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  202. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  203. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  204. package/dist/workspace/trust/test-repo.d.ts +42 -0
  205. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  206. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  207. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  208. package/dist/workspace/trust/verify.d.ts +66 -0
  209. package/dist/workspace/trust/verify.d.ts.map +1 -0
  210. package/dist/workspace/which-chant.d.ts +58 -0
  211. package/dist/workspace/which-chant.d.ts.map +1 -0
  212. package/dist/workspace/workspace-kinds.schema.json +133 -0
  213. package/package.json +2 -1
  214. package/src/audit/discover.ts +66 -78
  215. package/src/audit/report-html.ts +3 -1
  216. package/src/audit/report-model.ts +14 -1
  217. package/src/audit/terraform-state.test.ts +4 -9
  218. package/src/audit/terraform-state.ts +4 -5
  219. package/src/cli/commands/audit-truncation.test.ts +167 -0
  220. package/src/cli/commands/audit.ts +89 -60
  221. package/src/cli/commands/check-lexicon.ts +24 -0
  222. package/src/cli/commands/doctor.test.ts +30 -0
  223. package/src/cli/commands/doctor.ts +18 -0
  224. package/src/cli/commands/import-agents.ts +6 -2
  225. package/src/cli/commands/init.ts +22 -3
  226. package/src/cli/commands/lint.test.ts +17 -23
  227. package/src/cli/commands/lint.ts +24 -84
  228. package/src/cli/commands/update.ts +13 -4
  229. package/src/cli/commands/vendor.ts +11 -6
  230. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  231. package/src/cli/handlers/build-member.test.ts +127 -0
  232. package/src/cli/handlers/build.ts +35 -1
  233. package/src/cli/handlers/components.test.ts +127 -0
  234. package/src/cli/handlers/components.ts +70 -8
  235. package/src/cli/handlers/dev.ts +5 -3
  236. package/src/cli/handlers/fan-out.ts +9 -2
  237. package/src/cli/handlers/init.ts +58 -1
  238. package/src/cli/handlers/operator.test.ts +68 -0
  239. package/src/cli/handlers/operator.ts +37 -9
  240. package/src/cli/handlers/promote.test.ts +118 -2
  241. package/src/cli/handlers/promote.ts +82 -16
  242. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  243. package/src/cli/handlers/run-generate.test.ts +6 -8
  244. package/src/cli/handlers/run-generate.ts +36 -1
  245. package/src/cli/handlers/run.test.ts +47 -0
  246. package/src/cli/handlers/run.ts +28 -5
  247. package/src/cli/handlers/vendor.ts +147 -43
  248. package/src/cli/main.test.ts +105 -1
  249. package/src/cli/main.ts +232 -17
  250. package/src/cli/plugins.ts +17 -17
  251. package/src/cli/registry.ts +32 -0
  252. package/src/cli/static-config-read.test.ts +208 -0
  253. package/src/codegen/fetch.test.ts +63 -1
  254. package/src/codegen/fetch.ts +30 -1
  255. package/src/components/cli-support.ts +6 -1
  256. package/src/components/discover.test.ts +27 -57
  257. package/src/components/discover.ts +13 -68
  258. package/src/components/driver.test.ts +78 -1
  259. package/src/components/driver.ts +43 -7
  260. package/src/components/gate-plan.ts +53 -0
  261. package/src/components/promote.test.ts +276 -2
  262. package/src/components/promote.ts +119 -15
  263. package/src/components/sandbox/driver.ts +8 -8
  264. package/src/components/sandbox/run.ts +4 -1
  265. package/src/components/verbs/build-archive.ts +27 -21
  266. package/src/components/verbs/run-agent.ts +3 -5
  267. package/src/config-static.test.ts +136 -0
  268. package/src/config-static.ts +232 -0
  269. package/src/config.test.ts +2 -2
  270. package/src/config.ts +48 -15
  271. package/src/content-digest.test.ts +120 -0
  272. package/src/content-digest.ts +21 -0
  273. package/src/discovery/files.test.ts +10 -10
  274. package/src/discovery/files.ts +25 -81
  275. package/src/discovery/fold-import.ts +49 -5
  276. package/src/discovery/index.ts +1 -0
  277. package/src/discovery/sandbox/driver.ts +73 -35
  278. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  279. package/src/discovery/sandbox/fork.ts +83 -43
  280. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  281. package/src/discovery/walk.test.ts +263 -0
  282. package/src/discovery/walk.ts +334 -0
  283. package/src/effect-receipt.ts +2 -2
  284. package/src/graph-ir.ts +4 -1
  285. package/src/join-key.test.ts +25 -0
  286. package/src/join-key.ts +39 -0
  287. package/src/lexicon-module.ts +108 -10
  288. package/src/lexicon.ts +79 -1
  289. package/src/lifecycle/build-ledger-store.ts +11 -10
  290. package/src/lifecycle/digest.ts +9 -5
  291. package/src/lifecycle/gate-ledger.test.ts +20 -0
  292. package/src/lifecycle/gate-ledger.ts +28 -6
  293. package/src/lifecycle/git.test.ts +13 -1
  294. package/src/lifecycle/git.ts +221 -176
  295. package/src/lifecycle/lease.ts +25 -10
  296. package/src/lifecycle/legacy-digest.test.ts +115 -23
  297. package/src/lifecycle/legacy-digest.ts +60 -19
  298. package/src/lifecycle/member-ledger.test.ts +319 -0
  299. package/src/lifecycle/member-ledger.ts +145 -0
  300. package/src/lifecycle/plan-digest.ts +7 -7
  301. package/src/lifecycle/release-ledger.test.ts +20 -0
  302. package/src/lifecycle/release-ledger.ts +23 -7
  303. package/src/lifecycle/status.test.ts +151 -1
  304. package/src/lifecycle/status.ts +228 -7
  305. package/src/op/activities/activity-contracts.ts +37 -0
  306. package/src/op/activities/index.ts +10 -0
  307. package/src/op/activities/propose-upgrade.ts +206 -0
  308. package/src/op/discover.test.ts +14 -4
  309. package/src/op/discover.ts +19 -27
  310. package/src/op/gate-name.ts +8 -0
  311. package/src/op/gate-summary.test.ts +14 -0
  312. package/src/op/gate-summary.ts +25 -2
  313. package/src/op/gate.ts +36 -7
  314. package/src/op/generate-pipeline.test.ts +14 -5
  315. package/src/project-root.test.ts +66 -1
  316. package/src/project-root.ts +48 -0
  317. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  318. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  319. package/src/workspace/behold-kinds.test.ts +158 -0
  320. package/src/workspace/check-contract.test.ts +127 -0
  321. package/src/workspace/check.schema.json +347 -0
  322. package/src/workspace/checks/generated.test.ts +287 -0
  323. package/src/workspace/checks/generated.ts +475 -0
  324. package/src/workspace/checks/ledgers.test.ts +139 -0
  325. package/src/workspace/checks/ledgers.ts +246 -0
  326. package/src/workspace/checks/links.ts +183 -0
  327. package/src/workspace/checks/pipelines.test.ts +133 -0
  328. package/src/workspace/checks/pipelines.ts +225 -0
  329. package/src/workspace/checks-repo.test.ts +71 -0
  330. package/src/workspace/checks.test.ts +450 -0
  331. package/src/workspace/checks.ts +461 -0
  332. package/src/workspace/compose-graph.test.ts +146 -0
  333. package/src/workspace/compose-graph.ts +228 -0
  334. package/src/workspace/compose-reports.test.ts +109 -0
  335. package/src/workspace/compose-reports.ts +186 -0
  336. package/src/workspace/declaration.schema.json +434 -0
  337. package/src/workspace/declaration.test.ts +347 -0
  338. package/src/workspace/declaration.ts +623 -0
  339. package/src/workspace/generated-files.test.ts +114 -0
  340. package/src/workspace/generated-files.ts +94 -0
  341. package/src/workspace/glob.ts +49 -0
  342. package/src/workspace/graph-cli.ts +220 -0
  343. package/src/workspace/graph-contract.test.ts +204 -0
  344. package/src/workspace/graph.schema.json +662 -0
  345. package/src/workspace/init.test.ts +223 -0
  346. package/src/workspace/init.ts +585 -0
  347. package/src/workspace/jsonc.test.ts +65 -0
  348. package/src/workspace/jsonc.ts +231 -0
  349. package/src/workspace/kinds.test.ts +288 -0
  350. package/src/workspace/kinds.ts +506 -0
  351. package/src/workspace/lineage-check.ts +264 -0
  352. package/src/workspace/lineage-cli.ts +134 -0
  353. package/src/workspace/lineage-init-dir.test.ts +243 -0
  354. package/src/workspace/lineage-init.test.ts +231 -0
  355. package/src/workspace/lineage-init.ts +560 -0
  356. package/src/workspace/lineage-lock.test.ts +224 -0
  357. package/src/workspace/lineage-lock.ts +365 -0
  358. package/src/workspace/lineage-merge.ts +44 -0
  359. package/src/workspace/lineage-migrations.test.ts +178 -0
  360. package/src/workspace/lineage-migrations.ts +343 -0
  361. package/src/workspace/lineage-update.ts +160 -0
  362. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  363. package/src/workspace/lineage-upgrade.test.ts +474 -0
  364. package/src/workspace/lineage-upgrade.ts +723 -0
  365. package/src/workspace/lineage-vendor.test.ts +126 -0
  366. package/src/workspace/lineage-vendor.ts +215 -0
  367. package/src/workspace/lineage-version.ts +127 -0
  368. package/src/workspace/links.test.ts +344 -0
  369. package/src/workspace/links.ts +331 -0
  370. package/src/workspace/ls-contract.test.ts +200 -0
  371. package/src/workspace/ls.schema.json +190 -0
  372. package/src/workspace/ls.ts +254 -0
  373. package/src/workspace/member-commands.e2e.test.ts +244 -0
  374. package/src/workspace/member-commands.test.ts +217 -0
  375. package/src/workspace/member-commands.ts +540 -0
  376. package/src/workspace/member-handles.test.ts +76 -0
  377. package/src/workspace/member-handles.ts +228 -0
  378. package/src/workspace/member-pipeline.test.ts +135 -0
  379. package/src/workspace/member-pipeline.ts +317 -0
  380. package/src/workspace/member-run.ts +146 -0
  381. package/src/workspace/read-contract.test.ts +129 -0
  382. package/src/workspace/reason-codes.test.ts +113 -0
  383. package/src/workspace/reason-codes.ts +81 -0
  384. package/src/workspace/records-cli.ts +86 -14
  385. package/src/workspace/records-contract.test.ts +5 -0
  386. package/src/workspace/records.schema.json +32 -2
  387. package/src/workspace/records.ts +3 -2
  388. package/src/workspace/root-refusal.ts +97 -0
  389. package/src/workspace/status-contract.test.ts +308 -0
  390. package/src/workspace/status.schema.json +210 -0
  391. package/src/workspace/status.ts +412 -0
  392. package/src/workspace/template-manifest.test.ts +79 -0
  393. package/src/workspace/template-manifest.ts +221 -0
  394. package/src/workspace/tree.ts +132 -0
  395. package/src/workspace/trust/attestor.ts +93 -0
  396. package/src/workspace/trust/policy.ts +306 -0
  397. package/src/workspace/trust/provenance.ts +181 -0
  398. package/src/workspace/trust/ssh-commit.ts +138 -0
  399. package/src/workspace/trust/test-repo.ts +138 -0
  400. package/src/workspace/trust/trust.test.ts +397 -0
  401. package/src/workspace/trust/verify-cli.ts +56 -0
  402. package/src/workspace/trust/verify.ts +194 -0
  403. package/src/workspace/which-chant.test.ts +117 -0
  404. package/src/workspace/which-chant.ts +151 -0
  405. package/src/workspace/workspace-kinds.schema.json +133 -0
  406. package/dist/components/unbound-gate-approval.d.ts +0 -24
  407. package/dist/components/unbound-gate-approval.d.ts.map +0 -1
  408. package/dist/discovery/convergence.d.ts +0 -97
  409. package/dist/discovery/convergence.d.ts.map +0 -1
  410. package/src/cli/commands/audit-walk-warnings.test.ts +0 -156
  411. package/src/components/unbound-gate-approval.test.ts +0 -82
  412. package/src/components/unbound-gate-approval.ts +0 -51
  413. package/src/discovery/convergence.test.ts +0 -231
  414. package/src/discovery/convergence.ts +0 -399
  415. package/src/op/discover-convergence.test.ts +0 -70
@@ -0,0 +1,151 @@
1
+ /**
2
+ * `chant workspace upgrade [<scope>] [--to <ref|dir>] [--allow-code] [--dry-run]
3
+ * [--output <file>] [--json]` (#2550).
4
+ *
5
+ * Stages the upgrade in a worktree (see ./lineage-upgrade.ts), then decides
6
+ * its gate. The gate is `workspace-upgrade/<scope>` on the gate ledger, and it
7
+ * binds the digest of the patch the staging produced. The first run records
8
+ * the pending fact and exits 3, the code `chant run` gives a gated run. After
9
+ * `chant approve workspace-upgrade <scope>`, the same command stages the same
10
+ * patch, finds the approval for its digest, and applies the patch to the tree.
11
+ * A patch that differs from the approved one needs a new approval.
12
+ */
13
+
14
+ import { writeFileSync } from "node:fs";
15
+ import { resolve } from "node:path";
16
+ import { formatError, formatInfo, formatSuccess, formatWarning } from "../cli/format";
17
+ import type { CommandContext } from "../cli/registry";
18
+ import { approveCommand, describeGateMismatch, evaluateGate, gitGateLedgerPort, type GateLedgerPort } from "../op/gate";
19
+ import { WORKSPACE_UPGRADE_GATE_OP } from "../op/gate-name";
20
+ import { LockError } from "./lineage-lock";
21
+ import { applyStagedUpgrade, describeStaged, stageUpgrade, type ChantRunner, type StagedUpgrade } from "./lineage-upgrade";
22
+
23
+ const USAGE = "chant workspace upgrade [<scope>] [--to <ref|dir>] [--allow-code] [--dry-run] [--output <patch file>] [--json]";
24
+
25
+ /** The exit code of a gated upgrade: the same as `chant run`'s gated run. */
26
+ export const UPGRADE_GATED_EXIT = 3;
27
+
28
+ export interface UpgradeCommandOptions {
29
+ root: string;
30
+ scope?: string;
31
+ to?: string;
32
+ allowCode?: boolean;
33
+ dryRun?: boolean;
34
+ output?: string;
35
+ json?: boolean;
36
+ /** For tests. */
37
+ runChant?: ChantRunner;
38
+ ledger?: GateLedgerPort;
39
+ now?: string;
40
+ }
41
+
42
+ export type UpgradeOutcome = "up-to-date" | "checks-failed" | "dry-run" | "gated" | "applied";
43
+
44
+ export interface UpgradeCommandResult {
45
+ outcome: UpgradeOutcome;
46
+ exitCode: number;
47
+ staged?: Omit<StagedUpgrade, "dispose" | "patch" | "worktree" | "worktreeProject" | "lock">;
48
+ }
49
+
50
+ /** The command, with its output. Returns the exit code and what happened. */
51
+ export async function upgradeCommand(opts: UpgradeCommandOptions): Promise<UpgradeCommandResult> {
52
+ const staged = await stageUpgrade({
53
+ root: opts.root,
54
+ scope: opts.scope,
55
+ to: opts.to,
56
+ allowCode: opts.allowCode,
57
+ runChant: opts.runChant,
58
+ });
59
+ try {
60
+ const { dispose: _d, patch: _p, worktree: _w, worktreeProject: _wp, lock: _l, ...view } = staged;
61
+ const report = (outcome: UpgradeOutcome, exitCode: number, extra: Record<string, unknown> = {}): UpgradeCommandResult => {
62
+ if (opts.json) console.log(JSON.stringify({ outcome, ...view, ...extra }, null, 2));
63
+ return { outcome, exitCode, staged: view };
64
+ };
65
+ if (!opts.json) console.log(describeStaged(staged).join("\n"));
66
+
67
+ if (!opts.json && !staged.lock.tracked) {
68
+ console.error(
69
+ formatWarning({
70
+ message: `${staged.lock.path} is not tracked by git (a .gitignore covers it), so it is written beside the patch rather than in it. Commit it with \`git add -f\` so the lineage travels with the project.`,
71
+ }),
72
+ );
73
+ }
74
+ if (!staged.changed) {
75
+ if (!opts.json) console.error(formatSuccess(`scope "${staged.scope}" is already at ${staged.to ?? "its source"}; nothing to upgrade`));
76
+ return report("up-to-date", 0);
77
+ }
78
+ if (opts.output) writeFileSync(resolve(opts.output), staged.patch);
79
+ if (!staged.checksOk) {
80
+ if (!opts.json) {
81
+ for (const c of staged.checks.filter((c) => c.status === "failed")) {
82
+ console.error(formatError({ message: `${c.name} failed in the staging worktree`, hint: c.detail }));
83
+ }
84
+ console.error(formatError({ message: "the upgrade was not gated: its checks must pass first. The tree is unchanged." }));
85
+ }
86
+ return report("checks-failed", 1);
87
+ }
88
+ if (opts.dryRun) {
89
+ if (!opts.json) console.error(formatInfo("--dry-run: staged and checked; no gate recorded, the tree is unchanged"));
90
+ return report("dry-run", 0);
91
+ }
92
+
93
+ const gate = staged.scope;
94
+ const check = await evaluateGate(opts.ledger ?? gitGateLedgerPort({ cwd: staged.repo }), {
95
+ op: WORKSPACE_UPGRADE_GATE_OP,
96
+ gate,
97
+ description: `upgrade ${staged.template} ${staged.from ?? ""} -> ${staged.to ?? ""} in scope ${staged.scope}`.replace(/\s+/g, " "),
98
+ planDigest: staged.digest,
99
+ ...(staged.governance ? { approval: staged.governance.approval } : {}),
100
+ ...(opts.now ? { now: opts.now } : {}),
101
+ });
102
+ if (!check.satisfied) {
103
+ if (!opts.json) {
104
+ if (check.mismatch) console.error(formatWarning({ message: describeGateMismatch(WORKSPACE_UPGRADE_GATE_OP, gate, check.mismatch) }));
105
+ if (check.pushWarning) console.error(formatWarning({ message: check.pushWarning }));
106
+ if (check.quorum) console.error(formatInfo(`approvals so far: ${check.quorum.approvers.length} of ${check.quorum.need}`));
107
+ console.error(
108
+ formatInfo(
109
+ `Gated: the patch ${staged.digest} waits for approval. Review it (--output <file> writes it), then run \`${approveCommand(WORKSPACE_UPGRADE_GATE_OP, gate)}\` and repeat this command to apply it.`,
110
+ ),
111
+ );
112
+ }
113
+ return report("gated", UPGRADE_GATED_EXIT, { pending: check.pending });
114
+ }
115
+
116
+ applyStagedUpgrade(staged);
117
+ if (!opts.json) {
118
+ console.error(formatSuccess(`applied the approved upgrade of "${staged.scope}" (${staged.digest}), approved by ${check.resolution.resolvedBy}. Review and commit it.`));
119
+ if (staged.manualSteps.length > 0) {
120
+ console.error(formatInfo(`${staged.manualSteps.length} manual step(s) are open; \`chant workspace check\` fails until each is merged and resolved.`));
121
+ }
122
+ }
123
+ return report("applied", 0, { approvedBy: check.resolution.resolvedBy });
124
+ } finally {
125
+ staged.dispose();
126
+ }
127
+ }
128
+
129
+ export async function runWorkspaceUpgrade(ctx: CommandContext): Promise<number> {
130
+ const { args } = ctx;
131
+ if (args.extraPositional2) {
132
+ console.error(formatError({ message: `unexpected argument: ${args.extraPositional2}`, hint: USAGE }));
133
+ return 1;
134
+ }
135
+ try {
136
+ const result = await upgradeCommand({
137
+ root: process.cwd(),
138
+ scope: args.extraPositional,
139
+ to: args.migrateTo,
140
+ allowCode: args.allowCode,
141
+ dryRun: args.dryRun,
142
+ output: args.output,
143
+ json: args.json,
144
+ });
145
+ return result.exitCode;
146
+ } catch (err) {
147
+ if (!(err instanceof LockError)) throw err;
148
+ console.error(formatError({ message: err.message, hint: USAGE }));
149
+ return 1;
150
+ }
151
+ }
@@ -0,0 +1,474 @@
1
+ /**
2
+ * chant #2550 — `chant workspace upgrade`, end to end over real git
3
+ * repositories: a template with tagged versions, a project made with
4
+ * `init --from <template>@v1`, edits in the project, and upgrades that merge,
5
+ * conflict, migrate, refuse a gap, and wait on a gate bound to the patch.
6
+ */
7
+
8
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
9
+ import { execFileSync } from "node:child_process";
10
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { dirname, join } from "node:path";
13
+ import type { GateLedgerPort } from "../op/gate";
14
+ import type { GateResolutionRecord, PendingGateRecord } from "../lifecycle/gate-ledger";
15
+ import { WORKSPACE_UPGRADE_GATE_OP } from "../op/gate-name";
16
+ import { proposeWorkspaceUpgrade } from "../op/activities/propose-upgrade";
17
+ import { checkLineage } from "./lineage-check";
18
+ import { initFromCommand } from "./lineage-init";
19
+ import { LOCK_FILE, fileHash, readLock, writeLock } from "./lineage-lock";
20
+ import { stageUpgrade, isGovernancePath, UPGRADE_DIR, type ChantRunner } from "./lineage-upgrade";
21
+ import { upgradeCommand } from "./lineage-upgrade-cli";
22
+
23
+ const ENV = { ...process.env, GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t" };
24
+
25
+ let root: string;
26
+ let tpl: string;
27
+ let proj: string;
28
+
29
+ function git(cwd: string, args: string[]): string {
30
+ return execFileSync("git", args, { cwd, encoding: "utf-8", env: ENV, stdio: ["ignore", "pipe", "pipe"] }).trim();
31
+ }
32
+ function put(base: string, rel: string, content: string): void {
33
+ mkdirSync(dirname(join(base, rel)), { recursive: true });
34
+ writeFileSync(join(base, rel), content);
35
+ }
36
+ function read(base: string, rel: string): string {
37
+ return readFileSync(join(base, rel), "utf-8");
38
+ }
39
+ function release(tag: string, files: Record<string, string | null>): void {
40
+ for (const [rel, content] of Object.entries(files)) {
41
+ if (content === null) rmSync(join(tpl, rel));
42
+ else put(tpl, rel, content);
43
+ }
44
+ git(tpl, ["add", "-A"]);
45
+ git(tpl, ["commit", "-q", "--allow-empty", "-m", tag]);
46
+ git(tpl, ["tag", tag]);
47
+ }
48
+ function commitProject(message: string): void {
49
+ git(proj, ["add", "-A"]);
50
+ git(proj, ["commit", "-q", "-m", message]);
51
+ }
52
+
53
+ /** A gate ledger in memory that a test can approve into. */
54
+ function ledger(): GateLedgerPort & { pending: PendingGateRecord[]; approve(digest: string, at: string, by?: string): void } {
55
+ const pending: PendingGateRecord[] = [];
56
+ const resolutions: GateResolutionRecord[] = [];
57
+ return {
58
+ pending,
59
+ approve(digest, at, by = "alice") {
60
+ resolutions.push({ version: 1, op: WORKSPACE_UPGRADE_GATE_OP, gate: pending.at(-1)?.gate ?? ".", resolvedBy: by, timestamp: at, planDigest: digest });
61
+ },
62
+ async read() {
63
+ return { resolutions: [...resolutions], pending: [...pending] };
64
+ },
65
+ async appendPending(input) {
66
+ const record: PendingGateRecord = { version: 1, kind: "pending", ...input };
67
+ pending.push(record);
68
+ return { record, pushed: true };
69
+ },
70
+ };
71
+ }
72
+
73
+ const passing: ChantRunner = async () => ({ exitCode: 0, output: "" });
74
+
75
+ const V1 = {
76
+ "README.md": "starter\n",
77
+ "src/a.ts": "one\ntwo\nthree\nfour\nfive\nsix\nseven\n",
78
+ "src/old.ts": "export const old = 1;\n\n\n\nexport const tail = 1;\n",
79
+ };
80
+
81
+ beforeEach(async () => {
82
+ root = realpathSync(mkdtempSync(join(tmpdir(), "chant-upgrade-test-")));
83
+ tpl = join(root, "tpl");
84
+ proj = join(root, "proj");
85
+ mkdirSync(tpl);
86
+ git(tpl, ["init", "-q", "-b", "main"]);
87
+ release("v1.0.0", V1);
88
+ const made = await initFromCommand({ from: `${tpl}@v1.0.0`, path: proj });
89
+ expect(made.error).toBeUndefined();
90
+ git(proj, ["init", "-q", "-b", "main"]);
91
+ commitProject("init from v1");
92
+ vi.spyOn(console, "log").mockImplementation(() => undefined);
93
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
94
+ });
95
+ afterEach(() => {
96
+ vi.restoreAllMocks();
97
+ rmSync(root, { recursive: true, force: true });
98
+ });
99
+
100
+ describe("chant workspace upgrade", () => {
101
+ test("merges a customised file whose hunks are clean, and updates an unedited one", async () => {
102
+ put(proj, "src/a.ts", "ONE (ours)\ntwo\nthree\nfour\nfive\nsix\nseven\n");
103
+ commitProject("edit");
104
+ release("v2.0.0", { "README.md": "starter v2\n", "src/a.ts": "one\ntwo\nthree\nfour\nfive\nsix\nSEVEN (theirs)\n", "src/new.ts": "new\n" });
105
+
106
+ const staged = await stageUpgrade({ root: proj, to: "v2.0.0", runChant: passing });
107
+ try {
108
+ expect(staged.merged).toEqual(["src/a.ts"]);
109
+ expect(staged.written.sort()).toEqual(["README.md", "src/new.ts"]);
110
+ expect(staged.manualSteps).toEqual([]);
111
+ expect(staged.changedPaths).toEqual([LOCK_FILE, "README.md", "src/a.ts", "src/new.ts"]);
112
+ expect(staged.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
113
+ // The project's tree is untouched until the gate is approved.
114
+ expect(read(proj, "README.md")).toBe("starter\n");
115
+ expect(existsSync(join(proj, "src/new.ts"))).toBe(false);
116
+ expect(read(staged.worktreeProject, "src/a.ts")).toBe("ONE (ours)\ntwo\nthree\nfour\nfive\nsix\nSEVEN (theirs)\n");
117
+ // Build and lint are skipped: the template is not a chant project. The lineage check ran.
118
+ expect(staged.checks.map((c) => `${c.name}:${c.status}`)).toEqual(["build:skipped", "lint:skipped", "workspace check:passed"]);
119
+ } finally {
120
+ staged.dispose();
121
+ }
122
+ expect(existsSync(join(proj, UPGRADE_DIR))).toBe(false);
123
+ expect(git(proj, ["worktree", "list"]).split("\n")).toHaveLength(1);
124
+ });
125
+
126
+ test("gates on the patch digest, then applies exactly the approved patch", async () => {
127
+ release("v2.0.0", { "README.md": "starter v2\n" });
128
+ const port = ledger();
129
+
130
+ const first = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port, now: "2026-09-23T10:00:00.000Z" });
131
+ expect(first.outcome).toBe("gated");
132
+ expect(first.exitCode).toBe(3);
133
+ expect(port.pending).toHaveLength(1);
134
+ expect(port.pending[0]).toMatchObject({ op: "workspace-upgrade", gate: ".", planDigest: first.staged!.digest });
135
+ expect(read(proj, "README.md")).toBe("starter\n");
136
+
137
+ // An approval for another patch does not apply this one.
138
+ port.approve(`sha256:${"0".repeat(64)}`, "2026-09-23T10:01:00.000Z");
139
+ const mismatched = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port, now: "2026-09-23T10:02:00.000Z" });
140
+ expect(mismatched.outcome).toBe("gated");
141
+ expect(read(proj, "README.md")).toBe("starter\n");
142
+
143
+ port.approve(first.staged!.digest, "2026-09-23T10:03:00.000Z");
144
+ const second = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port, now: "2026-09-23T10:04:00.000Z" });
145
+ expect(second.outcome).toBe("applied");
146
+ expect(second.staged!.digest).toBe(first.staged!.digest);
147
+ expect(read(proj, "README.md")).toBe("starter v2\n");
148
+ const lineage = readLock(proj)!.scopes["."];
149
+ expect(lineage.ref).toBe("v2.0.0");
150
+ expect(lineage.address?.commit).toBe(git(tpl, ["rev-parse", "v2.0.0^{commit}"]));
151
+ expect(lineage.files["README.md"].sha256).toBe(fileHash("starter v2\n"));
152
+
153
+ // Committed, the scope is at v2, and upgrading again has nothing to do.
154
+ commitProject("upgrade");
155
+ const again = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port });
156
+ expect(again.outcome).toBe("up-to-date");
157
+ });
158
+
159
+ test("a lock that a .gitignore covers is applied beside the patch, and the digest covers it", async () => {
160
+ put(proj, ".gitignore", ".chant/\n");
161
+ git(proj, ["rm", "-q", "--cached", LOCK_FILE]);
162
+ commitProject("ignore .chant/, as some templates do");
163
+ release("v2.0.0", {});
164
+ const port = ledger();
165
+ const first = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port, now: "2026-09-23T10:00:00.000Z" });
166
+ // Only the lock changes, and it is not in the patch.
167
+ expect(first.outcome).toBe("gated");
168
+ expect(first.staged!.changedPaths).toEqual([]);
169
+ port.approve(first.staged!.digest, "2026-09-23T10:01:00.000Z");
170
+ const applied = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port, now: "2026-09-23T10:02:00.000Z" });
171
+ expect(applied.outcome).toBe("applied");
172
+ expect(readLock(proj)!.scopes["."].ref).toBe("v2.0.0");
173
+ const again = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port });
174
+ expect(again.outcome).toBe("up-to-date");
175
+ });
176
+
177
+ test("a customised file with a conflicting hunk is kept whole and becomes one manual step", async () => {
178
+ put(proj, "src/a.ts", "one\ntwo (ours)\nthree\nfour\nfive\nsix\nseven (ours)\n");
179
+ commitProject("edit");
180
+ release("v2.0.0", { "src/a.ts": "one\ntwo (theirs)\nthree\nfour\nfive\nsix\nseven\n" });
181
+ const port = ledger();
182
+
183
+ const first = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port, now: "2026-09-23T10:00:00.000Z" });
184
+ expect(first.staged!.manualSteps).toEqual([{ path: "src/a.ts", reason: "changed-locally", upstream: fileHash("one\ntwo (theirs)\nthree\nfour\nfive\nsix\nseven\n") }]);
185
+ expect(first.staged!.merged).toEqual([]);
186
+ // The file is not in the patch; only the lock records the step.
187
+ expect(first.staged!.changedPaths).toEqual([LOCK_FILE]);
188
+ expect(first.staged!.checksOk).toBe(true);
189
+
190
+ port.approve(first.staged!.digest, "2026-09-23T10:01:00.000Z");
191
+ const applied = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port, now: "2026-09-23T10:02:00.000Z" });
192
+ expect(applied.outcome).toBe("applied");
193
+ expect(read(proj, "src/a.ts")).toBe("one\ntwo (ours)\nthree\nfour\nfive\nsix\nseven (ours)\n");
194
+ // Open manual steps fail check until resolved.
195
+ const report = checkLineage(proj);
196
+ expect(report.ok).toBe(false);
197
+ expect(report.findings).toMatchObject([{ code: "manual-step-open", scope: ".", path: "src/a.ts" }]);
198
+ // And a further upgrade is refused while the step is open.
199
+ commitProject("upgrade with a step");
200
+ await expect(stageUpgrade({ root: proj, to: "v2.0.0", runChant: passing })).rejects.toThrow(/open manual step/);
201
+ });
202
+
203
+ test("runs a declarative migration before the merge, records it, and checks its post-condition", async () => {
204
+ put(proj, "src/old.ts", "export const old = 1;\n\n\n\nexport const tail = 2; // ours\n");
205
+ commitProject("edit");
206
+ release("v2.0.0", {
207
+ "src/old.ts": null,
208
+ "src/new.ts": "export const renamed = 1;\n\n\n\nexport const tail = 1;\n",
209
+ ".chant/migrations/rename.json": JSON.stringify({
210
+ id: "rename-old",
211
+ from: { versions: ">=1.0.0 <2.0.0" },
212
+ to: "2.0.0",
213
+ body: { type: "declarative", steps: [{ op: "move", from: "src/old.ts", to: "src/new.ts" }] },
214
+ post: [{ check: "exists", path: "src/new.ts" }, { check: "absent", path: "src/old.ts" }],
215
+ }),
216
+ });
217
+
218
+ const staged = await stageUpgrade({ root: proj, to: "v2.0.0", runChant: passing });
219
+ try {
220
+ expect(staged.migrations).toEqual(["rename-old"]);
221
+ expect(staged.merged).toEqual(["src/new.ts"]);
222
+ expect(read(staged.worktreeProject, "src/new.ts")).toBe("export const renamed = 1;\n\n\n\nexport const tail = 2; // ours\n");
223
+ expect(existsSync(join(staged.worktreeProject, "src/old.ts"))).toBe(false);
224
+ // The template's migrations never land in the project.
225
+ expect(existsSync(join(staged.worktreeProject, ".chant/migrations"))).toBe(false);
226
+ const lock = readLock(staged.worktreeProject)!.scopes["."];
227
+ expect(lock.migrations).toEqual(["rename-old"]);
228
+ expect(Object.keys(lock.files).sort()).toEqual(["README.md", "src/a.ts", "src/new.ts"]);
229
+ } finally {
230
+ staged.dispose();
231
+ }
232
+ });
233
+
234
+ test("a failed post-condition refuses the upgrade", async () => {
235
+ release("v2.0.0", {
236
+ ".chant/migrations/m.json": JSON.stringify({
237
+ id: "wants-file",
238
+ from: { versions: "1.x" },
239
+ to: "2.0.0",
240
+ body: { type: "declarative", steps: [{ op: "replace", path: "README.md", find: "starter", with: "begin" }] },
241
+ post: [{ check: "contains", path: "README.md", text: "never there" }],
242
+ }),
243
+ });
244
+ await expect(stageUpgrade({ root: proj, to: "v2.0.0", runChant: passing })).rejects.toThrow(/post-condition does not hold/);
245
+ expect(existsSync(join(proj, UPGRADE_DIR))).toBe(false);
246
+ });
247
+
248
+ test("a gap in the migration chain is refused", async () => {
249
+ release("v2.0.0", {});
250
+ release("v3.0.0", {
251
+ ".chant/migrations/three.json": JSON.stringify({
252
+ id: "two-to-three",
253
+ from: { versions: ">=2.0.0 <3.0.0" },
254
+ to: "3.0.0",
255
+ body: { type: "declarative", steps: [{ op: "delete", path: "src/old.ts" }] },
256
+ }),
257
+ });
258
+ await expect(stageUpgrade({ root: proj, to: "v3.0.0", runChant: passing })).rejects.toThrow(/gap in the migration chain: two-to-three .* the scope is at 1\.0\.0/);
259
+ });
260
+
261
+ test("a code migration runs only with allowCode", async () => {
262
+ release("v2.0.0", {
263
+ ".chant/migrations/code.json": JSON.stringify({ id: "by-code", from: { versions: "^1.0.0" }, to: "2.0.0", body: { type: "code", module: "code.mjs" }, post: [{ check: "exists", path: "MIGRATED" }] }),
264
+ ".chant/migrations/code.mjs": "import { writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nexport default async ({ dir }) => writeFileSync(join(dir, 'MIGRATED'), 'yes\\n');\n",
265
+ });
266
+ await expect(stageUpgrade({ root: proj, to: "v2.0.0", runChant: passing })).rejects.toThrow(/--allow-code/);
267
+ const staged = await stageUpgrade({ root: proj, to: "v2.0.0", allowCode: true, runChant: passing });
268
+ try {
269
+ expect(staged.migrations).toEqual(["by-code"]);
270
+ expect(staged.changedPaths).toContain("MIGRATED");
271
+ } finally {
272
+ staged.dispose();
273
+ }
274
+ });
275
+
276
+ test("failing build or lint in the worktree stops the upgrade before its gate", async () => {
277
+ put(proj, "chant.config.ts", "export default {};\n");
278
+ commitProject("a chant project");
279
+ release("v2.0.0", { "README.md": "v2\n" });
280
+ const port = ledger();
281
+ const calls: string[] = [];
282
+ const lintFails: ChantRunner = async (command, cwd) => {
283
+ calls.push(`${command} in ${cwd.includes(UPGRADE_DIR) ? "worktree" : cwd}`);
284
+ return command === "lint" ? { exitCode: 1, output: "lint error" } : { exitCode: 0, output: "" };
285
+ };
286
+ const result = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: lintFails, ledger: port });
287
+ expect(calls).toEqual(["build in worktree", "lint in worktree"]);
288
+ expect(result.outcome).toBe("checks-failed");
289
+ expect(result.exitCode).toBe(1);
290
+ expect(port.pending).toHaveLength(0);
291
+ expect(read(proj, "README.md")).toBe("starter\n");
292
+ });
293
+
294
+ test("governance changes need human approval under the rules at HEAD", async () => {
295
+ release("v2.0.0", { "ops/deploy.op.ts": "export {};\n", ".github/workflows/ci.yml": "on: push\n" });
296
+ const port = ledger();
297
+ const first = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port, now: "2026-09-23T10:00:00.000Z" });
298
+ expect(first.outcome).toBe("gated");
299
+ expect(first.staged!.governance).toMatchObject({ paths: [".github/workflows/ci.yml", "ops/deploy.op.ts"], approval: { mode: "log-only", quorum: { count: 1 } } });
300
+ expect(port.pending[0].approval).toEqual({ mode: "log-only", quorum: { count: 1 } });
301
+ expect(isGovernancePath("CODEOWNERS")).toBe(true);
302
+ expect(isGovernancePath("src/main.ts")).toBe(false);
303
+ });
304
+
305
+ test("refuses a scope with uncommitted changes, and a scope that does not exist", async () => {
306
+ put(proj, "README.md", "dirty\n");
307
+ await expect(stageUpgrade({ root: proj, to: "v1.0.0" })).rejects.toThrow(/uncommitted changes/);
308
+ await expect(stageUpgrade({ root: proj, scope: "vendor/x" })).rejects.toThrow(/no scope "vendor\/x"/);
309
+ });
310
+
311
+ test("a vendor scope merges what it can: unedited files update, edited ones become manual steps", async () => {
312
+ put(proj, "shared/web/index.ts", "export const v = 1;\n");
313
+ put(proj, "shared/web/util.ts", "export const u = 1;\n");
314
+ put(proj, "vendor/web/index.ts", "export const v = 1; // ours\n");
315
+ put(proj, "vendor/web/util.ts", "export const u = 1;\n");
316
+ const lock = readLock(proj)!;
317
+ lock.scopes["vendor/web"] = {
318
+ kind: "vendor",
319
+ name: "web",
320
+ template: "local:shared/web",
321
+ source: { type: "local", path: "shared/web" },
322
+ address: { digest: `sha256:${"a".repeat(64)}` },
323
+ parameters: {},
324
+ migrations: [],
325
+ files: {
326
+ "index.ts": { class: "owned", sha256: fileHash("export const v = 1;\n") },
327
+ "util.ts": { class: "owned", sha256: fileHash("export const u = 1;\n") },
328
+ },
329
+ manualSteps: [],
330
+ };
331
+ writeLock(proj, lock);
332
+ commitProject("vendor");
333
+ put(proj, "shared/web/index.ts", "export const v = 2;\n");
334
+ put(proj, "shared/web/util.ts", "export const u = 2;\n");
335
+ commitProject("source moves");
336
+
337
+ const staged = await stageUpgrade({ root: proj, scope: "vendor/web", runChant: passing });
338
+ try {
339
+ expect(staged.written).toEqual(["util.ts"]);
340
+ expect(staged.manualSteps.map((s) => `${s.path}:${s.reason}`)).toEqual(["index.ts:changed-locally"]);
341
+ } finally {
342
+ staged.dispose();
343
+ }
344
+ });
345
+ });
346
+
347
+ describe("proposeWorkspaceUpgrade", () => {
348
+ test("writes a proposal branch and pushes it, never the default branch", async () => {
349
+ const remote = join(root, "remote.git");
350
+ git(root, ["init", "-q", "--bare", "-b", "main", remote]);
351
+ git(proj, ["remote", "add", "origin", remote]);
352
+ git(proj, ["push", "-q", "origin", "main"]);
353
+ git(proj, ["remote", "set-head", "origin", "main"]);
354
+ release("v2.0.0", { "README.md": "starter v2\n" });
355
+ const mainBefore = git(proj, ["rev-parse", "main"]);
356
+
357
+ const report = await proposeWorkspaceUpgrade({ cwd: proj, to: "v2.0.0", _runChant: passing });
358
+ expect(report).toMatchObject({ mode: "report", changed: true, proposed: false, checksOk: true, from: "v1.0.0", to: "v2.0.0" });
359
+
360
+ const branch = await proposeWorkspaceUpgrade({ cwd: proj, to: "v2.0.0", mode: "branch", _runChant: passing });
361
+ expect(branch).toMatchObject({ proposed: true, branch: "chant/upgrade/root", pushed: true, digest: report.digest });
362
+ expect(git(remote, ["rev-parse", "chant/upgrade/root"])).toBe(branch.commit);
363
+ expect(git(remote, ["show", "chant/upgrade/root:README.md"])).toBe("starter v2");
364
+ expect(git(remote, ["rev-parse", "main"])).toBe(mainBefore);
365
+ expect(git(proj, ["rev-parse", "main"])).toBe(mainBefore);
366
+ expect(read(proj, "README.md")).toBe("starter\n");
367
+
368
+ await expect(proposeWorkspaceUpgrade({ cwd: proj, to: "v2.0.0", mode: "branch", branch: "main", _runChant: passing })).rejects.toThrow(/will not write the default branch "main"/);
369
+ expect(git(remote, ["rev-parse", "main"])).toBe(mainBefore);
370
+ });
371
+
372
+ test("opens a pull request once and edits it on the next run", async () => {
373
+ const remote = join(root, "remote.git");
374
+ git(root, ["init", "-q", "--bare", "-b", "main", remote]);
375
+ git(proj, ["remote", "add", "origin", remote]);
376
+ git(proj, ["push", "-q", "origin", "main"]);
377
+ git(proj, ["remote", "set-head", "origin", "main"]);
378
+ release("v2.0.0", { "README.md": "starter v2\n" });
379
+ const gh: string[][] = [];
380
+ let open = "";
381
+ const run = async (bin: "git" | "gh", args: string[], cwd: string): Promise<string> => {
382
+ if (bin === "git") return execFileSync("git", args, { cwd, encoding: "utf-8", env: ENV, stdio: ["ignore", "pipe", "pipe"] });
383
+ gh.push(args);
384
+ if (args[1] === "list") return open;
385
+ if (args[1] === "create") {
386
+ open = "https://example.invalid/pr/1";
387
+ return `${open}\n`;
388
+ }
389
+ return "";
390
+ };
391
+ const first = await proposeWorkspaceUpgrade({ cwd: proj, to: "v2.0.0", mode: "pull-request", _run: run, _runChant: passing });
392
+ expect(first.prUrl).toBe("https://example.invalid/pr/1");
393
+ const create = gh.find((a) => a[1] === "create")!;
394
+ expect(create.slice(0, 6)).toEqual(["pr", "create", "--base", "main", "--head", "chant/upgrade/root"]);
395
+ expect(create[create.indexOf("--body") + 1]).toContain(first.digest);
396
+ await proposeWorkspaceUpgrade({ cwd: proj, to: "v2.0.0", mode: "pull-request", _run: run, _runChant: passing });
397
+ expect(gh.filter((a) => a[1] === "create")).toHaveLength(1);
398
+ expect(gh.filter((a) => a[1] === "edit")).toHaveLength(1);
399
+ });
400
+ });
401
+
402
+ describe("chant workspace upgrade with template parameters (#2627)", () => {
403
+ const TITLE_V1 = 'export const title = "{{chant:name}}";\ntwo\nthree\nfour\nfive\nsix\nseven\n';
404
+
405
+ beforeEach(async () => {
406
+ // A template that declares a parameter, and a project made from it with a value.
407
+ tpl = join(root, "ptpl");
408
+ proj = join(root, "pproj");
409
+ mkdirSync(tpl);
410
+ git(tpl, ["init", "-q", "-b", "main"]);
411
+ release("v1.0.0", {
412
+ "chant.template.json": JSON.stringify({ parameters: { name: { type: "string", default: "Starter" } }, files: ["app/title.ts", "README.md"] }),
413
+ "app/title.ts": TITLE_V1,
414
+ "README.md": "# {{chant:name}}\n",
415
+ });
416
+ const made = await initFromCommand({ from: `${tpl}@v1.0.0`, path: proj, params: { name: "Acme" } });
417
+ expect(made.error).toBeUndefined();
418
+ git(proj, ["init", "-q", "-b", "main"]);
419
+ commitProject("init from v1 with name=Acme");
420
+ });
421
+
422
+ test("the merge base carries the recorded value, so an edited file merges and an unedited one updates", async () => {
423
+ expect(read(proj, "app/title.ts")).toBe(TITLE_V1.replace("{{chant:name}}", "Acme"));
424
+ expect(existsSync(join(proj, "chant.template.json"))).toBe(false);
425
+ expect(readLock(proj)!.scopes["."].parameters).toEqual({ name: "Acme" });
426
+
427
+ put(proj, "app/title.ts", 'export const title = "Acme";\ntwo\nthree\nfour\nfive\nsix\nseven (ours)\n');
428
+ commitProject("edit");
429
+ release("v2.0.0", {
430
+ "chant.template.json": JSON.stringify({
431
+ parameters: { name: { type: "string", default: "Starter" }, owner: { type: "string", default: "platform" } },
432
+ files: ["app/title.ts", "README.md", "OWNERS"],
433
+ }),
434
+ "app/title.ts": 'export const title = "{{chant:name}}";\ntwo (theirs)\nthree\nfour\nfive\nsix\nseven\n',
435
+ "README.md": "# {{chant:name}} v2\n",
436
+ OWNERS: "{{chant:owner}}\n",
437
+ });
438
+
439
+ const staged = await stageUpgrade({ root: proj, to: "v2.0.0", runChant: passing });
440
+ try {
441
+ expect(staged.manualSteps).toEqual([]);
442
+ expect(staged.merged).toEqual(["app/title.ts"]);
443
+ expect(staged.written.sort()).toEqual(["OWNERS", "README.md"]);
444
+ expect(read(staged.worktreeProject, "app/title.ts")).toBe('export const title = "Acme";\ntwo (theirs)\nthree\nfour\nfive\nsix\nseven (ours)\n');
445
+ expect(read(staged.worktreeProject, "README.md")).toBe("# Acme v2\n");
446
+ expect(read(staged.worktreeProject, "OWNERS")).toBe("platform\n");
447
+ expect(existsSync(join(staged.worktreeProject, "chant.template.json"))).toBe(false);
448
+ const lineage = readLock(staged.worktreeProject)!.scopes["."];
449
+ // The recorded value is kept, and a parameter the new version adds takes its default.
450
+ expect(lineage.parameters).toEqual({ name: "Acme", owner: "platform" });
451
+ expect(lineage.files["README.md"].sha256).toBe(fileHash("# Acme v2\n"));
452
+ expect(lineage.files["chant.template.json"]).toBeUndefined();
453
+ } finally {
454
+ staged.dispose();
455
+ }
456
+ });
457
+
458
+ test("an upgrade to the same version changes nothing", async () => {
459
+ const staged = await stageUpgrade({ root: proj, to: "v1.0.0", runChant: passing });
460
+ try {
461
+ expect(staged.changed).toBe(false);
462
+ expect(staged.manualSteps).toEqual([]);
463
+ } finally {
464
+ staged.dispose();
465
+ }
466
+ });
467
+
468
+ test("a new parameter with no default refuses the upgrade", async () => {
469
+ release("v2.0.0", {
470
+ "chant.template.json": JSON.stringify({ parameters: { name: { type: "string" }, region: { type: "string" } }, files: ["README.md"] }),
471
+ });
472
+ await expect(stageUpgrade({ root: proj, to: "v2.0.0", runChant: passing })).rejects.toThrow(/region has no default/);
473
+ });
474
+ });