@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,208 @@
1
+ /**
2
+ * chant#2591, chant#2589 — `init --force`, `dev onboard`, `import --agents`
3
+ * and `audit` learn the lexicons a project declares by path without running
4
+ * its `chant.config.ts`.
5
+ *
6
+ * The CLI is spawned the way `bin/chant` runs it, because `main.ts` loads the
7
+ * config before dispatch for most commands, and a test calling the command
8
+ * functions would miss that load. Each fixture's config writes a marker file
9
+ * when it runs. The marker must never appear, and each command's output must
10
+ * still name the lexicon's path.
11
+ */
12
+ import { describe, test, expect, beforeEach, afterEach } from "vitest";
13
+ import { spawnSync } from "node:child_process";
14
+ import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { dirname, join, resolve } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { auditCommand } from "./commands/audit";
19
+ import { runCommandInProcess } from "./main";
20
+ import { resetLexiconModules } from "../lexicon-module";
21
+ import { captureRun } from "../workspace/member-run";
22
+
23
+ const thisDir = dirname(fileURLToPath(import.meta.url));
24
+ const mainTs = resolve(thisDir, "main.ts");
25
+ const repoRoot = resolve(thisDir, "../../../..");
26
+ const tsxLoader = join(repoRoot, "node_modules", "tsx", "dist", "loader.mjs");
27
+
28
+ let root: string;
29
+ let project: string;
30
+ let marker: string;
31
+ let home: string;
32
+ let moduleMarker: string;
33
+
34
+ function writeConfig(lexicons: string): void {
35
+ writeFileSync(
36
+ join(project, "chant.config.ts"),
37
+ `import { writeFileSync } from "node:fs";\n` +
38
+ `writeFileSync(${JSON.stringify(marker)}, "ran");\n` +
39
+ `export default { lexicons: ${lexicons} };\n`,
40
+ );
41
+ }
42
+
43
+ beforeEach(() => {
44
+ resetLexiconModules();
45
+ root = realpathSync(mkdtempSync(join(tmpdir(), "chant-2591-cli-")));
46
+ project = join(root, "project");
47
+ marker = join(root, "config-ran");
48
+ home = join(root, "home");
49
+ moduleMarker = join(root, "module-ran");
50
+ mkdirSync(join(project, ".github", "workflows"), { recursive: true });
51
+ mkdirSync(join(project, "gh"), { recursive: true });
52
+ mkdirSync(join(project, ".git"));
53
+ // The path lexicon's module is project code too. Audit must never import it.
54
+ writeFileSync(
55
+ join(project, "gh", "index.ts"),
56
+ `import { writeFileSync } from "node:fs";\nwriteFileSync(${JSON.stringify(moduleMarker)}, "ran");\nexport {};\n`,
57
+ );
58
+ writeFileSync(
59
+ join(project, ".github", "workflows", "ci.yml"),
60
+ "on: push\njobs:\n a:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n",
61
+ );
62
+ mkdirSync(join(home, ".claude"), { recursive: true });
63
+ writeFileSync(join(home, ".claude", "mcp.json"), JSON.stringify({ mcpServers: { a: { command: "x" } } }));
64
+ writeConfig(`[{ name: "github", module: "./gh/index.ts" }]`);
65
+ });
66
+
67
+ afterEach(() => {
68
+ resetLexiconModules();
69
+ rmSync(root, { recursive: true, force: true });
70
+ });
71
+
72
+ function chant(args: string[], cwd: string): string {
73
+ const result = spawnSync(process.execPath, ["--import", tsxLoader, mainTs, ...args], {
74
+ cwd,
75
+ env: { PATH: process.env.PATH ?? "", HOME: home },
76
+ encoding: "utf-8",
77
+ input: "n\n",
78
+ timeout: 60_000,
79
+ });
80
+ return `${result.stdout}\n${result.stderr}`;
81
+ }
82
+
83
+ const COMMANDS: Array<{ name: string; args: () => string[]; cwd: () => string }> = [
84
+ { name: "init --force", args: () => ["init", project, "--lexicon", "github", "--force", "--skip-mcp"], cwd: () => root },
85
+ { name: "dev onboard", args: () => ["dev", "onboard", "github"], cwd: () => project },
86
+ { name: "import --agents", args: () => ["import", "--agents", "--lexicon", "github", "--output", join(root, "out")], cwd: () => project },
87
+ ];
88
+
89
+ describe("commands that read path lexicons statically run no project code", () => {
90
+ for (const command of COMMANDS) {
91
+ for (const sandbox of [false, true]) {
92
+ test(`${command.name}${sandbox ? " --sandbox" : ""} names the path and never runs chant.config.ts`, () => {
93
+ const output = chant([...command.args(), ...(sandbox ? ["--sandbox"] : [])], command.cwd());
94
+ expect(output).toContain("./gh/index.ts");
95
+ expect(existsSync(marker)).toBe(false);
96
+ }, 90_000);
97
+ }
98
+ }
99
+
100
+ test("audit never runs chant.config.ts or the path lexicon's module", () => {
101
+ chant(["audit", project], root);
102
+ expect(existsSync(marker)).toBe(false);
103
+ expect(existsSync(moduleMarker)).toBe(false);
104
+ }, 90_000);
105
+
106
+ test("a config it cannot read is reported, and still not run", () => {
107
+ writeConfig(`process.env.CI ? ["github"] : [{ name: "github", module: "./gh/index.ts" }]`);
108
+ const output = chant(["dev", "onboard", "github"], project);
109
+ expect(output).toContain("could not read the lexicons in");
110
+ expect(output).toContain("treated as a package");
111
+ expect(existsSync(marker)).toBe(false);
112
+
113
+ const audit = chant(["audit", project], root);
114
+ expect(audit).toContain("could not read the lexicons in");
115
+ expect(existsSync(marker)).toBe(false);
116
+ }, 180_000);
117
+
118
+ test("control: build does run chant.config.ts", () => {
119
+ chant(["build", "."], project);
120
+ expect(existsSync(marker)).toBe(true);
121
+ }, 90_000);
122
+ });
123
+
124
+ describe("audit names a path lexicon by its path and leaves it out of the install line (chant#2589)", () => {
125
+ test("text output", async () => {
126
+ const result = await auditCommand({ path: project, plugins: [] });
127
+ expect(result.status).toBe("no-lexicons");
128
+ expect(result.output).toContain("github is declared by path: ");
129
+ expect(result.output).toContain("gh/index.ts");
130
+ const install = result.output.split("\n").find((line) => line.includes("npx "));
131
+ expect(install).toBeDefined();
132
+ expect(install).not.toContain("@intentius/chant-lexicon-github");
133
+ expect(existsSync(marker)).toBe(false);
134
+ });
135
+
136
+ test("json output", async () => {
137
+ const result = await auditCommand({ path: project, plugins: [], format: "json" });
138
+ const report = JSON.parse(result.output) as { install: string };
139
+ expect(report.install).toMatch(/^npx -p @intentius\/chant( |$)/);
140
+ expect(report.install).not.toContain("@intentius/chant-lexicon-github");
141
+ expect(existsSync(marker)).toBe(false);
142
+ });
143
+
144
+ test("a partial run names the path in its coverage note", async () => {
145
+ // Some other audit lexicon loaded, github not: the workflow is skipped and the note names the path.
146
+ const { loadPlugin } = await import("./plugins");
147
+ const docker = await loadPlugin("docker");
148
+ const result = await auditCommand({ path: project, plugins: [docker] });
149
+ expect(result.output).toContain("github is declared by path: ");
150
+ expect(result.output).not.toContain("npm i @intentius/chant-lexicon-github");
151
+ expect(existsSync(marker)).toBe(false);
152
+ expect(existsSync(moduleMarker)).toBe(false);
153
+ });
154
+ });
155
+
156
+ /**
157
+ * chant#2618 — the other ways into `audit`: main's dispatch run in-process
158
+ * (what `workspace member-run` calls once per member), and `workspace audit`
159
+ * through the CLI, which reaches it through that member-run process.
160
+ */
161
+ describe("chant audit never evaluates the audited repository's config (chant#2618)", () => {
162
+ test("main's in-process dispatch runs neither the config nor the path lexicon's module", async () => {
163
+ const run = await captureRun(() => runCommandInProcess(["audit", project]));
164
+ expect(run.exitCode).toBe(0);
165
+ expect(existsSync(marker)).toBe(false);
166
+ expect(existsSync(moduleMarker)).toBe(false);
167
+ }, 90_000);
168
+
169
+ test("workspace audit runs no member's config and no path lexicon's module", () => {
170
+ // The project becomes the root member of a workspace, with a second
171
+ // member whose config and path lexicon leave markers of their own.
172
+ const apiMarker = join(root, "api-config-ran");
173
+ const apiModuleMarker = join(root, "api-module-ran");
174
+ const api = join(project, "api");
175
+ mkdirSync(join(api, ".github", "workflows"), { recursive: true });
176
+ mkdirSync(join(api, "gh"), { recursive: true });
177
+ writeFileSync(
178
+ join(api, "chant.config.ts"),
179
+ `import { writeFileSync } from "node:fs";\nwriteFileSync(${JSON.stringify(apiMarker)}, "ran");\n` +
180
+ `export default { lexicons: [{ name: "github", module: "./gh/index.ts" }] };\n`,
181
+ );
182
+ writeFileSync(
183
+ join(api, "gh", "index.ts"),
184
+ `import { writeFileSync } from "node:fs";\nwriteFileSync(${JSON.stringify(apiModuleMarker)}, "ran");\nexport {};\n`,
185
+ );
186
+ writeFileSync(
187
+ join(api, ".github", "workflows", "ci.yml"),
188
+ "on: push\njobs:\n a:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n",
189
+ );
190
+ writeFileSync(
191
+ join(project, "chant.workspace.json"),
192
+ JSON.stringify({
193
+ name: "acme",
194
+ schema: 1,
195
+ members: [
196
+ { name: "root", dir: ".", kind: "chant" },
197
+ { name: "api", dir: "api", kind: "chant" },
198
+ ],
199
+ }),
200
+ );
201
+ const output = chant(["workspace", "audit", "--json"], project);
202
+ expect(output).toContain('"api"');
203
+ expect(existsSync(marker)).toBe(false);
204
+ expect(existsSync(moduleMarker)).toBe(false);
205
+ expect(existsSync(apiMarker)).toBe(false);
206
+ expect(existsSync(apiModuleMarker)).toBe(false);
207
+ }, 180_000);
208
+ });
@@ -1,5 +1,8 @@
1
1
  import { describe, test, expect, vi, afterEach } from "vitest";
2
- import { extractFromTar, fetchWithRetry, TransientFetchError } from "./fetch";
2
+ import { extractFromTar, fetchWithCache, fetchWithRetry, TransientFetchError } from "./fetch";
3
+ import { mkdtempSync, utimesSync, writeFileSync } from "fs";
4
+ import { tmpdir } from "os";
5
+ import { join } from "path";
3
6
 
4
7
  /**
5
8
  * Build a minimal valid tar buffer with a single file entry.
@@ -318,3 +321,62 @@ describe("fetchWithRetry", () => {
318
321
  expect((err as TransientFetchError).message).toContain("1 attempt");
319
322
  });
320
323
  });
324
+
325
+ describe("fetchWithCache", () => {
326
+ afterEach(() => {
327
+ vi.unstubAllGlobals();
328
+ vi.restoreAllMocks();
329
+ });
330
+
331
+ function cacheAt(ageMs: number, content = "stale bytes"): string {
332
+ const file = join(mkdtempSync(join(tmpdir(), "chant-fetch-cache-")), "spec.bin");
333
+ writeFileSync(file, content);
334
+ const then = (Date.now() - ageMs) / 1000;
335
+ utimesSync(file, then, then);
336
+ return file;
337
+ }
338
+
339
+ test("returns the stale cached copy when every download attempt fails transiently", async () => {
340
+ const cacheFile = cacheAt(2 * 24 * 60 * 60 * 1000);
341
+ const fetchMock = vi.fn().mockRejectedValue(new Error("ECONNRESET"));
342
+ vi.stubGlobal("fetch", fetchMock);
343
+ const errors: string[] = [];
344
+ vi.spyOn(console, "error").mockImplementation((m: unknown) => { errors.push(String(m)); });
345
+
346
+ const data = await fetchWithCache({ url: "https://example.test/spec", cacheFile, retries: 1, backoffMs: 1 });
347
+
348
+ expect(data.toString()).toBe("stale bytes");
349
+ expect(fetchMock).toHaveBeenCalledTimes(2);
350
+ expect(errors.join("\n")).toContain("Using the cached copy");
351
+ });
352
+
353
+ test("force ignores the stale copy and throws", async () => {
354
+ const cacheFile = cacheAt(2 * 24 * 60 * 60 * 1000);
355
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("ECONNRESET")));
356
+
357
+ await expect(
358
+ fetchWithCache({ url: "https://example.test/spec", cacheFile, retries: 1, backoffMs: 1 }, true),
359
+ ).rejects.toBeInstanceOf(TransientFetchError);
360
+ });
361
+
362
+ test("a permanent failure with a stale copy still throws", async () => {
363
+ const cacheFile = cacheAt(2 * 24 * 60 * 60 * 1000);
364
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("gone", { status: 404 })));
365
+
366
+ await expect(
367
+ fetchWithCache({ url: "https://example.test/spec", cacheFile, retries: 1, backoffMs: 1 }),
368
+ ).rejects.toThrow();
369
+ });
370
+
371
+ test("passes attemptTimeoutMs through as the attempt signal", async () => {
372
+ const cacheFile = cacheAt(2 * 24 * 60 * 60 * 1000);
373
+ const fetchMock = vi.fn().mockResolvedValue(new Response("fresh"));
374
+ vi.stubGlobal("fetch", fetchMock);
375
+
376
+ const data = await fetchWithCache({ url: "https://example.test/spec", cacheFile, attemptTimeoutMs: 120_000 });
377
+
378
+ expect(data.toString()).toBe("fresh");
379
+ const init = fetchMock.mock.calls[0][1] as RequestInit;
380
+ expect(init.signal).toBeInstanceOf(AbortSignal);
381
+ });
382
+ });
@@ -18,6 +18,16 @@ export interface FetchConfig {
18
18
  cacheFile: string;
19
19
  /** Cache TTL in milliseconds (default: 24 hours). */
20
20
  cacheTtlMs?: number;
21
+ /**
22
+ * Per-attempt timeout for the download in milliseconds (default: 15 seconds).
23
+ * A source archive of tens of megabytes needs more than the default, which
24
+ * is sized for a JSON schema.
25
+ */
26
+ attemptTimeoutMs?: number;
27
+ /** Retries after the first attempt (default: 4). */
28
+ retries?: number;
29
+ /** Base back-off between attempts in milliseconds (default: 1 second). */
30
+ backoffMs?: number;
21
31
  }
22
32
 
23
33
  // ── Transient-aware fetch ──────────────────────────────────────────
@@ -198,7 +208,26 @@ export async function fetchWithCache(config: FetchConfig, force = false): Promis
198
208
  }
199
209
  }
200
210
 
201
- const response = await fetchWithRetry(config.url);
211
+ let response: Response;
212
+ try {
213
+ response = await fetchWithRetry(
214
+ config.url,
215
+ config.retries ?? DEFAULT_RETRIES,
216
+ config.backoffMs ?? DEFAULT_BACKOFF_MS,
217
+ undefined,
218
+ config.attemptTimeoutMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS,
219
+ );
220
+ } catch (e) {
221
+ // A stale copy beats a failed build. The archives this caches are pinned
222
+ // by tag or by version in the URL, so a copy past its TTL is the same
223
+ // bytes; the TTL exists to notice an unpinned URL moving, not to expire
224
+ // good data. `force` means the caller needs fresh bytes, so it still throws.
225
+ if (!force && e instanceof TransientFetchError && existsSync(config.cacheFile)) {
226
+ console.error(`${e.message} Using the cached copy at ${config.cacheFile} from ${new Date(statSync(config.cacheFile).mtimeMs).toISOString()}.`);
227
+ return readFileSync(config.cacheFile) as unknown as Buffer;
228
+ }
229
+ throw e;
230
+ }
202
231
  const arrayBuffer = await response.arrayBuffer();
203
232
  const data = Buffer.from(arrayBuffer);
204
233
 
@@ -39,7 +39,7 @@ import {
39
39
  type DriverRunResult,
40
40
  } from "./driver";
41
41
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
42
- import type { GateLedgerPort } from "../op/gate";
42
+ import type { GateDigestMismatch, GateLedgerPort } from "../op/gate";
43
43
  import { gateName } from "../op/gate-name";
44
44
  import { isLexiconPlugin, type LexiconPlugin, type ComponentPipelineOptions } from "../lexicon";
45
45
  import type { RunProgressEvent } from "./run-progress";
@@ -432,6 +432,8 @@ export interface RunComponentsResult {
432
432
  /** Whether this run's own append reached the remote (#2310); absent when the pending fact was already standing. */
433
433
  pushed?: boolean;
434
434
  pushWarning?: string;
435
+ /** Set when an approval stands for this gate, but for another plan or none (#2574). */
436
+ mismatch?: GateDigestMismatch;
435
437
  };
436
438
  /** This run's resolved build-time parameters (chant #1108) — the component-driver counterpart of `../cli/commands/build.ts`'s `BuildResult.buildParams`. Present only once the run actually reached dispatch (mirrors `BuildResult.buildParams`, which is likewise absent on an early-error return). */
437
439
  buildParams?: BuildParamProvenance[];
@@ -584,6 +586,7 @@ export async function runComponents(
584
586
  gate: run.gate,
585
587
  ...(run.gatePushed !== undefined ? { pushed: run.gatePushed } : {}),
586
588
  ...(run.gatePushWarning ? { pushWarning: run.gatePushWarning } : {}),
589
+ ...(run.gateMismatch ? { mismatch: run.gateMismatch } : {}),
587
590
  },
588
591
  }
589
592
  : {}),
@@ -632,6 +635,7 @@ export async function runComponents(
632
635
  gate: componentResult.gate,
633
636
  ...(componentResult.gatePushed !== undefined ? { gatePushed: componentResult.gatePushed } : {}),
634
637
  ...(componentResult.gatePushWarning ? { gatePushWarning: componentResult.gatePushWarning } : {}),
638
+ ...(componentResult.gateMismatch ? { gateMismatch: componentResult.gateMismatch } : {}),
635
639
  }
636
640
  : {}),
637
641
  componentOutputs,
@@ -648,6 +652,7 @@ export async function runComponents(
648
652
  gate: componentResult.gate,
649
653
  ...(componentResult.gatePushed !== undefined ? { pushed: componentResult.gatePushed } : {}),
650
654
  ...(componentResult.gatePushWarning ? { pushWarning: componentResult.gatePushWarning } : {}),
655
+ ...(componentResult.gateMismatch ? { mismatch: componentResult.gateMismatch } : {}),
651
656
  },
652
657
  }
653
658
  : {}),
@@ -280,76 +280,46 @@ describe("discoverComponents", () => {
280
280
  expect(result.errors.some((e) => e.type === "import")).toBe(true);
281
281
  });
282
282
 
283
- test("the first chant.config.ts encountered becomes the source root and is still scanned", async () => {
284
- // Matches findInfraFiles's own rule: the *first* config directory found
285
- // becomes `sourceRoot` and is descended into, same as any other
286
- // directory — the skip only applies to a *second* nested config found
287
- // after that (see the next test).
288
- const childDir = join(testDir, "child-project");
289
- await mkdir(childDir, { recursive: true });
290
- await writeFile(join(childDir, "chant.config.ts"), `export default {};`);
291
- await writeFile(
292
- join(childDir, "child.component.ts"),
293
- `
294
- export const childSvc = {
295
- name: "child-svc",
296
- dependsOn: [],
297
- deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
298
- };
299
- `,
300
- );
301
- await writeFile(
302
- join(testDir, "root.component.ts"),
303
- `
304
- export const rootSvc = {
305
- name: "root-svc",
283
+ const svc = (exportName: string, name: string) => `
284
+ export const ${exportName} = {
285
+ name: "${name}",
306
286
  dependsOn: [],
307
287
  deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
308
288
  };
309
- `,
310
- );
289
+ `;
311
290
 
312
- const result = await discoverComponents(testDir);
313
-
314
- expect(result.components.has("root-svc")).toBe(true);
315
- expect(result.components.has("child-svc")).toBe(true);
316
- });
317
-
318
- test("a second nested chant.config.ts (a true child project) is not descended into", async () => {
291
+ test("from outside every project, every child project is read (#2527)", async () => {
292
+ // The source-root quirk is gone: no child config is special for being
293
+ // found first. The scan root has no config at or above it, so every
294
+ // child project's components are discovered, nested ones included.
319
295
  const firstConfigDir = join(testDir, "first-config");
320
296
  const nestedChildProject = join(firstConfigDir, "nested-child-project");
321
297
  await mkdir(nestedChildProject, { recursive: true });
322
298
  await writeFile(join(firstConfigDir, "chant.config.ts"), `export default {};`);
323
299
  await writeFile(join(nestedChildProject, "chant.config.ts"), `export default {};`);
324
- await writeFile(
325
- join(firstConfigDir, "first.component.ts"),
326
- `
327
- export const firstSvc = {
328
- name: "first-svc",
329
- dependsOn: [],
330
- deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
331
- };
332
- `,
333
- );
334
- await writeFile(
335
- join(nestedChildProject, "nested.component.ts"),
336
- `
337
- export const nestedSvc = {
338
- name: "nested-svc",
339
- dependsOn: [],
340
- deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
341
- };
342
- `,
343
- );
300
+ await writeFile(join(testDir, "root.component.ts"), svc("rootSvc", "root-svc"));
301
+ await writeFile(join(firstConfigDir, "first.component.ts"), svc("firstSvc", "first-svc"));
302
+ await writeFile(join(nestedChildProject, "nested.component.ts"), svc("nestedSvc", "nested-svc"));
344
303
 
345
304
  const result = await discoverComponents(testDir);
346
305
 
347
- // first-config/ has no config above it, so it becomes the source root
348
- // and is scanned; nested-child-project/'s own chant.config.ts is the
349
- // *second* config encountered, so it is treated as a separate project
350
- // scope and not descended into.
306
+ expect(result.components.has("root-svc")).toBe(true);
351
307
  expect(result.components.has("first-svc")).toBe(true);
352
- expect(result.components.has("nested-svc")).toBe(false);
308
+ expect(result.components.has("nested-svc")).toBe(true);
309
+ });
310
+
311
+ test("from inside a project, a child project is a boundary (#2527)", async () => {
312
+ const childDir = join(testDir, "child-project");
313
+ await mkdir(childDir, { recursive: true });
314
+ await writeFile(join(testDir, "chant.config.ts"), `export default {};`);
315
+ await writeFile(join(childDir, "chant.config.ts"), `export default {};`);
316
+ await writeFile(join(testDir, "root.component.ts"), svc("rootSvc", "root-svc"));
317
+ await writeFile(join(childDir, "child.component.ts"), svc("childSvc", "child-svc"));
318
+
319
+ const result = await discoverComponents(testDir);
320
+
321
+ expect(result.components.has("root-svc")).toBe(true);
322
+ expect(result.components.has("child-svc")).toBe(false);
353
323
  });
354
324
 
355
325
  // ── chant #1108 — build-time parameters populated before import ────────────
@@ -40,18 +40,16 @@
40
40
  * `./component.ts`, already proves the plain-JSON round-trip).
41
41
  */
42
42
 
43
- import { readdir } from "node:fs/promises";
44
- import { join, resolve } from "node:path";
43
+ import { resolve } from "node:path";
45
44
  import { pathToFileURL } from "node:url";
46
- import { existsSync } from "node:fs";
47
45
  import { DiscoveryError } from "../errors";
48
46
  import { isComponent, type Component } from "./component";
49
47
  import type { BuildParamProvenance } from "../provenance";
50
48
  import { buildParamValues } from "../build-params";
51
49
  import { setBuildParams } from "../params";
52
- import { compileDiscoveryFilter, hasDiscoveryMarker } from "../discovery/files";
50
+ import { hasDiscoveryMarkerSync } from "../discovery/files";
53
51
  import { resolveDiscoveryGlobs } from "../config";
54
- import { warnDiscoveryChanges } from "../discovery/convergence";
52
+ import { walkDiscovery, workspaceMemberDirs } from "../discovery/walk";
55
53
 
56
54
  /** One discovered component, paired with the file it was exported from. */
57
55
  export interface DiscoveredComponent {
@@ -118,77 +116,24 @@ export interface ImportedComponentModule {
118
116
  }
119
117
 
120
118
  /**
121
- * Recursively find every `*.component.ts` file under `path`, the same
122
- * child-project boundary rule `findInfraFiles` (`../discovery/files.ts`)
123
- * uses: a subdirectory with its own `chant.config.ts` is a separate scope
124
- * once the outer project's own source root has been established. The
125
- * project's `exclude`/`include` globs and the skip marker apply here as they
126
- * do there (#2519).
119
+ * Every `*.component.ts` file under `path`, through the one discovery walk
120
+ * (`../discovery/walk.ts`, #2527): the same dot-directory, `dist`,
121
+ * git-ignore and child-project rules as source discovery, the project's
122
+ * `exclude`/`include` globs and the skip marker (#2519). Scanned from outside
123
+ * any project, every child project is read.
127
124
  */
128
125
  async function findComponentFiles(path: string): Promise<string[]> {
129
- const files: string[] = [];
130
- let sourceRoot: string | null = null;
131
- const skip = compileDiscoveryFilter(await resolveDiscoveryGlobs(path));
132
- const skippedChildren: string[] = [];
133
-
134
- async function scanDirectory(dir: string): Promise<void> {
135
- let entries;
136
- try {
137
- entries = await readdir(dir, { withFileTypes: true });
138
- } catch {
139
- return;
140
- }
141
-
142
- for (const entry of entries) {
143
- const fullPath = join(dir, entry.name);
144
-
145
- if (entry.isDirectory()) {
146
- // Skip dependency and tool/VCS directories: component sources never live
147
- // in them, and descending in is both wasted work and a correctness bug —
148
- // e.g. a CI runner like gitlab-ci-local stages a copy of the project
149
- // under `.gitlab-ci-local/`, which discovery would otherwise pick up as
150
- // duplicate component names alongside the originals.
151
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
152
-
153
- const configPath = join(fullPath, "chant.config.ts");
154
- if (existsSync(configPath)) {
155
- if (sourceRoot === null) {
156
- sourceRoot = fullPath;
157
- } else {
158
- skippedChildren.push(fullPath);
159
- continue;
160
- }
161
- }
162
- await scanDirectory(fullPath);
163
- } else if (
164
- entry.isFile() &&
165
- entry.name.endsWith(".component.ts") &&
166
- !entry.name.endsWith(".test.component.ts") &&
167
- !entry.name.endsWith(".spec.component.ts") &&
168
- !skip?.(fullPath) &&
169
- !(await hasDiscoveryMarker(fullPath))
170
- ) {
171
- files.push(fullPath);
172
- }
173
- }
174
- }
175
-
176
- await scanDirectory(path);
177
- // #2527's warning release: the list returned is today's, unchanged.
178
- await warnDiscoveryChanges({
126
+ return walkDiscovery({
179
127
  walker: "components",
180
128
  root: path,
181
- files,
182
- sourceRoot,
183
- skippedChildren,
184
- fileOk: async (name, full) =>
129
+ globs: await resolveDiscoveryGlobs(path),
130
+ excludeDirs: await workspaceMemberDirs(path),
131
+ accept: (name, full) =>
185
132
  name.endsWith(".component.ts") &&
186
133
  !name.endsWith(".test.component.ts") &&
187
134
  !name.endsWith(".spec.component.ts") &&
188
- !skip?.(full) &&
189
- !(await hasDiscoveryMarker(full)),
135
+ !hasDiscoveryMarkerSync(full),
190
136
  });
191
- return files;
192
137
  }
193
138
 
194
139
  /**