@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,624 @@
1
+ /**
2
+ * `chant workspace upgrade <scope>`: bring one lineage scope to a newer
3
+ * version of its template (#2550, D9, ws-005, ws-032).
4
+ *
5
+ * The upgrade is a core command, never an Op the template ships, so the
6
+ * template cannot change the rules that judge its own upgrade. It works in the
7
+ * six steps of D9:
8
+ *
9
+ * 1. Fetch the target version of the template. For a git source that is one
10
+ * `git fetch` of the target ref, plus the commit the scope was made from.
11
+ * This is the command's only network step, catalogued in
12
+ * `test/egress-catalogue.ts`. A local repository reaches nothing.
13
+ * 2. Rebuild the merge base offline: each recorded file's content at the base
14
+ * commit, kept only when it has the hash the lock recorded. A file the
15
+ * project never edited is its own merge base. Vendor sources keep no
16
+ * history, so only unedited vendored files have a base.
17
+ * 3. Migrate in a separate git worktree, checked out at HEAD under
18
+ * `.chant/upgrade/`. The chain is planned from the template's
19
+ * `.chant/migrations/`, and a gap refuses the upgrade.
20
+ * 4. Merge per file (ws-005): an edited file takes the new version only when
21
+ * every hunk merges cleanly. Otherwise it stays as it was, with one manual
22
+ * step. The worktree never holds conflict markers.
23
+ * 5. Check the worktree: `chant build` and `chant lint` when the project is a
24
+ * chant project, and the lineage checks of `chant workspace check`.
25
+ * 6. Bind the gate to the digest of the resulting patch: the pending fact and
26
+ * the approval name it, so an approval covers exactly this patch.
27
+ *
28
+ * The project's own tree changes only once the gate is approved, when the
29
+ * patch is applied to it. A patch that changes governance files (Op files,
30
+ * `chant.config`, CI workflows, CODEOWNERS) needs human approvals, as many as
31
+ * the strictest gate in the Op files it changes asks for at HEAD. The
32
+ * upgraded Op files never decide their own gate.
33
+ */
34
+
35
+ import { execFileSync, spawn } from "node:child_process";
36
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
37
+ import { tmpdir } from "node:os";
38
+ import { basename, join, relative, resolve, sep } from "node:path";
39
+ import { computePlanDigest } from "../lifecycle/plan-digest";
40
+ import type { ResolvedGateApproval } from "../op/gate-approval";
41
+ import { checkLineage, findingKey, type CheckFinding } from "./lineage-check";
42
+ import { git, readTemplateTree } from "./lineage-init";
43
+ import { LOCK_FILE, LockError, fileHash, readLock, renderLock, scopeKey, writeLock, type Lineage, type LineageLock, type ManualStep } from "./lineage-lock";
44
+ import { mergeFile } from "./lineage-merge";
45
+ import { assertCodeAllowed, planMigrations, runMigration, splitMigrations, type LoadedMigration } from "./lineage-migrations";
46
+ import { applyUpstream, type UpdateResult } from "./lineage-update";
47
+ import { carryParameters, readManifest, substituteParameters } from "./template-manifest";
48
+
49
+ /** The kind the patch digest is taken under, so it never collides with another kind of plan. */
50
+ export const UPGRADE_PLAN_KIND = "workspace-upgrade";
51
+
52
+ /** Where staging worktrees live, relative to the project root. */
53
+ export const UPGRADE_DIR = ".chant/upgrade";
54
+
55
+ export class UpgradeError extends LockError {
56
+ override name = "UpgradeError";
57
+ }
58
+
59
+ // ── Checks ───────────────────────────────────────────────────────────────────
60
+
61
+ export interface UpgradeCheck {
62
+ name: "build" | "lint" | "workspace check";
63
+ status: "passed" | "failed" | "skipped";
64
+ /** Why a check was skipped, or the tail of a failed command's output. */
65
+ detail?: string;
66
+ }
67
+
68
+ /** Runs `chant build` or `chant lint` in a directory. */
69
+ export type ChantRunner = (command: "build" | "lint", cwd: string) => Promise<{ exitCode: number; output: string }>;
70
+
71
+ /**
72
+ * The default runner: this same chant, in a child process. The child sees the
73
+ * worktree's sources and resolves packages the way the project does, since the
74
+ * worktree sits inside the project.
75
+ */
76
+ export const spawnChant: ChantRunner = (command, cwd) =>
77
+ new Promise((resolvePromise, reject) => {
78
+ const entry = process.argv[1];
79
+ if (!entry) {
80
+ reject(new UpgradeError("cannot find the chant entry point to run build and lint in the worktree"));
81
+ return;
82
+ }
83
+ const child = spawn(process.execPath, [...process.execArgv, entry, command], { cwd, stdio: ["ignore", "pipe", "pipe"], env: process.env });
84
+ const out: Buffer[] = [];
85
+ child.stdout.on("data", (c: Buffer) => out.push(c));
86
+ child.stderr.on("data", (c: Buffer) => out.push(c));
87
+ child.on("error", reject);
88
+ child.on("close", (code) => resolvePromise({ exitCode: code ?? 1, output: Buffer.concat(out).toString("utf-8") }));
89
+ });
90
+
91
+ // ── Options and result ───────────────────────────────────────────────────────
92
+
93
+ export interface UpgradeOptions {
94
+ /** The directory holding `.chant/workspace.lock.json`. */
95
+ root: string;
96
+ /** The scope to upgrade: `"."` or a vendor scope's directory. */
97
+ scope?: string;
98
+ /** The target ref (git) or label (vendor). Defaults to the ref the scope is pinned at. */
99
+ to?: string;
100
+ /** Run migrations whose body is code. */
101
+ allowCode?: boolean;
102
+ /** Replaces the child-process build and lint. For tests. */
103
+ runChant?: ChantRunner;
104
+ }
105
+
106
+ export interface GovernanceChange {
107
+ /** Changed governance paths, relative to the repository root. */
108
+ paths: string[];
109
+ /** The approval the gate asks for, from the rules at HEAD. */
110
+ approval: ResolvedGateApproval;
111
+ /** Where the rules came from. */
112
+ rules: string;
113
+ }
114
+
115
+ export interface StagedUpgrade {
116
+ scope: string;
117
+ template: string;
118
+ kind: Lineage["kind"];
119
+ /** The pin before and after. */
120
+ from: string | null;
121
+ to: string | null;
122
+ commit?: { from?: string; to: string };
123
+ migrations: string[];
124
+ written: string[];
125
+ merged: string[];
126
+ removed: string[];
127
+ kept: string[];
128
+ skipped: UpdateResult["skipped"];
129
+ manualSteps: ManualStep[];
130
+ /** Paths the patch changes, relative to the repository root. */
131
+ changedPaths: string[];
132
+ patch: string;
133
+ /**
134
+ * The new lock. When git tracks it, its change is in `patch`. A template
135
+ * whose `.gitignore` covers `.chant/` leaves it untracked; the text is then
136
+ * applied beside the patch and covered by the digest on its own.
137
+ */
138
+ lock: { path: string; tracked: boolean; text: string };
139
+ /** Whether the upgrade changes anything: the patch, or an untracked lock. */
140
+ changed: boolean;
141
+ /** `computePlanDigest(UPGRADE_PLAN_KIND, { scope, patch })`, plus `lock` when untracked: what the gate binds. */
142
+ digest: string;
143
+ governance: GovernanceChange | null;
144
+ checks: UpgradeCheck[];
145
+ checksOk: boolean;
146
+ /** The repository root and the commit the worktree was made from. */
147
+ repo: string;
148
+ head: string;
149
+ /** The staging worktree, and the project root inside it. */
150
+ worktree: string;
151
+ worktreeProject: string;
152
+ /** Remove the worktree. Safe to call twice. */
153
+ dispose(): void;
154
+ }
155
+
156
+ // ── Fetch and base ───────────────────────────────────────────────────────────
157
+
158
+ interface Upstream {
159
+ files: Map<string, Buffer>;
160
+ executable: Set<string>;
161
+ base: Map<string, Buffer>;
162
+ migrations: LoadedMigration[];
163
+ modules: Map<string, Buffer>;
164
+ commit?: string;
165
+ tree?: string;
166
+ /** The parameter values substituted into `files` (#2627), for a git source. */
167
+ parameters?: Record<string, string>;
168
+ }
169
+
170
+ /**
171
+ * The template's files as the project would have them (#2627): the manifest
172
+ * removed and the parameters substituted, with the recorded values and the
173
+ * defaults of parameters this version adds.
174
+ */
175
+ function instantiate(raw: Map<string, Buffer>, recorded: Record<string, unknown>, label: string): { files: Map<string, Buffer>; parameters: Record<string, string> } {
176
+ try {
177
+ const manifest = readManifest(raw);
178
+ const parameters = carryParameters(manifest, recorded);
179
+ return { files: substituteParameters(raw, manifest, parameters), parameters };
180
+ } catch (err) {
181
+ throw new UpgradeError(`${label}: ${(err as Error).message.replace(/; pass --param .*$/, "")}`);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Fetch the target ref, and the commit the scope was made from, of a git
187
+ * source into a scratch repository, and read both trees. The base fetch may
188
+ * fail (a server that refuses fetches by commit); the base is then left empty,
189
+ * and every edited file the template changed becomes a manual step.
190
+ */
191
+ function fetchGit(root: string, lineage: Lineage, ref: string): Upstream {
192
+ const source = lineage.source;
193
+ if (source.type !== "git") throw new UpgradeError("not a git source");
194
+ const url = source.url.startsWith(".") ? resolve(root, source.url) : source.url;
195
+ const label = `${source.repo}@${ref}`;
196
+ const scratch = mkdtempSync(join(tmpdir(), "chant-upgrade-"));
197
+ try {
198
+ git(scratch, ["init", "-q"]);
199
+ try {
200
+ // The network step of `chant workspace upgrade`, catalogued in test/egress-catalogue.ts.
201
+ git(scratch, ["fetch", "-q", "--depth", "1", url, ref]);
202
+ } catch (err) {
203
+ const stderr = (err as { stderr?: string }).stderr?.toString().trim();
204
+ throw new UpgradeError(`could not fetch ${label}${stderr ? `: ${stderr}` : ""}`);
205
+ }
206
+ const commit = git(scratch, ["rev-parse", "FETCH_HEAD^{commit}"]);
207
+ const target = readTemplateTree(scratch, commit, source.path, label);
208
+
209
+ const raw = new Map<string, Buffer>();
210
+ const executable = new Set<string>();
211
+ for (const [path, f] of target.files) {
212
+ raw.set(path, f.data);
213
+ if (f.executable) executable.add(path);
214
+ }
215
+ // The target with the scope's parameters substituted (#2627), so an
216
+ // unedited file that carries a value compares equal to its base.
217
+ const instantiated = instantiate(raw, lineage.parameters, label);
218
+
219
+ let base = new Map<string, Buffer>();
220
+ const baseCommit = lineage.address?.commit;
221
+ if (baseCommit && baseCommit !== commit) {
222
+ try {
223
+ git(scratch, ["fetch", "-q", "--depth", "1", url, baseCommit]);
224
+ const oldLabel = `${source.repo}@${baseCommit.slice(0, 12)}`;
225
+ const old = readTemplateTree(scratch, baseCommit, source.path, oldLabel);
226
+ // The base is rebuilt the way init wrote it: the old version's
227
+ // manifest, with the values the lock recorded.
228
+ base = instantiate(new Map([...old.files].map(([path, f]) => [path, f.data])), lineage.parameters, oldLabel).files;
229
+ } catch {
230
+ // No base from the source; the tree still supplies it for unedited files.
231
+ }
232
+ } else if (baseCommit) {
233
+ base = new Map(instantiated.files);
234
+ }
235
+
236
+ const split = splitMigrations(instantiated.files);
237
+ return {
238
+ files: split.files,
239
+ executable,
240
+ base,
241
+ migrations: split.migrations,
242
+ modules: split.modules,
243
+ commit,
244
+ tree: target.tree,
245
+ parameters: instantiated.parameters,
246
+ };
247
+ } finally {
248
+ rmSync(scratch, { recursive: true, force: true });
249
+ }
250
+ }
251
+
252
+ async function fetchUpstream(root: string, lineage: Lineage, ref: string | undefined): Promise<Upstream> {
253
+ const source = lineage.source;
254
+ if (source.type === "git") {
255
+ if (!ref) throw new UpgradeError("the scope records no ref; pass --to <ref>");
256
+ return fetchGit(root, lineage, ref);
257
+ }
258
+ if (source.type === "local" || source.type === "archive") {
259
+ const { resolveVendorSource } = await import("../cli/commands/vendor");
260
+ const files = await resolveVendorSource(source, root);
261
+ return { files, executable: new Set(), base: new Map(), migrations: [], modules: new Map() };
262
+ }
263
+ throw new UpgradeError(
264
+ `scope made by \`chant init --template\` (${lineage.template}) cannot be upgraded yet: its merge base is the older lexicon's render, which is not available offline. Upgrade git-sourced and vendor scopes for now.`,
265
+ );
266
+ }
267
+
268
+ /** The merge base per recorded file: from the source's old version, or from an unedited file in the tree. */
269
+ function rebuildBase(scopeDir: string, lineage: Lineage, fromSource: Map<string, Buffer>): Map<string, Buffer> {
270
+ const base = new Map<string, Buffer>();
271
+ for (const [path, entry] of Object.entries(lineage.files)) {
272
+ const old = fromSource.get(path);
273
+ if (old && fileHash(old) === entry.sha256) {
274
+ base.set(path, old);
275
+ continue;
276
+ }
277
+ const abs = join(scopeDir, path);
278
+ if (existsSync(abs)) {
279
+ const data = readFileSync(abs);
280
+ if (fileHash(data) === entry.sha256) base.set(path, data);
281
+ }
282
+ }
283
+ return base;
284
+ }
285
+
286
+ // ── Governance ───────────────────────────────────────────────────────────────
287
+
288
+ const GOVERNANCE = [
289
+ /(^|\/)[^/]+\.op\.[cm]?[jt]s$/,
290
+ /(^|\/)chant\.config\.(ts|js|mjs|cjs|json)$/,
291
+ /(^|\/)chant\.workspace\.jsonc?$/,
292
+ /(^|\/)\.github\/workflows\//,
293
+ /(^|\/)\.forgejo\/workflows\//,
294
+ /(^|\/)\.gitea\/workflows\//,
295
+ /(^|\/)\.gitlab-ci\.ya?ml$/,
296
+ /(^|\/)CODEOWNERS$/,
297
+ ];
298
+
299
+ /** Whether a repository path is a governance file: one that decides gates, pipelines or review. */
300
+ export function isGovernancePath(path: string): boolean {
301
+ return GOVERNANCE.some((re) => re.test(path));
302
+ }
303
+
304
+ /**
305
+ * The approval an upgrade gate asks for when the patch changes governance
306
+ * files. Only human approvals count, and the gate needs as many as the
307
+ * strictest gate declared at HEAD in any Op file the patch changes, at least
308
+ * one. The Op files are read from the project's tree, which the upgrade
309
+ * requires to match HEAD for every path the patch touches.
310
+ */
311
+ async function rulesAtHead(root: string, repo: string, paths: string[]): Promise<ResolvedGateApproval> {
312
+ const opFiles = new Set(paths.filter((p) => GOVERNANCE[0].test(p)));
313
+ let count = 1;
314
+ let roles: string[] | undefined;
315
+ if (opFiles.size > 0) {
316
+ const { discoverOps } = await import("../op/discover");
317
+ const { ops } = await discoverOps({ cwd: root });
318
+ for (const { config, filePath } of ops.values()) {
319
+ const rel = relative(repo, filePath).split(sep).join("/");
320
+ if (!opFiles.has(rel)) continue;
321
+ for (const approval of gateApprovals(config)) {
322
+ const need = approval.quorum?.count ?? 1;
323
+ if (need > count) {
324
+ count = need;
325
+ roles = approval.quorum?.roles;
326
+ }
327
+ }
328
+ }
329
+ }
330
+ return { mode: "log-only", quorum: { count, ...(roles && roles.length > 0 ? { roles } : {}) } };
331
+ }
332
+
333
+ type StepLike = { kind: string; approval?: { quorum?: { count: number; roles?: string[] } }; steps?: StepLike[] };
334
+
335
+ function gateApprovals(config: { phases?: Array<{ steps: StepLike[] }>; onFailure?: Array<{ steps: StepLike[] }> }): Array<{ quorum?: { count: number; roles?: string[] } }> {
336
+ const out: Array<{ quorum?: { count: number; roles?: string[] } }> = [];
337
+ const visit = (step: StepLike): void => {
338
+ if (step.kind === "gate") out.push(step.approval ?? {});
339
+ if (step.kind === "effect") (step.steps ?? []).forEach(visit);
340
+ };
341
+ for (const p of [...(config.phases ?? []), ...(config.onFailure ?? [])]) p.steps.forEach(visit);
342
+ return out;
343
+ }
344
+
345
+ // ── Stage ────────────────────────────────────────────────────────────────────
346
+
347
+ function gitOut(cwd: string, args: string[]): string {
348
+ return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
349
+ }
350
+
351
+ function isChantProject(dir: string): boolean {
352
+ return ["chant.config.ts", "chant.config.json", "chant.config.js", "chant.config.mjs"].some((f) => existsSync(join(dir, f)));
353
+ }
354
+
355
+ function tail(text: string, lines = 30): string {
356
+ return text.trimEnd().split("\n").slice(-lines).join("\n");
357
+ }
358
+
359
+ /**
360
+ * Stage an upgrade: steps 1 to 5 and the digest of step 6. Nothing in the
361
+ * project's tree changes. The caller decides the gate, then applies the patch
362
+ * with {@link applyStagedUpgrade} or commits it with {@link commitStagedUpgrade},
363
+ * and always calls `dispose()`.
364
+ */
365
+ export async function stageUpgrade(options: UpgradeOptions): Promise<StagedUpgrade> {
366
+ const root = resolve(options.root);
367
+ const scope = scopeKey(options.scope ?? ".");
368
+ const lock = readLock(root);
369
+ if (!lock) throw new UpgradeError(`no ${LOCK_FILE} in ${root}`);
370
+ const lineage = lock.scopes[scope];
371
+ if (!lineage) {
372
+ throw new UpgradeError(`${LOCK_FILE} has no scope "${scope}" (scopes: ${Object.keys(lock.scopes).join(", ") || "none"})`);
373
+ }
374
+ if (lineage.manualSteps.length > 0) {
375
+ throw new UpgradeError(
376
+ `scope "${scope}" has ${lineage.manualSteps.length} open manual step(s) from its last update (${lineage.manualSteps.map((s) => s.path).join(", ")}). Merge them and run \`chant workspace lineage resolve <path>\` first.`,
377
+ );
378
+ }
379
+
380
+ let repo: string;
381
+ let head: string;
382
+ try {
383
+ repo = gitOut(root, ["rev-parse", "--show-toplevel"]).trim();
384
+ head = gitOut(root, ["rev-parse", "HEAD"]).trim();
385
+ } catch {
386
+ throw new UpgradeError("chant workspace upgrade stages its change in a git worktree, so the project must be in a git repository with at least one commit");
387
+ }
388
+ const projectRel = relative(repo, root).split(sep).join("/");
389
+ const scopeRepoPath = [projectRel, scope === "." ? "" : scope].filter(Boolean).join("/") || ".";
390
+ const lockRepoPath = [projectRel, LOCK_FILE].filter(Boolean).join("/");
391
+ let lockTracked = true;
392
+ try {
393
+ gitOut(repo, ["ls-files", "--error-unmatch", "--", lockRepoPath]);
394
+ } catch {
395
+ lockTracked = false;
396
+ }
397
+ const dirty = gitOut(repo, ["status", "--porcelain", "--untracked-files=all", "--", scopeRepoPath, lockRepoPath])
398
+ .split("\n")
399
+ .filter(Boolean)
400
+ .filter((line) => !line.slice(3).startsWith(`${[projectRel, UPGRADE_DIR].filter(Boolean).join("/")}/`));
401
+ if (dirty.length > 0) {
402
+ throw new UpgradeError(
403
+ `the scope has uncommitted changes (${dirty.slice(0, 5).map((l) => l.slice(3)).join(", ")}${dirty.length > 5 ? ", …" : ""}). Commit or stash them: the upgrade stages from HEAD and applies its patch to the tree.`,
404
+ );
405
+ }
406
+
407
+ // 1. Fetch.
408
+ const ref = options.to ?? lineage.ref;
409
+ const upstream = await fetchUpstream(root, lineage, ref);
410
+
411
+ // 2. The merge base, offline from here on.
412
+ const base = rebuildBase(join(root, scope), lineage, upstream.base);
413
+
414
+ // Plan the chain before anything is staged, so a gap refuses early.
415
+ const plan = lineage.kind === "template" ? planMigrations(lineage, ref, upstream.migrations) : { chain: [] };
416
+ assertCodeAllowed(plan.chain, !!options.allowCode);
417
+
418
+ // 3. The worktree, inside the project so the project's packages resolve.
419
+ const holder = join(root, UPGRADE_DIR);
420
+ mkdirSync(holder, { recursive: true });
421
+ const worktree = mkdtempSync(join(holder, `${scope === "." ? "root" : basename(scope)}-`));
422
+ let disposed = false;
423
+ const dispose = (): void => {
424
+ if (disposed) return;
425
+ disposed = true;
426
+ try {
427
+ gitOut(repo, ["worktree", "remove", "--force", worktree]);
428
+ } catch {
429
+ // Fall through to the directory removal and a prune.
430
+ }
431
+ rmSync(worktree, { recursive: true, force: true });
432
+ try {
433
+ gitOut(repo, ["worktree", "prune"]);
434
+ } catch {
435
+ // Nothing to prune.
436
+ }
437
+ if (existsSync(holder) && readdirSync(holder).length === 0) rmSync(holder, { recursive: true, force: true });
438
+ };
439
+
440
+ try {
441
+ gitOut(repo, ["worktree", "add", "-q", "--detach", worktree, head]);
442
+ const worktreeProject = projectRel ? join(worktree, projectRel) : worktree;
443
+ const scopeDir = join(worktreeProject, scope);
444
+ const next = structuredClone(lock) as LineageLock;
445
+ const staged = next.scopes[scope];
446
+ const scratch = mkdtempSync(join(tmpdir(), "chant-upgrade-code-"));
447
+ try {
448
+ for (const m of plan.chain) {
449
+ await runMigration(m, {
450
+ dir: scopeDir,
451
+ lineage: staged,
452
+ modules: upstream.modules,
453
+ allowCode: !!options.allowCode,
454
+ scratch,
455
+ onMove: (from, to) => {
456
+ const data = base.get(from);
457
+ base.delete(from);
458
+ if (data) base.set(to, data);
459
+ },
460
+ });
461
+ }
462
+ } finally {
463
+ rmSync(scratch, { recursive: true, force: true });
464
+ }
465
+
466
+ // 4. Merge per file.
467
+ const result = applyUpstream(scopeDir, staged, upstream.files, { base, merge: mergeFile });
468
+ for (const path of result.written) if (upstream.executable.has(path)) chmodSync(join(scopeDir, path), 0o755);
469
+ if (ref !== undefined) staged.ref = ref;
470
+ if (upstream.parameters) staged.parameters = upstream.parameters;
471
+ if (upstream.commit) staged.address = { ...staged.address!, commit: upstream.commit, tree: upstream.tree };
472
+ writeLock(worktreeProject, next);
473
+ const lockText = renderLock(next);
474
+
475
+ // The patch, taken before anything else runs in the worktree.
476
+ gitOut(worktree, ["add", "-A"]);
477
+ if (!lockTracked) gitOut(worktree, ["rm", "--cached", "-q", "--ignore-unmatch", "--", lockRepoPath]);
478
+ const patch = gitOut(worktree, ["diff", "--cached", "--binary", "--full-index", "--no-color", "--no-ext-diff", head]);
479
+ const changedPaths = gitOut(worktree, ["diff", "--cached", "--name-only", "-z", head]).split("\0").filter(Boolean).sort();
480
+ const lockChanged = !lockTracked && lockText !== readFileSync(join(root, LOCK_FILE), "utf-8");
481
+ const changed = patch.length > 0 || lockChanged;
482
+ const digest = computePlanDigest(UPGRADE_PLAN_KIND, lockTracked ? { scope, patch } : { scope, patch, lock: lockText });
483
+
484
+ // Governance: decided by the rules at HEAD, never by the upgraded files.
485
+ const governed = changedPaths.filter(isGovernancePath);
486
+ let governance: GovernanceChange | null = null;
487
+ if (governed.length > 0) {
488
+ const existing = governed.filter((p) => existsSync(join(repo, p)));
489
+ const touched = existing.length === 0 ? [] : gitOut(repo, ["status", "--porcelain", "--", ...existing]).split("\n").filter(Boolean);
490
+ if (touched.length > 0) {
491
+ throw new UpgradeError(`the upgrade changes governance files that have uncommitted edits (${touched.map((l) => l.slice(3)).join(", ")}). Commit them first: their gates are read at HEAD.`);
492
+ }
493
+ governance = { paths: governed, approval: await rulesAtHead(root, repo, governed), rules: head };
494
+ }
495
+
496
+ // 5. Checks.
497
+ const checks: UpgradeCheck[] = [];
498
+ if (changed) {
499
+ if (isChantProject(worktreeProject)) {
500
+ const run = options.runChant ?? spawnChant;
501
+ for (const command of ["build", "lint"] as const) {
502
+ const r = await run(command, worktreeProject);
503
+ checks.push(r.exitCode === 0 ? { name: command, status: "passed" } : { name: command, status: "failed", detail: tail(r.output) });
504
+ }
505
+ } else {
506
+ for (const name of ["build", "lint"] as const) checks.push({ name, status: "skipped", detail: "not a chant project (no chant.config)" });
507
+ }
508
+ checks.push(lineageCheck(root, worktreeProject, result.manualSteps, scope));
509
+ }
510
+
511
+ return {
512
+ scope,
513
+ template: lineage.template,
514
+ kind: lineage.kind,
515
+ from: lineage.ref ?? null,
516
+ to: staged.ref ?? null,
517
+ ...(upstream.commit ? { commit: { ...(lineage.address?.commit ? { from: lineage.address.commit } : {}), to: upstream.commit } } : {}),
518
+ migrations: plan.chain.map((m) => m.migration.id),
519
+ written: result.written,
520
+ merged: result.merged,
521
+ removed: result.removed,
522
+ kept: result.kept,
523
+ skipped: result.skipped,
524
+ manualSteps: result.manualSteps,
525
+ changedPaths,
526
+ patch,
527
+ lock: { path: lockRepoPath, tracked: lockTracked, text: lockText },
528
+ changed,
529
+ digest,
530
+ governance,
531
+ checks,
532
+ checksOk: checks.every((c) => c.status !== "failed"),
533
+ repo,
534
+ head,
535
+ worktree,
536
+ worktreeProject,
537
+ dispose,
538
+ };
539
+ } catch (err) {
540
+ dispose();
541
+ throw err;
542
+ }
543
+ }
544
+
545
+ /**
546
+ * `chant workspace check` in the worktree. A finding already present at HEAD
547
+ * is not the upgrade's doing, and the upgrade's own manual steps are what the
548
+ * gate approves; any other finding fails the check.
549
+ */
550
+ function lineageCheck(root: string, worktreeProject: string, ownSteps: ManualStep[], scope: string): UpgradeCheck {
551
+ const before = new Set(checkLineage(root).findings.map(findingKey));
552
+ const own = new Set(ownSteps.map((s) => (scope === "." ? s.path : `${scope}/${s.path}`)));
553
+ const report = checkLineage(worktreeProject);
554
+ const fresh = report.findings.filter(
555
+ (f: CheckFinding) => !before.has(findingKey(f)) && !(f.code === "manual-step-open" && f.path !== undefined && own.has(f.path)),
556
+ );
557
+ if (fresh.length === 0) return { name: "workspace check", status: "passed" };
558
+ return { name: "workspace check", status: "failed", detail: fresh.map((f) => `${f.path ?? f.scope ?? ""}: ${f.message}`).join("\n") };
559
+ }
560
+
561
+ // ── Apply or propose ─────────────────────────────────────────────────────────
562
+
563
+ /** Apply the staged patch to the project's tree. Leaves it uncommitted. */
564
+ export function applyStagedUpgrade(staged: StagedUpgrade): void {
565
+ if (!staged.lock.tracked) writeFileSync(join(staged.repo, staged.lock.path), staged.lock.text);
566
+ if (!staged.patch) return;
567
+ const file = join(mkdtempSync(join(tmpdir(), "chant-upgrade-patch-")), "upgrade.patch");
568
+ try {
569
+ writeFileSync(file, staged.patch);
570
+ execFileSync("git", ["apply", "--binary", "--whitespace=nowarn", file], { cwd: staged.repo, stdio: ["ignore", "pipe", "pipe"] });
571
+ } catch (err) {
572
+ const stderr = (err as { stderr?: Buffer }).stderr?.toString().trim();
573
+ throw new UpgradeError(`the approved patch no longer applies to the tree${stderr ? `: ${stderr}` : ""}`);
574
+ } finally {
575
+ rmSync(join(file, ".."), { recursive: true, force: true });
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Commit the staged patch in the worktree and point `branch` at it. Returns
581
+ * the commit. The caller has already refused the default branch.
582
+ */
583
+ export function commitStagedUpgrade(staged: StagedUpgrade, branch: string, message: string): string {
584
+ const identity: string[] = [];
585
+ const has = (key: string): boolean => {
586
+ try {
587
+ return gitOut(staged.worktree, ["config", key]).trim().length > 0;
588
+ } catch {
589
+ return false;
590
+ }
591
+ };
592
+ if (!has("user.name")) identity.push("-c", "user.name=chant");
593
+ if (!has("user.email")) identity.push("-c", "user.email=chant@localhost");
594
+ // An ignored lock is still part of the proposal: the branch starts tracking it.
595
+ if (!staged.lock.tracked) gitOut(staged.worktree, ["add", "-f", "--", staged.lock.path]);
596
+ gitOut(staged.worktree, [...identity, "commit", "-q", "--no-verify", "-m", message]);
597
+ const commit = gitOut(staged.worktree, ["rev-parse", "HEAD"]).trim();
598
+ gitOut(staged.repo, ["branch", "-f", branch, commit]);
599
+ return commit;
600
+ }
601
+
602
+ /** One line per staged change, for the terminal and for a PR body. */
603
+ export function describeStaged(staged: StagedUpgrade): string[] {
604
+ const lines: string[] = [];
605
+ const at = (p: string) => (staged.scope === "." ? p : `${staged.scope}/${p}`);
606
+ lines.push(`${staged.scope} ${staged.template} ${staged.from ?? "(no ref)"} -> ${staged.to ?? "(no ref)"}`);
607
+ for (const id of staged.migrations) lines.push(` migration: ${id}`);
608
+ for (const p of staged.written) lines.push(` updated: ${at(p)}`);
609
+ for (const p of staged.merged) lines.push(` merged: ${at(p)}`);
610
+ for (const p of staged.removed) lines.push(` removed: ${at(p)}`);
611
+ for (const s of staged.skipped) {
612
+ if (s.class === "generated") lines.push(` generated, not merged: ${at(s.path)}${s.command ? ` (rebuild with ${s.command})` : ""}`);
613
+ }
614
+ for (const s of staged.manualSteps) lines.push(` manual step: ${at(s.path)} (${s.reason})`);
615
+ if (staged.governance) {
616
+ const q = staged.governance.approval.quorum!;
617
+ lines.push(
618
+ ` governance: ${staged.governance.paths.join(", ")}; needs ${q.count} human approval(s)${q.roles ? ` with a role in ${q.roles.join(", ")}` : ""}, from the rules at ${staged.governance.rules.slice(0, 12)}`,
619
+ );
620
+ }
621
+ for (const c of staged.checks) lines.push(` ${c.name}: ${c.status}${c.status === "skipped" && c.detail ? ` (${c.detail})` : ""}`);
622
+ lines.push(` patch: ${staged.changedPaths.length} file(s)${staged.lock.tracked ? "" : ` and the untracked ${LOCK_FILE}`}, ${staged.digest}`);
623
+ return lines;
624
+ }