@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,263 @@
1
+ import { afterEach, describe, expect, test } from "vitest";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, relative } from "node:path";
6
+ import { walkDiscovery, workspaceMemberDirs, type DiscoveryWalker } from "./walk";
7
+ import { findInfraFiles } from "./files";
8
+ import { discoverOps } from "../op/discover";
9
+ import type { DiscoveryGlobs } from "../config";
10
+ import { auditCommand } from "../cli/commands/audit";
11
+
12
+ /**
13
+ * chant#2527: one discovery walk. Every walker skips dot-directories, `dist`
14
+ * and git-ignored paths, stops at child projects inside a project, reads
15
+ * every child project outside one, and lets `include` re-admit what it skips.
16
+ */
17
+
18
+ const dirs: string[] = [];
19
+ afterEach(() => {
20
+ for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
21
+ });
22
+
23
+ function tree(files: Record<string, string>, opts: { git?: boolean } = {}): string {
24
+ const root = mkdtempSync(join(tmpdir(), "chant-2527-"));
25
+ dirs.push(root);
26
+ for (const [path, body] of Object.entries(files)) {
27
+ mkdirSync(dirname(join(root, path)), { recursive: true });
28
+ writeFileSync(join(root, path), body);
29
+ }
30
+ if (opts.git) execFileSync("git", ["init", "-q"], { cwd: root });
31
+ return root;
32
+ }
33
+
34
+ const WALKERS: DiscoveryWalker[] = ["source", "lint", "components", "ops", "audit"];
35
+
36
+ function walk(walker: DiscoveryWalker, root: string, globs?: Partial<DiscoveryGlobs>): string[] {
37
+ return walkDiscovery({
38
+ walker,
39
+ root,
40
+ accept: (name) => name.endsWith(".ts"),
41
+ globs: globs ? { root, exclude: [], include: [], ...globs } : undefined,
42
+ })
43
+ .map((f) => relative(root, f).split("\\").join("/"))
44
+ .sort();
45
+ }
46
+
47
+ describe("the rules every walker shares (#2527)", () => {
48
+ test.each(WALKERS)("%s skips dot-directories, dist and node_modules", (walker) => {
49
+ const root = tree({
50
+ "a.ts": "",
51
+ ".cache/b.ts": "",
52
+ "dist/c.ts": "",
53
+ "src/dist/d.ts": "",
54
+ "node_modules/x/e.ts": "",
55
+ ".github/f.ts": "",
56
+ });
57
+ const expected = walker === "audit" ? [".github/f.ts", "a.ts"] : ["a.ts"];
58
+ expect(walk(walker, root)).toEqual(expected);
59
+ });
60
+
61
+ test.each(WALKERS)("%s skips git-ignored paths, nested .gitignore included", (walker) => {
62
+ const root = tree(
63
+ {
64
+ ".gitignore": "vendor/\n",
65
+ "a.ts": "",
66
+ "vendor/b.ts": "",
67
+ "pkg/.gitignore": "gen.ts\n",
68
+ "pkg/gen.ts": "",
69
+ "pkg/keep.ts": "",
70
+ },
71
+ { git: true },
72
+ );
73
+ expect(walk(walker, root)).toEqual(["a.ts", "pkg/keep.ts"]);
74
+ });
75
+
76
+ test("a scan root that is itself ignored is read", () => {
77
+ const root = tree({ ".gitignore": "/carveout/\n", "carveout/src/main.ts": "" }, { git: true });
78
+ const src = join(root, "carveout", "src");
79
+ expect(walk("lint", src)).toEqual(["main.ts"]);
80
+ });
81
+
82
+ test("a tree outside git is read without ignore rules", () => {
83
+ const root = tree({ ".gitignore": "*.ts\n", "a.ts": "" });
84
+ expect(walk("source", root)).toEqual(["a.ts"]);
85
+ });
86
+ });
87
+
88
+ describe("child projects (#2527)", () => {
89
+ const files = {
90
+ "chant.config.ts": "export default {};\n",
91
+ "main.ts": "",
92
+ "east/chant.config.ts": "export default {};\n",
93
+ "east/e.ts": "",
94
+ "west/chant.config.json": JSON.stringify({ lexicons: ["aws"] }),
95
+ "west/w.ts": "",
96
+ "lintonly/chant.config.json": JSON.stringify({ rules: { COR004: "off" } }),
97
+ "lintonly/l.ts": "",
98
+ };
99
+
100
+ test.each(WALKERS)("inside a project, %s stops at every child project, .json configs included, and not at a lint-only fragment", (walker) => {
101
+ const root = tree(files);
102
+ expect(walk(walker, root)).toEqual(["chant.config.ts", "lintonly/l.ts", "main.ts"]);
103
+ });
104
+
105
+ test("the first-config quirk is gone: no child project is read as the project's own source", () => {
106
+ const root = tree({ "chant.config.ts": "export default {};\n", "src/chant.config.ts": "export default {};\n", "src/a.ts": "", "zz/chant.config.ts": "export default {};\n", "zz/b.ts": "" });
107
+ expect(walk("source", root)).toEqual(["chant.config.ts"]);
108
+ });
109
+
110
+ test.each(["source", "components", "lint"] as DiscoveryWalker[])("outside a project, %s reads every child project", (walker) => {
111
+ const root = tree({ "package.json": "{}", "a/chant.config.ts": "", "a/x.ts": "", "b/chant.config.ts": "", "b/y.ts": "" });
112
+ expect(walk(walker, root)).toEqual(["a/chant.config.ts", "a/x.ts", "b/chant.config.ts", "b/y.ts"]);
113
+ });
114
+
115
+ test("Op discovery stops at child projects even with no config at its root", () => {
116
+ const root = tree({ "package.json": "{}", "a/chant.config.ts": "", "a/x.ts": "", "top.ts": "" });
117
+ expect(walk("ops", root)).toEqual(["top.ts"]);
118
+ });
119
+
120
+ test("discoverOps from a configless git root does not collect a sibling project's Ops", async () => {
121
+ const root = tree(
122
+ {
123
+ "ops/local.op.ts": `export default { props: { name: "local", phases: [] } };\n`,
124
+ "sibling/chant.config.json": JSON.stringify({ lexicons: ["aws"] }),
125
+ "sibling/ops/theirs.op.ts": `export default { props: { name: "theirs", phases: [] } };\n`,
126
+ },
127
+ { git: true },
128
+ );
129
+ const { ops, errors } = await discoverOps({ cwd: root });
130
+ expect(errors).toEqual([]);
131
+ expect([...ops.keys()]).toEqual(["local"]);
132
+ });
133
+ });
134
+
135
+ describe("include and exclude (#2519, #2527)", () => {
136
+ test("include re-admits a dot-directory, dist, an ignored path and a child project", () => {
137
+ const root = tree(
138
+ {
139
+ "chant.config.ts": "export default {};\n",
140
+ ".gitignore": "gen/\n",
141
+ ".cache/a.ts": "",
142
+ "dist/b.ts": "",
143
+ "gen/c.ts": "",
144
+ "stack/chant.config.ts": "",
145
+ "stack/d.ts": "",
146
+ "main.ts": "",
147
+ },
148
+ { git: true },
149
+ );
150
+ expect(walk("source", root)).toEqual(["chant.config.ts", "main.ts"]);
151
+ expect(walk("source", root, { include: [".cache", "dist/b.ts", "gen", "stack"] })).toEqual([
152
+ ".cache/a.ts",
153
+ "chant.config.ts",
154
+ "dist/b.ts",
155
+ "gen/c.ts",
156
+ "main.ts",
157
+ "stack/chant.config.ts",
158
+ "stack/d.ts",
159
+ ]);
160
+ });
161
+
162
+ test("an include below a skipped directory reads only what it names there", () => {
163
+ const root = tree({ ".tool/keep/a.ts": "", ".tool/drop/b.ts": "", ".tool/c.ts": "" });
164
+ expect(walk("source", root, { include: [".tool/keep/**"] })).toEqual([".tool/keep/a.ts"]);
165
+ });
166
+
167
+ test("exclude drops files, include wins over it, and include never reaches node_modules", () => {
168
+ const root = tree({ "ops/run.ts": "", "ops/stack.ts": "", "node_modules/x/a.ts": "", "main.ts": "" });
169
+ expect(walk("ops", root, { exclude: ["ops"], include: ["ops/stack.ts", "node_modules"] })).toEqual(["main.ts", "ops/stack.ts"]);
170
+ });
171
+
172
+ test("findInfraFiles reads include from the project config", async () => {
173
+ const root = tree({
174
+ "chant.config.json": JSON.stringify({ lexicons: ["aws"], include: ["src"] }),
175
+ "src/chant.config.ts": "export default { lint: {} };\n",
176
+ "src/a.ts": "",
177
+ });
178
+ const files = (await findInfraFiles(root)).map((f) => relative(root, f)).sort();
179
+ expect(files).toEqual(["src/a.ts", "src/chant.config.ts"]);
180
+ });
181
+ });
182
+
183
+ describe("workspace members and groups (#2525 rule 3, read by #2534's reader)", () => {
184
+ test("nothing is excluded without a workspace declaration", async () => {
185
+ const root = tree({ "chant.config.json": JSON.stringify({ lexicons: ["aws"] }), "members/a/x.ts": "" }, { git: true });
186
+ expect(await workspaceMemberDirs(root)).toEqual([]);
187
+ });
188
+
189
+ test("with chant.workspace.json, member directories below the scan root leave the walk", async () => {
190
+ const root = tree(
191
+ {
192
+ "chant.workspace.json": JSON.stringify({
193
+ name: "w",
194
+ schema: 1,
195
+ members: [
196
+ { name: "root", dir: ".", kind: "chant" },
197
+ { name: "a", dir: "members/a", kind: "chant" },
198
+ ],
199
+ }),
200
+ "main.ts": "",
201
+ "members/a/x.ts": "",
202
+ "members/b/y.ts": "",
203
+ },
204
+ { git: true },
205
+ );
206
+ const excludeDirs = await workspaceMemberDirs(root);
207
+ expect(excludeDirs.map((d) => relative(root, d))).toEqual(["members/a"]);
208
+ const files = walkDiscovery({ walker: "source", root, accept: (n) => n.endsWith(".ts"), excludeDirs, globs: { root, exclude: [], include: ["members"] } });
209
+ expect(files.map((f) => relative(root, f)).sort()).toEqual(["main.ts", "members/b/y.ts"]);
210
+ // Scanned from inside the member, the member is the project and is read.
211
+ expect(await workspaceMemberDirs(join(root, "members", "a"))).toEqual([]);
212
+ });
213
+
214
+ test("an example group's matches leave the walk too, and a .jsonc declaration counts", async () => {
215
+ const root = tree(
216
+ {
217
+ "chant.workspace.jsonc": `{
218
+ // groups are read through #2534's declaration reader
219
+ "name": "w",
220
+ "schema": 1,
221
+ "members": [
222
+ { "name": "root", "dir": ".", "kind": "chant" },
223
+ { "name": "examples", "kind": "examples", "glob": "examples/*" },
224
+ ],
225
+ }`,
226
+ "main.ts": "",
227
+ "examples/one/chant.config.ts": "export default {};",
228
+ "examples/one/x.ts": "",
229
+ "examples/notes/readme.ts": "",
230
+ },
231
+ { git: true },
232
+ );
233
+ const excludeDirs = await workspaceMemberDirs(root);
234
+ // `examples/notes` holds no chant project, so the group leaves it alone.
235
+ expect(excludeDirs.map((d) => relative(root, d))).toEqual(["examples/one"]);
236
+ });
237
+
238
+ test("chant audit at the workspace root leaves members out", async () => {
239
+ const root = tree(
240
+ {
241
+ "chant.workspace.json": JSON.stringify({
242
+ name: "w",
243
+ schema: 1,
244
+ members: [
245
+ { name: "root", dir: ".", kind: "chant" },
246
+ { name: "a", dir: "members/a", kind: "chant" },
247
+ ],
248
+ }),
249
+ "terraform.tfstate": "{}",
250
+ "members/a/terraform.tfstate": "{}",
251
+ },
252
+ { git: true },
253
+ );
254
+ // No lexicons: TF023 still runs, and its paths show what the walk read.
255
+ const result = await auditCommand({ path: root, format: "json", plugins: [] });
256
+ expect(result.findings.filter((f) => f.checkId === "TF023").map((f) => f.file)).toEqual(["terraform.tfstate"]);
257
+ });
258
+
259
+ test("a declaration that fails to read excludes nothing", async () => {
260
+ const root = tree({ "chant.workspace.json": "{ not json", "members/a/x.ts": "" }, { git: true });
261
+ expect(await workspaceMemberDirs(root)).toEqual([]);
262
+ });
263
+ });
@@ -0,0 +1,334 @@
1
+ /**
2
+ * The one discovery walk (chant#2527).
3
+ *
4
+ * Every command that decides which files chant reads walks the tree here:
5
+ * source discovery (`./files.ts`'s `findInfraFiles`, behind build, graph,
6
+ * list, explain and lexicon detection), `chant lint`, component discovery,
7
+ * Op discovery and the `chant audit` walk. They used to have five walkers
8
+ * with five sets of rules. v0.80.0 warned about every file whose status the
9
+ * convergence changes (#2571), and this module is that convergence. It is on
10
+ * #2525's level-0 exception list.
11
+ *
12
+ * The rules, for every walker:
13
+ *
14
+ * - `node_modules` and `.git` are never entered.
15
+ * - Dot-directories are skipped. The audit walk keeps `.github` and
16
+ * `.forgejo`, which hold the CI files it audits.
17
+ * - `dist` is skipped.
18
+ * - Git-ignored paths are skipped, nested `.gitignore` files, `.git/info/exclude`
19
+ * and the global excludes file included, the way git itself decides. Only
20
+ * paths below the scan root are asked about: when the scan root is itself
21
+ * ignored, nothing under it is dropped for being ignored, so building or
22
+ * linting an ignored directory still reads it.
23
+ * - When the scan root is inside a chant project, a child directory holding a
24
+ * project config is a boundary and is not entered. A `chant.config.json`
25
+ * holding only lint keys is a fragment, not a project config, and is not a
26
+ * boundary. A `chant.config.ts` always is one, since telling a lint-only one
27
+ * apart would mean running it. When the scan root is outside every project,
28
+ * every child project is read. Op discovery always stops at child projects
29
+ * below its root.
30
+ * - The project's `exclude` globs (#2519) drop what they match, and its
31
+ * `include` globs re-admit what `exclude` or any rule above would skip.
32
+ * `node_modules`, `.git` and workspace members are the exceptions: include
33
+ * never re-admits them.
34
+ * - Workspace members and group matches leave the root project's walk
35
+ * (#2525 rule 3), and only when a workspace declaration exists. Without one nothing is loaded and
36
+ * nothing is excluded; see {@link workspaceMemberDirs}.
37
+ *
38
+ * The walk is synchronous: the audit walk it replaced was, and the glob and
39
+ * ignore facts are gathered once before it starts.
40
+ */
41
+
42
+ import { execFileSync } from "node:child_process";
43
+ import { closeSync, existsSync, openSync, readdirSync, readSync, statSync, type Dirent } from "node:fs";
44
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
45
+ // @ts-ignore — picomatch has no types declaration
46
+ import picomatch from "picomatch";
47
+ import { findProjectConfig, findWorkspaceRoot } from "../project-root";
48
+ import { isLintOnlyFragment, type DiscoveryGlobs } from "../config";
49
+
50
+ /** The commands that walk the tree. */
51
+ export type DiscoveryWalker = "source" | "lint" | "components" | "ops" | "audit";
52
+
53
+ /** Dot-directories the audit walk enters anyway, since they hold the CI files it audits. */
54
+ export const AUDIT_DOT_DIRS: ReadonlySet<string> = new Set([".github", ".forgejo"]);
55
+
56
+ /** Directories no rule and no glob ever lets the walk into. */
57
+ const NEVER_ENTER = new Set(["node_modules", ".git"]);
58
+
59
+ /** One walk: which walker, where from, which files it wants, and the facts only some walkers have. */
60
+ export interface WalkOptions {
61
+ walker: DiscoveryWalker;
62
+ /** The directory the walk starts from. Joined onto, never resolved, so relative roots give relative paths. */
63
+ root: string;
64
+ /**
65
+ * Which files the walker wants, by name and full path. Called only for a
66
+ * file every rule above has let through, so a marker check here reads only
67
+ * files that could be returned.
68
+ */
69
+ accept: (name: string, fullPath: string) => boolean;
70
+ /**
71
+ * The project's discovery globs. Omitted, none apply. Callers that can load
72
+ * the project config resolve them with `resolveDiscoveryGlobs`.
73
+ */
74
+ globs?: DiscoveryGlobs;
75
+ /** Sort each directory's entries by name. The audit walk does, for a stable limit. */
76
+ sorted?: boolean;
77
+ /** Follow symlinked directories and files. `chant lint` always did; the others never did. */
78
+ followSymlinks?: boolean;
79
+ /**
80
+ * The audit walk's file limit. Every file the walk takes counts, accepted or
81
+ * not; `truncated` is set once a file past the limit is met.
82
+ */
83
+ limit?: { max: number; truncated: boolean };
84
+ /**
85
+ * Called for a dot-directory the walk skips. Returning true takes the
86
+ * directory's own path as a file without entering it (the audit walk's
87
+ * `.terraform`, which TF023 reports by its presence).
88
+ */
89
+ takeSkippedDir?: (name: string, fullPath: string) => boolean;
90
+ /**
91
+ * Paths the git-ignore rule leaves alone, to be judged by the caller. The
92
+ * audit walk keeps Terraform state paths for TF023, which has its own
93
+ * `.gitignore` reading.
94
+ */
95
+ keepIgnored?: (fullPath: string) => boolean;
96
+ /** Directories that leave this walk, absolute. See {@link workspaceMemberDirs}. */
97
+ excludeDirs?: readonly string[];
98
+ }
99
+
100
+ // ── Globs ────────────────────────────────────────────────────────────────────
101
+
102
+ /** A project's `exclude` and `include` globs, compiled against the config's directory. */
103
+ interface CompiledGlobs {
104
+ /** True when an `exclude` pattern matches the path or a directory above it. */
105
+ excluded(full: string): boolean;
106
+ /** True when an `include` pattern matches the path or a directory above it. */
107
+ included(full: string): boolean;
108
+ /** True when some `include` pattern names a path strictly below directory `full`. */
109
+ includeBelow(full: string): boolean;
110
+ }
111
+
112
+ /** Path parts below `home`, or undefined for `home` itself or a path outside it. */
113
+ function partsBelow(home: string, full: string): string[] | undefined {
114
+ const rel = relative(home, resolve(full));
115
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return undefined;
116
+ return rel.split(sep);
117
+ }
118
+
119
+ function compileGlobs(globs: DiscoveryGlobs | undefined): CompiledGlobs | undefined {
120
+ if (!globs || (globs.exclude.length === 0 && globs.include.length === 0)) return undefined;
121
+ const home = resolve(globs.root);
122
+ const matcher = (patterns: string[]): ((full: string) => boolean) => {
123
+ if (patterns.length === 0) return () => false;
124
+ const match: (p: string) => boolean = picomatch(patterns, { dot: true });
125
+ return (full) => {
126
+ const parts = partsBelow(home, full);
127
+ return parts !== undefined && parts.some((_, i) => match(parts.slice(0, i + 1).join("/")));
128
+ };
129
+ };
130
+ // The literal directory part of each include pattern: `.cache/keep/**`
131
+ // gives `.cache/keep`. The walk enters a skipped directory on the way to it.
132
+ const bases = globs.include.map((p) => (picomatch.scan(p) as { base: string }).base).filter((b) => b !== "");
133
+ return {
134
+ excluded: matcher(globs.exclude),
135
+ included: matcher(globs.include),
136
+ includeBelow: (full) => {
137
+ const parts = partsBelow(home, full);
138
+ if (parts === undefined) return false;
139
+ const rel = parts.join("/");
140
+ return bases.some((b) => b.startsWith(`${rel}/`));
141
+ },
142
+ };
143
+ }
144
+
145
+ // ── Git ──────────────────────────────────────────────────────────────────────
146
+
147
+ /**
148
+ * The untracked paths git ignores under `root`, absolute, with fully ignored
149
+ * directories listed once. One `git ls-files` call. A tree outside git, a
150
+ * missing `git`, or a root that is itself ignored yields an empty set: ignore
151
+ * rules are asked only about paths below the scan root.
152
+ */
153
+ export function gitIgnoredBelow(root: string): Set<string> {
154
+ let out: string;
155
+ try {
156
+ out = execFileSync("git", ["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"], {
157
+ cwd: root,
158
+ encoding: "utf-8",
159
+ stdio: ["ignore", "pipe", "ignore"],
160
+ maxBuffer: 256 * 1024 * 1024,
161
+ });
162
+ } catch {
163
+ return new Set();
164
+ }
165
+ const ignored = new Set<string>();
166
+ const abs = resolve(root);
167
+ for (const entry of out.split("\0")) {
168
+ if (entry === "") continue;
169
+ const rel = entry.endsWith("/") ? entry.slice(0, -1) : entry;
170
+ // `./` (or `.`): the scan root is itself ignored.
171
+ if (rel === "." || rel === "") return new Set();
172
+ ignored.add(join(abs, rel));
173
+ }
174
+ return ignored;
175
+ }
176
+
177
+ // ── Boundaries ───────────────────────────────────────────────────────────────
178
+
179
+ /** A directory that holds a project config: `chant.config.ts`, or a `chant.config.json` that is not a lint-only fragment. */
180
+ export function isProjectBoundary(dir: string): boolean {
181
+ if (existsSync(join(dir, "chant.config.ts"))) return true;
182
+ const json = join(dir, "chant.config.json");
183
+ return existsSync(json) && !isLintOnlyFragment(json);
184
+ }
185
+
186
+ /** Whether child projects end the walk from `root`: always for Ops, otherwise when `root` is inside a project. */
187
+ export function childProjectsAreBoundaries(walker: DiscoveryWalker, root: string): boolean {
188
+ return walker === "ops" || findProjectConfig(root).configPath !== undefined;
189
+ }
190
+
191
+ // ── Workspace members ────────────────────────────────────────────────────────
192
+
193
+ /**
194
+ * The directories a workspace declaration moves out of the root project's
195
+ * discovery (#2525 rule 3), absolute and strictly below `root`, or none:
196
+ * every member's directory but the root's, and every group match
197
+ * (`rootExclusions` in `../workspace/declaration.ts`).
198
+ *
199
+ * Level 0 pays for `findWorkspaceRoot`, which only tests whether a
200
+ * `chant.workspace.json` or `.jsonc` exists from `root` up to the git root,
201
+ * and loads nothing. Only when a declaration is found are the workspace
202
+ * modules loaded to read it. A declaration that fails to read excludes
203
+ * nothing here; `chant workspace ls` is where its errors are reported. This
204
+ * is the only place discovery asks about workspaces; the walkers pass the
205
+ * result as `excludeDirs`.
206
+ */
207
+ export async function workspaceMemberDirs(root: string): Promise<string[]> {
208
+ const found = findWorkspaceRoot(root);
209
+ if (!found) return [];
210
+ const start = resolve(root);
211
+ const [{ readDeclaration, resolveGroups, rootExclusions }, { workingTree }] = await Promise.all([
212
+ import("../workspace/declaration"),
213
+ import("../workspace/tree"),
214
+ ]);
215
+ let dirs: string[];
216
+ try {
217
+ const tree = workingTree(found.dir);
218
+ const declaration = readDeclaration(tree);
219
+ dirs = rootExclusions(declaration, resolveGroups(declaration, tree)).map((e) => resolve(found.dir, e.dir));
220
+ } catch {
221
+ return [];
222
+ }
223
+ return dirs.filter((d) => partsBelow(start, d) !== undefined);
224
+ }
225
+
226
+ // ── Markers ──────────────────────────────────────────────────────────────────
227
+
228
+ /** The first `bytes` of a file as UTF-8, or "" when it can't be read. */
229
+ export function readHead(file: string, bytes: number): string {
230
+ let fd: number | undefined;
231
+ try {
232
+ fd = openSync(file, "r");
233
+ const buf = Buffer.alloc(bytes);
234
+ const n = readSync(fd, buf, 0, bytes, 0);
235
+ return buf.toString("utf8", 0, n);
236
+ } catch {
237
+ return "";
238
+ } finally {
239
+ if (fd !== undefined) closeSync(fd);
240
+ }
241
+ }
242
+
243
+ // ── The walk ─────────────────────────────────────────────────────────────────
244
+
245
+ /** Walk `opts.root` under the rules in the module doc and return the files `accept` took. */
246
+ export function walkDiscovery(opts: WalkOptions): string[] {
247
+ const out: string[] = [];
248
+ const globs = compileGlobs(opts.globs);
249
+ const ignored = gitIgnoredBelow(opts.root);
250
+ const boundaries = childProjectsAreBoundaries(opts.walker, opts.root);
251
+ const excludeDirs = new Set((opts.excludeDirs ?? []).map((d) => resolve(d)));
252
+ const dotDirsKept = opts.walker === "audit" ? AUDIT_DOT_DIRS : new Set<string>();
253
+ const limit = opts.limit;
254
+
255
+ const isIgnored = (full: string): boolean => ignored.size > 0 && ignored.has(resolve(full));
256
+
257
+ /** Take a path; false once the limit is reached. */
258
+ const take = (full: string): boolean => {
259
+ if (limit && out.length >= limit.max) {
260
+ limit.truncated = true;
261
+ return false;
262
+ }
263
+ out.push(full);
264
+ return true;
265
+ };
266
+
267
+ const kind = (dir: string, e: Dirent): "dir" | "file" | undefined => {
268
+ if (e.isDirectory()) return "dir";
269
+ if (e.isFile()) return "file";
270
+ if (opts.followSymlinks && e.isSymbolicLink()) {
271
+ try {
272
+ const s = statSync(join(dir, e.name));
273
+ return s.isDirectory() ? "dir" : s.isFile() ? "file" : undefined;
274
+ } catch {
275
+ return undefined;
276
+ }
277
+ }
278
+ return undefined;
279
+ };
280
+
281
+ /**
282
+ * Walk `dir`. `probing` is true below a directory a rule skipped that the
283
+ * walk entered only because an `include` pattern names something inside
284
+ * it: there, only what `include` matches is taken.
285
+ */
286
+ const walk = (dir: string, probing: boolean): boolean => {
287
+ let entries: Dirent[];
288
+ try {
289
+ entries = readdirSync(dir, { withFileTypes: true });
290
+ } catch {
291
+ return true;
292
+ }
293
+ if (opts.sorted) entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
294
+ for (const e of entries) {
295
+ if (limit?.truncated) return false;
296
+ const full = join(dir, e.name);
297
+ const type = kind(dir, e);
298
+ if (type === "dir") {
299
+ if (NEVER_ENTER.has(e.name) || excludeDirs.has(resolve(full))) continue;
300
+ const dotSkip = e.name.startsWith(".") && !dotDirsKept.has(e.name);
301
+ if (dotSkip && opts.takeSkippedDir?.(e.name, full)) {
302
+ if (!take(full)) return false;
303
+ continue;
304
+ }
305
+ const ruleSkip =
306
+ dotSkip ||
307
+ e.name === "dist" ||
308
+ (isIgnored(full) && !opts.keepIgnored?.(full)) ||
309
+ (boundaries && isProjectBoundary(full));
310
+ if (ruleSkip || probing) {
311
+ if (globs?.included(full)) {
312
+ if (!walk(full, false)) return false;
313
+ } else if (globs?.includeBelow(full)) {
314
+ if (!walk(full, true)) return false;
315
+ }
316
+ continue;
317
+ }
318
+ if (!walk(full, false)) return false;
319
+ } else if (type === "file") {
320
+ const skipped =
321
+ probing ||
322
+ (isIgnored(full) && !opts.keepIgnored?.(full)) ||
323
+ (globs?.excluded(full) ?? false);
324
+ if (skipped && !globs?.included(full)) continue;
325
+ if (!opts.accept(e.name, full)) continue;
326
+ if (!take(full)) return false;
327
+ }
328
+ }
329
+ return true;
330
+ };
331
+
332
+ walk(opts.root, false);
333
+ return out;
334
+ }
@@ -38,7 +38,7 @@
38
38
  * `sha256:<hex>` like the build-ledger digests in ./lifecycle/build-ledger.ts).
39
39
  */
40
40
 
41
- import { createHash } from "node:crypto";
41
+ import { contentDigest } from "./content-digest";
42
42
  import { DECLARABLE_MARKER, type Declarable } from "./declarable";
43
43
  import { isIntrinsic, type Intrinsic } from "./intrinsic";
44
44
 
@@ -293,7 +293,7 @@ function encodeCanonical(value: unknown, path: string, seen: Set<object>): strin
293
293
  }
294
294
 
295
295
  function sha256Digest(canonical: string): string {
296
- return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
296
+ return contentDigest(canonical);
297
297
  }
298
298
 
299
299
  // ─────────────────────────────────────────────────────────────────────────
package/src/graph-ir.ts CHANGED
@@ -230,7 +230,10 @@ export interface IRExport {
230
230
  /** A cross-stack import this stack consumes — a `Parameter` fed from another
231
231
  * stack's output at deploy time. A viewer matches an import `name` to another
232
232
  * stack's export `name` to draw the cross-stack edge; the parameter's in-stack
233
- * consumers are ordinary `$ref` edges to `node` (#513). */
233
+ * consumers are ordinary `$ref` edges to `node` (#513). A reader that infers
234
+ * that match by name uses `joinKey()` from `./join-key.ts` and labels the edge
235
+ * `exact` or `folded`, the way `chant workspace graph` labels joins across
236
+ * members (#2539, ws-008). */
234
237
  export interface IRImport {
235
238
  /** Parameter name — the cross-stack handle (e.g. "clusterArn"). */
236
239
  name: string;
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { joinKey, joinLabel } from "./join-key";
3
+
4
+ describe("joinKey (#2539, ws-008)", () => {
5
+ test("folds case and punctuation", () => {
6
+ for (const name of ["ClusterArn", "clusterArn", "cluster_arn", "cluster-arn", "CLUSTER.ARN", "Cluster Arn"]) {
7
+ expect(joinKey(name)).toBe("clusterarn");
8
+ }
9
+ });
10
+
11
+ test("keeps digits and letters outside ASCII", () => {
12
+ expect(joinKey("Subnet1")).toBe("subnet1");
13
+ expect(joinKey("Subnet1")).not.toBe(joinKey("Subnet2"));
14
+ expect(joinKey("Größe")).toBe("größe");
15
+ });
16
+
17
+ test("labels a join exact or folded, and says when names don't join", () => {
18
+ expect(joinLabel("ClusterArn", "ClusterArn")).toBe("exact");
19
+ expect(joinLabel("clusterArn", "ClusterArn")).toBe("folded");
20
+ expect(joinLabel("vpc_id", "VpcId")).toBe("folded");
21
+ expect(joinLabel("VpcId", "SubnetId")).toBeUndefined();
22
+ expect(joinLabel("", "")).toBeUndefined();
23
+ expect(joinLabel("--", "__")).toBeUndefined();
24
+ });
25
+ });