@intentius/chant 0.80.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 (414) hide show
  1. package/dist/audit/discover.d.ts +8 -16
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-html.d.ts +5 -0
  4. package/dist/audit/report-html.d.ts.map +1 -1
  5. package/dist/audit/report-model.d.ts +13 -0
  6. package/dist/audit/report-model.d.ts.map +1 -1
  7. package/dist/audit/terraform-state.d.ts +4 -5
  8. package/dist/audit/terraform-state.d.ts.map +1 -1
  9. package/dist/cli/commands/audit.d.ts +25 -9
  10. package/dist/cli/commands/audit.d.ts.map +1 -1
  11. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  12. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  13. package/dist/cli/commands/doctor.d.ts.map +1 -1
  14. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/lint.d.ts.map +1 -1
  17. package/dist/cli/commands/update.d.ts +6 -0
  18. package/dist/cli/commands/update.d.ts.map +1 -1
  19. package/dist/cli/commands/vendor.d.ts +5 -0
  20. package/dist/cli/commands/vendor.d.ts.map +1 -1
  21. package/dist/cli/handlers/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/components.d.ts.map +1 -1
  23. package/dist/cli/handlers/dev.d.ts.map +1 -1
  24. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  25. package/dist/cli/handlers/init.d.ts.map +1 -1
  26. package/dist/cli/handlers/operator.d.ts +7 -0
  27. package/dist/cli/handlers/operator.d.ts.map +1 -1
  28. package/dist/cli/handlers/promote.d.ts +7 -4
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/run-generate.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts +4 -1
  32. package/dist/cli/handlers/run.d.ts.map +1 -1
  33. package/dist/cli/handlers/vendor.d.ts +8 -3
  34. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  35. package/dist/cli/main.d.ts +12 -0
  36. package/dist/cli/main.d.ts.map +1 -1
  37. package/dist/cli/plugins.d.ts +12 -7
  38. package/dist/cli/plugins.d.ts.map +1 -1
  39. package/dist/cli/registry.d.ts +32 -0
  40. package/dist/cli/registry.d.ts.map +1 -1
  41. package/dist/codegen/fetch.d.ts +10 -0
  42. package/dist/codegen/fetch.d.ts.map +1 -1
  43. package/dist/components/cli-support.d.ts +3 -1
  44. package/dist/components/cli-support.d.ts.map +1 -1
  45. package/dist/components/discover.d.ts.map +1 -1
  46. package/dist/components/driver.d.ts +16 -2
  47. package/dist/components/driver.d.ts.map +1 -1
  48. package/dist/components/gate-plan.d.ts +40 -0
  49. package/dist/components/gate-plan.d.ts.map +1 -0
  50. package/dist/components/promote.d.ts +60 -7
  51. package/dist/components/promote.d.ts.map +1 -1
  52. package/dist/components/sandbox/driver.d.ts.map +1 -1
  53. package/dist/components/sandbox/run.d.ts.map +1 -1
  54. package/dist/components/verbs/build-archive.d.ts +22 -11
  55. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  56. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  57. package/dist/config-static.d.ts +59 -0
  58. package/dist/config-static.d.ts.map +1 -0
  59. package/dist/config.d.ts +32 -10
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/content-digest.d.ts +17 -0
  62. package/dist/content-digest.d.ts.map +1 -0
  63. package/dist/discovery/files.d.ts +10 -5
  64. package/dist/discovery/files.d.ts.map +1 -1
  65. package/dist/discovery/fold-import.d.ts +20 -4
  66. package/dist/discovery/fold-import.d.ts.map +1 -1
  67. package/dist/discovery/index.d.ts.map +1 -1
  68. package/dist/discovery/sandbox/driver.d.ts +20 -0
  69. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  70. package/dist/discovery/sandbox/fork.d.ts +20 -0
  71. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  72. package/dist/discovery/walk.d.ts +119 -0
  73. package/dist/discovery/walk.d.ts.map +1 -0
  74. package/dist/graph-ir.d.ts +4 -1
  75. package/dist/graph-ir.d.ts.map +1 -1
  76. package/dist/join-key.d.ts +31 -0
  77. package/dist/join-key.d.ts.map +1 -0
  78. package/dist/lexicon-module.d.ts +51 -6
  79. package/dist/lexicon-module.d.ts.map +1 -1
  80. package/dist/lexicon.d.ts +58 -1
  81. package/dist/lexicon.d.ts.map +1 -1
  82. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  83. package/dist/lifecycle/digest.d.ts +5 -1
  84. package/dist/lifecycle/digest.d.ts.map +1 -1
  85. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  86. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  87. package/dist/lifecycle/git.d.ts +64 -0
  88. package/dist/lifecycle/git.d.ts.map +1 -1
  89. package/dist/lifecycle/lease.d.ts +8 -1
  90. package/dist/lifecycle/lease.d.ts.map +1 -1
  91. package/dist/lifecycle/legacy-digest.d.ts +41 -8
  92. package/dist/lifecycle/legacy-digest.d.ts.map +1 -1
  93. package/dist/lifecycle/member-ledger.d.ts +67 -0
  94. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  95. package/dist/lifecycle/plan-digest.d.ts +4 -3
  96. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  97. package/dist/lifecycle/release-ledger.d.ts +18 -2
  98. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  99. package/dist/lifecycle/status.d.ts +90 -5
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/op/activities/activity-contracts.d.ts +32 -0
  102. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  103. package/dist/op/activities/index.d.ts +2 -0
  104. package/dist/op/activities/index.d.ts.map +1 -1
  105. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  106. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  107. package/dist/op/discover.d.ts.map +1 -1
  108. package/dist/op/gate-name.d.ts +7 -0
  109. package/dist/op/gate-name.d.ts.map +1 -1
  110. package/dist/op/gate-summary.d.ts +16 -0
  111. package/dist/op/gate-summary.d.ts.map +1 -1
  112. package/dist/op/gate.d.ts +16 -2
  113. package/dist/op/gate.d.ts.map +1 -1
  114. package/dist/project-root.d.ts +26 -0
  115. package/dist/project-root.d.ts.map +1 -1
  116. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  117. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  118. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  119. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  120. package/dist/workspace/checks/generated.d.ts +156 -0
  121. package/dist/workspace/checks/generated.d.ts.map +1 -0
  122. package/dist/workspace/checks/ledgers.d.ts +78 -0
  123. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  124. package/dist/workspace/checks/links.d.ts +25 -0
  125. package/dist/workspace/checks/links.d.ts.map +1 -0
  126. package/dist/workspace/checks/pipelines.d.ts +76 -0
  127. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  128. package/dist/workspace/checks.d.ts +161 -0
  129. package/dist/workspace/checks.d.ts.map +1 -0
  130. package/dist/workspace/compose-graph.d.ts +115 -0
  131. package/dist/workspace/compose-graph.d.ts.map +1 -0
  132. package/dist/workspace/compose-reports.d.ts +68 -0
  133. package/dist/workspace/compose-reports.d.ts.map +1 -0
  134. package/dist/workspace/declaration.d.ts +220 -0
  135. package/dist/workspace/declaration.d.ts.map +1 -0
  136. package/dist/workspace/declaration.schema.json +434 -0
  137. package/dist/workspace/generated-files.d.ts +65 -0
  138. package/dist/workspace/generated-files.d.ts.map +1 -0
  139. package/dist/workspace/glob.d.ts +13 -0
  140. package/dist/workspace/glob.d.ts.map +1 -0
  141. package/dist/workspace/graph-cli.d.ts +75 -0
  142. package/dist/workspace/graph-cli.d.ts.map +1 -0
  143. package/dist/workspace/init.d.ts +96 -0
  144. package/dist/workspace/init.d.ts.map +1 -0
  145. package/dist/workspace/jsonc.d.ts +38 -0
  146. package/dist/workspace/jsonc.d.ts.map +1 -0
  147. package/dist/workspace/kinds.d.ts +192 -0
  148. package/dist/workspace/kinds.d.ts.map +1 -0
  149. package/dist/workspace/lineage-check.d.ts +84 -0
  150. package/dist/workspace/lineage-check.d.ts.map +1 -0
  151. package/dist/workspace/lineage-cli.d.ts +31 -0
  152. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  153. package/dist/workspace/lineage-init.d.ts +101 -0
  154. package/dist/workspace/lineage-init.d.ts.map +1 -0
  155. package/dist/workspace/lineage-lock.d.ts +258 -0
  156. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  157. package/dist/workspace/lineage-merge.d.ts +14 -0
  158. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  159. package/dist/workspace/lineage-migrations.d.ts +178 -0
  160. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  161. package/dist/workspace/lineage-update.d.ts +59 -0
  162. package/dist/workspace/lineage-update.d.ts.map +1 -0
  163. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  164. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  165. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  166. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  167. package/dist/workspace/lineage-vendor.d.ts +63 -0
  168. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  169. package/dist/workspace/lineage-version.d.ts +32 -0
  170. package/dist/workspace/lineage-version.d.ts.map +1 -0
  171. package/dist/workspace/links.d.ts +150 -0
  172. package/dist/workspace/links.d.ts.map +1 -0
  173. package/dist/workspace/ls.d.ts +99 -0
  174. package/dist/workspace/ls.d.ts.map +1 -0
  175. package/dist/workspace/member-commands.d.ts +139 -0
  176. package/dist/workspace/member-commands.d.ts.map +1 -0
  177. package/dist/workspace/member-handles.d.ts +54 -0
  178. package/dist/workspace/member-handles.d.ts.map +1 -0
  179. package/dist/workspace/member-pipeline.d.ts +143 -0
  180. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  181. package/dist/workspace/member-run.d.ts +67 -0
  182. package/dist/workspace/member-run.d.ts.map +1 -0
  183. package/dist/workspace/reason-codes.d.ts +65 -0
  184. package/dist/workspace/reason-codes.d.ts.map +1 -0
  185. package/dist/workspace/records-cli.d.ts +28 -4
  186. package/dist/workspace/records-cli.d.ts.map +1 -1
  187. package/dist/workspace/records.d.ts.map +1 -1
  188. package/dist/workspace/root-refusal.d.ts +53 -0
  189. package/dist/workspace/root-refusal.d.ts.map +1 -0
  190. package/dist/workspace/status.d.ts +150 -0
  191. package/dist/workspace/status.d.ts.map +1 -0
  192. package/dist/workspace/template-manifest.d.ts +90 -0
  193. package/dist/workspace/template-manifest.d.ts.map +1 -0
  194. package/dist/workspace/tree.d.ts +36 -0
  195. package/dist/workspace/tree.d.ts.map +1 -0
  196. package/dist/workspace/trust/attestor.d.ts +67 -0
  197. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  198. package/dist/workspace/trust/policy.d.ts +111 -0
  199. package/dist/workspace/trust/policy.d.ts.map +1 -0
  200. package/dist/workspace/trust/provenance.d.ts +67 -0
  201. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  202. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  203. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  204. package/dist/workspace/trust/test-repo.d.ts +42 -0
  205. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  206. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  207. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  208. package/dist/workspace/trust/verify.d.ts +66 -0
  209. package/dist/workspace/trust/verify.d.ts.map +1 -0
  210. package/dist/workspace/which-chant.d.ts +58 -0
  211. package/dist/workspace/which-chant.d.ts.map +1 -0
  212. package/dist/workspace/workspace-kinds.schema.json +133 -0
  213. package/package.json +2 -1
  214. package/src/audit/discover.ts +66 -78
  215. package/src/audit/report-html.ts +3 -1
  216. package/src/audit/report-model.ts +14 -1
  217. package/src/audit/terraform-state.test.ts +4 -9
  218. package/src/audit/terraform-state.ts +4 -5
  219. package/src/cli/commands/audit-truncation.test.ts +167 -0
  220. package/src/cli/commands/audit.ts +89 -60
  221. package/src/cli/commands/check-lexicon.ts +24 -0
  222. package/src/cli/commands/doctor.test.ts +30 -0
  223. package/src/cli/commands/doctor.ts +18 -0
  224. package/src/cli/commands/import-agents.ts +6 -2
  225. package/src/cli/commands/init.ts +22 -3
  226. package/src/cli/commands/lint.test.ts +17 -23
  227. package/src/cli/commands/lint.ts +24 -84
  228. package/src/cli/commands/update.ts +13 -4
  229. package/src/cli/commands/vendor.ts +11 -6
  230. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  231. package/src/cli/handlers/build-member.test.ts +127 -0
  232. package/src/cli/handlers/build.ts +35 -1
  233. package/src/cli/handlers/components.test.ts +127 -0
  234. package/src/cli/handlers/components.ts +70 -8
  235. package/src/cli/handlers/dev.ts +5 -3
  236. package/src/cli/handlers/fan-out.ts +9 -2
  237. package/src/cli/handlers/init.ts +54 -1
  238. package/src/cli/handlers/operator.test.ts +68 -0
  239. package/src/cli/handlers/operator.ts +37 -9
  240. package/src/cli/handlers/promote.test.ts +118 -2
  241. package/src/cli/handlers/promote.ts +82 -16
  242. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  243. package/src/cli/handlers/run-generate.test.ts +6 -8
  244. package/src/cli/handlers/run-generate.ts +36 -1
  245. package/src/cli/handlers/run.test.ts +47 -0
  246. package/src/cli/handlers/run.ts +28 -5
  247. package/src/cli/handlers/vendor.ts +147 -43
  248. package/src/cli/main.test.ts +105 -1
  249. package/src/cli/main.ts +230 -17
  250. package/src/cli/plugins.ts +17 -17
  251. package/src/cli/registry.ts +32 -0
  252. package/src/cli/static-config-read.test.ts +208 -0
  253. package/src/codegen/fetch.test.ts +63 -1
  254. package/src/codegen/fetch.ts +30 -1
  255. package/src/components/cli-support.ts +6 -1
  256. package/src/components/discover.test.ts +27 -57
  257. package/src/components/discover.ts +13 -68
  258. package/src/components/driver.test.ts +78 -1
  259. package/src/components/driver.ts +43 -7
  260. package/src/components/gate-plan.ts +53 -0
  261. package/src/components/promote.test.ts +276 -2
  262. package/src/components/promote.ts +119 -15
  263. package/src/components/sandbox/driver.ts +8 -8
  264. package/src/components/sandbox/run.ts +4 -1
  265. package/src/components/verbs/build-archive.ts +27 -21
  266. package/src/components/verbs/run-agent.ts +3 -5
  267. package/src/config-static.test.ts +136 -0
  268. package/src/config-static.ts +232 -0
  269. package/src/config.test.ts +2 -2
  270. package/src/config.ts +48 -15
  271. package/src/content-digest.test.ts +120 -0
  272. package/src/content-digest.ts +21 -0
  273. package/src/discovery/files.test.ts +10 -10
  274. package/src/discovery/files.ts +25 -81
  275. package/src/discovery/fold-import.ts +49 -5
  276. package/src/discovery/index.ts +1 -0
  277. package/src/discovery/sandbox/driver.ts +73 -35
  278. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  279. package/src/discovery/sandbox/fork.ts +83 -43
  280. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  281. package/src/discovery/walk.test.ts +263 -0
  282. package/src/discovery/walk.ts +334 -0
  283. package/src/effect-receipt.ts +2 -2
  284. package/src/graph-ir.ts +4 -1
  285. package/src/join-key.test.ts +25 -0
  286. package/src/join-key.ts +39 -0
  287. package/src/lexicon-module.ts +108 -10
  288. package/src/lexicon.ts +79 -1
  289. package/src/lifecycle/build-ledger-store.ts +11 -10
  290. package/src/lifecycle/digest.ts +9 -5
  291. package/src/lifecycle/gate-ledger.test.ts +20 -0
  292. package/src/lifecycle/gate-ledger.ts +28 -6
  293. package/src/lifecycle/git.test.ts +13 -1
  294. package/src/lifecycle/git.ts +221 -176
  295. package/src/lifecycle/lease.ts +25 -10
  296. package/src/lifecycle/legacy-digest.test.ts +115 -23
  297. package/src/lifecycle/legacy-digest.ts +60 -19
  298. package/src/lifecycle/member-ledger.test.ts +319 -0
  299. package/src/lifecycle/member-ledger.ts +145 -0
  300. package/src/lifecycle/plan-digest.ts +7 -7
  301. package/src/lifecycle/release-ledger.test.ts +20 -0
  302. package/src/lifecycle/release-ledger.ts +23 -7
  303. package/src/lifecycle/status.test.ts +151 -1
  304. package/src/lifecycle/status.ts +228 -7
  305. package/src/op/activities/activity-contracts.ts +37 -0
  306. package/src/op/activities/index.ts +10 -0
  307. package/src/op/activities/propose-upgrade.ts +206 -0
  308. package/src/op/discover.test.ts +14 -4
  309. package/src/op/discover.ts +19 -27
  310. package/src/op/gate-name.ts +8 -0
  311. package/src/op/gate-summary.test.ts +14 -0
  312. package/src/op/gate-summary.ts +25 -2
  313. package/src/op/gate.ts +36 -7
  314. package/src/op/generate-pipeline.test.ts +14 -5
  315. package/src/project-root.test.ts +66 -1
  316. package/src/project-root.ts +48 -0
  317. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  318. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  319. package/src/workspace/behold-kinds.test.ts +162 -0
  320. package/src/workspace/check-contract.test.ts +127 -0
  321. package/src/workspace/check.schema.json +347 -0
  322. package/src/workspace/checks/generated.test.ts +287 -0
  323. package/src/workspace/checks/generated.ts +475 -0
  324. package/src/workspace/checks/ledgers.test.ts +139 -0
  325. package/src/workspace/checks/ledgers.ts +246 -0
  326. package/src/workspace/checks/links.ts +183 -0
  327. package/src/workspace/checks/pipelines.test.ts +133 -0
  328. package/src/workspace/checks/pipelines.ts +225 -0
  329. package/src/workspace/checks-repo.test.ts +71 -0
  330. package/src/workspace/checks.test.ts +450 -0
  331. package/src/workspace/checks.ts +461 -0
  332. package/src/workspace/compose-graph.test.ts +146 -0
  333. package/src/workspace/compose-graph.ts +228 -0
  334. package/src/workspace/compose-reports.test.ts +109 -0
  335. package/src/workspace/compose-reports.ts +186 -0
  336. package/src/workspace/declaration.schema.json +434 -0
  337. package/src/workspace/declaration.test.ts +347 -0
  338. package/src/workspace/declaration.ts +623 -0
  339. package/src/workspace/generated-files.test.ts +114 -0
  340. package/src/workspace/generated-files.ts +94 -0
  341. package/src/workspace/glob.ts +49 -0
  342. package/src/workspace/graph-cli.ts +220 -0
  343. package/src/workspace/graph-contract.test.ts +204 -0
  344. package/src/workspace/graph.schema.json +662 -0
  345. package/src/workspace/init.test.ts +223 -0
  346. package/src/workspace/init.ts +585 -0
  347. package/src/workspace/jsonc.test.ts +65 -0
  348. package/src/workspace/jsonc.ts +231 -0
  349. package/src/workspace/kinds.test.ts +288 -0
  350. package/src/workspace/kinds.ts +506 -0
  351. package/src/workspace/lineage-check.ts +264 -0
  352. package/src/workspace/lineage-cli.ts +114 -0
  353. package/src/workspace/lineage-init.test.ts +231 -0
  354. package/src/workspace/lineage-init.ts +365 -0
  355. package/src/workspace/lineage-lock.test.ts +224 -0
  356. package/src/workspace/lineage-lock.ts +357 -0
  357. package/src/workspace/lineage-merge.ts +44 -0
  358. package/src/workspace/lineage-migrations.test.ts +178 -0
  359. package/src/workspace/lineage-migrations.ts +343 -0
  360. package/src/workspace/lineage-update.ts +160 -0
  361. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  362. package/src/workspace/lineage-upgrade.test.ts +474 -0
  363. package/src/workspace/lineage-upgrade.ts +624 -0
  364. package/src/workspace/lineage-vendor.test.ts +126 -0
  365. package/src/workspace/lineage-vendor.ts +215 -0
  366. package/src/workspace/lineage-version.ts +127 -0
  367. package/src/workspace/links.test.ts +344 -0
  368. package/src/workspace/links.ts +331 -0
  369. package/src/workspace/ls-contract.test.ts +200 -0
  370. package/src/workspace/ls.schema.json +190 -0
  371. package/src/workspace/ls.ts +254 -0
  372. package/src/workspace/member-commands.e2e.test.ts +244 -0
  373. package/src/workspace/member-commands.test.ts +217 -0
  374. package/src/workspace/member-commands.ts +540 -0
  375. package/src/workspace/member-handles.test.ts +76 -0
  376. package/src/workspace/member-handles.ts +228 -0
  377. package/src/workspace/member-pipeline.test.ts +135 -0
  378. package/src/workspace/member-pipeline.ts +317 -0
  379. package/src/workspace/member-run.ts +146 -0
  380. package/src/workspace/read-contract.test.ts +129 -0
  381. package/src/workspace/reason-codes.test.ts +113 -0
  382. package/src/workspace/reason-codes.ts +81 -0
  383. package/src/workspace/records-cli.ts +86 -14
  384. package/src/workspace/records-contract.test.ts +5 -0
  385. package/src/workspace/records.schema.json +32 -2
  386. package/src/workspace/records.ts +3 -2
  387. package/src/workspace/root-refusal.ts +97 -0
  388. package/src/workspace/status-contract.test.ts +308 -0
  389. package/src/workspace/status.schema.json +210 -0
  390. package/src/workspace/status.ts +412 -0
  391. package/src/workspace/template-manifest.test.ts +79 -0
  392. package/src/workspace/template-manifest.ts +221 -0
  393. package/src/workspace/tree.ts +132 -0
  394. package/src/workspace/trust/attestor.ts +93 -0
  395. package/src/workspace/trust/policy.ts +306 -0
  396. package/src/workspace/trust/provenance.ts +181 -0
  397. package/src/workspace/trust/ssh-commit.ts +138 -0
  398. package/src/workspace/trust/test-repo.ts +138 -0
  399. package/src/workspace/trust/trust.test.ts +397 -0
  400. package/src/workspace/trust/verify-cli.ts +56 -0
  401. package/src/workspace/trust/verify.ts +194 -0
  402. package/src/workspace/which-chant.test.ts +117 -0
  403. package/src/workspace/which-chant.ts +151 -0
  404. package/src/workspace/workspace-kinds.schema.json +133 -0
  405. package/dist/components/unbound-gate-approval.d.ts +0 -24
  406. package/dist/components/unbound-gate-approval.d.ts.map +0 -1
  407. package/dist/discovery/convergence.d.ts +0 -97
  408. package/dist/discovery/convergence.d.ts.map +0 -1
  409. package/src/cli/commands/audit-walk-warnings.test.ts +0 -156
  410. package/src/components/unbound-gate-approval.test.ts +0 -82
  411. package/src/components/unbound-gate-approval.ts +0 -51
  412. package/src/discovery/convergence.test.ts +0 -231
  413. package/src/discovery/convergence.ts +0 -399
  414. package/src/op/discover-convergence.test.ts +0 -70
@@ -0,0 +1,357 @@
1
+ /**
2
+ * The lineage lock, `.chant/workspace.lock.json` (#2524 D9, #2540).
3
+ *
4
+ * The lock records where a project's files came from. It holds one lineage per
5
+ * scope, keyed by the scope's directory relative to the lock's root:
6
+ *
7
+ * - a project made by `chant init --from <repo>@<ref>` or
8
+ * `chant init --template <name>` has one scope, `"."`;
9
+ * - each `chant vendor` target is a scope of kind `vendor` (copied, no
10
+ * parameters), which replaces its entry in `vendor.json` (ws-038).
11
+ *
12
+ * A lineage names its template, the content address the files came from, the
13
+ * parameter values used, the migrations applied so far and, per file, a class
14
+ * (`owned`, `generated` or `seed`) and the hash the file had when chant wrote
15
+ * it. That hash is the merge base: a file whose content still has it was not
16
+ * edited, and an update may replace it. A file that differs is the project's
17
+ * own, and an update that also changed it becomes a manual step instead of an
18
+ * overwrite. `chant workspace upgrade` (#2550) builds on exactly this.
19
+ *
20
+ * Lineage works for a plain project (#2525 rule 4, ws-013): the lock sits at
21
+ * the project root and needs no `chant.workspace.json`. Like everything under
22
+ * `workspace/`, this module loads only when a lock exists or a command that
23
+ * writes one runs, never on a level-0 path (#2525 rule 5).
24
+ */
25
+
26
+ import { createHash } from "node:crypto";
27
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
28
+ import { dirname, join, posix } from "node:path";
29
+ import { z } from "zod";
30
+ import { WorkspaceReadError } from "./declaration";
31
+ import { classifyFile, declaredFilesFor, type DeclaredFiles } from "./generated-files";
32
+
33
+ /** Where the lock lives, relative to the project (or workspace) root. */
34
+ export const LOCK_FILE = ".chant/workspace.lock.json";
35
+
36
+ /** The format this chant reads and writes. A reader refuses a newer one. */
37
+ export const LOCK_VERSION = 1;
38
+
39
+ // ── Schema ───────────────────────────────────────────────────────────────────
40
+
41
+ const Sha256 = z.string().regex(/^sha256:[0-9a-f]{64}$/, "expected sha256:<64 hex>");
42
+
43
+ /**
44
+ * How an update treats a file (D9, D14).
45
+ *
46
+ * - `owned`: from the template; replaced by an update when it still has its
47
+ * recorded hash, merged or turned into a manual step when it does not.
48
+ * - `generated`: rebuilt by `command` rather than merged.
49
+ * - `seed`: written once at init and never touched again.
50
+ */
51
+ export const FILE_CLASSES = ["owned", "generated", "seed"] as const;
52
+ export type FileClass = (typeof FILE_CLASSES)[number];
53
+
54
+ const LockFileEntrySchema = z
55
+ .object({
56
+ class: z.enum(FILE_CLASSES),
57
+ /** The file's hash as chant last wrote it: the merge base. */
58
+ sha256: Sha256,
59
+ /** For `generated` files, the command that rebuilds them. */
60
+ command: z.string().min(1).optional(),
61
+ })
62
+ .strict();
63
+
64
+ /** Where a template's files are fetched from. */
65
+ const SourceSchema = z.discriminatedUnion("type", [
66
+ /** A git repository at a ref, optionally one directory of it (`#<member>`). */
67
+ z.object({ type: z.literal("git"), repo: z.string().min(1), url: z.string().min(1), path: z.string().optional() }).strict(),
68
+ /** A lexicon's `initTemplates`, as `chant init --lexicon <lexicon> --template <template>` renders them. */
69
+ z.object({ type: z.literal("lexicon"), lexicon: z.string().min(1), template: z.string().min(1) }).strict(),
70
+ /** `chant vendor` sources, unchanged from `vendor.json`. */
71
+ z.object({ type: z.literal("local"), path: z.string().min(1) }).strict(),
72
+ z.object({ type: z.literal("archive"), url: z.string().url(), subpath: z.string().optional() }).strict(),
73
+ ]);
74
+ export type LineageSource = z.infer<typeof SourceSchema>;
75
+
76
+ /**
77
+ * What the files came from, precisely enough to fetch or render them again.
78
+ * `digest` is always present: the content hash of the file set as chant wrote
79
+ * it (the same hash `vendor.json` called `checksum`). A git source adds the
80
+ * commit and the tree of the scope's directory; a lexicon template adds the
81
+ * package and chant versions that rendered it.
82
+ */
83
+ const AddressSchema = z
84
+ .object({
85
+ digest: Sha256,
86
+ commit: z.string().regex(/^[0-9a-f]{40,64}$/).optional(),
87
+ tree: z.string().regex(/^[0-9a-f]{40,64}$/).optional(),
88
+ package: z.string().optional(),
89
+ version: z.string().nullable().optional(),
90
+ chant: z.string().optional(),
91
+ })
92
+ .strict();
93
+
94
+ /**
95
+ * Why an update left a file alone. Closed: a reader may switch on it.
96
+ *
97
+ * - `changed-locally`: both the project and the source changed the file.
98
+ * - `deleted-locally`: the project deleted a file the source changed.
99
+ * - `exists-locally`: the source added a file the project already has.
100
+ * - `removed-upstream`: the source removed a file the project changed.
101
+ */
102
+ export const MANUAL_STEP_REASONS = ["changed-locally", "deleted-locally", "exists-locally", "removed-upstream"] as const;
103
+ export type ManualStepReason = (typeof MANUAL_STEP_REASONS)[number];
104
+
105
+ const ManualStepSchema = z
106
+ .object({
107
+ path: z.string().min(1),
108
+ reason: z.enum(MANUAL_STEP_REASONS),
109
+ /** The source's version of the file, or null when the source removed it. */
110
+ upstream: Sha256.nullable(),
111
+ })
112
+ .strict();
113
+ export type ManualStep = z.infer<typeof ManualStepSchema>;
114
+
115
+ const LineageSchema = z
116
+ .object({
117
+ kind: z.enum(["template", "vendor"]),
118
+ /** For a vendor scope, the name `chant vendor pull <name>` takes. */
119
+ name: z.string().min(1).optional(),
120
+ /** Stable identity of the source template, e.g. `github.com/acme/starter#service` or `lexicon:aws/node-pipeline`. */
121
+ template: z.string().min(1),
122
+ source: SourceSchema,
123
+ /** The pin as the user wrote it: a git ref, a tag or a version label. */
124
+ ref: z.string().optional(),
125
+ address: AddressSchema.nullable(),
126
+ /** The parameter values the template was instantiated with. Always empty for vendor scopes. */
127
+ parameters: z.record(z.string(), z.unknown()),
128
+ /** Migrations applied since instantiation (#2550). */
129
+ migrations: z.array(z.string()),
130
+ /** Per file, relative to the scope directory, in sorted order. */
131
+ files: z.record(z.string(), LockFileEntrySchema),
132
+ manualSteps: z.array(ManualStepSchema),
133
+ })
134
+ .strict();
135
+ export type Lineage = z.infer<typeof LineageSchema>;
136
+
137
+ const LockSchema = z
138
+ .object({
139
+ lockVersion: z.literal(LOCK_VERSION),
140
+ scopes: z.record(z.string(), LineageSchema),
141
+ })
142
+ .strict();
143
+ export type LineageLock = z.infer<typeof LockSchema>;
144
+
145
+ // ── Hashes ───────────────────────────────────────────────────────────────────
146
+
147
+ /** `sha256:<hex>` of one file's bytes. */
148
+ export function fileHash(data: Buffer | string): string {
149
+ return `sha256:${createHash("sha256").update(data).digest("hex")}`;
150
+ }
151
+
152
+ /**
153
+ * The content hash of a file set, independent of order: sha256 over each
154
+ * `path\0content\0` in sorted-path order. Identical to `vendor.json`'s
155
+ * `checksum`, so a migrated entry keeps its digest.
156
+ */
157
+ export function contentDigest(files: Map<string, Buffer>): string {
158
+ const h = createHash("sha256");
159
+ for (const path of [...files.keys()].sort()) {
160
+ h.update(path);
161
+ h.update("\0");
162
+ h.update(files.get(path)!);
163
+ h.update("\0");
164
+ }
165
+ return `sha256:${h.digest("hex")}`;
166
+ }
167
+
168
+ // ── Scope paths ──────────────────────────────────────────────────────────────
169
+
170
+ /** Normalise a scope directory to the lock's key form: posix, relative, `.` for the root. */
171
+ export function scopeKey(dir: string): string {
172
+ const norm = posix.normalize(dir.split("\\").join("/")).replace(/\/+$/, "");
173
+ if (norm === "" || norm === ".") return ".";
174
+ if (norm.startsWith("/") || norm === ".." || norm.startsWith("../")) {
175
+ throw new LockError(`scope "${dir}" must be a path inside the project`);
176
+ }
177
+ return norm.replace(/^\.\//, "");
178
+ }
179
+
180
+ // ── Read and write ───────────────────────────────────────────────────────────
181
+
182
+ export class LockError extends Error {
183
+ override name = "LockError";
184
+ }
185
+
186
+ export function lockPath(root: string): string {
187
+ return join(root, LOCK_FILE);
188
+ }
189
+
190
+ export function lockExists(root: string): boolean {
191
+ return existsSync(lockPath(root));
192
+ }
193
+
194
+ export function emptyLock(): LineageLock {
195
+ return { lockVersion: LOCK_VERSION, scopes: {} };
196
+ }
197
+
198
+ /** Read and validate the lock at `root`, or return null when there is none. */
199
+ export function readLock(root: string): LineageLock | null {
200
+ const path = lockPath(root);
201
+ if (!existsSync(path)) return null;
202
+ return parseLock(readFileSync(path, "utf-8"));
203
+ }
204
+
205
+ /** Validate a lock's text, such as one read from a revision (`chant workspace check --at`, #2536). */
206
+ export function parseLock(text: string): LineageLock {
207
+ let raw: unknown;
208
+ try {
209
+ raw = JSON.parse(text);
210
+ } catch (err) {
211
+ throw new LockError(`${LOCK_FILE} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
212
+ }
213
+ const version = (raw as { lockVersion?: unknown } | null)?.lockVersion;
214
+ if (typeof version === "number" && version > LOCK_VERSION) {
215
+ throw new LockError(`${LOCK_FILE} has lockVersion ${version}; this chant reads up to ${LOCK_VERSION}. Upgrade chant.`);
216
+ }
217
+ const parsed = LockSchema.safeParse(raw);
218
+ if (!parsed.success) {
219
+ throw new LockError(
220
+ `invalid ${LOCK_FILE}: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`,
221
+ );
222
+ }
223
+ for (const key of Object.keys(parsed.data.scopes)) {
224
+ if (scopeKey(key) !== key) throw new LockError(`invalid ${LOCK_FILE}: scope "${key}" is not in normal form ("${scopeKey(key)}")`);
225
+ }
226
+ return parsed.data;
227
+ }
228
+
229
+ /** Keys sorted at every level that is a map, so the file diffs cleanly. */
230
+ function canonical(lock: LineageLock): LineageLock {
231
+ const scopes: Record<string, Lineage> = {};
232
+ for (const key of Object.keys(lock.scopes).sort()) {
233
+ const s = lock.scopes[key];
234
+ const files: Lineage["files"] = {};
235
+ for (const f of Object.keys(s.files).sort()) files[f] = s.files[f];
236
+ scopes[key] = {
237
+ kind: s.kind,
238
+ ...(s.name !== undefined ? { name: s.name } : {}),
239
+ template: s.template,
240
+ source: s.source,
241
+ ...(s.ref !== undefined ? { ref: s.ref } : {}),
242
+ address: s.address,
243
+ parameters: s.parameters,
244
+ migrations: s.migrations,
245
+ files,
246
+ manualSteps: [...s.manualSteps].sort((a, b) => a.path.localeCompare(b.path)),
247
+ };
248
+ }
249
+ return { lockVersion: lock.lockVersion, scopes };
250
+ }
251
+
252
+ /** Serialise the lock deterministically. */
253
+ export function renderLock(lock: LineageLock): string {
254
+ return JSON.stringify(canonical(lock), null, 2) + "\n";
255
+ }
256
+
257
+ export function writeLock(root: string, lock: LineageLock): void {
258
+ const checked = LockSchema.parse(lock);
259
+ const path = lockPath(root);
260
+ mkdirSync(dirname(path), { recursive: true });
261
+ writeFileSync(path, renderLock(checked));
262
+ }
263
+
264
+ // ── Building a lineage ───────────────────────────────────────────────────────
265
+
266
+ /**
267
+ * The class a file gets (D14), from the same list the drift check reads
268
+ * (`generated-files.ts`): a member's declared generated files, passed as
269
+ * `declared`, then the implicit rules. Skills that `chant update` rewrites
270
+ * are generated, `.mcp.json` is a seed, a hand-written entry and the rest are
271
+ * owned. `.chant/types/` is ignored, and {@link fileEntries} leaves it out.
272
+ */
273
+ export function defaultFileClass(path: string, declared?: DeclaredFiles): { class: FileClass; command?: string } {
274
+ const c = classifyFile(path, declared);
275
+ if (c.class === "generated") return { class: "generated", command: c.command };
276
+ return { class: c.class === "ignored" ? "owned" : c.class };
277
+ }
278
+
279
+ /**
280
+ * The declared generated files for the scope directory `absDir`, from the
281
+ * workspace declaration above it, if any (#2541). A declaration that can't
282
+ * be read is a {@link LockError}, never an empty list.
283
+ */
284
+ export function declaredFilesAt(absDir: string): DeclaredFiles {
285
+ try {
286
+ return declaredFilesFor(absDir);
287
+ } catch (err) {
288
+ if (err instanceof WorkspaceReadError) throw new LockError(`${err.code}: ${err.describe()}`);
289
+ throw err;
290
+ }
291
+ }
292
+
293
+ /** Per-file entries for a file set, each with its default class. Ignored paths (`.chant/types/`) are left out. */
294
+ export function fileEntries(files: Map<string, Buffer>, declared?: DeclaredFiles): Lineage["files"] {
295
+ const out: Lineage["files"] = {};
296
+ for (const path of [...files.keys()].sort()) {
297
+ if (classifyFile(path, declared).class === "ignored") continue;
298
+ out[path] = { ...defaultFileClass(path, declared), sha256: fileHash(files.get(path)!) };
299
+ }
300
+ return out;
301
+ }
302
+
303
+ // ── Status against the working tree ──────────────────────────────────────────
304
+
305
+ export interface ScopeStatus {
306
+ scope: string;
307
+ lineage: Lineage;
308
+ /** Files whose content no longer has the recorded hash (generated files excluded). */
309
+ customised: string[];
310
+ /** Files recorded in the lock and missing from the tree. */
311
+ missing: string[];
312
+ }
313
+
314
+ /** Compare each recorded file of a scope with the tree under `root`. */
315
+ export function scopeStatus(root: string, scope: string, lineage: Lineage): ScopeStatus {
316
+ const customised: string[] = [];
317
+ const missing: string[] = [];
318
+ for (const [path, entry] of Object.entries(lineage.files)) {
319
+ // A generated file is rebuilt by its command, never merged, and is often
320
+ // gitignored (`skills/`), so neither an edit nor its absence means anything.
321
+ if (entry.class === "generated") continue;
322
+ const abs = join(root, scope, path);
323
+ if (!existsSync(abs)) {
324
+ missing.push(path);
325
+ continue;
326
+ }
327
+ if (fileHash(readFileSync(abs)) !== entry.sha256) customised.push(path);
328
+ }
329
+ return { scope, lineage, customised, missing };
330
+ }
331
+
332
+ /**
333
+ * Close a manual step: the file as it stands is the project's resolution, and
334
+ * the source's version becomes its new merge base. For a file the source
335
+ * removed, the file leaves the lineage and stays in the tree as the project's.
336
+ */
337
+ export function resolveManualStep(lock: LineageLock, filePath: string): { scope: string; step: ManualStep } | null {
338
+ const norm = scopeKey(filePath);
339
+ // Deepest scope first: a vendor scope inside the root scope owns its files.
340
+ const scopes = Object.entries(lock.scopes).sort((a, b) => b[0].length - a[0].length);
341
+ for (const [scope, lineage] of scopes) {
342
+ const prefix = scope === "." ? "" : `${scope}/`;
343
+ if (prefix && !norm.startsWith(prefix)) continue;
344
+ const rel = norm.slice(prefix.length);
345
+ const index = lineage.manualSteps.findIndex((s) => s.path === rel);
346
+ if (index < 0) continue;
347
+ const [step] = lineage.manualSteps.splice(index, 1);
348
+ if (step.upstream === null) {
349
+ delete lineage.files[rel];
350
+ } else {
351
+ const prior = lineage.files[rel];
352
+ lineage.files[rel] = { ...(prior ?? defaultFileClass(rel)), sha256: step.upstream };
353
+ }
354
+ return { scope, step };
355
+ }
356
+ return null;
357
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The three-way merge of one file (#2550, ws-005).
3
+ *
4
+ * ws-005 merges per file: a customised file takes the template's changes only
5
+ * when every hunk merges cleanly. Otherwise the file stays as it was and the
6
+ * upgrade records one manual step for it. So this answers with the merged
7
+ * bytes or with null, never with conflict markers.
8
+ *
9
+ * The merge is `git merge-file`, run on three scratch files. It reads no
10
+ * repository and reaches no network. A binary file never merges.
11
+ */
12
+
13
+ import { execFileSync } from "node:child_process";
14
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+
18
+ /** Merge `theirs` into `ours` against `base`. Null when any hunk conflicts, or the file is binary. */
19
+ export function mergeFile(base: Buffer, ours: Buffer, theirs: Buffer): Buffer | null {
20
+ if ([base, ours, theirs].some(isBinary)) return null;
21
+ const dir = mkdtempSync(join(tmpdir(), "chant-merge-"));
22
+ try {
23
+ const [o, b, t] = ["ours", "base", "theirs"].map((n) => join(dir, n));
24
+ writeFileSync(o, ours);
25
+ writeFileSync(b, base);
26
+ writeFileSync(t, theirs);
27
+ try {
28
+ // Exit 0: merged cleanly. A positive exit is the number of conflicts.
29
+ execFileSync("git", ["merge-file", "-q", o, b, t], { stdio: ["ignore", "ignore", "pipe"] });
30
+ } catch (err) {
31
+ const status = (err as { status?: number }).status;
32
+ if (typeof status === "number" && status > 0 && status < 128) return null;
33
+ throw err;
34
+ }
35
+ return readFileSync(o);
36
+ } finally {
37
+ rmSync(dir, { recursive: true, force: true });
38
+ }
39
+ }
40
+
41
+ /** Git's own test: a NUL byte in the first 8000 bytes. */
42
+ function isBinary(data: Buffer): boolean {
43
+ return data.subarray(0, 8000).includes(0);
44
+ }
@@ -0,0 +1,178 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { Lineage } from "./lineage-lock";
6
+ import { mergeFile } from "./lineage-merge";
7
+ import { planMigrations, runMigration, splitMigrations, type LoadedMigration } from "./lineage-migrations";
8
+ import { applyUpstream } from "./lineage-update";
9
+ import { compareVersions, parseVersion, satisfies } from "./lineage-version";
10
+
11
+ function lineage(ref: string | undefined, migrations: string[] = []): Lineage {
12
+ return {
13
+ kind: "template",
14
+ template: "github.com/acme/starter",
15
+ source: { type: "git", repo: "acme/starter", url: "https://github.com/acme/starter.git" },
16
+ ...(ref ? { ref } : {}),
17
+ address: null,
18
+ parameters: {},
19
+ migrations,
20
+ files: {},
21
+ manualSteps: [],
22
+ };
23
+ }
24
+
25
+ function migration(id: string, versions: string, to: string, template?: string): LoadedMigration {
26
+ return {
27
+ file: `.chant/migrations/${id}.json`,
28
+ migration: { id, from: { versions, ...(template ? { template } : {}) }, to, body: { type: "declarative", steps: [{ op: "delete", path: "x" }] }, post: [] },
29
+ };
30
+ }
31
+
32
+ describe("versions", () => {
33
+ test("a version is read from a tag, with or without a prefix", () => {
34
+ expect(parseVersion("v1.4.0")).toEqual({ major: 1, minor: 4, patch: 0, pre: "" });
35
+ expect(parseVersion("2")).toEqual({ major: 2, minor: 0, patch: 0, pre: "" });
36
+ expect(parseVersion("chant-v0.80.0")).toEqual({ major: 0, minor: 80, patch: 0, pre: "" });
37
+ expect(parseVersion("v2.0.0-rc.1")?.pre).toBe("rc.1");
38
+ expect(parseVersion("main")).toBeNull();
39
+ expect(parseVersion("4624a0b6ca83")).toBeNull();
40
+ });
41
+
42
+ test("ranges", () => {
43
+ const v = (s: string) => parseVersion(s)!;
44
+ expect(satisfies(v("1.5.0"), ">=1.0.0 <2.0.0")).toBe(true);
45
+ expect(satisfies(v("2.0.0"), ">=1.0.0 <2.0.0")).toBe(false);
46
+ expect(satisfies(v("1.9.3"), "^1.2")).toBe(true);
47
+ expect(satisfies(v("0.3.1"), "^0.3.0")).toBe(true);
48
+ expect(satisfies(v("0.4.0"), "^0.3.0")).toBe(false);
49
+ expect(satisfies(v("1.2.9"), "~1.2.0")).toBe(true);
50
+ expect(satisfies(v("1.3.0"), "~1.2.0")).toBe(false);
51
+ expect(satisfies(v("1.7.0"), "1.x")).toBe(true);
52
+ expect(satisfies(v("3.0.0"), "1.x || >=3")).toBe(true);
53
+ expect(satisfies(v("1.2.3"), "1.2.3")).toBe(true);
54
+ expect(satisfies(v("1.2.9"), "<=1.2")).toBe(true);
55
+ expect(satisfies(v("2.0.0-rc.1"), ">=1.0.0")).toBe(false);
56
+ expect(satisfies(v("7.0.0"), "*")).toBe(true);
57
+ expect(compareVersions(v("2.0.0-rc.1"), v("2.0.0"))).toBe(-1);
58
+ });
59
+ });
60
+
61
+ describe("planMigrations", () => {
62
+ test("orders the chain by the version each migration brings the scope to", () => {
63
+ const plan = planMigrations(lineage("v1.0.0"), "v3.1.0", [
64
+ migration("b", ">=2.0.0 <3.0.0", "3.0.0"),
65
+ migration("a", "1.x", "2.0.0"),
66
+ migration("future", ">=3.0.0", "4.0.0"),
67
+ migration("past", "0.x", "1.0.0"),
68
+ ]);
69
+ expect(plan.chain.map((m) => m.migration.id)).toEqual(["a", "b"]);
70
+ });
71
+
72
+ test("skips migrations already applied and those for another template", () => {
73
+ const plan = planMigrations(lineage("v1.0.0", ["a"]), "v2.0.0", [
74
+ migration("a", "1.x", "2.0.0"),
75
+ migration("other", "1.x", "2.0.0", "github.com/someone/else"),
76
+ migration("mine", "1.x", "2.0.0", "github.com/acme/starter"),
77
+ ]);
78
+ expect(plan.chain.map((m) => m.migration.id)).toEqual(["mine"]);
79
+ });
80
+
81
+ test("refuses a gap, a downgrade, and a chain it cannot compute", () => {
82
+ expect(() => planMigrations(lineage("v1.0.0"), "v3.0.0", [migration("b", ">=2.0.0 <3.0.0", "3.0.0")])).toThrow(/gap in the migration chain/);
83
+ expect(() => planMigrations(lineage("v2.0.0"), "v1.0.0", [])).toThrow(/only goes forward/);
84
+ expect(() => planMigrations(lineage("main"), "v2.0.0", [migration("a", "1.x", "2.0.0")])).toThrow(/not a version/);
85
+ // With nothing to run, no version is needed.
86
+ expect(planMigrations(lineage("main"), "main", []).chain).toEqual([]);
87
+ });
88
+ });
89
+
90
+ describe("migration files", () => {
91
+ const enc = (o: unknown) => Buffer.from(JSON.stringify(o));
92
+ test("are split out of the template's files and validated", () => {
93
+ const files = new Map<string, Buffer>([
94
+ ["src/a.ts", Buffer.from("a")],
95
+ [".chant/migrations/one.json", enc({ id: "one", from: { versions: "1.x" }, to: "2.0.0", body: { type: "declarative", steps: [{ op: "delete", path: "a" }] } })],
96
+ [".chant/migrations/two.mjs", Buffer.from("export default () => {}")],
97
+ ]);
98
+ const split = splitMigrations(files);
99
+ expect([...split.files.keys()]).toEqual(["src/a.ts"]);
100
+ expect(split.migrations.map((m) => m.migration.id)).toEqual(["one"]);
101
+ expect([...split.modules.keys()]).toEqual(["two.mjs"]);
102
+ });
103
+
104
+ test("refuse an unknown step, a bad range, an escaping path and a missing code module", () => {
105
+ const one = (m: unknown) => splitMigrations(new Map([[".chant/migrations/m.json", enc(m)]]));
106
+ const base = { id: "m", from: { versions: "1.x" }, to: "2.0.0" };
107
+ expect(() => one({ ...base, body: { type: "declarative", steps: [{ op: "chmod", path: "a" }] } })).toThrow(/invalid migration/);
108
+ expect(() => one({ ...base, from: { versions: ">>1" }, body: { type: "declarative", steps: [{ op: "delete", path: "a" }] } })).toThrow(/not a version range/);
109
+ expect(() => one({ ...base, body: { type: "declarative", steps: [{ op: "delete", path: "../a" }] } })).toThrow(/inside the scope/);
110
+ expect(() => one({ ...base, body: { type: "code", module: "gone.mjs" } })).toThrow(/is not in the template/);
111
+ });
112
+ });
113
+
114
+ describe("declarative steps", () => {
115
+ let dir: string;
116
+ beforeEach(() => {
117
+ dir = mkdtempSync(join(tmpdir(), "chant-migration-"));
118
+ });
119
+ afterEach(() => rmSync(dir, { recursive: true, force: true }));
120
+
121
+ test("json-set and json-delete edit a JSON file at a pointer", async () => {
122
+ writeFileSync(join(dir, "c.json"), JSON.stringify({ a: { b: 1, gone: true } }));
123
+ const l = lineage("v1");
124
+ await runMigration(
125
+ {
126
+ file: "m.json",
127
+ migration: {
128
+ id: "json",
129
+ from: { versions: "*" },
130
+ to: "2.0.0",
131
+ body: {
132
+ type: "declarative",
133
+ steps: [
134
+ { op: "json-set", path: "c.json", pointer: "/a/c~1d", value: [1] },
135
+ { op: "json-delete", path: "c.json", pointer: "/a/gone" },
136
+ ],
137
+ },
138
+ post: [{ check: "contains", path: "c.json", text: '"c/d"' }],
139
+ },
140
+ },
141
+ { dir, lineage: l, modules: new Map(), allowCode: false, scratch: dir },
142
+ );
143
+ expect(JSON.parse(readFileSync(join(dir, "c.json"), "utf-8"))).toEqual({ a: { b: 1, "c/d": [1] } });
144
+ expect(l.migrations).toEqual(["json"]);
145
+ });
146
+ });
147
+
148
+ describe("applyUpstream class rules", () => {
149
+ test("seed and generated files are never written or merged", () => {
150
+ const dir = mkdtempSync(join(tmpdir(), "chant-classes-"));
151
+ try {
152
+ mkdirSync(join(dir, "skills/x"), { recursive: true });
153
+ writeFileSync(join(dir, ".mcp.json"), "{}");
154
+ writeFileSync(join(dir, "skills/x/SKILL.md"), "old");
155
+ const l = lineage("v1");
156
+ l.files = {
157
+ ".mcp.json": { class: "seed", sha256: `sha256:${"1".repeat(64)}` },
158
+ "skills/x/SKILL.md": { class: "generated", sha256: `sha256:${"2".repeat(64)}`, command: "chant update" },
159
+ };
160
+ const result = applyUpstream(dir, l, new Map([[".mcp.json", Buffer.from("{ new }")], ["skills/x/SKILL.md", Buffer.from("new")]]), { base: new Map(), merge: mergeFile });
161
+ expect(result.written).toEqual([]);
162
+ expect(result.skipped).toEqual([
163
+ { path: ".mcp.json", class: "seed" },
164
+ { path: "skills/x/SKILL.md", class: "generated", command: "chant update" },
165
+ ]);
166
+ expect(readFileSync(join(dir, ".mcp.json"), "utf-8")).toBe("{}");
167
+ } finally {
168
+ rmSync(dir, { recursive: true, force: true });
169
+ }
170
+ });
171
+
172
+ test("mergeFile refuses a conflicting hunk and a binary file", () => {
173
+ const b = Buffer.from("a\nb\nc\n");
174
+ expect(mergeFile(b, Buffer.from("A\nb\nc\n"), Buffer.from("a\nb\nC\n"))?.toString()).toBe("A\nb\nC\n");
175
+ expect(mergeFile(b, Buffer.from("a\nX\nc\n"), Buffer.from("a\nY\nc\n"))).toBeNull();
176
+ expect(mergeFile(Buffer.from([0, 1]), Buffer.from([0, 2]), Buffer.from([0, 3]))).toBeNull();
177
+ });
178
+ });