@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,343 @@
1
+ /**
2
+ * Template migrations (#2550, D9).
3
+ *
4
+ * A template ships its migrations beside its files, under
5
+ * `.chant/migrations/`, one JSON file each. The metadata is data: an id, the
6
+ * template versions it applies `from` (a template id and a version range), the
7
+ * version it brings the scope `to`, a body, and a post-condition. The body is
8
+ * either a closed list of declarative steps or a module, marked
9
+ * `"type": "code"`, which runs only when the caller allows code.
10
+ *
11
+ * `chant workspace upgrade` plans a chain from the scope's version to the
12
+ * target: every migration whose `to` lies in between, ordered by `to`. Each
13
+ * must accept the version the scope has reached when its turn comes. When one
14
+ * does not, a migration is missing from the chain, and the upgrade is refused
15
+ * rather than applied in part.
16
+ *
17
+ * Migrations run inside the upgrade's staging worktree, before the per-file
18
+ * merge. A `move` or `delete` updates the lineage too, so the merge that
19
+ * follows compares the right merge base with the new version.
20
+ */
21
+
22
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
23
+ import { dirname, join, posix } from "node:path";
24
+ import { pathToFileURL } from "node:url";
25
+ import { z } from "zod";
26
+ import { LockError, type Lineage } from "./lineage-lock";
27
+ import { compareVersions, formatVersion, parseVersion, satisfies, validateRange, VersionRangeError, type Version } from "./lineage-version";
28
+
29
+ /** Where a template keeps its migrations, relative to the template's directory. */
30
+ export const MIGRATIONS_DIR = ".chant/migrations";
31
+
32
+ const RelPath = z
33
+ .string()
34
+ .min(1)
35
+ .refine((p) => {
36
+ const n = posix.normalize(p);
37
+ return !n.startsWith("/") && n !== ".." && !n.startsWith("../") && !p.includes("\\");
38
+ }, "must be a relative path inside the scope");
39
+
40
+ /** The declarative steps. Closed: a step chant does not know is refused. */
41
+ const StepSchema = z.discriminatedUnion("op", [
42
+ /** Move a file. The lineage entry moves with it, merge base included. */
43
+ z.strictObject({ op: z.literal("move"), from: RelPath, to: RelPath }),
44
+ /** Delete a file and drop it from the lineage. */
45
+ z.strictObject({ op: z.literal("delete"), path: RelPath }),
46
+ /** Replace every occurrence of a literal string in a file. */
47
+ z.strictObject({ op: z.literal("replace"), path: RelPath, find: z.string().min(1), with: z.string() }),
48
+ /** Set a value in a JSON file at a JSON Pointer, creating objects on the way. */
49
+ z.strictObject({ op: z.literal("json-set"), path: RelPath, pointer: z.string().startsWith("/"), value: z.unknown() }),
50
+ /** Remove a key from a JSON file at a JSON Pointer. */
51
+ z.strictObject({ op: z.literal("json-delete"), path: RelPath, pointer: z.string().startsWith("/") }),
52
+ ]);
53
+ export type MigrationStep = z.infer<typeof StepSchema>;
54
+
55
+ /** What must hold after a migration ran. Every check must pass. */
56
+ const PostSchema = z.discriminatedUnion("check", [
57
+ z.strictObject({ check: z.literal("exists"), path: RelPath }),
58
+ z.strictObject({ check: z.literal("absent"), path: RelPath }),
59
+ z.strictObject({ check: z.literal("contains"), path: RelPath, text: z.string().min(1) }),
60
+ z.strictObject({ check: z.literal("not-contains"), path: RelPath, text: z.string().min(1) }),
61
+ ]);
62
+ export type MigrationCheck = z.infer<typeof PostSchema>;
63
+
64
+ const Range = z.string().min(1).superRefine((r, ctx) => {
65
+ try {
66
+ validateRange(r);
67
+ } catch (err) {
68
+ ctx.addIssue({ code: "custom", message: err instanceof VersionRangeError ? err.message : String(err) });
69
+ }
70
+ });
71
+
72
+ const MigrationSchema = z.strictObject({
73
+ $schema: z.string().optional(),
74
+ /** Unique within the template. Recorded in the lock once applied. */
75
+ id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "letters, digits, '.', '_' and '-'"),
76
+ description: z.string().optional(),
77
+ from: z.strictObject({
78
+ /** The template id the migration applies to. Omitted, it applies to the template it ships in. */
79
+ template: z.string().min(1).optional(),
80
+ /** The versions it can be applied to. */
81
+ versions: Range,
82
+ }),
83
+ /** The version the scope is at once the migration ran. */
84
+ to: z.string().refine((v) => parseVersion(v) !== null, "must be a version, such as 2.0.0"),
85
+ body: z.discriminatedUnion("type", [
86
+ z.strictObject({ type: z.literal("declarative"), steps: z.array(StepSchema).min(1) }),
87
+ /**
88
+ * Code: a module in the migrations directory whose default export is
89
+ * `async ({ dir }) => void`, run with the scope directory as `dir`.
90
+ */
91
+ z.strictObject({ type: z.literal("code"), module: RelPath }),
92
+ ]),
93
+ post: z.array(PostSchema).default([]),
94
+ });
95
+ export type Migration = z.infer<typeof MigrationSchema>;
96
+
97
+ export class MigrationError extends LockError {
98
+ override name = "MigrationError";
99
+ }
100
+
101
+ export interface LoadedMigration {
102
+ migration: Migration;
103
+ /** The file it was read from, relative to the template directory. */
104
+ file: string;
105
+ }
106
+
107
+ /**
108
+ * Read the migrations out of a template's files (relative path to bytes).
109
+ * Returns them and the files that remain once `.chant/migrations/` is taken
110
+ * out: migrations are the template's, never copied into a project.
111
+ */
112
+ export function splitMigrations(files: Map<string, Buffer>): { migrations: LoadedMigration[]; modules: Map<string, Buffer>; files: Map<string, Buffer> } {
113
+ const rest = new Map<string, Buffer>();
114
+ const migrations: LoadedMigration[] = [];
115
+ const modules = new Map<string, Buffer>();
116
+ const prefix = `${MIGRATIONS_DIR}/`;
117
+ for (const [path, data] of files) {
118
+ if (!path.startsWith(prefix)) {
119
+ rest.set(path, data);
120
+ continue;
121
+ }
122
+ const rel = path.slice(prefix.length);
123
+ if (!rel.endsWith(".json")) {
124
+ modules.set(rel, data);
125
+ continue;
126
+ }
127
+ let raw: unknown;
128
+ try {
129
+ raw = JSON.parse(data.toString("utf-8"));
130
+ } catch (err) {
131
+ throw new MigrationError(`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
132
+ }
133
+ const parsed = MigrationSchema.safeParse(raw);
134
+ if (!parsed.success) {
135
+ throw new MigrationError(
136
+ `invalid migration ${path}: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`,
137
+ );
138
+ }
139
+ migrations.push({ migration: parsed.data, file: path });
140
+ }
141
+ const ids = new Set<string>();
142
+ for (const { migration, file } of migrations) {
143
+ if (ids.has(migration.id)) throw new MigrationError(`migration id "${migration.id}" is used twice (${file})`);
144
+ ids.add(migration.id);
145
+ if (migration.body.type === "code" && !modules.has(migration.body.module)) {
146
+ throw new MigrationError(`${file}: its code module ${MIGRATIONS_DIR}/${migration.body.module} is not in the template`);
147
+ }
148
+ }
149
+ return { migrations, modules, files: rest };
150
+ }
151
+
152
+ // ── Planning ─────────────────────────────────────────────────────────────────
153
+
154
+ export interface MigrationPlan {
155
+ /** In the order they run. */
156
+ chain: LoadedMigration[];
157
+ from: Version | null;
158
+ to: Version | null;
159
+ }
160
+
161
+ /**
162
+ * The migrations that take `lineage` from its version to `targetRef`, in order.
163
+ *
164
+ * Refused, with a {@link MigrationError}:
165
+ * - a target below the scope's version;
166
+ * - a pending migration when either end of the upgrade is not a version, so
167
+ * no chain can be computed;
168
+ * - a gap: a migration in range whose `from` does not accept the version the
169
+ * scope has reached by then.
170
+ */
171
+ export function planMigrations(lineage: Lineage, targetRef: string | undefined, available: LoadedMigration[]): MigrationPlan {
172
+ const from = parseVersion(lineage.ref);
173
+ const to = parseVersion(targetRef);
174
+ if (from && to && compareVersions(to, from) < 0) {
175
+ throw new MigrationError(`the target ${targetRef} (${formatVersion(to)}) is older than the scope's ${lineage.ref} (${formatVersion(from)}); an upgrade only goes forward`);
176
+ }
177
+ const applied = new Set(lineage.migrations);
178
+ const mine = available.filter(
179
+ ({ migration: m }) => (m.from.template === undefined || m.from.template === lineage.template) && !applied.has(m.id),
180
+ );
181
+ if (mine.length === 0) return { chain: [], from, to };
182
+
183
+ if (!from || !to) {
184
+ const which = !from ? `the scope's ref ${lineage.ref ?? "(none)"}` : `the target ${targetRef ?? "(none)"}`;
185
+ throw new MigrationError(
186
+ `the template has ${mine.length} migration(s) not yet applied (${mine.map((m) => m.migration.id).join(", ")}), but ${which} is not a version, so no chain can be planned. Pin tagged versions, such as v1.2.0.`,
187
+ );
188
+ }
189
+
190
+ const inRange = mine
191
+ .map((m) => ({ ...m, target: parseVersion(m.migration.to)! }))
192
+ .filter((m) => compareVersions(m.target, from) > 0 && compareVersions(m.target, to) <= 0)
193
+ .sort((a, b) => compareVersions(a.target, b.target) || a.migration.id.localeCompare(b.migration.id));
194
+
195
+ const chain: LoadedMigration[] = [];
196
+ let at = from;
197
+ let i = 0;
198
+ while (i < inRange.length) {
199
+ const step = inRange[i].target;
200
+ const group = inRange.filter((m) => compareVersions(m.target, step) === 0);
201
+ for (const m of group) {
202
+ if (!satisfies(at, m.migration.from.versions)) {
203
+ throw new MigrationError(
204
+ `gap in the migration chain: ${m.migration.id} (${m.file}) upgrades ${m.migration.from.versions} to ${m.migration.to}, but the scope is at ${formatVersion(at)} when it would run. A migration that brings ${formatVersion(at)} into ${m.migration.from.versions} is missing, so the upgrade is refused.`,
205
+ );
206
+ }
207
+ chain.push({ migration: m.migration, file: m.file });
208
+ }
209
+ at = step;
210
+ i += group.length;
211
+ }
212
+ return { chain, from, to };
213
+ }
214
+
215
+ // ── Running ──────────────────────────────────────────────────────────────────
216
+
217
+ export interface RunMigrationOptions {
218
+ /** The scope directory in the staging worktree. */
219
+ dir: string;
220
+ lineage: Lineage;
221
+ /** Code modules from the template, relative to its migrations directory. */
222
+ modules: Map<string, Buffer>;
223
+ /** Run `"type": "code"` bodies. Without it, a chain holding one is refused. */
224
+ allowCode: boolean;
225
+ /** Where a code module is written before it is imported. */
226
+ scratch: string;
227
+ /** Told of each file a declarative step moves, so a caller keyed by path (the merge base) can follow. */
228
+ onMove?: (from: string, to: string) => void;
229
+ }
230
+
231
+ /** Refuse a chain holding code when code is not allowed, before anything runs. */
232
+ export function assertCodeAllowed(chain: LoadedMigration[], allowCode: boolean): void {
233
+ const code = chain.filter((m) => m.migration.body.type === "code");
234
+ if (code.length > 0 && !allowCode) {
235
+ throw new MigrationError(
236
+ `the chain includes ${code.length} code migration(s) (${code.map((m) => m.migration.id).join(", ")}), which run the template's own code. Read them, then pass --allow-code to run them.`,
237
+ );
238
+ }
239
+ }
240
+
241
+ /** Run one migration in the worktree, then its post-condition. Updates `lineage`. */
242
+ export async function runMigration(m: LoadedMigration, opts: RunMigrationOptions): Promise<void> {
243
+ const { migration } = m;
244
+ const body = migration.body;
245
+ if (body.type === "code") {
246
+ if (!opts.allowCode) throw new MigrationError(`${migration.id} is a code migration; pass --allow-code to run it`);
247
+ const file = join(opts.scratch, "migrations", body.module);
248
+ mkdirSync(dirname(file), { recursive: true });
249
+ for (const [rel, data] of opts.modules) {
250
+ const abs = join(opts.scratch, "migrations", rel);
251
+ mkdirSync(dirname(abs), { recursive: true });
252
+ writeFileSync(abs, data);
253
+ }
254
+ const mod = (await import(pathToFileURL(file).href)) as { default?: unknown };
255
+ if (typeof mod.default !== "function") throw new MigrationError(`${migration.id}: ${body.module} has no default export function`);
256
+ await (mod.default as (ctx: { dir: string }) => unknown)({ dir: opts.dir });
257
+ } else {
258
+ for (const step of body.steps) {
259
+ applyStep(step, opts.dir, opts.lineage, migration.id);
260
+ if (step.op === "move") opts.onMove?.(step.from, step.to);
261
+ }
262
+ }
263
+ const failed = migration.post.filter((c) => !holds(c, opts.dir));
264
+ if (failed.length > 0) {
265
+ throw new MigrationError(
266
+ `migration ${migration.id} ran, but its post-condition does not hold: ${failed.map(describeCheck).join("; ")}. The upgrade is refused.`,
267
+ );
268
+ }
269
+ opts.lineage.migrations.push(migration.id);
270
+ }
271
+
272
+ function applyStep(step: MigrationStep, dir: string, lineage: Lineage, id: string): void {
273
+ const abs = (p: string) => join(dir, p);
274
+ const need = (p: string) => {
275
+ if (!existsSync(abs(p))) throw new MigrationError(`migration ${id}: ${step.op} needs ${p}, which is not in the scope`);
276
+ };
277
+ switch (step.op) {
278
+ case "move": {
279
+ need(step.from);
280
+ if (existsSync(abs(step.to))) throw new MigrationError(`migration ${id}: cannot move ${step.from} to ${step.to}, which exists`);
281
+ mkdirSync(dirname(abs(step.to)), { recursive: true });
282
+ renameSync(abs(step.from), abs(step.to));
283
+ const entry = lineage.files[step.from];
284
+ if (entry) {
285
+ delete lineage.files[step.from];
286
+ lineage.files[step.to] = entry;
287
+ }
288
+ return;
289
+ }
290
+ case "delete":
291
+ if (existsSync(abs(step.path))) rmSync(abs(step.path));
292
+ delete lineage.files[step.path];
293
+ return;
294
+ case "replace": {
295
+ need(step.path);
296
+ const text = readFileSync(abs(step.path), "utf-8");
297
+ writeFileSync(abs(step.path), text.split(step.find).join(step.with));
298
+ return;
299
+ }
300
+ case "json-set":
301
+ case "json-delete": {
302
+ need(step.path);
303
+ let doc: unknown;
304
+ try {
305
+ doc = JSON.parse(readFileSync(abs(step.path), "utf-8"));
306
+ } catch {
307
+ throw new MigrationError(`migration ${id}: ${step.path} is not valid JSON`);
308
+ }
309
+ const keys = step.pointer.slice(1).split("/").map((k) => k.replace(/~1/g, "/").replace(/~0/g, "~"));
310
+ const last = keys.pop()!;
311
+ let node = doc as Record<string, unknown>;
312
+ for (const k of keys) {
313
+ if (typeof node[k] !== "object" || node[k] === null) {
314
+ if (step.op === "json-delete") return;
315
+ node[k] = {};
316
+ }
317
+ node = node[k] as Record<string, unknown>;
318
+ }
319
+ if (step.op === "json-set") node[last] = step.value;
320
+ else delete node[last];
321
+ writeFileSync(abs(step.path), JSON.stringify(doc, null, 2) + "\n");
322
+ return;
323
+ }
324
+ }
325
+ }
326
+
327
+ function holds(c: MigrationCheck, dir: string): boolean {
328
+ const abs = join(dir, c.path);
329
+ switch (c.check) {
330
+ case "exists":
331
+ return existsSync(abs);
332
+ case "absent":
333
+ return !existsSync(abs);
334
+ case "contains":
335
+ return existsSync(abs) && readFileSync(abs, "utf-8").includes(c.text);
336
+ case "not-contains":
337
+ return !existsSync(abs) || !readFileSync(abs, "utf-8").includes(c.text);
338
+ }
339
+ }
340
+
341
+ function describeCheck(c: MigrationCheck): string {
342
+ return "text" in c ? `${c.check} ${JSON.stringify(c.text)} in ${c.path}` : `${c.path} ${c.check}`;
343
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Bring one lineage scope to a new version of its source, file by file,
3
+ * without deleting the project's edits (#2540, ws-038).
4
+ *
5
+ * For each path, three hashes decide what happens: the merge base recorded in
6
+ * the lock, the source's new version, and the file in the tree.
7
+ *
8
+ * | tree | source | result |
9
+ * |-----------------|--------------------------|-----------------------------------------|
10
+ * | equals base | anything | replaced (or removed) by the source |
11
+ * | equals source | anything | kept; base moves to the source |
12
+ * | edited | unchanged from base | kept; base stays |
13
+ * | edited | changed, merges cleanly | merged; base moves to the source (*) |
14
+ * | edited | changed or removed | kept; a manual step |
15
+ * | deleted | unchanged from base | stays deleted |
16
+ * | deleted | changed | stays deleted; a manual step |
17
+ * | untracked file | adds the same path | kept; a manual step |
18
+ *
19
+ * The table is for `owned` files. A `seed` file is never touched again, and a
20
+ * `generated` file is left for its command to rebuild (D9, D14). Files in the
21
+ * scope directory that the lineage does not list are never touched. This is
22
+ * the per-file rule of ws-005.
23
+ *
24
+ * (*) Only when the caller passes {@link MergeOptions}: the merge base's
25
+ * content, rebuilt by `chant workspace upgrade` (#2550), and a merge function.
26
+ * A file merges only when every hunk is clean; otherwise it is kept as it was
27
+ * and becomes one manual step. `chant vendor pull` passes no base, since a
28
+ * vendor source keeps no history to rebuild it from, so every conflicting
29
+ * path there is a manual step.
30
+ */
31
+
32
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
33
+ import { dirname, join } from "node:path";
34
+ import { contentDigest, declaredFilesAt, defaultFileClass, fileHash, type Lineage, type ManualStep } from "./lineage-lock";
35
+
36
+ export interface UpdateResult {
37
+ written: string[];
38
+ /** Edited files that took the source's changes through a clean three-way merge. */
39
+ merged: string[];
40
+ /** `seed` files, which an update never touches, and `generated` files, which their command rebuilds. */
41
+ skipped: Array<{ path: string; class: "seed" | "generated"; command?: string }>;
42
+ removed: string[];
43
+ kept: string[];
44
+ manualSteps: ManualStep[];
45
+ }
46
+
47
+ /** The three-way merge, for `chant workspace upgrade`. */
48
+ export interface MergeOptions {
49
+ /** The merge base's content, by path. A path missing here cannot be merged. */
50
+ base: Map<string, Buffer>;
51
+ /** Merged bytes, or null when a hunk conflicts. */
52
+ merge: (base: Buffer, ours: Buffer, theirs: Buffer) => Buffer | null;
53
+ }
54
+
55
+ /**
56
+ * Apply `upstream` (relative path to bytes) to the scope directory `dir`,
57
+ * updating `lineage` in place: its files, digest and manual steps. Open manual
58
+ * steps for paths this update touches again are replaced, others are kept.
59
+ */
60
+ export function applyUpstream(dir: string, lineage: Lineage, upstream: Map<string, Buffer>, merging?: MergeOptions): UpdateResult {
61
+ const result: UpdateResult = { written: [], merged: [], skipped: [], removed: [], kept: [], manualSteps: [] };
62
+ const steps = new Map(lineage.manualSteps.map((s) => [s.path, s]));
63
+ const paths = new Set([...Object.keys(lineage.files), ...upstream.keys()]);
64
+ // The declaration's generated list is read again on every update, so a
65
+ // file the member lists after the lock was written is rebuilt, not merged,
66
+ // and a file it marks hand-written is merged like any other (#2541).
67
+ const declared = declaredFilesAt(dir);
68
+
69
+ for (const path of [...paths].sort()) {
70
+ const abs = join(dir, path);
71
+ const recorded = lineage.files[path];
72
+ if (recorded && declared.has(path)) lineage.files[path] = { ...defaultFileClass(path, declared), sha256: recorded.sha256 };
73
+ const entry = lineage.files[path];
74
+ if (entry && entry.class !== "owned") {
75
+ // D9: a seed is written once, and a generated file is rebuilt by its
76
+ // command rather than merged. Neither is written here.
77
+ result.skipped.push({ path, class: entry.class, ...(entry.command ? { command: entry.command } : {}) });
78
+ continue;
79
+ }
80
+ const base = lineage.files[path]?.sha256;
81
+ const incoming = upstream.get(path);
82
+ const theirs = incoming ? fileHash(incoming) : undefined;
83
+ const local = existsSync(abs) ? fileHash(readFileSync(abs)) : undefined;
84
+ const step = (reason: ManualStep["reason"]): void => {
85
+ const s: ManualStep = { path, reason, upstream: theirs ?? null };
86
+ steps.set(path, s);
87
+ result.manualSteps.push(s);
88
+ };
89
+ const record = (sha: string): void => {
90
+ lineage.files[path] = { ...(lineage.files[path] ?? defaultFileClass(path, declared)), sha256: sha };
91
+ steps.delete(path);
92
+ };
93
+
94
+ if (theirs === undefined) {
95
+ // The source no longer has the file.
96
+ if (local === undefined || local === base) {
97
+ if (local !== undefined) {
98
+ rmSync(abs);
99
+ result.removed.push(path);
100
+ }
101
+ delete lineage.files[path];
102
+ steps.delete(path);
103
+ } else {
104
+ step("removed-upstream");
105
+ result.kept.push(path);
106
+ }
107
+ continue;
108
+ }
109
+
110
+ if (local === theirs) {
111
+ record(theirs);
112
+ continue;
113
+ }
114
+ if (local === undefined) {
115
+ if (base === undefined) {
116
+ write(abs, incoming!);
117
+ result.written.push(path);
118
+ record(theirs);
119
+ } else if (base === theirs) {
120
+ steps.delete(path);
121
+ } else {
122
+ step("deleted-locally");
123
+ }
124
+ continue;
125
+ }
126
+ if (base !== undefined && local === base) {
127
+ write(abs, incoming!);
128
+ result.written.push(path);
129
+ record(theirs);
130
+ continue;
131
+ }
132
+ // The tree has its own version of the file.
133
+ if (base !== undefined && base === theirs) {
134
+ result.kept.push(path);
135
+ steps.delete(path);
136
+ continue;
137
+ }
138
+ const baseData = merging?.base.get(path);
139
+ if (merging && baseData && fileHash(baseData) === base) {
140
+ const merged = merging.merge(baseData, readFileSync(abs), incoming!);
141
+ if (merged) {
142
+ writeFileSync(abs, merged);
143
+ result.merged.push(path);
144
+ record(theirs);
145
+ continue;
146
+ }
147
+ }
148
+ result.kept.push(path);
149
+ step(base === undefined ? "exists-locally" : "changed-locally");
150
+ }
151
+
152
+ lineage.manualSteps = [...steps.values()].sort((a, b) => a.path.localeCompare(b.path));
153
+ lineage.address = { ...(lineage.address ?? {}), digest: contentDigest(upstream) };
154
+ return result;
155
+ }
156
+
157
+ function write(abs: string, data: Buffer): void {
158
+ mkdirSync(dirname(abs), { recursive: true });
159
+ writeFileSync(abs, data);
160
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * `chant workspace upgrade [<scope>] [--to <ref>] [--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>] [--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
+ }