@intentius/chant 0.80.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/dist/audit/discover.d.ts +8 -16
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-html.d.ts +5 -0
  4. package/dist/audit/report-html.d.ts.map +1 -1
  5. package/dist/audit/report-model.d.ts +13 -0
  6. package/dist/audit/report-model.d.ts.map +1 -1
  7. package/dist/audit/terraform-state.d.ts +4 -5
  8. package/dist/audit/terraform-state.d.ts.map +1 -1
  9. package/dist/cli/commands/audit.d.ts +25 -9
  10. package/dist/cli/commands/audit.d.ts.map +1 -1
  11. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  12. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  13. package/dist/cli/commands/doctor.d.ts.map +1 -1
  14. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/lint.d.ts.map +1 -1
  17. package/dist/cli/commands/update.d.ts +6 -0
  18. package/dist/cli/commands/update.d.ts.map +1 -1
  19. package/dist/cli/commands/vendor.d.ts +5 -0
  20. package/dist/cli/commands/vendor.d.ts.map +1 -1
  21. package/dist/cli/handlers/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/components.d.ts.map +1 -1
  23. package/dist/cli/handlers/dev.d.ts.map +1 -1
  24. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  25. package/dist/cli/handlers/init.d.ts.map +1 -1
  26. package/dist/cli/handlers/operator.d.ts +7 -0
  27. package/dist/cli/handlers/operator.d.ts.map +1 -1
  28. package/dist/cli/handlers/promote.d.ts +7 -4
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/run-generate.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts +4 -1
  32. package/dist/cli/handlers/run.d.ts.map +1 -1
  33. package/dist/cli/handlers/vendor.d.ts +8 -3
  34. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  35. package/dist/cli/main.d.ts +12 -0
  36. package/dist/cli/main.d.ts.map +1 -1
  37. package/dist/cli/plugins.d.ts +12 -7
  38. package/dist/cli/plugins.d.ts.map +1 -1
  39. package/dist/cli/registry.d.ts +32 -0
  40. package/dist/cli/registry.d.ts.map +1 -1
  41. package/dist/codegen/fetch.d.ts +10 -0
  42. package/dist/codegen/fetch.d.ts.map +1 -1
  43. package/dist/components/cli-support.d.ts +3 -1
  44. package/dist/components/cli-support.d.ts.map +1 -1
  45. package/dist/components/discover.d.ts.map +1 -1
  46. package/dist/components/driver.d.ts +16 -2
  47. package/dist/components/driver.d.ts.map +1 -1
  48. package/dist/components/gate-plan.d.ts +40 -0
  49. package/dist/components/gate-plan.d.ts.map +1 -0
  50. package/dist/components/promote.d.ts +60 -7
  51. package/dist/components/promote.d.ts.map +1 -1
  52. package/dist/components/sandbox/driver.d.ts.map +1 -1
  53. package/dist/components/sandbox/run.d.ts.map +1 -1
  54. package/dist/components/verbs/build-archive.d.ts +22 -11
  55. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  56. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  57. package/dist/config-static.d.ts +59 -0
  58. package/dist/config-static.d.ts.map +1 -0
  59. package/dist/config.d.ts +32 -10
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/content-digest.d.ts +17 -0
  62. package/dist/content-digest.d.ts.map +1 -0
  63. package/dist/discovery/files.d.ts +10 -5
  64. package/dist/discovery/files.d.ts.map +1 -1
  65. package/dist/discovery/fold-import.d.ts +20 -4
  66. package/dist/discovery/fold-import.d.ts.map +1 -1
  67. package/dist/discovery/index.d.ts.map +1 -1
  68. package/dist/discovery/sandbox/driver.d.ts +20 -0
  69. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  70. package/dist/discovery/sandbox/fork.d.ts +20 -0
  71. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  72. package/dist/discovery/walk.d.ts +119 -0
  73. package/dist/discovery/walk.d.ts.map +1 -0
  74. package/dist/graph-ir.d.ts +4 -1
  75. package/dist/graph-ir.d.ts.map +1 -1
  76. package/dist/join-key.d.ts +31 -0
  77. package/dist/join-key.d.ts.map +1 -0
  78. package/dist/lexicon-module.d.ts +51 -6
  79. package/dist/lexicon-module.d.ts.map +1 -1
  80. package/dist/lexicon.d.ts +58 -1
  81. package/dist/lexicon.d.ts.map +1 -1
  82. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  83. package/dist/lifecycle/digest.d.ts +5 -1
  84. package/dist/lifecycle/digest.d.ts.map +1 -1
  85. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  86. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  87. package/dist/lifecycle/git.d.ts +64 -0
  88. package/dist/lifecycle/git.d.ts.map +1 -1
  89. package/dist/lifecycle/lease.d.ts +8 -1
  90. package/dist/lifecycle/lease.d.ts.map +1 -1
  91. package/dist/lifecycle/legacy-digest.d.ts +41 -8
  92. package/dist/lifecycle/legacy-digest.d.ts.map +1 -1
  93. package/dist/lifecycle/member-ledger.d.ts +67 -0
  94. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  95. package/dist/lifecycle/plan-digest.d.ts +4 -3
  96. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  97. package/dist/lifecycle/release-ledger.d.ts +18 -2
  98. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  99. package/dist/lifecycle/status.d.ts +90 -5
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/op/activities/activity-contracts.d.ts +32 -0
  102. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  103. package/dist/op/activities/index.d.ts +2 -0
  104. package/dist/op/activities/index.d.ts.map +1 -1
  105. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  106. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  107. package/dist/op/discover.d.ts.map +1 -1
  108. package/dist/op/gate-name.d.ts +7 -0
  109. package/dist/op/gate-name.d.ts.map +1 -1
  110. package/dist/op/gate-summary.d.ts +16 -0
  111. package/dist/op/gate-summary.d.ts.map +1 -1
  112. package/dist/op/gate.d.ts +16 -2
  113. package/dist/op/gate.d.ts.map +1 -1
  114. package/dist/project-root.d.ts +26 -0
  115. package/dist/project-root.d.ts.map +1 -1
  116. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  117. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  118. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  119. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  120. package/dist/workspace/checks/generated.d.ts +156 -0
  121. package/dist/workspace/checks/generated.d.ts.map +1 -0
  122. package/dist/workspace/checks/ledgers.d.ts +78 -0
  123. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  124. package/dist/workspace/checks/links.d.ts +25 -0
  125. package/dist/workspace/checks/links.d.ts.map +1 -0
  126. package/dist/workspace/checks/pipelines.d.ts +76 -0
  127. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  128. package/dist/workspace/checks.d.ts +161 -0
  129. package/dist/workspace/checks.d.ts.map +1 -0
  130. package/dist/workspace/compose-graph.d.ts +115 -0
  131. package/dist/workspace/compose-graph.d.ts.map +1 -0
  132. package/dist/workspace/compose-reports.d.ts +68 -0
  133. package/dist/workspace/compose-reports.d.ts.map +1 -0
  134. package/dist/workspace/declaration.d.ts +220 -0
  135. package/dist/workspace/declaration.d.ts.map +1 -0
  136. package/dist/workspace/declaration.schema.json +434 -0
  137. package/dist/workspace/generated-files.d.ts +65 -0
  138. package/dist/workspace/generated-files.d.ts.map +1 -0
  139. package/dist/workspace/glob.d.ts +13 -0
  140. package/dist/workspace/glob.d.ts.map +1 -0
  141. package/dist/workspace/graph-cli.d.ts +75 -0
  142. package/dist/workspace/graph-cli.d.ts.map +1 -0
  143. package/dist/workspace/init.d.ts +96 -0
  144. package/dist/workspace/init.d.ts.map +1 -0
  145. package/dist/workspace/jsonc.d.ts +38 -0
  146. package/dist/workspace/jsonc.d.ts.map +1 -0
  147. package/dist/workspace/kinds.d.ts +192 -0
  148. package/dist/workspace/kinds.d.ts.map +1 -0
  149. package/dist/workspace/lineage-check.d.ts +84 -0
  150. package/dist/workspace/lineage-check.d.ts.map +1 -0
  151. package/dist/workspace/lineage-cli.d.ts +35 -0
  152. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  153. package/dist/workspace/lineage-init.d.ts +144 -0
  154. package/dist/workspace/lineage-init.d.ts.map +1 -0
  155. package/dist/workspace/lineage-lock.d.ts +271 -0
  156. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  157. package/dist/workspace/lineage-merge.d.ts +14 -0
  158. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  159. package/dist/workspace/lineage-migrations.d.ts +178 -0
  160. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  161. package/dist/workspace/lineage-update.d.ts +59 -0
  162. package/dist/workspace/lineage-update.d.ts.map +1 -0
  163. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  164. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  165. package/dist/workspace/lineage-upgrade.d.ts +152 -0
  166. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  167. package/dist/workspace/lineage-vendor.d.ts +63 -0
  168. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  169. package/dist/workspace/lineage-version.d.ts +32 -0
  170. package/dist/workspace/lineage-version.d.ts.map +1 -0
  171. package/dist/workspace/links.d.ts +150 -0
  172. package/dist/workspace/links.d.ts.map +1 -0
  173. package/dist/workspace/ls.d.ts +99 -0
  174. package/dist/workspace/ls.d.ts.map +1 -0
  175. package/dist/workspace/member-commands.d.ts +139 -0
  176. package/dist/workspace/member-commands.d.ts.map +1 -0
  177. package/dist/workspace/member-handles.d.ts +54 -0
  178. package/dist/workspace/member-handles.d.ts.map +1 -0
  179. package/dist/workspace/member-pipeline.d.ts +143 -0
  180. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  181. package/dist/workspace/member-run.d.ts +67 -0
  182. package/dist/workspace/member-run.d.ts.map +1 -0
  183. package/dist/workspace/reason-codes.d.ts +65 -0
  184. package/dist/workspace/reason-codes.d.ts.map +1 -0
  185. package/dist/workspace/records-cli.d.ts +28 -4
  186. package/dist/workspace/records-cli.d.ts.map +1 -1
  187. package/dist/workspace/records.d.ts.map +1 -1
  188. package/dist/workspace/root-refusal.d.ts +53 -0
  189. package/dist/workspace/root-refusal.d.ts.map +1 -0
  190. package/dist/workspace/status.d.ts +150 -0
  191. package/dist/workspace/status.d.ts.map +1 -0
  192. package/dist/workspace/template-manifest.d.ts +90 -0
  193. package/dist/workspace/template-manifest.d.ts.map +1 -0
  194. package/dist/workspace/tree.d.ts +36 -0
  195. package/dist/workspace/tree.d.ts.map +1 -0
  196. package/dist/workspace/trust/attestor.d.ts +67 -0
  197. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  198. package/dist/workspace/trust/policy.d.ts +111 -0
  199. package/dist/workspace/trust/policy.d.ts.map +1 -0
  200. package/dist/workspace/trust/provenance.d.ts +67 -0
  201. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  202. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  203. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  204. package/dist/workspace/trust/test-repo.d.ts +42 -0
  205. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  206. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  207. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  208. package/dist/workspace/trust/verify.d.ts +66 -0
  209. package/dist/workspace/trust/verify.d.ts.map +1 -0
  210. package/dist/workspace/which-chant.d.ts +58 -0
  211. package/dist/workspace/which-chant.d.ts.map +1 -0
  212. package/dist/workspace/workspace-kinds.schema.json +133 -0
  213. package/package.json +2 -1
  214. package/src/audit/discover.ts +66 -78
  215. package/src/audit/report-html.ts +3 -1
  216. package/src/audit/report-model.ts +14 -1
  217. package/src/audit/terraform-state.test.ts +4 -9
  218. package/src/audit/terraform-state.ts +4 -5
  219. package/src/cli/commands/audit-truncation.test.ts +167 -0
  220. package/src/cli/commands/audit.ts +89 -60
  221. package/src/cli/commands/check-lexicon.ts +24 -0
  222. package/src/cli/commands/doctor.test.ts +30 -0
  223. package/src/cli/commands/doctor.ts +18 -0
  224. package/src/cli/commands/import-agents.ts +6 -2
  225. package/src/cli/commands/init.ts +22 -3
  226. package/src/cli/commands/lint.test.ts +17 -23
  227. package/src/cli/commands/lint.ts +24 -84
  228. package/src/cli/commands/update.ts +13 -4
  229. package/src/cli/commands/vendor.ts +11 -6
  230. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  231. package/src/cli/handlers/build-member.test.ts +127 -0
  232. package/src/cli/handlers/build.ts +35 -1
  233. package/src/cli/handlers/components.test.ts +127 -0
  234. package/src/cli/handlers/components.ts +70 -8
  235. package/src/cli/handlers/dev.ts +5 -3
  236. package/src/cli/handlers/fan-out.ts +9 -2
  237. package/src/cli/handlers/init.ts +58 -1
  238. package/src/cli/handlers/operator.test.ts +68 -0
  239. package/src/cli/handlers/operator.ts +37 -9
  240. package/src/cli/handlers/promote.test.ts +118 -2
  241. package/src/cli/handlers/promote.ts +82 -16
  242. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  243. package/src/cli/handlers/run-generate.test.ts +6 -8
  244. package/src/cli/handlers/run-generate.ts +36 -1
  245. package/src/cli/handlers/run.test.ts +47 -0
  246. package/src/cli/handlers/run.ts +28 -5
  247. package/src/cli/handlers/vendor.ts +147 -43
  248. package/src/cli/main.test.ts +105 -1
  249. package/src/cli/main.ts +232 -17
  250. package/src/cli/plugins.ts +17 -17
  251. package/src/cli/registry.ts +32 -0
  252. package/src/cli/static-config-read.test.ts +208 -0
  253. package/src/codegen/fetch.test.ts +63 -1
  254. package/src/codegen/fetch.ts +30 -1
  255. package/src/components/cli-support.ts +6 -1
  256. package/src/components/discover.test.ts +27 -57
  257. package/src/components/discover.ts +13 -68
  258. package/src/components/driver.test.ts +78 -1
  259. package/src/components/driver.ts +43 -7
  260. package/src/components/gate-plan.ts +53 -0
  261. package/src/components/promote.test.ts +276 -2
  262. package/src/components/promote.ts +119 -15
  263. package/src/components/sandbox/driver.ts +8 -8
  264. package/src/components/sandbox/run.ts +4 -1
  265. package/src/components/verbs/build-archive.ts +27 -21
  266. package/src/components/verbs/run-agent.ts +3 -5
  267. package/src/config-static.test.ts +136 -0
  268. package/src/config-static.ts +232 -0
  269. package/src/config.test.ts +2 -2
  270. package/src/config.ts +48 -15
  271. package/src/content-digest.test.ts +120 -0
  272. package/src/content-digest.ts +21 -0
  273. package/src/discovery/files.test.ts +10 -10
  274. package/src/discovery/files.ts +25 -81
  275. package/src/discovery/fold-import.ts +49 -5
  276. package/src/discovery/index.ts +1 -0
  277. package/src/discovery/sandbox/driver.ts +73 -35
  278. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  279. package/src/discovery/sandbox/fork.ts +83 -43
  280. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  281. package/src/discovery/walk.test.ts +263 -0
  282. package/src/discovery/walk.ts +334 -0
  283. package/src/effect-receipt.ts +2 -2
  284. package/src/graph-ir.ts +4 -1
  285. package/src/join-key.test.ts +25 -0
  286. package/src/join-key.ts +39 -0
  287. package/src/lexicon-module.ts +108 -10
  288. package/src/lexicon.ts +79 -1
  289. package/src/lifecycle/build-ledger-store.ts +11 -10
  290. package/src/lifecycle/digest.ts +9 -5
  291. package/src/lifecycle/gate-ledger.test.ts +20 -0
  292. package/src/lifecycle/gate-ledger.ts +28 -6
  293. package/src/lifecycle/git.test.ts +13 -1
  294. package/src/lifecycle/git.ts +221 -176
  295. package/src/lifecycle/lease.ts +25 -10
  296. package/src/lifecycle/legacy-digest.test.ts +115 -23
  297. package/src/lifecycle/legacy-digest.ts +60 -19
  298. package/src/lifecycle/member-ledger.test.ts +319 -0
  299. package/src/lifecycle/member-ledger.ts +145 -0
  300. package/src/lifecycle/plan-digest.ts +7 -7
  301. package/src/lifecycle/release-ledger.test.ts +20 -0
  302. package/src/lifecycle/release-ledger.ts +23 -7
  303. package/src/lifecycle/status.test.ts +151 -1
  304. package/src/lifecycle/status.ts +228 -7
  305. package/src/op/activities/activity-contracts.ts +37 -0
  306. package/src/op/activities/index.ts +10 -0
  307. package/src/op/activities/propose-upgrade.ts +206 -0
  308. package/src/op/discover.test.ts +14 -4
  309. package/src/op/discover.ts +19 -27
  310. package/src/op/gate-name.ts +8 -0
  311. package/src/op/gate-summary.test.ts +14 -0
  312. package/src/op/gate-summary.ts +25 -2
  313. package/src/op/gate.ts +36 -7
  314. package/src/op/generate-pipeline.test.ts +14 -5
  315. package/src/project-root.test.ts +66 -1
  316. package/src/project-root.ts +48 -0
  317. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  318. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  319. package/src/workspace/behold-kinds.test.ts +158 -0
  320. package/src/workspace/check-contract.test.ts +127 -0
  321. package/src/workspace/check.schema.json +347 -0
  322. package/src/workspace/checks/generated.test.ts +287 -0
  323. package/src/workspace/checks/generated.ts +475 -0
  324. package/src/workspace/checks/ledgers.test.ts +139 -0
  325. package/src/workspace/checks/ledgers.ts +246 -0
  326. package/src/workspace/checks/links.ts +183 -0
  327. package/src/workspace/checks/pipelines.test.ts +133 -0
  328. package/src/workspace/checks/pipelines.ts +225 -0
  329. package/src/workspace/checks-repo.test.ts +71 -0
  330. package/src/workspace/checks.test.ts +450 -0
  331. package/src/workspace/checks.ts +461 -0
  332. package/src/workspace/compose-graph.test.ts +146 -0
  333. package/src/workspace/compose-graph.ts +228 -0
  334. package/src/workspace/compose-reports.test.ts +109 -0
  335. package/src/workspace/compose-reports.ts +186 -0
  336. package/src/workspace/declaration.schema.json +434 -0
  337. package/src/workspace/declaration.test.ts +347 -0
  338. package/src/workspace/declaration.ts +623 -0
  339. package/src/workspace/generated-files.test.ts +114 -0
  340. package/src/workspace/generated-files.ts +94 -0
  341. package/src/workspace/glob.ts +49 -0
  342. package/src/workspace/graph-cli.ts +220 -0
  343. package/src/workspace/graph-contract.test.ts +204 -0
  344. package/src/workspace/graph.schema.json +662 -0
  345. package/src/workspace/init.test.ts +223 -0
  346. package/src/workspace/init.ts +585 -0
  347. package/src/workspace/jsonc.test.ts +65 -0
  348. package/src/workspace/jsonc.ts +231 -0
  349. package/src/workspace/kinds.test.ts +288 -0
  350. package/src/workspace/kinds.ts +506 -0
  351. package/src/workspace/lineage-check.ts +264 -0
  352. package/src/workspace/lineage-cli.ts +134 -0
  353. package/src/workspace/lineage-init-dir.test.ts +243 -0
  354. package/src/workspace/lineage-init.test.ts +231 -0
  355. package/src/workspace/lineage-init.ts +560 -0
  356. package/src/workspace/lineage-lock.test.ts +224 -0
  357. package/src/workspace/lineage-lock.ts +365 -0
  358. package/src/workspace/lineage-merge.ts +44 -0
  359. package/src/workspace/lineage-migrations.test.ts +178 -0
  360. package/src/workspace/lineage-migrations.ts +343 -0
  361. package/src/workspace/lineage-update.ts +160 -0
  362. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  363. package/src/workspace/lineage-upgrade.test.ts +474 -0
  364. package/src/workspace/lineage-upgrade.ts +723 -0
  365. package/src/workspace/lineage-vendor.test.ts +126 -0
  366. package/src/workspace/lineage-vendor.ts +215 -0
  367. package/src/workspace/lineage-version.ts +127 -0
  368. package/src/workspace/links.test.ts +344 -0
  369. package/src/workspace/links.ts +331 -0
  370. package/src/workspace/ls-contract.test.ts +200 -0
  371. package/src/workspace/ls.schema.json +190 -0
  372. package/src/workspace/ls.ts +254 -0
  373. package/src/workspace/member-commands.e2e.test.ts +244 -0
  374. package/src/workspace/member-commands.test.ts +217 -0
  375. package/src/workspace/member-commands.ts +540 -0
  376. package/src/workspace/member-handles.test.ts +76 -0
  377. package/src/workspace/member-handles.ts +228 -0
  378. package/src/workspace/member-pipeline.test.ts +135 -0
  379. package/src/workspace/member-pipeline.ts +317 -0
  380. package/src/workspace/member-run.ts +146 -0
  381. package/src/workspace/read-contract.test.ts +129 -0
  382. package/src/workspace/reason-codes.test.ts +113 -0
  383. package/src/workspace/reason-codes.ts +81 -0
  384. package/src/workspace/records-cli.ts +86 -14
  385. package/src/workspace/records-contract.test.ts +5 -0
  386. package/src/workspace/records.schema.json +32 -2
  387. package/src/workspace/records.ts +3 -2
  388. package/src/workspace/root-refusal.ts +97 -0
  389. package/src/workspace/status-contract.test.ts +308 -0
  390. package/src/workspace/status.schema.json +210 -0
  391. package/src/workspace/status.ts +412 -0
  392. package/src/workspace/template-manifest.test.ts +79 -0
  393. package/src/workspace/template-manifest.ts +221 -0
  394. package/src/workspace/tree.ts +132 -0
  395. package/src/workspace/trust/attestor.ts +93 -0
  396. package/src/workspace/trust/policy.ts +306 -0
  397. package/src/workspace/trust/provenance.ts +181 -0
  398. package/src/workspace/trust/ssh-commit.ts +138 -0
  399. package/src/workspace/trust/test-repo.ts +138 -0
  400. package/src/workspace/trust/trust.test.ts +397 -0
  401. package/src/workspace/trust/verify-cli.ts +56 -0
  402. package/src/workspace/trust/verify.ts +194 -0
  403. package/src/workspace/which-chant.test.ts +117 -0
  404. package/src/workspace/which-chant.ts +151 -0
  405. package/src/workspace/workspace-kinds.schema.json +133 -0
  406. package/dist/components/unbound-gate-approval.d.ts +0 -24
  407. package/dist/components/unbound-gate-approval.d.ts.map +0 -1
  408. package/dist/discovery/convergence.d.ts +0 -97
  409. package/dist/discovery/convergence.d.ts.map +0 -1
  410. package/src/cli/commands/audit-walk-warnings.test.ts +0 -156
  411. package/src/components/unbound-gate-approval.test.ts +0 -82
  412. package/src/components/unbound-gate-approval.ts +0 -51
  413. package/src/discovery/convergence.test.ts +0 -231
  414. package/src/discovery/convergence.ts +0 -399
  415. package/src/op/discover-convergence.test.ts +0 -70
@@ -0,0 +1,264 @@
1
+ /**
2
+ * `chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated]`:
3
+ * the lineage checks (#2550, D9), and the declaration checks (#2535, D16)
4
+ * when a declaration sits between the current directory and the git root.
5
+ *
6
+ * D9 says open manual steps fail `check`. So far, `check` checks the
7
+ * lineage lock only: the lock must be readable, and no scope may have an open
8
+ * manual step. It needs no `chant.workspace.json`, the way `chant workspace
9
+ * lineage` needs none, and a directory without a lock passes with nothing to
10
+ * check. The declaration checks report `WSP` findings through lint's
11
+ * reporters (`./checks.ts`), and cover member ledgers (#2538), recorded
12
+ * pipelines (#2542) and generated files (#2541) as well as the declaration
13
+ * itself (#2641). `--generated` runs each declared generator too.
14
+ *
15
+ * `chant workspace upgrade` runs the same checks in its staging worktree
16
+ * before it reaches its gate.
17
+ */
18
+
19
+ import { realpathSync } from "node:fs";
20
+ import { join, relative, resolve, sep } from "node:path";
21
+ import { formatError, formatSuccess } from "../cli/format";
22
+ import type { CommandContext } from "../cli/registry";
23
+ import type { LintDiagnostic, LintRule } from "../lint/rule";
24
+ import { findWorkspaceRoot } from "../project-root";
25
+ import type { DeclarationCheckReport } from "./checks";
26
+ import { LOCK_FILE, LockError, parseLock, readLock } from "./lineage-lock";
27
+ import type { ReasonCode } from "./reason-codes";
28
+ import type { WorkspaceTree } from "./tree";
29
+
30
+ /** Closed: a reader may switch on it. */
31
+ export const CHECK_CODES = ["lock-invalid", "manual-step-open"] as const satisfies readonly ReasonCode[];
32
+ export type CheckCode = (typeof CHECK_CODES)[number];
33
+
34
+ export interface CheckFinding {
35
+ code: CheckCode;
36
+ /** The scope the finding is in, or null for the lock as a whole. */
37
+ scope: string | null;
38
+ /** The file, relative to the lock's directory, when the finding is about one. */
39
+ path?: string;
40
+ message: string;
41
+ }
42
+
43
+ export interface CheckReport {
44
+ lock: string | null;
45
+ ok: boolean;
46
+ findings: CheckFinding[];
47
+ }
48
+
49
+ /**
50
+ * Run the lineage checks over the lock at `root`. Never throws a
51
+ * {@link LockError}. `lockText` is the lock's text when it comes from
52
+ * somewhere other than the disk, such as a revision (`--at`), with null
53
+ * for no lock.
54
+ */
55
+ export function checkLineage(root: string, lockText?: string | null): CheckReport {
56
+ let lock;
57
+ try {
58
+ lock = lockText === undefined ? readLock(root) : lockText === null ? null : parseLock(lockText);
59
+ } catch (err) {
60
+ if (!(err instanceof LockError)) throw err;
61
+ return { lock: LOCK_FILE, ok: false, findings: [{ code: "lock-invalid", scope: null, path: LOCK_FILE, message: err.message }] };
62
+ }
63
+ if (!lock) return { lock: null, ok: true, findings: [] };
64
+ const findings: CheckFinding[] = [];
65
+ for (const [scope, lineage] of Object.entries(lock.scopes)) {
66
+ for (const step of lineage.manualSteps) {
67
+ const path = scope === "." ? step.path : `${scope}/${step.path}`;
68
+ findings.push({
69
+ code: "manual-step-open",
70
+ scope,
71
+ path,
72
+ message: `manual step open (${step.reason}): merge by hand, then \`chant workspace lineage resolve ${path}\``,
73
+ });
74
+ }
75
+ }
76
+ return { lock: LOCK_FILE, ok: findings.length === 0, findings };
77
+ }
78
+
79
+ /** A finding's identity, for comparing two reports. */
80
+ export function findingKey(f: CheckFinding): string {
81
+ return `${f.code}\0${f.scope ?? ""}\0${f.path ?? ""}`;
82
+ }
83
+
84
+ const USAGE = "chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated]";
85
+ const FORMATS = ["stylish", "json", "sarif"] as const;
86
+
87
+ /** A lock finding as a lint diagnostic, for `--format json` and `--format sarif`. */
88
+ function lockDiagnostic(f: CheckFinding): LintDiagnostic {
89
+ return { file: f.path ?? LOCK_FILE, line: 1, column: 1, ruleId: f.code, severity: "error", message: f.message };
90
+ }
91
+
92
+ const LOCK_RULES: LintRule[] = [
93
+ { id: "lock-invalid", severity: "error", category: "correctness", description: "The lineage lock can be read.", helpUri: "https://intentius.io/chant/cli/workspace-check/", check: () => [] },
94
+ {
95
+ id: "manual-step-open",
96
+ severity: "error",
97
+ category: "correctness",
98
+ description: "No scope in the lineage lock has an open manual step.",
99
+ helpUri: "https://intentius.io/chant/cli/workspace-check/",
100
+ check: () => [],
101
+ },
102
+ ];
103
+
104
+ /** SARIF says `inSource` for a comment in the code; a declaration's `suppress` is `external`. */
105
+ function markExternalSuppressions(sarif: string): string {
106
+ const doc = JSON.parse(sarif) as { runs: { results: { suppressions?: { kind: string }[] }[] }[] };
107
+ for (const run of doc.runs) for (const r of run.results) for (const s of r.suppressions ?? []) s.kind = "external";
108
+ return JSON.stringify(doc, null, 2);
109
+ }
110
+
111
+ /** The path from tree directory `from` to tree directory `to`, both relative to the git root. */
112
+ function posixRelative(from: string, to: string): string {
113
+ return relative(`/${from}`, `/${to}`) || ".";
114
+ }
115
+
116
+ /** The version of the `check` document this chant writes. */
117
+ export const CHECK_CONTRACT_VERSION = 1;
118
+
119
+ /** `$id` of the JSON Schema for the `--format json` output, shipped beside this file. */
120
+ export const CHECK_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/check/v1/check.schema.json";
121
+
122
+ /** Why `check` could not run at all: only the `--at` codes. A declaration that can't be read is a WSP001 finding instead. */
123
+ export const CHECK_ERROR_CODES = ["not-a-git-repository", "revision-unknown"] as const satisfies readonly ReasonCode[];
124
+ export type CheckErrorCode = (typeof CHECK_ERROR_CODES)[number];
125
+
126
+ /** What `check --format json` prints (#2536). `check.schema.json` describes it. */
127
+ export type CheckDocument =
128
+ | (CheckReport & {
129
+ $schema: string;
130
+ contract: number;
131
+ chant: string;
132
+ at: string | null;
133
+ /** The declaration's workspace, or null when there is none; `name` is null when it can't be read. */
134
+ workspace: { name: string | null; root: string } | null;
135
+ declaration?: DeclarationCheckReport;
136
+ })
137
+ | { $schema: string; contract: number; chant: string; error: { code: CheckErrorCode; message: string; location: null } };
138
+
139
+ /**
140
+ * Run every check from `cwd`, in the working tree or at revision `at`, and
141
+ * build the document the JSON formats print. Paths in findings are relative
142
+ * to `cwd`. `runGenerators` is `--generated`; with `at` it does nothing,
143
+ * since the member facts describe the working tree, not the revision.
144
+ */
145
+ export async function runChecks(cwd: string, at?: string, options: { runGenerators?: boolean } = {}): Promise<CheckDocument> {
146
+ // Loaded here, not at the top: `workspace upgrade` imports this module for checkLineage alone.
147
+ const [{ findDeclarationDir, readDeclaration, readerVersion }, { gitTop, gitTree, resolveCommit, workingTree }] = await Promise.all([
148
+ import("./declaration"),
149
+ import("./tree"),
150
+ ]);
151
+ const head = { $schema: CHECK_OUTPUT_SCHEMA_ID, contract: CHECK_CONTRACT_VERSION, chant: readerVersion() };
152
+ let report: CheckReport;
153
+ /** The declaration's directory on disk, its root relative to the git root, and the files to check. */
154
+ let found: { dir: string; root: string; tree: WorkspaceTree } | undefined;
155
+ let commit: string | null = null;
156
+ if (at === undefined) {
157
+ report = checkLineage(cwd);
158
+ // Declaration checks run when a declaration sits between here and the git
159
+ // root (#2535). Without one the command stays a lock check.
160
+ const f = findWorkspaceRoot(cwd);
161
+ if (f) {
162
+ const top = gitTop(f.dir);
163
+ const rel = top ? relative(top, realpathSync(f.dir)).split(sep).join("/") : f.dir;
164
+ found = { dir: f.dir, root: rel === "" ? "." : rel, tree: workingTree(f.dir) };
165
+ }
166
+ } else {
167
+ const top = gitTop(cwd);
168
+ if (!top) return { ...head, error: { code: "not-a-git-repository", message: "--at reads git objects, and this directory is not in a git repository", location: null } };
169
+ const c = resolveCommit(top, at);
170
+ if (!c) return { ...head, error: { code: "revision-unknown", message: `--at ${at} names no commit in this repository`, location: null } };
171
+ commit = c;
172
+ const rel = relative(top, realpathSync(resolve(cwd))).split(sep).join("/");
173
+ const start = rel.startsWith("..") ? "" : rel;
174
+ const whole = gitTree(top, c);
175
+ const lockAt = start ? `${start}/${LOCK_FILE}` : LOCK_FILE;
176
+ report = checkLineage(cwd, whole.stat(lockAt) === "file" ? whole.read(lockAt) : null);
177
+ const dir = findDeclarationDir(whole, start);
178
+ // The root on disk is reached from cwd, so paths print relative to cwd as it was given.
179
+ if (dir !== undefined) found = { dir: resolve(cwd, posixRelative(start, dir)), root: dir === "" ? "." : dir, tree: dir === "" ? whole : gitTree(top, c, dir) };
180
+ }
181
+
182
+ let declaration: DeclarationCheckReport | undefined;
183
+ let workspace: { name: string | null; root: string } | null = null;
184
+ if (found) {
185
+ const { runDeclarationChecks } = await import("./checks");
186
+ const { dir, tree } = found;
187
+ declaration = await runDeclarationChecks(dir, (file) => relative(cwd, join(dir, file)).split(sep).join("/"), {
188
+ runGenerators: options.runGenerators === true,
189
+ ...(commit !== null ? { tree } : {}),
190
+ });
191
+ let name: string | null = null;
192
+ try {
193
+ name = readDeclaration(tree, "", { rootChant: true }).name;
194
+ } catch {
195
+ // The WSP001 finding already says why.
196
+ }
197
+ workspace = { name, root: found.root };
198
+ }
199
+ const ok = report.ok && (declaration?.ok ?? true);
200
+ return { ...head, at: commit, workspace, ...report, ok, ...(declaration ? { declaration } : {}) };
201
+ }
202
+
203
+ export async function runWorkspaceCheck(ctx: CommandContext): Promise<number> {
204
+ const root = process.cwd();
205
+ if (ctx.args.extraPositional) {
206
+ console.error(formatError({ message: `chant workspace check takes no argument (got ${ctx.args.extraPositional})`, hint: USAGE }));
207
+ return 1;
208
+ }
209
+ const format = (ctx.args.format || "stylish") as (typeof FORMATS)[number];
210
+ if (!FORMATS.includes(format)) {
211
+ console.error(formatError({ message: `--format ${format} is not a check format; use stylish, json or sarif`, hint: USAGE }));
212
+ return 1;
213
+ }
214
+ // The root's chant reads the declaration (ws-021). Only a workspace with a
215
+ // declaration loads this.
216
+ if (ctx.args.at !== undefined || findWorkspaceRoot(root)) {
217
+ const { handToRootChant } = await import("./which-chant");
218
+ const handed = await handToRootChant(root, ctx.args.at);
219
+ if (handed !== undefined) return handed;
220
+ }
221
+ const doc = await runChecks(root, ctx.args.at, { runGenerators: ctx.args.generated === true });
222
+ if ("error" in doc) {
223
+ if (ctx.args.json || format === "json") console.log(JSON.stringify(doc, null, 2));
224
+ else console.error(formatError({ message: `${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
225
+ return 1;
226
+ }
227
+ const { declaration, ok } = doc;
228
+ const report = { lock: doc.lock, findings: doc.findings };
229
+
230
+ // --format json is the read contract's document. --json keeps the shape it
231
+ // had before the contract, the lineage report with `declaration` beside it.
232
+ if (format === "json") {
233
+ console.log(JSON.stringify(doc, null, 2));
234
+ return ok ? 0 : 1;
235
+ }
236
+ if (ctx.args.json) {
237
+ console.log(JSON.stringify({ lock: doc.lock, ok, findings: doc.findings, ...(declaration ? { declaration } : {}) }, null, 2));
238
+ return ok ? 0 : 1;
239
+ }
240
+ // The reporters load only when there is something to report through them,
241
+ // so a plain lock check loads what it did before.
242
+ if (format === "sarif") {
243
+ const { formatSarif } = await import("../cli/reporters/stylish");
244
+ const diagnostics = [...report.findings.map(lockDiagnostic), ...(declaration?.diagnostics ?? [])];
245
+ const suppressed = declaration?.suppressed ?? [];
246
+ const { workspaceCheckRules } = await import("./checks");
247
+ console.log(markExternalSuppressions(formatSarif(diagnostics, [...LOCK_RULES, ...workspaceCheckRules()], suppressed, doc.chant)));
248
+ return ok ? 0 : 1;
249
+ }
250
+
251
+ if (!report.lock) {
252
+ if (!declaration) console.error(formatSuccess(`no ${LOCK_FILE}: nothing to check`));
253
+ } else if (report.findings.length === 0) {
254
+ console.error(formatSuccess(`${LOCK_FILE}: no open manual steps`));
255
+ } else {
256
+ for (const f of report.findings) console.error(` ${f.path ?? f.scope ?? ""}: ${f.message}`);
257
+ console.error(formatError({ message: `${report.findings.length} check(s) failed` }));
258
+ }
259
+ if (declaration) {
260
+ const { formatStylish } = await import("../cli/reporters/stylish");
261
+ console.log(formatStylish(declaration.diagnostics, declaration.suppressed));
262
+ }
263
+ return ok ? 0 : 1;
264
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * `chant workspace lineage [--json]` and
3
+ * `chant workspace lineage resolve <path>` (#2540).
4
+ *
5
+ * The first shows each scope in `.chant/workspace.lock.json`: where it came
6
+ * from, which files the project has edited since, and which manual steps are
7
+ * open. The second closes a manual step once the file has been merged by
8
+ * hand. Neither needs a `chant.workspace.json`: lineage works for a plain
9
+ * project (#2525 rule 4).
10
+ */
11
+
12
+ import { formatError, formatSuccess } from "../cli/format";
13
+ import type { CommandContext } from "../cli/registry";
14
+ import { LOCK_FILE, LockError, readLock, resolveManualStep, scopeStatus, writeLock, type LineageSource, type ManualStep } from "./lineage-lock";
15
+
16
+ const USAGE = "chant workspace lineage [--json] | chant workspace lineage resolve <path>";
17
+
18
+ export interface LineageScopeView {
19
+ scope: string;
20
+ kind: "template" | "vendor";
21
+ name?: string;
22
+ template: string;
23
+ /** Where the files came from, as the lock records it: its `type` and the repository, directory, lexicon or URL. */
24
+ source: LineageSource;
25
+ ref?: string;
26
+ address: Record<string, unknown> | null;
27
+ files: number;
28
+ edited: string[];
29
+ missing: string[];
30
+ manualSteps: ManualStep[];
31
+ }
32
+
33
+ /** One line naming a scope's source kind and where it points (#2647). */
34
+ export function describeSource(source: LineageSource): string {
35
+ switch (source.type) {
36
+ case "git":
37
+ return `git ${source.repo}${source.path ? `#${source.path}` : ""}`;
38
+ case "dir":
39
+ return `dir ${source.path}${source.member ? `#${source.member}` : ""}`;
40
+ case "lexicon":
41
+ return `lexicon ${source.lexicon}/${source.template}`;
42
+ case "local":
43
+ return `local ${source.path}`;
44
+ case "archive":
45
+ return `archive ${source.url}${source.subpath ? `#${source.subpath}` : ""}`;
46
+ }
47
+ }
48
+
49
+ /** The lock as `--json` prints it: one view per scope, with its state in the tree. */
50
+ export function lineageView(root: string): { lock: string; scopes: LineageScopeView[] } | null {
51
+ const lock = readLock(root);
52
+ if (!lock) return null;
53
+ const scopes = Object.entries(lock.scopes).map(([scope, lineage]) => {
54
+ const st = scopeStatus(root, scope, lineage);
55
+ return {
56
+ scope,
57
+ kind: lineage.kind,
58
+ ...(lineage.name !== undefined ? { name: lineage.name } : {}),
59
+ template: lineage.template,
60
+ source: lineage.source,
61
+ ...(lineage.ref !== undefined ? { ref: lineage.ref } : {}),
62
+ address: lineage.address,
63
+ files: Object.keys(lineage.files).length,
64
+ edited: st.customised,
65
+ missing: st.missing,
66
+ manualSteps: lineage.manualSteps,
67
+ };
68
+ });
69
+ return { lock: LOCK_FILE, scopes };
70
+ }
71
+
72
+ export async function runWorkspaceLineage(ctx: CommandContext): Promise<number> {
73
+ const { args } = ctx;
74
+ const root = process.cwd();
75
+ try {
76
+ if (args.extraPositional === "resolve") return resolve(root, args.extraPositional2);
77
+ if (args.extraPositional) {
78
+ console.error(formatError({ message: `Unknown lineage subcommand: ${args.extraPositional}`, hint: USAGE }));
79
+ return 1;
80
+ }
81
+ const view = lineageView(root);
82
+ if (!view) {
83
+ console.error(formatError({ message: `no ${LOCK_FILE} in ${root}`, hint: "chant init --from or --template writes one; so does chant vendor migrate" }));
84
+ return 1;
85
+ }
86
+ if (args.json) {
87
+ console.log(JSON.stringify(view, null, 2));
88
+ return 0;
89
+ }
90
+ const lines: string[] = [];
91
+ for (const s of view.scopes) {
92
+ const pin = s.ref ? `@${s.ref}` : "";
93
+ const at = typeof s.address?.commit === "string" ? ` (${s.address.commit.slice(0, 12)})` : "";
94
+ lines.push(`${s.scope} ${s.kind}${s.name ? ` ${s.name}` : ""} ${s.template}${pin}${at}`);
95
+ lines.push(` source: ${describeSource(s.source)}`);
96
+ lines.push(` ${s.files} file(s), ${s.edited.length} edited, ${s.missing.length} deleted`);
97
+ for (const m of s.manualSteps) lines.push(` manual step: ${s.scope === "." ? "" : `${s.scope}/`}${m.path} (${m.reason})`);
98
+ }
99
+ const open = view.scopes.reduce((n, s) => n + s.manualSteps.length, 0);
100
+ lines.push(`${view.scopes.length} scope(s), ${open} manual step(s) open`);
101
+ console.log(lines.join("\n"));
102
+ return 0;
103
+ } catch (err) {
104
+ if (!(err instanceof LockError)) throw err;
105
+ console.error(formatError({ message: err.message }));
106
+ return 1;
107
+ }
108
+ }
109
+
110
+ function resolve(root: string, path: string | undefined): number {
111
+ if (!path) {
112
+ console.error(formatError({ message: "resolve needs the path of a file with an open manual step", hint: USAGE }));
113
+ return 1;
114
+ }
115
+ const lock = readLock(root);
116
+ if (!lock) {
117
+ console.error(formatError({ message: `no ${LOCK_FILE} in ${root}` }));
118
+ return 1;
119
+ }
120
+ const closed = resolveManualStep(lock, path);
121
+ if (!closed) {
122
+ console.error(formatError({ message: `no open manual step for ${path}`, hint: "chant workspace lineage lists them" }));
123
+ return 1;
124
+ }
125
+ writeLock(root, lock);
126
+ console.error(
127
+ formatSuccess(
128
+ closed.step.upstream === null
129
+ ? `${path}: the source removed this file; it is now the project's own`
130
+ : `${path}: resolved; the file as it stands is kept, and the source's version is its new merge base`,
131
+ ),
132
+ );
133
+ return 0;
134
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * chant #2647: `chant init --from <dir>[#<member>]`, a template directory on
3
+ * disk. The same files and parameters as the git form for the same tree, a
4
+ * lock whose address is the digest alone, and `chant workspace upgrade`
5
+ * from a directory, which rebuilds its merge base only while the recorded
6
+ * directory still holds what the scope was made from.
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
10
+ import { execFileSync } from "node:child_process";
11
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { dirname, join } from "node:path";
14
+ import { initFromCommand, parseDirSpec, recordedDirPath } from "./lineage-init";
15
+ import { LOCK_FILE, readLock } from "./lineage-lock";
16
+ import { describeSource, lineageView } from "./lineage-cli";
17
+ import { stageUpgrade, type ChantRunner } from "./lineage-upgrade";
18
+
19
+ const ENV = { ...process.env, GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t" };
20
+
21
+ let root: string;
22
+
23
+ function git(cwd: string, args: string[]): string {
24
+ return execFileSync("git", args, { cwd, encoding: "utf-8", env: ENV, stdio: ["ignore", "pipe", "pipe"] }).trim();
25
+ }
26
+ function put(base: string, rel: string, content: string): void {
27
+ mkdirSync(dirname(join(base, rel)), { recursive: true });
28
+ writeFileSync(join(base, rel), content);
29
+ }
30
+ function read(base: string, rel: string): string {
31
+ return readFileSync(join(base, rel), "utf-8");
32
+ }
33
+ /** Every file under `dir` except the lock, with its content and executable bit. */
34
+ function tree(dir: string, prefix = ""): Record<string, string> {
35
+ const out: Record<string, string> = {};
36
+ for (const e of readdirSync(join(dir, prefix), { withFileTypes: true })) {
37
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
38
+ if (e.isDirectory()) Object.assign(out, tree(dir, rel));
39
+ else if (rel !== LOCK_FILE) out[rel] = `${statSync(join(dir, rel)).mode & 0o100 ? "x " : ""}${readFileSync(join(dir, rel), "utf-8")}`;
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /** A template directory with a manifest, an executable, a seed and a lock of its own. */
45
+ function writeTemplate(dir: string, version = "1"): void {
46
+ put(dir, "svc/README.md", `template v${version}\n`);
47
+ put(dir, "svc/src/main.ts", 'export const name = "{{chant:name}}";\n');
48
+ put(dir, "svc/run.sh", "#!/bin/sh\n");
49
+ execFileSync("chmod", ["+x", join(dir, "svc/run.sh")]);
50
+ put(dir, "svc/.mcp.json", "{}\n");
51
+ put(dir, `svc/${LOCK_FILE}`, JSON.stringify({ lockVersion: 1, scopes: {} }));
52
+ put(
53
+ dir,
54
+ "svc/chant.template.json",
55
+ JSON.stringify({ parameters: { name: { type: "string", default: "app" }, owner: { type: "string", default: "ops" } }, files: ["src/main.ts"] }),
56
+ );
57
+ }
58
+
59
+ beforeEach(() => {
60
+ root = realpathSync(mkdtempSync(join(tmpdir(), "chant-init-dir-test-")));
61
+ vi.spyOn(console, "log").mockImplementation(() => undefined);
62
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
63
+ });
64
+ afterEach(() => {
65
+ vi.restoreAllMocks();
66
+ rmSync(root, { recursive: true, force: true });
67
+ });
68
+
69
+ describe("parseDirSpec", () => {
70
+ test("an existing directory is the directory form, with or without #<member>", () => {
71
+ mkdirSync(join(root, "tpl", "svc"), { recursive: true });
72
+ expect(parseDirSpec("tpl#svc", root)).toEqual({ kind: "dir", path: "tpl", abs: join(root, "tpl"), member: "svc" });
73
+ expect(parseDirSpec(join(root, "tpl"), root)).toEqual({ kind: "dir", path: join(root, "tpl"), abs: join(root, "tpl") });
74
+ });
75
+ test("a directory whose name parses as <repo>@<ref> is still a directory", () => {
76
+ mkdirSync(join(root, "starter@v1"));
77
+ expect(parseDirSpec("starter@v1", root)).toMatchObject({ kind: "dir", abs: join(root, "starter@v1") });
78
+ });
79
+ test("<repo>@<ref> with no such directory is the git form", () => {
80
+ expect(parseDirSpec("acme/starter@v1.2.0#service", root)).toBeNull();
81
+ });
82
+ test("a path with no @ that is not a directory is refused", () => {
83
+ expect(() => parseDirSpec("missing", root)).toThrow(/no directory missing, and not <repo>@<ref>/);
84
+ put(root, "file", "x");
85
+ expect(() => parseDirSpec("file", root)).toThrow(/file is not a directory/);
86
+ mkdirSync(join(root, "tpl"));
87
+ expect(() => parseDirSpec("tpl#../x", root)).toThrow(/not a directory in the directory/);
88
+ });
89
+ test("a relative directory is recorded from the project, an absolute one as given", () => {
90
+ expect(recordedDirPath("../tpl", join(root, "tpl"), join(root, "proj"))).toBe("../tpl");
91
+ expect(recordedDirPath("tpl", join(root, "tpl"), root)).toBe("./tpl");
92
+ expect(recordedDirPath(join(root, "tpl"), join(root, "tpl"), join(root, "proj"))).toBe(join(root, "tpl"));
93
+ });
94
+ });
95
+
96
+ describe("chant init --from <dir>", () => {
97
+ test("copies the same files, with the same parameters and digest, as the git form of the same tree", async () => {
98
+ const tpl = join(root, "tpl");
99
+ writeTemplate(tpl);
100
+ git(tpl, ["init", "-q", "-b", "main"]);
101
+ git(tpl, ["add", "-A"]);
102
+ git(tpl, ["commit", "-q", "-m", "v1"]);
103
+
104
+ const fromGit = await initFromCommand({ from: `${tpl}@main#svc`, path: join(root, "a"), params: { name: "billing" } });
105
+ const fromDir = await initFromCommand({ from: `${tpl}#svc`, path: join(root, "b"), params: { name: "billing" } });
106
+ expect(fromGit.error).toBeUndefined();
107
+ expect(fromDir.error).toBeUndefined();
108
+ expect(fromDir.createdFiles).toEqual(fromGit.createdFiles);
109
+ expect(tree(join(root, "b"))).toEqual(tree(join(root, "a")));
110
+ expect(read(join(root, "b"), "src/main.ts")).toBe('export const name = "billing";\n');
111
+ expect(statSync(join(root, "b", "run.sh")).mode & 0o111).not.toBe(0);
112
+ expect(existsSync(join(root, "b", "chant.template.json"))).toBe(false);
113
+
114
+ const a = readLock(join(root, "a"))!.scopes["."];
115
+ const b = readLock(join(root, "b"))!.scopes["."];
116
+ expect(b.parameters).toEqual({ name: "billing", owner: "ops" });
117
+ expect(b.parameters).toEqual(a.parameters);
118
+ expect(b.files).toEqual(a.files);
119
+ expect(b.address).toEqual({ digest: a.address!.digest });
120
+ // The lock shape: the digest alone, a dir source, no ref.
121
+ expect(b.source).toEqual({ type: "dir", path: tpl, member: "svc" });
122
+ expect(b.template).toBe(`dir:${tpl}#svc`);
123
+ expect(b.ref).toBeUndefined();
124
+ expect(fromDir.template).toBe(`dir:${tpl}#svc`);
125
+ expect(fromDir.commit).toBeUndefined();
126
+ expect(a.source).toMatchObject({ type: "git", path: "svc" });
127
+ });
128
+
129
+ test("inside a git checkout, .gitignore is respected and untracked files are copied", async () => {
130
+ const tpl = join(root, "tpl");
131
+ git(root, ["init", "-q", "-b", "main"]);
132
+ put(tpl, "keep.ts", "keep\n");
133
+ put(tpl, ".gitignore", "out/\n*.log\n");
134
+ put(tpl, "out/built.js", "built\n");
135
+ put(tpl, "debug.log", "noise\n");
136
+ git(root, ["add", "tpl/keep.ts", "tpl/.gitignore"]);
137
+ git(root, ["commit", "-q", "-m", "t"]);
138
+ put(tpl, "untracked.ts", "new\n");
139
+
140
+ const result = await initFromCommand({ from: tpl, path: join(root, "proj") });
141
+ expect(result.error).toBeUndefined();
142
+ expect(result.createdFiles.sort()).toEqual([".gitignore", LOCK_FILE, "keep.ts", "untracked.ts"].sort());
143
+ });
144
+
145
+ test("outside git every file is copied, except node_modules, symbolic links and the template's own lock", async () => {
146
+ const tpl = join(root, "tpl");
147
+ writeTemplate(tpl);
148
+ put(tpl, "svc/.gitignore", "*.log\n");
149
+ put(tpl, "svc/debug.log", "copied: no git to read .gitignore\n");
150
+ put(tpl, "svc/node_modules/dep/index.js", "x\n");
151
+ symlinkSync("README.md", join(tpl, "svc", "link.md"));
152
+
153
+ const result = await initFromCommand({ from: `${tpl}#svc`, path: join(root, "proj") });
154
+ expect(result.error).toBeUndefined();
155
+ expect(result.createdFiles.sort()).toEqual([".gitignore", ".mcp.json", LOCK_FILE, "README.md", "debug.log", "run.sh", "src/main.ts"].sort());
156
+ expect(result.warnings).toEqual(["link.md is a symbolic link, not copied", "node_modules is node_modules, not copied"]);
157
+ expect(readLock(join(root, "proj"))!.scopes["."].files).not.toHaveProperty(LOCK_FILE);
158
+ });
159
+
160
+ test("refuses a missing member, a missing directory and an undeclared --param, writing nothing", async () => {
161
+ const tpl = join(root, "tpl");
162
+ writeTemplate(tpl);
163
+ const target = join(root, "proj");
164
+ expect((await initFromCommand({ from: `${tpl}#nope`, path: target })).error).toBe(`${tpl} has no directory nope`);
165
+ expect((await initFromCommand({ from: join(root, "missing"), path: target })).error).toMatch(/no directory .*missing, and not <repo>@<ref>/);
166
+ expect((await initFromCommand({ from: `${tpl}#svc`, path: target, params: { title: "x" } })).error).toBe("unknown parameter title (declared: name, owner)");
167
+ expect(existsSync(target)).toBe(false);
168
+ });
169
+
170
+ test("chant workspace lineage names the source kind and path", async () => {
171
+ const tpl = join(root, "tpl");
172
+ writeTemplate(tpl);
173
+ await initFromCommand({ from: `${tpl}#svc`, path: join(root, "proj") });
174
+ const view = lineageView(join(root, "proj"))!;
175
+ expect(view.scopes[0].source).toEqual({ type: "dir", path: tpl, member: "svc" });
176
+ expect(describeSource(view.scopes[0].source)).toBe(`dir ${tpl}#svc`);
177
+ expect(describeSource({ type: "git", repo: "acme/starter", url: "https://github.com/acme/starter.git", path: "svc" })).toBe("git acme/starter#svc");
178
+ });
179
+ });
180
+
181
+ describe("chant workspace upgrade on a directory scope", () => {
182
+ const passing: ChantRunner = async () => ({ exitCode: 0, output: "" });
183
+ let v1: string;
184
+ let v2: string;
185
+ let proj: string;
186
+
187
+ beforeEach(async () => {
188
+ v1 = join(root, "bundle-v1");
189
+ v2 = join(root, "bundle-v2");
190
+ proj = join(root, "proj");
191
+ writeTemplate(v1);
192
+ put(v1, "svc/src/lib.ts", "one\ntwo\nthree\nfour\nfive\nsix\nseven\n");
193
+ const made = await initFromCommand({ from: `${v1}#svc`, path: proj, params: { name: "billing" } });
194
+ expect(made.error).toBeUndefined();
195
+ git(proj, ["init", "-q", "-b", "main"]);
196
+ put(proj, "src/lib.ts", "ONE (ours)\ntwo\nthree\nfour\nfive\nsix\nseven\n");
197
+ git(proj, ["add", "-A"]);
198
+ git(proj, ["commit", "-q", "-m", "init and edit"]);
199
+
200
+ cpSync(v1, v2, { recursive: true });
201
+ put(v2, "svc/README.md", "template v2\n");
202
+ put(v2, "svc/src/lib.ts", "one\ntwo\nthree\nfour\nfive\nsix\nSEVEN (theirs)\n");
203
+ put(v2, "svc/src/new.ts", "new\n");
204
+ });
205
+
206
+ test("without --to it refuses, naming adopt-lineage", async () => {
207
+ await expect(stageUpgrade({ root: proj, runChant: passing })).rejects.toThrow(/no --to directory\. .*has no git history.*chant workspace adopt-lineage/);
208
+ });
209
+
210
+ test("with --to a directory, the recorded directory is the merge base while it holds the same digest", async () => {
211
+ const staged = await stageUpgrade({ root: proj, to: v2, runChant: passing });
212
+ try {
213
+ expect(staged.merged).toEqual(["src/lib.ts"]);
214
+ expect(staged.written.sort()).toEqual(["README.md", "src/new.ts"]);
215
+ expect(staged.manualSteps).toEqual([]);
216
+ expect(staged.from).toBe(`${v1}#svc`);
217
+ expect(staged.to).toBe(`${v2}#svc`);
218
+ expect(read(staged.worktreeProject, "src/lib.ts")).toBe("ONE (ours)\ntwo\nthree\nfour\nfive\nsix\nSEVEN (theirs)\n");
219
+ // The parameter carried into the new version's files.
220
+ expect(read(staged.worktreeProject, "src/main.ts")).toBe('export const name = "billing";\n');
221
+ const lock = readLock(staged.worktreeProject)!.scopes["."];
222
+ expect(lock.source).toEqual({ type: "dir", path: v2, member: "svc" });
223
+ expect(Object.keys(lock.address!)).toEqual(["digest"]);
224
+ expect(lock.ref).toBeUndefined();
225
+ expect(lock.parameters).toEqual({ name: "billing", owner: "ops" });
226
+ } finally {
227
+ staged.dispose();
228
+ }
229
+ });
230
+
231
+ test("refuses when the recorded directory changed or is gone, naming adopt-lineage", async () => {
232
+ put(v1, "svc/README.md", "replaced in place\n");
233
+ await expect(stageUpgrade({ root: proj, to: v2, runChant: passing })).rejects.toThrow(
234
+ /no longer holds the files the scope was made from.*chant workspace adopt-lineage/,
235
+ );
236
+ rmSync(v1, { recursive: true, force: true });
237
+ await expect(stageUpgrade({ root: proj, to: v2, runChant: passing })).rejects.toThrow(/is gone\. .*chant workspace adopt-lineage/);
238
+ });
239
+
240
+ test("--to must be a directory", async () => {
241
+ await expect(stageUpgrade({ root: proj, to: "v2.0.0", runChant: passing })).rejects.toThrow(/--to v2\.0\.0: a scope made from a directory upgrades from a directory/);
242
+ });
243
+ });