@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,475 @@
1
+ /**
2
+ * The generated-file checks (#2541, #2524 D14).
3
+ *
4
+ * A member lists the files a command writes in its `generated` entries, each
5
+ * with the command line that writes it and the sources it reads. Core adds
6
+ * the files `chant update` rewrites, `skills/*\/SKILL.md`, as implicit entries
7
+ * of every `chant` member (`../generated-files.ts` holds that list, and the
8
+ * lineage lock classes files from the same list).
9
+ *
10
+ * - `WSP101`: a generated file differs from what its generator writes.
11
+ * - `WSP102`: a declared generated file does not exist.
12
+ * - `WSP103`: a generator could not be run, failed, or wrote nothing.
13
+ * - `WSP104` (info): an entry is kept by hand, with the reason it gives.
14
+ * - `WSP105` (info): an entry was not compared, because its generator runs
15
+ * only on request or its output can't be produced without running code.
16
+ * - `WSP106`: an entry names a source that does not exist.
17
+ *
18
+ * They were WSP081 to WSP086 until #2641 moved them, since the pipeline
19
+ * checks of #2542 hold WSP081 to WSP083.
20
+ *
21
+ * What runs by default is what needs no member code: the file and its
22
+ * sources exist, and each implicit `SKILL.md` matches what the member's
23
+ * lexicons render, with the lexicons read from its config statically.
24
+ * Declared generators are commands that load member code and take seconds
25
+ * each, so they run only when asked ({@link GatherOptions.runGenerators},
26
+ * `chant workspace check --generated`).
27
+ *
28
+ * The checks are a pure function over {@link GeneratedFileFacts};
29
+ * {@link gatherGeneratedFacts} collects those facts from a checkout, and
30
+ * `chant workspace check` gathers them before it runs
31
+ * {@link GENERATED_CHECKS} (#2641).
32
+ */
33
+
34
+ import { execFileSync, spawnSync } from "node:child_process";
35
+ import { createHash } from "node:crypto";
36
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
37
+ import { tmpdir } from "node:os";
38
+ import { basename, delimiter, dirname, join, relative, resolve, sep } from "node:path";
39
+ import type { WorkspaceCheck } from "../checks";
40
+ import type { Declaration, Member } from "../declaration";
41
+ import { gitTop } from "../tree";
42
+
43
+ export const WSP_GENERATED_DRIFT = "WSP101";
44
+ export const WSP_GENERATED_MISSING = "WSP102";
45
+ export const WSP_GENERATOR_FAILED = "WSP103";
46
+ export const WSP_GENERATED_HAND_WRITTEN = "WSP104";
47
+ export const WSP_GENERATED_NOT_COMPARED = "WSP105";
48
+ export const WSP_GENERATED_SOURCE_MISSING = "WSP106";
49
+
50
+ export type GeneratedCheckId =
51
+ | typeof WSP_GENERATED_DRIFT
52
+ | typeof WSP_GENERATED_MISSING
53
+ | typeof WSP_GENERATOR_FAILED
54
+ | typeof WSP_GENERATED_HAND_WRITTEN
55
+ | typeof WSP_GENERATED_NOT_COMPARED
56
+ | typeof WSP_GENERATED_SOURCE_MISSING;
57
+
58
+ export interface GeneratedFinding {
59
+ id: GeneratedCheckId;
60
+ severity: "error" | "info";
61
+ member: string;
62
+ /** The file, relative to the workspace root. A glob for an implicit entry that could not be rendered. */
63
+ path: string;
64
+ /** A JSON Pointer into the declaration: the entry, or the member for an implicit file. */
65
+ pointer: string;
66
+ message: string;
67
+ }
68
+
69
+ /** What the generator produced for one file. */
70
+ export type GeneratorOutput =
71
+ | { status: "produced"; sha256: string }
72
+ | { status: "failed"; message: string }
73
+ | { status: "not-run"; reason: string };
74
+
75
+ /** What the checks need to know about one generated file. */
76
+ export interface GeneratedFileFacts {
77
+ member: string;
78
+ /** The member's directory, relative to the workspace root. */
79
+ memberDir: string;
80
+ /** The file, relative to the workspace root. */
81
+ path: string;
82
+ pointer: string;
83
+ /** The command line that writes the file. */
84
+ generator: string;
85
+ /** True for a file core registers (`skills/*\/SKILL.md`), false for a declared entry. */
86
+ implicit: boolean;
87
+ /** The reason, when the entry is kept by hand. */
88
+ handWritten: string | null;
89
+ /** Declared sources that do not exist, relative to the workspace root. */
90
+ missingSources: string[];
91
+ /** sha256 hex of the file in the tree, or null when it is missing. */
92
+ current: string | null;
93
+ output: GeneratorOutput;
94
+ }
95
+
96
+ const where = (f: GeneratedFileFacts) => (f.memberDir === "." ? "the workspace root" : f.memberDir);
97
+
98
+ function finding(f: GeneratedFileFacts, id: GeneratedCheckId, severity: GeneratedFinding["severity"], message: string): GeneratedFinding {
99
+ return { id, severity, member: f.member, path: f.path, pointer: f.pointer, message };
100
+ }
101
+
102
+ /** Every generated-file finding, in the order of the facts. */
103
+ export function checkGenerated(facts: readonly GeneratedFileFacts[]): GeneratedFinding[] {
104
+ const out: GeneratedFinding[] = [];
105
+ for (const f of facts) {
106
+ if (f.handWritten !== null) {
107
+ out.push(
108
+ finding(f, WSP_GENERATED_HAND_WRITTEN, "info", `${f.path} is kept by hand, so \`${f.generator}\` is not run for it: ${f.handWritten}`),
109
+ );
110
+ continue;
111
+ }
112
+ for (const s of f.missingSources) {
113
+ out.push(finding(f, WSP_GENERATED_SOURCE_MISSING, "error", `member ${f.member} lists ${s} as a source of ${f.path}, and ${s} does not exist`));
114
+ }
115
+ if (f.current === null && !f.implicit) {
116
+ out.push(finding(f, WSP_GENERATED_MISSING, "error", `${f.path} does not exist; run \`${f.generator}\` in ${where(f)} and commit the file`));
117
+ continue;
118
+ }
119
+ const o = f.output;
120
+ if (o.status === "not-run") {
121
+ out.push(finding(f, WSP_GENERATED_NOT_COMPARED, "info", `${f.path} was not compared with its generator's output: ${o.reason}`));
122
+ } else if (o.status === "failed") {
123
+ out.push(finding(f, WSP_GENERATOR_FAILED, "error", `\`${f.generator}\` could not regenerate ${f.path}: ${o.message}`));
124
+ } else if (f.current !== null && o.sha256 !== f.current) {
125
+ const fix = f.implicit
126
+ ? `run \`chant update\` in ${where(f)}, or list the file in member ${f.member}'s generated entries with handWritten and a reason`
127
+ : `run \`${f.generator}\` in ${where(f)} and commit the result, or mark the entry handWritten with a reason`;
128
+ out.push(finding(f, WSP_GENERATED_DRIFT, "error", `${f.path} differs from what \`${f.generator}\` writes; ${fix}`));
129
+ }
130
+ }
131
+ return out;
132
+ }
133
+
134
+ // ── As workspace checks ──────────────────────────────────────────────────────
135
+
136
+ function generatedCheck(id: GeneratedCheckId, name: string, severity: GeneratedFinding["severity"], description: string): WorkspaceCheck {
137
+ return {
138
+ id,
139
+ name,
140
+ description,
141
+ severity,
142
+ configurable: true,
143
+ check(ctx) {
144
+ return checkGenerated(ctx.facts?.generated ?? [])
145
+ .filter((f) => f.id === id)
146
+ .map((f) => ({ checkId: id, severity: this.severity, message: f.message, entity: f.member, pointer: f.pointer }));
147
+ },
148
+ };
149
+ }
150
+
151
+ /** The generated-file checks, which read `ctx.facts.generated`. They find nothing when the facts were not gathered. */
152
+ export const GENERATED_CHECKS: readonly WorkspaceCheck[] = [
153
+ generatedCheck(WSP_GENERATED_DRIFT, "generated-drift", "error", "A generated file is what its generator writes. Declared generators run only with --generated."),
154
+ generatedCheck(WSP_GENERATED_MISSING, "generated-missing", "error", "Every declared generated file exists."),
155
+ generatedCheck(WSP_GENERATOR_FAILED, "generator-failed", "error", "A declared generator runs, exits 0 and writes the file."),
156
+ generatedCheck(WSP_GENERATED_HAND_WRITTEN, "generated-hand-written", "info", "An entry kept by hand is reported with its reason, and its generator is not run."),
157
+ generatedCheck(WSP_GENERATED_NOT_COMPARED, "generated-not-compared", "info", "An entry not compared with its generator's output is reported with the reason."),
158
+ generatedCheck(WSP_GENERATED_SOURCE_MISSING, "generated-source-missing", "error", "Every source a generated entry names exists."),
159
+ ];
160
+
161
+ // ── Facts from a checkout ────────────────────────────────────────────────────
162
+
163
+ /** The skills a member's lexicons render, keyed by path relative to the member. */
164
+ export type SkillRender =
165
+ | { status: "rendered"; files: Map<string, string>; unread?: string }
166
+ | { status: "unknown"; reason: string };
167
+
168
+ export type RenderSkills = (memberDir: string) => Promise<SkillRender>;
169
+
170
+ export interface GeneratorRun {
171
+ cwd: string;
172
+ argv: string[];
173
+ env: NodeJS.ProcessEnv;
174
+ }
175
+
176
+ export type RunGenerator = (run: GeneratorRun) => { ok: true } | { ok: false; message: string };
177
+
178
+ export interface GatherOptions {
179
+ /** Run each declared generator and compare its output. Off by default: generators run member code. */
180
+ runGenerators?: boolean;
181
+ /** How implicit skills are rendered. Defaults to {@link renderSkillsStatically}. */
182
+ renderSkills?: RenderSkills;
183
+ /** How a generator runs. Defaults to {@link spawnGenerator}. */
184
+ runGenerator?: RunGenerator;
185
+ }
186
+
187
+ const sha256 = (data: Buffer | string) => createHash("sha256").update(data).digest("hex");
188
+
189
+ function hashOf(abs: string): string | null {
190
+ return existsSync(abs) ? sha256(readFileSync(abs)) : null;
191
+ }
192
+
193
+ /**
194
+ * Collect {@link GeneratedFileFacts} for every member of `declaration`, whose
195
+ * workspace root is `root`: its declared entries, then, for a `chant`
196
+ * member, its implicit `SKILL.md` files.
197
+ */
198
+ export async function gatherGeneratedFacts(root: string, declaration: Declaration, options: GatherOptions = {}): Promise<GeneratedFileFacts[]> {
199
+ const renderSkills = options.renderSkills ?? renderSkillsStatically;
200
+ const runGenerator = options.runGenerator ?? spawnGenerator;
201
+ const facts: GeneratedFileFacts[] = [];
202
+ for (const m of declaration.members) {
203
+ const memberAbs = join(root, m.dir);
204
+ const rel = (p: string) => (m.dir === "." ? p : `${m.dir}/${p}`);
205
+ const declared = new Set(m.generated.map((g) => g.path));
206
+ for (const g of m.generated) {
207
+ const abs = join(memberAbs, g.path);
208
+ const current = hashOf(abs);
209
+ const base = {
210
+ member: m.name,
211
+ memberDir: m.dir,
212
+ path: rel(g.path),
213
+ pointer: g.pointer,
214
+ generator: g.generator,
215
+ implicit: false,
216
+ handWritten: g.handWritten?.because ?? null,
217
+ missingSources: g.sources.filter((s) => !existsSync(join(root, s))),
218
+ current,
219
+ };
220
+ let output: GeneratorOutput;
221
+ if (g.handWritten || current === null) output = { status: "not-run", reason: "not needed" };
222
+ else if (!options.runGenerators) output = { status: "not-run", reason: `its generator runs only with --generated, since it runs member code` };
223
+ else output = regenerate(root, memberAbs, abs, g.generator, runGenerator);
224
+ facts.push({ ...base, output });
225
+ }
226
+ if (m.kind === "chant") facts.push(...(await skillFacts(m, memberAbs, declared, renderSkills)));
227
+ }
228
+ return facts;
229
+ }
230
+
231
+ async function skillFacts(m: Member, memberAbs: string, declared: Set<string>, renderSkills: RenderSkills): Promise<GeneratedFileFacts[]> {
232
+ const rel = (p: string) => (m.dir === "." ? p : `${m.dir}/${p}`);
233
+ const base = { member: m.name, memberDir: m.dir, pointer: m.pointer, generator: "chant update", implicit: true, handWritten: null, missingSources: [] };
234
+ let render: SkillRender;
235
+ try {
236
+ render = await renderSkills(memberAbs);
237
+ } catch (err) {
238
+ render = { status: "unknown", reason: `the member's lexicons could not be loaded (${err instanceof Error ? err.message : String(err)})` };
239
+ }
240
+ if (render.status === "unknown") {
241
+ return [{ ...base, path: rel("skills/*/SKILL.md"), current: null, output: { status: "not-run", reason: render.reason } }];
242
+ }
243
+ const out: GeneratedFileFacts[] = [];
244
+ for (const [path, content] of [...render.files].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
245
+ // A declared entry, hand-written or not, speaks for the file instead.
246
+ if (declared.has(path)) continue;
247
+ const current = hashOf(join(memberAbs, path));
248
+ // A skill chant update has not written (often gitignored) is not drift.
249
+ if (current === null) continue;
250
+ out.push({ ...base, path: rel(path), current, output: { status: "produced", sha256: sha256(content) } });
251
+ }
252
+ if (render.unread) {
253
+ out.push({ ...base, path: rel("skills/*/SKILL.md"), current: null, output: { status: "not-run", reason: render.unread } });
254
+ }
255
+ return out;
256
+ }
257
+
258
+ /**
259
+ * Render the skills `chant update` would write for the member at `memberDir`,
260
+ * in this process. The member's `lexicons` are read from its config
261
+ * statically, never by running it. Lexicons declared by module path are left
262
+ * out, since loading one runs member code.
263
+ */
264
+ export async function renderSkillsStatically(memberDir: string): Promise<SkillRender> {
265
+ const { readLexiconDeclarationsStatically } = await import("../../config-static");
266
+ const read = readLexiconDeclarationsStatically(memberDir);
267
+ if (read.status === "no-config") return { status: "rendered", files: new Map() };
268
+ if (read.status === "unknown") return { status: "unknown", reason: `the config's lexicons can't be read without running it (${read.reason})` };
269
+ const packages = read.entries.filter((e): e is string => typeof e === "string");
270
+ const byPath = read.entries.filter((e) => typeof e !== "string").map((e) => (e as { name: string }).name);
271
+ const files = new Map<string, string>();
272
+ if (packages.length > 0) {
273
+ const { loadPlugins } = await import("../../cli/plugins");
274
+ const { skillFilePath } = await import("../../cli/commands/update");
275
+ for (const plugin of await loadPlugins(packages)) {
276
+ for (const skill of plugin.skills?.() ?? []) files.set(skillFilePath(skill.name), skill.content);
277
+ }
278
+ }
279
+ return {
280
+ status: "rendered",
281
+ files,
282
+ ...(byPath.length > 0 ? { unread: `the skills of ${byPath.join(", ")}, declared by module path, would run member code to render` } : {}),
283
+ };
284
+ }
285
+
286
+ // ── Running a generator ──────────────────────────────────────────────────────
287
+
288
+ /**
289
+ * Split a command line into words, with '...' and "..." quoting and
290
+ * backslash escapes. Returns undefined for anything a shell would interpret:
291
+ * pipes, redirections, `;`, `&`, substitutions. A generator is one command.
292
+ */
293
+ export function splitCommandLine(line: string): string[] | undefined {
294
+ const words: string[] = [];
295
+ let word = "";
296
+ let inWord = false;
297
+ for (let i = 0; i < line.length; i++) {
298
+ const c = line[i];
299
+ if (c === "'") {
300
+ const end = line.indexOf("'", i + 1);
301
+ if (end < 0) return undefined;
302
+ word += line.slice(i + 1, end);
303
+ inWord = true;
304
+ i = end;
305
+ } else if (c === '"') {
306
+ let j = i + 1;
307
+ for (; j < line.length && line[j] !== '"'; j++) {
308
+ if (line[j] === "$" || line[j] === "`") return undefined;
309
+ if (line[j] === "\\" && (line[j + 1] === '"' || line[j + 1] === "\\")) j++;
310
+ word += line[j];
311
+ }
312
+ if (j >= line.length) return undefined;
313
+ inWord = true;
314
+ i = j;
315
+ } else if (c === "\\") {
316
+ if (i + 1 >= line.length) return undefined;
317
+ word += line[++i];
318
+ inWord = true;
319
+ } else if (/\s/.test(c)) {
320
+ if (inWord) words.push(word);
321
+ word = "";
322
+ inWord = false;
323
+ } else if ("|&;<>()$`*?[]{}~#".includes(c) && !(inWord && c === "#")) {
324
+ return undefined;
325
+ } else {
326
+ word += c;
327
+ inWord = true;
328
+ }
329
+ }
330
+ if (inWord) words.push(word);
331
+ return words.length > 0 ? words : undefined;
332
+ }
333
+
334
+ /** `node_modules/.bin` directories from `dir` up, nearest first, so `chant` is the member's own. */
335
+ export function binPath(dir: string): string[] {
336
+ const out: string[] = [];
337
+ for (let d = resolve(dir); ; d = dirname(d)) {
338
+ const bin = join(d, "node_modules", ".bin");
339
+ if (existsSync(bin)) out.push(bin);
340
+ if (dirname(d) === d) return out;
341
+ }
342
+ }
343
+
344
+ /** Run a generator with no shell, for up to ten minutes. */
345
+ export const spawnGenerator: RunGenerator = ({ cwd, argv, env }) => {
346
+ const r = spawnSync(argv[0], argv.slice(1), { cwd, env, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 10 * 60_000, maxBuffer: 64 * 1024 * 1024 });
347
+ if (r.error) return { ok: false, message: r.error.message };
348
+ if (r.status !== 0) {
349
+ const tail = (r.stderr || r.stdout || "").trim().split("\n").slice(-5).join("\n");
350
+ return { ok: false, message: `exited with ${r.status ?? r.signal}${tail ? `:\n${tail}` : ""}` };
351
+ }
352
+ return { ok: true };
353
+ };
354
+
355
+ const OUTPUT_FLAGS = ["-o", "--output"];
356
+
357
+ /**
358
+ * Produce the file at `target` (absolute) with `generator`, run in
359
+ * `memberAbs`, without leaving anything changed in the tree. When the
360
+ * command passes `-o` or `--output` with the file's path, that flag gets a
361
+ * temporary path instead. Otherwise the generator writes in place, and the
362
+ * git working tree is put back as it was afterwards.
363
+ */
364
+ export function regenerate(root: string, memberAbs: string, target: string, generator: string, run: RunGenerator): GeneratorOutput {
365
+ const argv = splitCommandLine(generator);
366
+ if (!argv) return { status: "failed", message: "the generator is not a single command: quoting must balance, and pipes, redirections and shell expansions are not run" };
367
+ const env = { ...process.env, PATH: [...binPath(memberAbs), process.env.PATH ?? ""].join(delimiter) };
368
+
369
+ const at = (i: number) => resolve(memberAbs, argv[i]) === target;
370
+ let flag = -1;
371
+ let inline = false;
372
+ for (let i = 0; i < argv.length; i++) {
373
+ if (OUTPUT_FLAGS.includes(argv[i]) && i + 1 < argv.length && at(i + 1)) {
374
+ flag = i + 1;
375
+ break;
376
+ }
377
+ const eq = OUTPUT_FLAGS.map((f) => `${f}=`).find((p) => argv[i].startsWith(p));
378
+ if (eq && resolve(memberAbs, argv[i].slice(eq.length)) === target) {
379
+ flag = i;
380
+ inline = true;
381
+ break;
382
+ }
383
+ }
384
+
385
+ if (flag >= 0) {
386
+ const dir = mkdtempSync(join(tmpdir(), "chant-generated-"));
387
+ try {
388
+ const temp = join(dir, basename(target));
389
+ const args = [...argv];
390
+ args[flag] = inline ? `${argv[flag].slice(0, argv[flag].indexOf("=") + 1)}${temp}` : temp;
391
+ const r = run({ cwd: memberAbs, argv: args, env });
392
+ if (!r.ok) return { status: "failed", message: r.message };
393
+ if (!existsSync(temp)) return { status: "failed", message: "it exited 0 and wrote nothing to its output path" };
394
+ return { status: "produced", sha256: sha256(readFileSync(temp)) };
395
+ } finally {
396
+ rmSync(dir, { recursive: true, force: true });
397
+ }
398
+ }
399
+
400
+ const top = gitTop(root);
401
+ if (!top) return { status: "failed", message: "it writes in place, and chant runs an in-place generator only in a git checkout, which it can put back afterwards" };
402
+ return withTreeRestored(top, [target], () => {
403
+ const r = run({ cwd: memberAbs, argv, env });
404
+ if (!r.ok) return { status: "failed", message: r.message };
405
+ if (!existsSync(target)) return { status: "failed", message: "it exited 0 and left no file at the entry's path" };
406
+ return { status: "produced", sha256: sha256(readFileSync(target)) };
407
+ });
408
+ }
409
+
410
+ /** Paths git reports as changed or untracked, relative to `top`, with their status. */
411
+ function changedPaths(top: string): Map<string, string> {
412
+ const out = execFileSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: top, encoding: "utf-8", maxBuffer: 256 * 1024 * 1024 });
413
+ const parts = out.split("\0");
414
+ const map = new Map<string, string>();
415
+ for (let i = 0; i < parts.length; i++) {
416
+ const entry = parts[i];
417
+ if (entry.length < 4) continue;
418
+ const xy = entry.slice(0, 2);
419
+ map.set(entry.slice(3), xy);
420
+ // A rename or copy is followed by its original path.
421
+ if (xy[0] === "R" || xy[0] === "C") i++;
422
+ }
423
+ return map;
424
+ }
425
+
426
+ /**
427
+ * Run `fn`, then put the working tree of the repository at `top` back as it
428
+ * was: files that were clean are checked out from the index again, new
429
+ * untracked files are removed, and files that were already changed get their
430
+ * earlier bytes back. `extra` files (absolute) are restored too, whatever
431
+ * git says about them, which covers a gitignored target. Files a generator
432
+ * writes under ignored paths are otherwise left.
433
+ */
434
+ export function withTreeRestored<T>(top: string, extra: string[], fn: () => T): T {
435
+ const before = changedPaths(top);
436
+ const snapshot = new Map<string, Buffer | null>();
437
+ const keep = (rel: string) => {
438
+ const abs = join(top, rel);
439
+ let kind: "file" | "missing" | "other" = "missing";
440
+ try {
441
+ kind = statSync(abs).isFile() ? "file" : "other";
442
+ } catch {
443
+ // Missing: restoring means removing whatever appears there.
444
+ }
445
+ // A directory entry (a submodule) is left to git.
446
+ if (kind !== "other") snapshot.set(rel, kind === "file" ? readFileSync(abs) : null);
447
+ };
448
+ for (const rel of before.keys()) keep(rel);
449
+ for (const abs of extra) keep(relative(top, abs).split(sep).join("/"));
450
+ try {
451
+ return fn();
452
+ } finally {
453
+ const after = changedPaths(top);
454
+ const checkout: string[] = [];
455
+ for (const rel of new Set([...snapshot.keys(), ...after.keys()])) {
456
+ const abs = join(top, rel);
457
+ if (snapshot.has(rel)) {
458
+ const was = snapshot.get(rel)!;
459
+ const now = existsSync(abs) ? readFileSync(abs) : null;
460
+ if (was === null && now !== null) rmSync(abs, { force: true });
461
+ else if (was !== null && (now === null || !now.equals(was))) {
462
+ mkdirSync(dirname(abs), { recursive: true });
463
+ writeFileSync(abs, was);
464
+ }
465
+ } else if (after.get(rel) === "??") {
466
+ rmSync(abs, { force: true });
467
+ } else {
468
+ checkout.push(rel);
469
+ }
470
+ }
471
+ for (let i = 0; i < checkout.length; i += 200) {
472
+ execFileSync("git", ["checkout", "--", ...checkout.slice(i, i + 200)], { cwd: top, stdio: "ignore" });
473
+ }
474
+ }
475
+ }
@@ -0,0 +1,139 @@
1
+ import { afterAll, describe, expect, test } from "vitest";
2
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { MEMBER_LEDGER_FLOOR } from "../../lifecycle/member-ledger";
6
+ import { parseDeclaration } from "../declaration";
7
+ import {
8
+ checkDistinctStacks,
9
+ checkFlatLedgerEnvironments,
10
+ checkLedgers,
11
+ gatherLedgerFacts,
12
+ memberChantVersion,
13
+ writesFlatLedger,
14
+ type MemberLedgerFacts,
15
+ } from "./ledgers";
16
+
17
+ const scratch: string[] = [];
18
+ afterAll(() => {
19
+ for (const d of scratch) rmSync(d, { recursive: true, force: true });
20
+ });
21
+
22
+ function tree(files: Record<string, string>): string {
23
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-ledger-check-")));
24
+ scratch.push(root);
25
+ for (const [path, text] of Object.entries(files)) {
26
+ mkdirSync(dirname(join(root, path)), { recursive: true });
27
+ writeFileSync(join(root, path), text);
28
+ }
29
+ return root;
30
+ }
31
+
32
+ const fact = (name: string, over: Partial<MemberLedgerFacts> = {}): MemberLedgerFacts => ({
33
+ name,
34
+ dir: `services/${name}`,
35
+ stack: name,
36
+ environments: ["prod"],
37
+ chantVersion: MEMBER_LEDGER_FLOOR,
38
+ ...over,
39
+ });
40
+
41
+ describe("WSP071: distinct ownership stacks (#2538, ws-037)", () => {
42
+ test("distinct stacks pass, and a member with no stack is not compared", () => {
43
+ expect(checkDistinctStacks([fact("api"), fact("web"), fact("jobs", { stack: null }), fact("cron", { stack: null })])).toEqual([]);
44
+ });
45
+
46
+ test("a shared stack fails once, naming every member that uses it", () => {
47
+ const findings = checkDistinctStacks([fact("api", { stack: "shop" }), fact("web", { stack: "shop" }), fact("jobs", { stack: "shop" })]);
48
+ expect(findings).toHaveLength(1);
49
+ expect(findings[0]).toMatchObject({ id: "WSP071", severity: "error", members: ["api", "web", "jobs"] });
50
+ expect(findings[0].message).toContain('"shop"');
51
+ });
52
+ });
53
+
54
+ describe("WSP072: flat-layout writers never share an environment (#2538, ws-036)", () => {
55
+ test("members at or above the floor write under _members/ and never clash", () => {
56
+ expect(checkFlatLedgerEnvironments([fact("api"), fact("web")])).toEqual([]);
57
+ });
58
+
59
+ test("one member below the floor alone is fine", () => {
60
+ expect(checkFlatLedgerEnvironments([fact("api", { chantVersion: "0.79.0" }), fact("web")])).toEqual([]);
61
+ });
62
+
63
+ test("two members below the floor that share an environment fail", () => {
64
+ const findings = checkFlatLedgerEnvironments([
65
+ fact("api", { chantVersion: "0.79.0", environments: ["prod", "staging"] }),
66
+ fact("web", { chantVersion: "0.80.0", environments: ["prod"] }),
67
+ ]);
68
+ expect(findings).toHaveLength(1);
69
+ expect(findings[0]).toMatchObject({ id: "WSP072", members: ["api", "web"] });
70
+ expect(findings[0].message).toContain('"prod"');
71
+ expect(findings[0].message).toContain("chant 0.79.0");
72
+ });
73
+
74
+ test("two members below the floor with different environments pass", () => {
75
+ expect(
76
+ checkFlatLedgerEnvironments([
77
+ fact("api", { chantVersion: "0.79.0", environments: ["api-prod"] }),
78
+ fact("web", { chantVersion: "0.79.0", environments: ["web-prod"] }),
79
+ ]),
80
+ ).toEqual([]);
81
+ });
82
+
83
+ test("the root member writes flat too, so it clashes with an old member on the same environment", () => {
84
+ expect(writesFlatLedger(fact("root", { dir: "." }))).toBe(true);
85
+ const findings = checkFlatLedgerEnvironments([fact("root", { dir: "." }), fact("api", { chantVersion: "0.60.0" })]);
86
+ expect(findings.map((f) => f.members)).toEqual([["root", "api"]]);
87
+ });
88
+
89
+ test("an unknown or unreadable chant version is not treated as old", () => {
90
+ expect(writesFlatLedger(fact("api", { chantVersion: null }))).toBe(false);
91
+ expect(writesFlatLedger(fact("api", { chantVersion: "latest" }))).toBe(false);
92
+ });
93
+
94
+ test("checkLedgers reports both kinds, stacks first", () => {
95
+ const findings = checkLedgers([
96
+ fact("api", { stack: "shop", chantVersion: "0.79.0" }),
97
+ fact("web", { stack: "shop", chantVersion: "0.79.0" }),
98
+ ]);
99
+ expect(findings.map((f) => f.id)).toEqual(["WSP071", "WSP072"]);
100
+ });
101
+ });
102
+
103
+ describe("facts from a checkout", () => {
104
+ test("memberChantVersion finds the nearest installed chant walking up", () => {
105
+ const root = tree({
106
+ "node_modules/@intentius/chant/package.json": JSON.stringify({ name: "@intentius/chant", version: "0.79.3" }),
107
+ "services/web/node_modules/@intentius/chant/package.json": JSON.stringify({ name: "@intentius/chant", version: "0.81.0" }),
108
+ "services/api/chant.config.json": "{}",
109
+ });
110
+ expect(memberChantVersion(join(root, "services/api"))).toBe("0.79.3");
111
+ expect(memberChantVersion(join(root, "services/web"))).toBe("0.81.0");
112
+ });
113
+
114
+ test("gatherLedgerFacts reads chant members' stacks and environments and skips the rest", async () => {
115
+ const root = tree({
116
+ "services/api/chant.config.json": JSON.stringify({ ownership: { stack: "shop", env: "prod" }, environments: ["staging"] }),
117
+ "services/web/chant.config.json": JSON.stringify({ ownership: { stack: "shop" }, environments: ["prod"] }),
118
+ "docs/package.json": "{}",
119
+ });
120
+ const declaration = parseDeclaration(
121
+ JSON.stringify({
122
+ name: "acme",
123
+ schema: 1,
124
+ members: [
125
+ { name: "api", dir: "services/api", kind: "chant" },
126
+ { name: "web", dir: "services/web", kind: "chant" },
127
+ { name: "docs", dir: "docs", kind: "other", because: "a docs site" },
128
+ ],
129
+ }),
130
+ "chant.workspace.json",
131
+ );
132
+ const facts = await gatherLedgerFacts(root, declaration);
133
+ expect(facts.map((f) => [f.name, f.stack, f.environments])).toEqual([
134
+ ["api", "shop", ["staging", "prod"]],
135
+ ["web", "shop", ["prod"]],
136
+ ]);
137
+ expect(checkLedgers(facts).map((f) => f.id)).toEqual(["WSP071"]);
138
+ });
139
+ });