@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
@@ -1,9 +1,10 @@
1
1
  import { getRuntime } from "../runtime-adapter";
2
- import { readdir } from "node:fs/promises";
3
2
  import { existsSync } from "node:fs";
4
3
  import { dirname, join, resolve } from "node:path";
5
4
  import type { OpConfig } from "./types";
6
- import { warnDiscoveryChanges } from "../discovery/convergence";
5
+ import { walkDiscovery, workspaceMemberDirs } from "../discovery/walk";
6
+ import { hasDiscoveryMarkerSync } from "../discovery/files";
7
+ import { resolveDiscoveryGlobs } from "../config";
7
8
 
8
9
  export interface DiscoveredOp {
9
10
  config: OpConfig;
@@ -53,28 +54,22 @@ async function findDiscoveryRoot(cwd?: string): Promise<string> {
53
54
  return gitRoot;
54
55
  }
55
56
 
56
- async function collectOpFiles(dir: string): Promise<string[]> {
57
- const files: string[] = [];
58
- let entries;
59
- try {
60
- entries = await readdir(dir, { withFileTypes: true });
61
- } catch {
62
- return files;
63
- }
64
- for (const entry of entries) {
65
- const fullPath = join(dir, entry.name);
66
- if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules" && entry.name !== "dist") {
67
- files.push(...await collectOpFiles(fullPath));
68
- } else if (
69
- entry.isFile() &&
70
- entry.name.endsWith(".op.ts") &&
71
- !entry.name.endsWith(".test.ts") &&
72
- !entry.name.endsWith(".spec.ts")
73
- ) {
74
- files.push(fullPath);
75
- }
76
- }
77
- return files;
57
+ /**
58
+ * Every `*.op.ts` file below the Op root, through the one discovery walk
59
+ * (`../discovery/walk.ts`, #2527). The walk down stops at child projects
60
+ * whatever the root is, so a configless layout whose Op root is the git root
61
+ * no longer collects its sibling projects' Ops. The project's
62
+ * `exclude`/`include` globs and the skip marker apply.
63
+ */
64
+ async function collectOpFiles(root: string): Promise<string[]> {
65
+ return walkDiscovery({
66
+ walker: "ops",
67
+ root,
68
+ globs: await resolveDiscoveryGlobs(root),
69
+ excludeDirs: await workspaceMemberDirs(root),
70
+ accept: (name, full) =>
71
+ name.endsWith(".op.ts") && !name.endsWith(".test.ts") && !name.endsWith(".spec.ts") && !hasDiscoveryMarkerSync(full),
72
+ });
78
73
  }
79
74
 
80
75
  /** The `OpConfig` behind an exported value, or `undefined` when the value is not an Op. An Op entity carries its config on `.props` (`./resource.ts`); `name` and `phases` are what every consumer of a discovered Op reads. */
@@ -149,9 +144,6 @@ export async function discoverOps(opts?: { cwd?: string }): Promise<OpDiscoveryR
149
144
 
150
145
  const root = await findDiscoveryRoot(opts?.cwd);
151
146
  const files = await collectOpFiles(root);
152
- // #2527's warning release: Op discovery stops at child projects below the
153
- // root next release, and skips git-ignored files. `files` is unchanged.
154
- await warnDiscoveryChanges({ walker: "ops", root, files });
155
147
 
156
148
  const nameToFile = new Map<string, string>();
157
149
 
@@ -29,6 +29,14 @@ import type { OpConfig, PhaseDefinition, StepDefinition } from "./types";
29
29
  */
30
30
  export const FAN_OUT_GATE_OP = "fan-out";
31
31
 
32
+ /**
33
+ * The op name a `chant workspace upgrade` gate is recorded under (#2550). The
34
+ * gate is the scope (`.` for a plain project), and it binds the digest of the
35
+ * upgrade's patch. Like a fan-out, the upgrade is a command with no `*.op.ts`
36
+ * behind it, so `chant approve` must not warn that it found none.
37
+ */
38
+ export const WORKSPACE_UPGRADE_GATE_OP = "workspace-upgrade";
39
+
32
40
  /** Either spelling of a gate step's name. `gate` since #2202; `signalName` through 0.59.0. */
33
41
  export interface GateNamed {
34
42
  gate?: string;
@@ -58,6 +58,20 @@ describe("gatedRunSummaryMarkdown (#2243)", () => {
58
58
  expect(md).toContain("and not the next run");
59
59
  });
60
60
 
61
+ // #2574 with #2538: a component gate in a workspace member names both its
62
+ // environment in the approve line and the member's gate ledger path.
63
+ test("a member's component gate names its environment and its member ledger path", () => {
64
+ const md = gatedRunSummaryMarkdown({
65
+ op: "search-service",
66
+ gate: "release",
67
+ expiresAt: "2026-09-08T00:00:00Z",
68
+ environment: "prod",
69
+ ledgerPrefix: "_members/services/search/",
70
+ });
71
+ expect(md).toContain("chant approve search-service release --env prod --approver <you>");
72
+ expect(md).toContain("`_members/services/search/_gates/search-service.jsonl`");
73
+ });
74
+
61
75
  test("a gate that binds no plan says nothing about one", () => {
62
76
  expect(gatedRunSummaryMarkdown(summary)).not.toContain("and not the next run");
63
77
  });
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { appendFileSync } from "node:fs";
22
22
  import { gateLedgerPath } from "../lifecycle/gate-ledger";
23
+ import { resolveMemberLedger } from "../lifecycle/member-ledger";
23
24
  import { approveCommand } from "./gate";
24
25
 
25
26
  /** What a gated run knows about the gate it stopped on. */
@@ -50,6 +51,28 @@ export interface GatedRunSummary {
50
51
  * to see that the digest has moved on.
51
52
  */
52
53
  planDigest?: string;
54
+ /**
55
+ * The project's ledger prefix, `_members/<member>/` for a workspace member
56
+ * (#2538), so the ledger path shown is the one the fact was written to.
57
+ * Absent at level 0 and for the root member.
58
+ */
59
+ ledgerPrefix?: string;
60
+ /** The environment a component gate's pending fact was recorded in (#2574). The `chant approve` line names it. */
61
+ environment?: string;
62
+ }
63
+
64
+ /**
65
+ * `{ ledgerPrefix }` for the project at `cwd` when it is a workspace member,
66
+ * else `{}`. A summary is a report, so a declaration that cannot be read
67
+ * leaves the path as it is rather than failing the report.
68
+ */
69
+ export async function summaryLedgerPrefix(cwd: string = process.cwd()): Promise<{ ledgerPrefix?: string }> {
70
+ try {
71
+ const { prefix } = await resolveMemberLedger(cwd);
72
+ return prefix ? { ledgerPrefix: prefix } : {};
73
+ } catch {
74
+ return {};
75
+ }
53
76
  }
54
77
 
55
78
  /**
@@ -78,10 +101,10 @@ export function gatedRunSummaryMarkdown(summary: GatedRunSummary): string {
78
101
  "Approve it, then re-run this workflow:",
79
102
  "",
80
103
  "```",
81
- `${approveCommand(summary.op, summary.gate)} --approver <you>`,
104
+ `${approveCommand(summary.op, summary.gate, summary.environment)} --approver <you>`,
82
105
  "```",
83
106
  "",
84
- `Ledger: \`${gateLedgerPath(summary.op)}\` on the \`chant/lifecycle\` branch.`,
107
+ `Ledger: \`${gateLedgerPath(summary.op, summary.ledgerPrefix)}\` on the \`chant/lifecycle\` branch.`,
85
108
  );
86
109
  if (summary.expiresAt) lines.push(`Expires: ${summary.expiresAt}`);
87
110
  if (summary.url) lines.push(`Approve at: ${summary.url}`);
package/src/op/gate.ts CHANGED
@@ -170,6 +170,14 @@ export interface GateCheckInput {
170
170
  * the standing pending fact satisfies the gate whatever has changed since.
171
171
  */
172
172
  planDigest?: string;
173
+ /**
174
+ * The environment this run deploys to (#2574). The component driver sets it,
175
+ * because a component's gate ledger is keyed by component and holds every
176
+ * environment's facts. Set, the gate reads only this environment's pending
177
+ * facts and resolutions (plus resolutions that record no environment, which
178
+ * come back as a mismatch), and the pending fact it records carries it.
179
+ */
180
+ environment?: string;
173
181
  /**
174
182
  * The gate's quorum and policy (#2508), context already resolved. Absent,
175
183
  * one approval passes the gate, which is the rule every gate had before.
@@ -194,6 +202,8 @@ export interface GateDigestMismatch {
194
202
  resolvedBy: string;
195
203
  /** When they recorded it. */
196
204
  timestamp: string;
205
+ /** The environment this run deploys to, on a component gate (#2574). The approve command a refusal prints names it. */
206
+ environment?: string;
197
207
  }
198
208
 
199
209
  /**
@@ -210,15 +220,18 @@ export interface GateDigestMismatch {
210
220
  export function describeGateMismatch(op: string, gate: string, mismatch: GateDigestMismatch): string {
211
221
  const why =
212
222
  mismatch.approved === undefined
213
- ? "That resolution predates plan-bound gates (#2300) and records no plan at all, so it cannot " +
214
- "answer for this one. Approving again binds it:"
223
+ ? mismatch.environment !== undefined
224
+ ? "That approval predates environment- and plan-bound component gates (#2574) and records neither, " +
225
+ "so it cannot answer for this one. Approving again binds it:"
226
+ : "That resolution predates plan-bound gates (#2300) and records no plan at all, so it cannot " +
227
+ "answer for this one. Approving again binds it:"
215
228
  : "The configuration or the live system changed between that approval and this plan, so it needs " +
216
229
  "a fresh one:";
217
230
  return (
218
231
  `Gate "${gate}" is approved, but not for this plan. ` +
219
232
  `approved: ${describePlanDigest(mismatch.approved)} (by ${mismatch.resolvedBy} at ${mismatch.timestamp}); ` +
220
233
  `planned: ${mismatch.planned}. ` +
221
- `${why} ${approveCommand(op, gate)}`
234
+ `${why} ${approveCommand(op, gate, mismatch.environment)}`
222
235
  );
223
236
  }
224
237
 
@@ -374,7 +387,17 @@ const EPOCH = new Date(0).toISOString();
374
387
  */
375
388
  export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput): Promise<GateCheck> {
376
389
  const now = input.now ?? new Date().toISOString();
377
- const { resolutions, pending } = await port.read(input.op);
390
+ const ledger = await port.read(input.op);
391
+ // #2574: a component's ledger holds every environment's facts. This run
392
+ // answers only to its own environment's pending facts and approvals. An
393
+ // approval that records no environment at all is kept, so it comes back as
394
+ // a mismatch with a message instead of silently not counting.
395
+ const env = input.environment;
396
+ const pending = env === undefined ? ledger.pending : ledger.pending.filter((p) => p.environment === env);
397
+ const resolutions =
398
+ env === undefined
399
+ ? ledger.resolutions
400
+ : ledger.resolutions.filter((r) => r.environment === undefined || r.environment === env);
378
401
 
379
402
  const standing = latestPendingGate(pending, input.gate);
380
403
  const since = standing?.timestamp ?? EPOCH;
@@ -410,6 +433,7 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
410
433
  planned: input.planDigest!,
411
434
  resolvedBy: mismatched.resolvedBy,
412
435
  timestamp: mismatched.timestamp,
436
+ ...(env !== undefined ? { environment: env } : {}),
413
437
  }
414
438
  : undefined;
415
439
  const asMismatch = { ...(mismatch ? { mismatch } : {}), ...(quorum ? { quorum } : {}) };
@@ -441,6 +465,7 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
441
465
  ...(input.runId ? { runId: input.runId } : {}),
442
466
  ...(url ? { url } : {}),
443
467
  ...(input.planDigest !== undefined ? { planDigest: input.planDigest } : {}),
468
+ ...(env !== undefined ? { environment: env } : {}),
444
469
  ...(input.approval ? { approval: input.approval } : {}),
445
470
  });
446
471
  return {
@@ -453,7 +478,11 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
453
478
  };
454
479
  }
455
480
 
456
- /** The line every renderer prints to say how a pending gate is cleared. */
457
- export function approveCommand(op: string, gate: string): string {
458
- return `chant approve ${op} ${gate}`;
481
+ /**
482
+ * The line every renderer prints to say how a pending gate is cleared. A
483
+ * component gate names its environment (#2574), so the approval picks that
484
+ * environment's pending fact when others stand beside it.
485
+ */
486
+ export function approveCommand(op: string, gate: string, environment?: string): string {
487
+ return environment === undefined ? `chant approve ${op} ${gate}` : `chant approve ${op} ${gate} --env ${environment}`;
459
488
  }
@@ -3,12 +3,16 @@
3
3
  * `../components/cli-support.ts`'s `generateComponentsPipeline`. Mirrors that
4
4
  * module's own test style (`cli-support.test.ts`): a minimal mocked lexicon
5
5
  * plugin satisfying `isLexiconPlugin`, real `discoverOps()` resolution
6
- * against this repo's actual `*.op.ts` fixtures (`examples/alb-deploy.op.ts`
7
- * — see `./discover.test.ts`) so Op-name validation exercises the real
8
- * discovery path rather than a stub.
6
+ * against this repo's actual `*.op.ts` fixtures
7
+ * (`examples/gitlab-aws-alb-infra/ops/alb-deploy.op.ts`, see
8
+ * `./discover.test.ts`) so Op-name validation exercises the real discovery
9
+ * path rather than a stub. Discovery starts in that example: since #2527 the
10
+ * walk from the git root stops at child projects, so the repo root finds none.
9
11
  */
10
12
 
11
13
  import { describe, test, expect, vi } from "vitest";
14
+ import { dirname, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
12
16
  import { generateOpsPipeline, withOpSchedules } from "./generate-pipeline";
13
17
  import type { DiscoveredOp } from "./discover";
14
18
  import type { OpConfig } from "./types";
@@ -34,6 +38,9 @@ vi.mock("@intentius/chant-lexicon-gitlab", () => ({
34
38
  },
35
39
  }));
36
40
 
41
+ /** The example project that holds `alb-deploy.op.ts`. */
42
+ const ALB_INFRA = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../examples/gitlab-aws-alb-infra");
43
+
37
44
  describe("generateOpsPipeline", () => {
38
45
  test("errors when the target lexicon has no generateOpPipeline", async () => {
39
46
  const result = await generateOpsPipeline([{ name: "alb-deploy", schedule: "0 6 * * *" }], "aws");
@@ -45,6 +52,8 @@ describe("generateOpsPipeline", () => {
45
52
  const result = await generateOpsPipeline(
46
53
  [{ name: "definitely-not-a-real-op", schedule: "0 6 * * *" }],
47
54
  "gitlab",
55
+ undefined,
56
+ ALB_INFRA,
48
57
  );
49
58
  expect(result.success).toBe(false);
50
59
  expect(result.error).toMatch(/Unknown Op\(s\): definitely-not-a-real-op/);
@@ -52,7 +61,7 @@ describe("generateOpsPipeline", () => {
52
61
 
53
62
  test("validates every named Op and delegates to the lexicon plugin", async () => {
54
63
  const specs: ScheduledOpSpec[] = [{ name: "alb-deploy", schedule: "0 6 * * *", findingMode: "issue" }];
55
- const result = await generateOpsPipeline(specs, "gitlab");
64
+ const result = await generateOpsPipeline(specs, "gitlab", undefined, ALB_INFRA);
56
65
  expect(result.success).toBe(true);
57
66
  expect(result.files).toEqual([{ name: "alb-deploy.yml", yaml: "# alb-deploy @ 0 6 * * *" }]);
58
67
  expect(result.jobs).toEqual([
@@ -66,7 +75,7 @@ describe("generateOpsPipeline", () => {
66
75
  // `PipelineAuditOp`, `ReconcileOp`) only ever sets `schedule`.
67
76
  test("a legacy `{ schedule }` spec with no `trigger` round-trips unchanged", async () => {
68
77
  const specs: ScheduledOpSpec[] = [{ name: "alb-deploy", schedule: "0 6 * * *" }];
69
- const result = await generateOpsPipeline(specs, "gitlab");
78
+ const result = await generateOpsPipeline(specs, "gitlab", undefined, ALB_INFRA);
70
79
  expect(result.success).toBe(true);
71
80
  expect(result.files).toEqual([{ name: "alb-deploy.yml", yaml: "# alb-deploy @ 0 6 * * *" }]);
72
81
  expect(result.jobs).toEqual([
@@ -2,7 +2,7 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest";
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { tmpdir } from "os";
5
- import { findProjectConfig, findProjectRoot } from "./project-root";
5
+ import { findProjectConfig, findProjectRoot, findWorkspaceRoot } from "./project-root";
6
6
 
7
7
  // Every test builds its own isolated directory tree under a fresh tmpdir —
8
8
  // never reused across tests, and never anywhere near the real repo's own
@@ -103,3 +103,68 @@ describe("findProjectConfig / findProjectRoot (chant #1117)", () => {
103
103
  expect(result.configPath).toBe(join(projectRoot, "chant.config.ts"));
104
104
  });
105
105
  });
106
+
107
+ describe("findWorkspaceRoot (chant #2534)", () => {
108
+ const declare = (dir: string, name = "chant.workspace.json") => {
109
+ mkdirSync(dir, { recursive: true });
110
+ writeFileSync(join(dir, name), "{}");
111
+ };
112
+
113
+ test("walks up to the nearest declaration, and never looks at chant.config", () => {
114
+ mkdirSync(join(root, ".git"));
115
+ declare(root);
116
+ const deep = join(root, "lexicons", "aws", "examples", "one");
117
+ mkdirSync(deep, { recursive: true });
118
+ writeFileSync(join(root, "lexicons", "aws", "package.json"), "{}");
119
+ writeFileSync(join(deep, "chant.config.ts"), "export default {};");
120
+ expect(findWorkspaceRoot(deep)).toEqual({ dir: root, file: join(root, "chant.workspace.json") });
121
+ // findProjectConfig is unchanged: it still stops at the project's own config.
122
+ expect(findProjectConfig(deep).dir).toBe(deep);
123
+ });
124
+
125
+ test("the nearest declaration wins, so a nested workspace finds itself", () => {
126
+ mkdirSync(join(root, ".git"));
127
+ declare(root);
128
+ const inner = join(root, "vendor", "kit");
129
+ declare(inner, "chant.workspace.jsonc");
130
+ mkdirSync(join(inner, "src"));
131
+ expect(findWorkspaceRoot(join(inner, "src"))).toEqual({ dir: inner, file: join(inner, "chant.workspace.jsonc") });
132
+ });
133
+
134
+ test("stops at the git root after checking it", () => {
135
+ declare(root);
136
+ const repo = join(root, "repo");
137
+ mkdirSync(join(repo, ".git"), { recursive: true });
138
+ mkdirSync(join(repo, "src"));
139
+ expect(findWorkspaceRoot(join(repo, "src"))).toBeUndefined();
140
+ declare(repo);
141
+ expect(findWorkspaceRoot(join(repo, "src"))?.dir).toBe(repo);
142
+ });
143
+
144
+ test("a .git file (a worktree or submodule) is a git root too", () => {
145
+ declare(root);
146
+ const repo = join(root, "wt");
147
+ mkdirSync(repo);
148
+ writeFileSync(join(repo, ".git"), "gitdir: /elsewhere\n");
149
+ expect(findWorkspaceRoot(repo)).toBeUndefined();
150
+ });
151
+
152
+ test("outside a git repository only the start directory is checked", () => {
153
+ declare(root);
154
+ const sub = join(root, "sub");
155
+ mkdirSync(sub);
156
+ expect(findWorkspaceRoot(sub)).toBeUndefined();
157
+ expect(findWorkspaceRoot(root)?.dir).toBe(root);
158
+ });
159
+
160
+ test("both names present is reported, not thrown", () => {
161
+ mkdirSync(join(root, ".git"));
162
+ declare(root);
163
+ declare(root, "chant.workspace.jsonc");
164
+ expect(findWorkspaceRoot(root)).toEqual({
165
+ dir: root,
166
+ file: join(root, "chant.workspace.json"),
167
+ conflicting: join(root, "chant.workspace.jsonc"),
168
+ });
169
+ });
170
+ });
@@ -76,3 +76,51 @@ export function findProjectConfig(startDir: string): ProjectConfigSearch {
76
76
  export function findProjectRoot(startDir: string): string {
77
77
  return findProjectConfig(startDir).dir;
78
78
  }
79
+
80
+ /** The two names a workspace declaration may have (#2524 D1). */
81
+ export const WORKSPACE_DECLARATION_FILES = ["chant.workspace.json", "chant.workspace.jsonc"] as const;
82
+
83
+ /** Where {@link findWorkspaceRoot} found a declaration. */
84
+ export interface WorkspaceRootSearch {
85
+ /** The directory holding the declaration. */
86
+ dir: string;
87
+ /** Absolute path of the declaration file. When both names are present this is the `.json` one. */
88
+ file: string;
89
+ /**
90
+ * Absolute path of the `.jsonc` file when `.json` is also there. Having both
91
+ * is an error, which the reader reports; the walk itself never throws.
92
+ */
93
+ conflicting?: string;
94
+ }
95
+
96
+ /**
97
+ * Walk up from `startDir` (inclusive) to the nearest `chant.workspace.json` or
98
+ * `chant.workspace.jsonc` (#2524 D1). The walk stops at the git root, the first
99
+ * directory holding `.git`, after checking it. Outside a git repository only
100
+ * `startDir` itself is checked, so a stray declaration in an unrelated
101
+ * ancestor is never picked up.
102
+ *
103
+ * It only tests whether files exist and loads nothing else, so a level-0
104
+ * command can afford to call it. {@link findProjectConfig} is unchanged and
105
+ * never looks at a declaration.
106
+ */
107
+ export function findWorkspaceRoot(startDir: string): WorkspaceRootSearch | undefined {
108
+ const start = resolve(startDir);
109
+ let top: string | undefined;
110
+ for (let dir = start; ; dir = dirname(dir)) {
111
+ if (existsSync(join(dir, ".git"))) {
112
+ top = dir;
113
+ break;
114
+ }
115
+ if (dirname(dir) === dir) break;
116
+ }
117
+ for (let dir = start; ; dir = dirname(dir)) {
118
+ const [json, jsonc] = WORKSPACE_DECLARATION_FILES.map((name) => join(dir, name));
119
+ const hasJson = existsSync(json);
120
+ const hasJsonc = existsSync(jsonc);
121
+ if (hasJson && hasJsonc) return { dir, file: json, conflicting: jsonc };
122
+ if (hasJson) return { dir, file: json };
123
+ if (hasJsonc) return { dir, file: jsonc };
124
+ if (top === undefined || dir === top || dirname(dir) === dir) return undefined;
125
+ }
126
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Helpers the read-contract tests share (#2536): throwaway git repositories,
3
+ * a draft 2020-12 validator, and a fake chant toolchain.
4
+ */
5
+
6
+ import { execFileSync } from "node:child_process";
7
+ import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+ import Ajv2020 from "ajv/dist/2020";
11
+ import { expect } from "vitest";
12
+
13
+ /** The chant checkout these tests run in. */
14
+ export const REPO = realpathSync(join(import.meta.dirname, "..", "..", "..", "..", ".."));
15
+
16
+ const scratch: string[] = [];
17
+
18
+ /** Remove every directory {@link scratchDir} and {@link repo} made. Call it from `afterAll`. */
19
+ export function cleanScratch(): void {
20
+ for (const d of scratch.splice(0)) rmSync(d, { recursive: true, force: true });
21
+ }
22
+
23
+ export function scratchDir(prefix = "chant-contract-"): string {
24
+ const d = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
25
+ scratch.push(d);
26
+ return d;
27
+ }
28
+
29
+ export function git(cwd: string, ...args: string[]): string {
30
+ return execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...args], {
31
+ cwd,
32
+ encoding: "utf-8",
33
+ stdio: ["ignore", "pipe", "pipe"],
34
+ }).trim();
35
+ }
36
+
37
+ /** Write `files` under `root`; a value of `{ text, mode }` sets the mode too. */
38
+ export function writeFiles(root: string, files: Record<string, string | { text: string; mode: number }>): void {
39
+ for (const [path, value] of Object.entries(files)) {
40
+ const full = join(root, path);
41
+ mkdirSync(dirname(full), { recursive: true });
42
+ writeFileSync(full, typeof value === "string" ? value : value.text);
43
+ if (typeof value !== "string") chmodSync(full, value.mode);
44
+ }
45
+ }
46
+
47
+ /** A git repository holding `files`, with nothing committed unless `commit` is set. */
48
+ export function repo(files: Record<string, string | { text: string; mode: number }>, commit = false): string {
49
+ const root = scratchDir("chant-contract-repo-");
50
+ writeFiles(root, files);
51
+ git(root, "init", "-q");
52
+ if (commit) {
53
+ git(root, "add", "-A");
54
+ git(root, "commit", "-q", "-m", "one");
55
+ }
56
+ return root;
57
+ }
58
+
59
+ export function commitAll(root: string, message = "next"): string {
60
+ git(root, "add", "-A");
61
+ git(root, "commit", "-q", "-m", message);
62
+ return git(root, "rev-parse", "HEAD");
63
+ }
64
+
65
+ const ajv = new Ajv2020({ strict: true, allErrors: true });
66
+
67
+ /** A validator for `schema`, and an assertion that a document passes it. */
68
+ export function contract(schema: object): { validate: ReturnType<typeof ajv.compile>; expectValid(doc: unknown): void } {
69
+ const validate = ajv.compile(schema);
70
+ return {
71
+ validate,
72
+ expectValid(doc) {
73
+ const ok = validate(doc);
74
+ expect(ok, `${JSON.stringify(validate.errors, null, 2)}\n${JSON.stringify(doc, null, 2).slice(0, 2000)}`).toBe(true);
75
+ },
76
+ };
77
+ }
78
+
79
+ export function validSchema(schema: object): boolean {
80
+ return ajv.validateSchema(schema) as boolean;
81
+ }
82
+
83
+ export const declaration = (members: unknown[], extra: Record<string, unknown> = {}) =>
84
+ JSON.stringify({ name: "acme", schema: 1, ...extra, members }, null, 2);
85
+
86
+ /**
87
+ * A chant older than `workspace member-run`: it answers `chant graph` in the
88
+ * member's directory with one node per line of the member's `ids.txt`.
89
+ */
90
+ export const FAKE_GRAPH_CHANT = `#!/bin/sh
91
+ case "$1" in
92
+ graph)
93
+ printf '{"version":1,"nodes":['
94
+ sep=""
95
+ while read -r id; do printf '%s{"id":"%s","kind":"Thing","lexicon":"fake","attrs":{}}' "$sep" "$id"; sep=","; done < ids.txt
96
+ printf '],"edges":[],"groups":{"byLexicon":{}}}\\n'
97
+ ;;
98
+ *) echo "Error: Unknown command: $1" >&2; exit 1 ;;
99
+ esac
100
+ `;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A throwaway git repository holding a workspace with two members (api and
3
+ * web), optionally a root member, and an example group with one match. Used
4
+ * by the per-member pipeline tests (#2542).
5
+ */
6
+
7
+ import { execFileSync } from "node:child_process";
8
+ import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+
12
+ /** A git repository holding a workspace with two members, a root member and an example group. */
13
+ export function twoMemberWorkspace(options: { root?: boolean; prefix?: string } = {}): string {
14
+ const repo = realpathSync(mkdtempSync(join(tmpdir(), "chant-member-pipeline-")));
15
+ execFileSync("git", ["init", "-q"], { cwd: repo });
16
+ const ws = options.prefix ? join(repo, options.prefix) : repo;
17
+ const members: unknown[] = [
18
+ { name: "api", dir: "services/api", kind: "chant" },
19
+ { name: "web", dir: "apps/web", kind: "chant" },
20
+ { name: "samples", kind: "examples", glob: "examples/*" },
21
+ ];
22
+ if (options.root) members.unshift({ name: "platform", dir: ".", kind: "chant" });
23
+ const files: Record<string, string> = {
24
+ "chant.workspace.json": JSON.stringify({ name: "shop", schema: 1, members }, null, 2),
25
+ "services/api/chant.config.ts": "export default {};\n",
26
+ "services/api/src/infra.ts": "\n",
27
+ "apps/web/chant.config.ts": "export default {};\n",
28
+ "examples/demo/chant.config.ts": "export default {};\n",
29
+ };
30
+ for (const [path, text] of Object.entries(files)) {
31
+ mkdirSync(dirname(join(ws, path)), { recursive: true });
32
+ writeFileSync(join(ws, path), text);
33
+ }
34
+ return repo;
35
+ }