@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,365 @@
1
+ /**
2
+ * The lineage lock at init (#2540, ws-047): `chant init --from <repo>@<ref>`
3
+ * and `chant init --template <name>` both leave `.chant/workspace.lock.json`
4
+ * with one lineage, scope `"."`, so a project made from a template can be
5
+ * upgraded later without first running adopt-lineage.
6
+ *
7
+ * `--from` fetches with `git`: the one network step, listed in the egress
8
+ * catalogue (`test/egress-catalogue.ts`). The commit and the tree of the
9
+ * chosen directory become the content address.
10
+ *
11
+ * Plain `chant init` without `--template` writes no lock. That keeps the
12
+ * output of an existing command unchanged (#2525 rule 2), and this module
13
+ * never loads for it.
14
+ */
15
+
16
+ import { execFileSync } from "node:child_process";
17
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
18
+ import { createRequire } from "node:module";
19
+ import { tmpdir } from "node:os";
20
+ import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
21
+ import {
22
+ LOCK_FILE,
23
+ LockError,
24
+ contentDigest,
25
+ declaredFilesAt,
26
+ emptyLock,
27
+ fileEntries,
28
+ readLock,
29
+ writeLock,
30
+ type Lineage,
31
+ } from "./lineage-lock";
32
+ import { MIGRATIONS_DIR } from "./lineage-migrations";
33
+ import { TEMPLATE_MANIFEST, readManifest, resolveParameters, substituteParameters } from "./template-manifest";
34
+
35
+ // ── The template spec ────────────────────────────────────────────────────────
36
+
37
+ export interface TemplateSpec {
38
+ /** The repository as written. */
39
+ repo: string;
40
+ /** What `git fetch` is given: a URL, or an absolute local path. */
41
+ url: string;
42
+ ref: string;
43
+ /** The directory inside the repository to instantiate (`#<member>`). */
44
+ member?: string;
45
+ /** Stable identity: `<host>/<path>` for a remote, the path as written for a local repo, plus `#<member>`. */
46
+ id: string;
47
+ }
48
+
49
+ /**
50
+ * Parse `<repo>@<ref>[#<member>]`. `<repo>` is a git URL, an scp-style
51
+ * `git@host:path`, a local path, or `owner/name` for a GitHub repository.
52
+ */
53
+ export function parseTemplateSpec(spec: string, cwd: string = process.cwd()): TemplateSpec {
54
+ let rest = spec.trim();
55
+ let member: string | undefined;
56
+ const hash = rest.lastIndexOf("#");
57
+ if (hash >= 0) {
58
+ member = rest.slice(hash + 1).replace(/^\/+|\/+$/g, "");
59
+ rest = rest.slice(0, hash);
60
+ if (!member || posix.normalize(member).startsWith("..")) throw new LockError(`--from ${spec}: "#${member}" is not a directory in the repository`);
61
+ member = posix.normalize(member);
62
+ }
63
+ const at = rest.lastIndexOf("@");
64
+ const lastSep = Math.max(rest.lastIndexOf("/"), rest.lastIndexOf(":"));
65
+ if (at <= 0 || at < lastSep || at === rest.length - 1) {
66
+ throw new LockError(`--from ${spec}: expected <repo>@<ref>[#<member>], e.g. acme/starter@v1.2.0`);
67
+ }
68
+ const repo = rest.slice(0, at);
69
+ const ref = rest.slice(at + 1);
70
+
71
+ let url: string;
72
+ let repoId: string;
73
+ const local = resolve(cwd, repo);
74
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(repo)) {
75
+ url = repo;
76
+ repoId = repo.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/^[^@/]*@/, "").replace(/\.git$/, "");
77
+ } else if (/^[^/@\s]+@[^:\s]+:/.test(repo)) {
78
+ url = repo;
79
+ repoId = repo.replace(/^[^@]+@/, "").replace(":", "/").replace(/\.git$/, "");
80
+ } else if (existsSync(local)) {
81
+ url = local;
82
+ repoId = repo;
83
+ } else if (/^[\w.-]+\/[\w.-]+$/.test(repo)) {
84
+ url = `https://github.com/${repo}.git`;
85
+ repoId = `github.com/${repo.replace(/\.git$/, "")}`;
86
+ } else if (/^[\w.-]+\.[a-z]{2,}\/[\w./-]+$/i.test(repo)) {
87
+ url = `https://${repo}${repo.endsWith(".git") ? "" : ".git"}`;
88
+ repoId = repo.replace(/\.git$/, "");
89
+ } else {
90
+ throw new LockError(`--from ${spec}: "${repo}" is not a URL, a local repository or owner/name`);
91
+ }
92
+ return { repo, url, ref, member, id: member ? `${repoId}#${member}` : repoId };
93
+ }
94
+
95
+ // ── Fetch ────────────────────────────────────────────────────────────────────
96
+
97
+ export interface FetchedTemplate {
98
+ commit: string;
99
+ tree: string;
100
+ /** Files under the chosen directory, relative to it. */
101
+ files: Map<string, { data: Buffer; executable: boolean }>;
102
+ /** Paths that were not copied, with the reason. */
103
+ skipped: Array<{ path: string; reason: string }>;
104
+ }
105
+
106
+ export function git(cwd: string, args: string[]): string {
107
+ return execFileSync("git", args, {
108
+ cwd,
109
+ encoding: "utf-8",
110
+ stdio: ["ignore", "pipe", "pipe"],
111
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
112
+ maxBuffer: 256 * 1024 * 1024,
113
+ }).trim();
114
+ }
115
+
116
+ /**
117
+ * Read the files of `member` (or the whole tree) at `commit` from a scratch
118
+ * repository that already holds the commit. Symbolic links, submodules and
119
+ * the template's own lineage lock are skipped, with the reason.
120
+ */
121
+ export function readTemplateTree(
122
+ scratch: string,
123
+ commit: string,
124
+ member: string | undefined,
125
+ label: string,
126
+ ): { tree: string; files: FetchedTemplate["files"]; skipped: FetchedTemplate["skipped"] } {
127
+ let tree: string;
128
+ try {
129
+ tree = git(scratch, ["rev-parse", member ? `${commit}:${member}` : `${commit}^{tree}`]);
130
+ if (git(scratch, ["cat-file", "-t", tree]) !== "tree") throw new Error("not a tree");
131
+ } catch {
132
+ throw new LockError(`${label} has no directory ${member}`);
133
+ }
134
+
135
+ const files: FetchedTemplate["files"] = new Map();
136
+ const skipped: FetchedTemplate["skipped"] = [];
137
+ const listing = execFileSync("git", ["ls-tree", "-r", "-z", tree], { cwd: scratch, maxBuffer: 256 * 1024 * 1024 }).toString("utf-8");
138
+ for (const row of listing.split("\0")) {
139
+ if (!row) continue;
140
+ const tab = row.indexOf("\t");
141
+ const [mode, type, sha] = row.slice(0, tab).split(" ");
142
+ const path = row.slice(tab + 1);
143
+ if (path === LOCK_FILE) {
144
+ skipped.push({ path, reason: "the template's own lineage lock" });
145
+ continue;
146
+ }
147
+ if (type !== "blob") {
148
+ skipped.push({ path, reason: `a ${type === "commit" ? "submodule" : type}` });
149
+ continue;
150
+ }
151
+ if (mode === "120000") {
152
+ skipped.push({ path, reason: "a symbolic link" });
153
+ continue;
154
+ }
155
+ const data = execFileSync("git", ["cat-file", "blob", sha], { cwd: scratch, maxBuffer: 256 * 1024 * 1024 });
156
+ files.set(path, { data, executable: mode === "100755" });
157
+ }
158
+ return { tree, files, skipped };
159
+ }
160
+
161
+ /**
162
+ * Fetch one commit of the template repository into a scratch repository and
163
+ * read the files of the chosen directory from it. The template's own lock is
164
+ * never copied: it describes the template's lineage, not the new project's.
165
+ */
166
+ export function fetchTemplate(spec: TemplateSpec): FetchedTemplate {
167
+ const scratch = mkdtempSync(join(tmpdir(), "chant-init-from-"));
168
+ try {
169
+ git(scratch, ["init", "-q"]);
170
+ try {
171
+ // The one network step of `chant init --from`, catalogued in test/egress-catalogue.ts.
172
+ git(scratch, ["fetch", "-q", "--depth", "1", spec.url, spec.ref]);
173
+ } catch (err) {
174
+ const stderr = (err as { stderr?: string }).stderr?.toString().trim();
175
+ throw new LockError(`could not fetch ${spec.repo}@${spec.ref}${stderr ? `: ${stderr}` : ""}`);
176
+ }
177
+ const commit = git(scratch, ["rev-parse", "FETCH_HEAD^{commit}"]);
178
+ return { commit, ...readTemplateTree(scratch, commit, spec.member, `${spec.repo}@${spec.ref}`) };
179
+ } finally {
180
+ rmSync(scratch, { recursive: true, force: true });
181
+ }
182
+ }
183
+
184
+ // ── init --from ──────────────────────────────────────────────────────────────
185
+
186
+ export interface InitFromOptions {
187
+ from: string;
188
+ /** Target directory (defaults to cwd). */
189
+ path?: string;
190
+ force?: boolean;
191
+ /** `--param name=value` values, parsed. Checked against the template's `chant.template.json` (#2627). */
192
+ params?: Record<string, string>;
193
+ }
194
+
195
+ export interface InitFromResult {
196
+ success: boolean;
197
+ createdFiles: string[];
198
+ warnings: string[];
199
+ error?: string;
200
+ spec?: TemplateSpec;
201
+ commit?: string;
202
+ /** The parameter values used, recorded in the lock. */
203
+ parameters?: Record<string, string>;
204
+ }
205
+
206
+ /** `chant init --from <repo>@<ref>[#<member>] [path]`. */
207
+ export async function initFromCommand(options: InitFromOptions): Promise<InitFromResult> {
208
+ const targetDir = resolve(options.path ?? ".");
209
+ const warnings: string[] = [];
210
+ const createdFiles: string[] = [];
211
+
212
+ let spec: TemplateSpec;
213
+ try {
214
+ spec = parseTemplateSpec(options.from);
215
+ } catch (err) {
216
+ return { success: false, createdFiles, warnings, error: (err as Error).message };
217
+ }
218
+
219
+ if (existsSync(targetDir)) {
220
+ const visible = readdirSync(targetDir).filter((f) => !f.startsWith("."));
221
+ if (visible.length > 0 && !options.force) {
222
+ return { success: false, createdFiles, warnings, error: "Directory is not empty. Use --force to initialize anyway." };
223
+ }
224
+ if (visible.length > 0) warnings.push("Initializing in non-empty directory");
225
+ }
226
+ if (existsSync(join(targetDir, LOCK_FILE))) {
227
+ return { success: false, createdFiles, warnings, error: `${LOCK_FILE} already exists; this directory already has a lineage` };
228
+ }
229
+
230
+ let fetched: FetchedTemplate;
231
+ try {
232
+ fetched = fetchTemplate(spec);
233
+ } catch (err) {
234
+ return { success: false, createdFiles, warnings, error: (err as Error).message };
235
+ }
236
+ for (const s of fetched.skipped) {
237
+ if (s.path !== LOCK_FILE) warnings.push(`${s.path} is ${s.reason}, not copied`);
238
+ }
239
+
240
+ // Parameters (#2627): checked and substituted before anything is written,
241
+ // so a refused --param leaves the target untouched.
242
+ let parameters: Record<string, string>;
243
+ let contents: Map<string, Buffer>;
244
+ try {
245
+ const raw = new Map([...fetched.files].map(([path, f]) => [path, f.data]));
246
+ const manifest = readManifest(raw);
247
+ parameters = resolveParameters(manifest, options.params ?? {});
248
+ contents = substituteParameters(raw, manifest, parameters);
249
+ } catch (err) {
250
+ return { success: false, createdFiles, warnings, error: (err as Error).message };
251
+ }
252
+
253
+ mkdirSync(targetDir, { recursive: true });
254
+ const written = new Map<string, Buffer>();
255
+ for (const [path, file] of [...fetched.files].sort(([a], [b]) => a.localeCompare(b))) {
256
+ // The template's migrations (#2550) and its manifest (#2627) are for chant, not part of a project.
257
+ if (path.startsWith(`${MIGRATIONS_DIR}/`) || path === TEMPLATE_MANIFEST) continue;
258
+ const data = contents.get(path)!;
259
+ const abs = join(targetDir, path);
260
+ if (existsSync(abs)) {
261
+ warnings.push(`${path} already exists, skipping`);
262
+ continue;
263
+ }
264
+ mkdirSync(dirname(abs), { recursive: true });
265
+ writeFileSync(abs, data);
266
+ if (file.executable) chmodSync(abs, 0o755);
267
+ written.set(path, data);
268
+ createdFiles.push(path);
269
+ }
270
+
271
+ const lineage: Lineage = {
272
+ kind: "template",
273
+ template: spec.id,
274
+ source: { type: "git", repo: spec.repo, url: portableUrl(spec.url, targetDir), ...(spec.member ? { path: spec.member } : {}) },
275
+ ref: spec.ref,
276
+ address: { digest: contentDigest(written), commit: fetched.commit, tree: fetched.tree },
277
+ parameters,
278
+ migrations: [],
279
+ files: fileEntries(written, declaredFilesAt(targetDir)),
280
+ manualSteps: [],
281
+ };
282
+ const lock = emptyLock();
283
+ lock.scopes["."] = lineage;
284
+ writeLock(targetDir, lock);
285
+ createdFiles.push(LOCK_FILE);
286
+
287
+ return { success: true, createdFiles, warnings, spec, commit: fetched.commit, parameters };
288
+ }
289
+
290
+ /** A local repository is recorded relative to the project, so the lock does not name this machine's paths. */
291
+ function portableUrl(url: string, targetDir: string): string {
292
+ if (!isAbsolute(url)) return url;
293
+ const rel = relative(targetDir, url).split(sep).join("/");
294
+ return rel.startsWith(".") ? rel : `./${rel}`;
295
+ }
296
+
297
+ // ── init --template ──────────────────────────────────────────────────────────
298
+
299
+ /** The installed version of a lexicon package, or null when it cannot be found. */
300
+ function lexiconPackageVersion(pkg: string, from: string): string | null {
301
+ for (const base of [join(from, "package.json"), import.meta.url]) {
302
+ try {
303
+ const require = createRequire(base);
304
+ let dir = dirname(require.resolve(pkg));
305
+ for (let i = 0; i < 6; i++) {
306
+ const candidate = join(dir, "package.json");
307
+ if (existsSync(candidate)) {
308
+ const json = JSON.parse(readFileSync(candidate, "utf-8")) as { name?: string; version?: string };
309
+ if (json.name === pkg) return json.version ?? null;
310
+ }
311
+ dir = dirname(dir);
312
+ }
313
+ } catch {
314
+ // Not resolvable from here; try the next base.
315
+ }
316
+ }
317
+ return null;
318
+ }
319
+
320
+ export interface TemplateLockInput {
321
+ targetDir: string;
322
+ lexicon: string;
323
+ template: string;
324
+ /** Files init wrote, relative to `targetDir`. */
325
+ createdFiles: string[];
326
+ chantVersion: string;
327
+ /** Set when the project's config declares the lexicon by path (#2520): the path, recorded instead of a package. */
328
+ lexiconModule?: string;
329
+ }
330
+
331
+ /**
332
+ * Record the lineage of a `chant init --template` project. Only files init
333
+ * actually wrote are listed; a file that already existed was not made from
334
+ * the template. `.chant/types/` is gitignored and left out (D14).
335
+ *
336
+ * Returns the lock's path relative to `targetDir`, or null when the target
337
+ * already has a lock (init never overwrites a lineage).
338
+ */
339
+ export function writeTemplateLock(input: TemplateLockInput): string | null {
340
+ if (readLock(input.targetDir)) return null;
341
+ const files = new Map<string, Buffer>();
342
+ for (const rel of input.createdFiles) {
343
+ if (rel.startsWith(".chant/")) continue;
344
+ files.set(rel, readFileSync(join(input.targetDir, rel)));
345
+ }
346
+ const pkg = input.lexiconModule ?? `@intentius/chant-lexicon-${input.lexicon}`;
347
+ const lock = emptyLock();
348
+ lock.scopes["."] = {
349
+ kind: "template",
350
+ template: `lexicon:${input.lexicon}/${input.template}`,
351
+ source: { type: "lexicon", lexicon: input.lexicon, template: input.template },
352
+ address: {
353
+ digest: contentDigest(files),
354
+ package: pkg,
355
+ version: input.lexiconModule ? null : lexiconPackageVersion(pkg, input.targetDir),
356
+ chant: input.chantVersion,
357
+ },
358
+ parameters: {},
359
+ migrations: [],
360
+ files: fileEntries(files, declaredFilesAt(input.targetDir)),
361
+ manualSteps: [],
362
+ };
363
+ writeLock(input.targetDir, lock);
364
+ return LOCK_FILE;
365
+ }
@@ -0,0 +1,224 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import {
6
+ LOCK_FILE,
7
+ LockError,
8
+ contentDigest,
9
+ emptyLock,
10
+ fileEntries,
11
+ fileHash,
12
+ readLock,
13
+ renderLock,
14
+ resolveManualStep,
15
+ scopeKey,
16
+ scopeStatus,
17
+ writeLock,
18
+ type Lineage,
19
+ } from "./lineage-lock";
20
+ import { applyUpstream } from "./lineage-update";
21
+ import { contentHash } from "../cli/commands/vendor";
22
+
23
+ let root: string;
24
+
25
+ function put(rel: string, content: string): void {
26
+ const abs = join(root, rel);
27
+ mkdirSync(dirname(abs), { recursive: true });
28
+ writeFileSync(abs, content);
29
+ }
30
+ const read = (rel: string) => readFileSync(join(root, rel), "utf-8");
31
+ const files = (o: Record<string, string>) => new Map(Object.entries(o).map(([k, v]) => [k, Buffer.from(v)]));
32
+
33
+ function vendorLineage(overrides: Partial<Lineage> = {}): Lineage {
34
+ return {
35
+ kind: "vendor",
36
+ name: "web",
37
+ template: "local:shared/web",
38
+ source: { type: "local", path: "shared/web" },
39
+ ref: "v1",
40
+ address: null,
41
+ parameters: {},
42
+ migrations: [],
43
+ files: {},
44
+ manualSteps: [],
45
+ ...overrides,
46
+ };
47
+ }
48
+
49
+ beforeEach(() => {
50
+ root = mkdtempSync(join(tmpdir(), "chant-lineage-"));
51
+ });
52
+ afterEach(() => {
53
+ rmSync(root, { recursive: true, force: true });
54
+ });
55
+
56
+ describe("the lock file", () => {
57
+ test("round-trips, with scopes, files and steps in sorted order", () => {
58
+ const lock = emptyLock();
59
+ lock.scopes["vendor/web"] = vendorLineage({
60
+ address: { digest: contentDigest(files({ b: "2", a: "1" })) },
61
+ files: fileEntries(files({ "z.ts": "z", "a.ts": "a" })),
62
+ });
63
+ lock.scopes["."] = { ...vendorLineage(), kind: "template", name: undefined, template: "lexicon:aws/default" };
64
+ delete lock.scopes["."].name;
65
+ writeLock(root, lock);
66
+ const text = readFileSync(join(root, LOCK_FILE), "utf-8");
67
+ expect(Object.keys(JSON.parse(text).scopes)).toEqual([".", "vendor/web"]);
68
+ expect(Object.keys(JSON.parse(text).scopes["vendor/web"].files)).toEqual(["a.ts", "z.ts"]);
69
+ expect(readLock(root)).toEqual(JSON.parse(text));
70
+ expect(renderLock(readLock(root)!)).toBe(text);
71
+ });
72
+
73
+ test("no lock reads as null", () => {
74
+ expect(readLock(root)).toBeNull();
75
+ });
76
+
77
+ test("a newer lockVersion is refused by name", () => {
78
+ put(LOCK_FILE, JSON.stringify({ lockVersion: 2, scopes: {} }));
79
+ expect(() => readLock(root)).toThrow(/lockVersion 2; this chant reads up to 1/);
80
+ });
81
+
82
+ test("an invalid lock names the field", () => {
83
+ put(LOCK_FILE, JSON.stringify({ lockVersion: 1, scopes: { ".": { kind: "template" } } }));
84
+ expect(() => readLock(root)).toThrow(LockError);
85
+ expect(() => readLock(root)).toThrow(/scopes\.\.\.template/);
86
+ });
87
+
88
+ test("a scope key must be normal", () => {
89
+ const lock = emptyLock();
90
+ lock.scopes["./vendor/web/"] = vendorLineage();
91
+ put(LOCK_FILE, JSON.stringify(lock));
92
+ expect(() => readLock(root)).toThrow(/not in normal form/);
93
+ });
94
+
95
+ test("the digest is the vendor.json checksum", () => {
96
+ const set = files({ "b.ts": "b", "a/x.ts": "x" });
97
+ expect(contentDigest(set)).toBe(contentHash(set));
98
+ });
99
+ });
100
+
101
+ describe("scopeKey", () => {
102
+ test("normalises to posix relative form", () => {
103
+ expect(scopeKey("")).toBe(".");
104
+ expect(scopeKey("./")).toBe(".");
105
+ expect(scopeKey("vendor/web/")).toBe("vendor/web");
106
+ expect(scopeKey("./vendor//web")).toBe("vendor/web");
107
+ });
108
+ test("refuses paths outside the project", () => {
109
+ expect(() => scopeKey("../x")).toThrow(LockError);
110
+ expect(() => scopeKey("/abs")).toThrow(LockError);
111
+ });
112
+ });
113
+
114
+ describe("file classes", () => {
115
+ test("skills are generated by chant update, .mcp.json is a seed, the rest owned", () => {
116
+ const entries = fileEntries(files({ "skills/a/SKILL.md": "s", ".mcp.json": "{}", "src/x.ts": "x" }));
117
+ expect(entries["skills/a/SKILL.md"]).toMatchObject({ class: "generated", command: "chant update" });
118
+ expect(entries[".mcp.json"].class).toBe("seed");
119
+ expect(entries["src/x.ts"].class).toBe("owned");
120
+ });
121
+
122
+ test("status ignores generated files and reports edits and deletions", () => {
123
+ put("skills/a/SKILL.md", "edited");
124
+ put("src/x.ts", "x");
125
+ put("src/y.ts", "edited");
126
+ const lineage = vendorLineage({ files: fileEntries(files({ "skills/a/SKILL.md": "s", "src/x.ts": "x", "src/y.ts": "y", "src/z.ts": "z" })) });
127
+ const st = scopeStatus(root, ".", lineage);
128
+ expect(st.customised).toEqual(["src/y.ts"]);
129
+ expect(st.missing).toEqual(["src/z.ts"]);
130
+ });
131
+ });
132
+
133
+ describe("applyUpstream — local edits are never overwritten", () => {
134
+ const base = { "same.ts": "1", "edited.ts": "1", "both.ts": "1", "gone.ts": "1", "gone-edited.ts": "1", "deleted.ts": "1", "deleted-changed.ts": "1" };
135
+
136
+ function setup(): Lineage {
137
+ for (const [k, v] of Object.entries(base)) put(`t/${k}`, v);
138
+ const lineage = vendorLineage({ files: fileEntries(files(base)) });
139
+ put("t/edited.ts", "mine");
140
+ put("t/both.ts", "mine");
141
+ put("t/gone-edited.ts", "mine");
142
+ rmSync(join(root, "t/deleted.ts"));
143
+ rmSync(join(root, "t/deleted-changed.ts"));
144
+ put("t/untracked.ts", "mine");
145
+ put("t/collides.ts", "mine");
146
+ return lineage;
147
+ }
148
+
149
+ const upstream = files({
150
+ "same.ts": "2",
151
+ "edited.ts": "1",
152
+ "both.ts": "2",
153
+ "deleted.ts": "1",
154
+ "deleted-changed.ts": "2",
155
+ "collides.ts": "theirs",
156
+ "new.ts": "new",
157
+ });
158
+
159
+ test("each case in the table", () => {
160
+ const lineage = setup();
161
+ const result = applyUpstream(join(root, "t"), lineage, upstream);
162
+
163
+ expect(read("t/same.ts")).toBe("2");
164
+ expect(read("t/edited.ts")).toBe("mine");
165
+ expect(read("t/both.ts")).toBe("mine");
166
+ expect(existsSync(join(root, "t/gone.ts"))).toBe(false);
167
+ expect(read("t/gone-edited.ts")).toBe("mine");
168
+ expect(existsSync(join(root, "t/deleted.ts"))).toBe(false);
169
+ expect(existsSync(join(root, "t/deleted-changed.ts"))).toBe(false);
170
+ expect(read("t/untracked.ts")).toBe("mine");
171
+ expect(read("t/collides.ts")).toBe("mine");
172
+ expect(read("t/new.ts")).toBe("new");
173
+
174
+ expect(result.written.sort()).toEqual(["new.ts", "same.ts"]);
175
+ expect(result.removed).toEqual(["gone.ts"]);
176
+ expect(lineage.manualSteps.map((s) => [s.path, s.reason])).toEqual([
177
+ ["both.ts", "changed-locally"],
178
+ ["collides.ts", "exists-locally"],
179
+ ["deleted-changed.ts", "deleted-locally"],
180
+ ["gone-edited.ts", "removed-upstream"],
181
+ ]);
182
+ // Merge bases: advanced where the file was taken, kept where it was not.
183
+ expect(lineage.files["same.ts"].sha256).toBe(fileHash("2"));
184
+ expect(lineage.files["edited.ts"].sha256).toBe(fileHash("1"));
185
+ expect(lineage.files["both.ts"].sha256).toBe(fileHash("1"));
186
+ expect(lineage.files["gone.ts"]).toBeUndefined();
187
+ expect(lineage.files["untracked.ts"]).toBeUndefined();
188
+ expect(lineage.address?.digest).toBe(contentDigest(upstream));
189
+ });
190
+
191
+ test("a second pull of the same source changes nothing and keeps the steps", () => {
192
+ const lineage = setup();
193
+ applyUpstream(join(root, "t"), lineage, upstream);
194
+ const before = JSON.stringify(lineage);
195
+ const again = applyUpstream(join(root, "t"), lineage, upstream);
196
+ expect(again.written).toEqual([]);
197
+ expect(JSON.stringify(lineage)).toBe(before);
198
+ });
199
+
200
+ test("resolving a step moves the base to upstream, and the next pull keeps the file", () => {
201
+ const lineage = setup();
202
+ applyUpstream(join(root, "t"), lineage, upstream);
203
+ const lock = emptyLock();
204
+ lock.scopes["t"] = lineage;
205
+ expect(resolveManualStep(lock, "t/both.ts")?.step.reason).toBe("changed-locally");
206
+ expect(lineage.files["both.ts"].sha256).toBe(fileHash("2"));
207
+ expect(resolveManualStep(lock, "t/gone-edited.ts")?.step.upstream).toBeNull();
208
+ expect(lineage.files["gone-edited.ts"]).toBeUndefined();
209
+ expect(resolveManualStep(lock, "t/collides.ts")).not.toBeNull();
210
+ expect(lineage.files["collides.ts"].sha256).toBe(fileHash("theirs"));
211
+ expect(resolveManualStep(lock, "t/nope.ts")).toBeNull();
212
+
213
+ const again = applyUpstream(join(root, "t"), lineage, upstream);
214
+ expect(again.manualSteps.map((s) => s.path)).toEqual(["deleted-changed.ts"]);
215
+ expect(read("t/both.ts")).toBe("mine");
216
+ });
217
+
218
+ test("the deepest scope owns a path", () => {
219
+ const lock = emptyLock();
220
+ lock.scopes["."] = vendorLineage({ manualSteps: [{ path: "vendor/web/a.ts", reason: "changed-locally", upstream: fileHash("x") }] });
221
+ lock.scopes["vendor/web"] = vendorLineage({ manualSteps: [{ path: "a.ts", reason: "changed-locally", upstream: fileHash("y") }] });
222
+ expect(resolveManualStep(lock, "vendor/web/a.ts")?.scope).toBe("vendor/web");
223
+ });
224
+ });