@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,9 @@
1
+ /**
2
+ * `chant workspace verify [--base <rev>] [--head <rev>] [--require attested] [--json]`
3
+ * (#2547): check a change against the trust policy at its base. See ./verify.ts.
4
+ *
5
+ * Exit 0 when the change passes, 1 when it does not or cannot be checked.
6
+ */
7
+ import type { CommandContext } from "../../cli/registry.js";
8
+ export declare function runWorkspaceVerify(ctx: CommandContext): Promise<number>;
9
+ //# sourceMappingURL=verify-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-cli.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/verify-cli.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAOzD,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAqB7E"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Verify a change against the policy at its base (#2524 D5, Threat model).
3
+ *
4
+ * This is what CI runs on a protected branch: every commit in `base..head` is
5
+ * asked of the attestors with the policy read at base, so nothing the change
6
+ * does to its own copy of the policy counts. A change that edits the policy
7
+ * (the signers file or `.chant/trust.json`) is a protected write: every commit
8
+ * that touches those files must be attested by a signer the base policy
9
+ * trusts, and by an admin when the base policy grants the admin role.
10
+ *
11
+ * Attestation is opt-in (#2525 rule 6). Without a signers file at base the
12
+ * check passes and says so, unless `--require attested` asks for more.
13
+ */
14
+ import { type CommitAttestor, type ProvenanceLevel } from "./attestor.js";
15
+ import { type BaseSource, type RecordProvenance } from "./provenance.js";
16
+ import { type ExcludedSigner } from "./policy.js";
17
+ export interface CommitVerdict extends RecordProvenance {
18
+ commit: string;
19
+ subject: string;
20
+ /** A merge that changes nothing of its own; its parents are checked instead. */
21
+ skipped?: string;
22
+ }
23
+ export interface ProtectedWrite {
24
+ commit: string;
25
+ paths: string[];
26
+ level: ProvenanceLevel;
27
+ principal?: string;
28
+ allowed: boolean;
29
+ reason: string;
30
+ }
31
+ export interface ChangeReport {
32
+ base: string | null;
33
+ baseFrom: BaseSource | null;
34
+ head: string | null;
35
+ policy: {
36
+ active: boolean;
37
+ signersPath: string;
38
+ principals: string[];
39
+ excluded: ExcludedSigner[];
40
+ writers: string[];
41
+ problems: string[];
42
+ };
43
+ require: ProvenanceLevel | null;
44
+ commits: CommitVerdict[];
45
+ protectedPaths: string[];
46
+ /** Protected paths whose content differs between the merge base and head. */
47
+ protectedChanged: string[];
48
+ protectedWrites: ProtectedWrite[];
49
+ notes: string[];
50
+ failures: string[];
51
+ ok: boolean;
52
+ }
53
+ export interface VerifyOptions {
54
+ repo: string;
55
+ base?: string;
56
+ head?: string;
57
+ require?: ProvenanceLevel;
58
+ attestors: readonly CommitAttestor[];
59
+ }
60
+ /**
61
+ * Commits in `range` that change any of `paths`, each with the paths it
62
+ * changes. Merges count only for what they change beyond every parent.
63
+ */
64
+ export declare function commitsTouching(repo: string, range: string, paths: string[]): Map<string, string[]>;
65
+ export declare function verifyChange(opts: VerifyOptions): ChangeReport;
66
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAA+C,KAAK,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACnH,OAAO,EAAiC,KAAK,cAAc,EAAoB,MAAM,UAAU,CAAC;AAEhG,MAAM,WAAW,aAAc,SAAQ,gBAAgB;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,eAAe,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,UAAU,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE;QACN,MAAM,EAAE,OAAO,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,QAAQ,EAAE,cAAc,EAAE,CAAC;QAC3B,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACF,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAChC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,6EAA6E;IAC7E,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,EAAE,EAAE,OAAO,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;CACtC;AAMD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAUnG;AAQD,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAqG9D"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Where a workspace read starts, and which chant does it (#2536; #2524 D15,
3
+ * ws-016, ws-021).
4
+ *
5
+ * {@link locateWorkspace} finds the declaration from a directory, in the
6
+ * working tree or in one commit's git objects (`--at <rev>`), and gives the
7
+ * tree every read-contract command reads from.
8
+ *
9
+ * Which chant reads the declaration: the root's. The root names its chant by
10
+ * pinning `@intentius/chant` in the declaration's `pins`. When it pins one,
11
+ * that version reads the declaration, and any other chant refuses with
12
+ * `root-chant-required` (`parseDeclaration`'s `rootChant` option). The CLI
13
+ * first hands the whole command to the pinned chant when the root has it
14
+ * installed ({@link handToRootChant}), so a user sees the refusal only when
15
+ * it isn't. When the root pins no chant, the reader's own chant reads the
16
+ * declaration if it meets `minReader`, and refuses with `reader-too-old`
17
+ * otherwise. Members are always read by their own toolchains.
18
+ */
19
+ import { type WorkspaceTree } from "./tree.js";
20
+ export interface LocatedWorkspace {
21
+ /** The files, rooted at the workspace root. */
22
+ tree: WorkspaceTree;
23
+ /** The workspace root relative to the git root, `"."` for the git root itself. Absolute outside git. */
24
+ root: string;
25
+ /** The workspace root on disk, where pinned packages and toolchains are installed. Also for `--at`. */
26
+ rootOnDisk: string;
27
+ /** The full commit id for `--at`, or null for the working tree. */
28
+ at: string | null;
29
+ /** The top of the git repository, when there is one. */
30
+ top: string | undefined;
31
+ }
32
+ /**
33
+ * Find the workspace whose declaration is nearest above `cwd`, in the working
34
+ * tree or at revision `at`. Throws a {@link WorkspaceReadError}:
35
+ * `declaration-missing`, or for `--at`, `not-a-git-repository` and
36
+ * `revision-unknown`.
37
+ */
38
+ export declare function locateWorkspace(cwd: string, at?: string): LocatedWorkspace;
39
+ /**
40
+ * The `bin/chant` of `@intentius/chant` at `version`, installed where the
41
+ * workspace root resolves packages. Undefined when it isn't installed there,
42
+ * or is installed at another version.
43
+ */
44
+ export declare function rootChantBin(rootOnDisk: string, version: string): string | undefined;
45
+ /** Set in the environment of a command handed to the root's chant, so it is handed on once at most. */
46
+ export declare const HANDED_TO_ROOT_ENV = "CHANT_WORKSPACE_ROOT_CHANT";
47
+ /** The version the declaration at `located` pins for `@intentius/chant`, or undefined. Never throws. */
48
+ export declare function declaredChantPin(located: LocatedWorkspace): string | undefined;
49
+ /**
50
+ * Hand a read-contract command to the root's chant (ws-021). When the
51
+ * declaration above `cwd` (at `at`, for `--at`) pins a chant other than this
52
+ * one, and the root has that chant installed, run the same command line
53
+ * under it and return its exit code. Otherwise return undefined, and this
54
+ * chant runs the command: it reads the declaration itself, or refuses with
55
+ * `root-chant-required` when the pinned chant isn't installed.
56
+ */
57
+ export declare function handToRootChant(cwd: string, at: string | undefined, argv?: string[]): Promise<number | undefined>;
58
+ //# sourceMappingURL=which-chant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"which-chant.d.ts","sourceRoot":"","sources":["../../src/workspace/which-chant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AASH,OAAO,EAA+C,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AAEzF,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,IAAI,EAAE,aAAa,CAAC;IACpB,wGAAwG;IACxG,IAAI,EAAE,MAAM,CAAC;IACb,uGAAuG;IACvG,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,wDAAwD;IACxD,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CACzB;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAgC1E;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcpF;AAED,uGAAuG;AACvG,eAAO,MAAM,kBAAkB,+BAA+B,CAAC;AAE/D,wGAAwG;AACxG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAY9E;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAoB9I"}
@@ -0,0 +1,133 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://intentius.io/chant/schemas/workspace/kinds/v1/workspace-kinds.schema.json",
4
+ "title": "workspace-kinds.json",
5
+ "description": "Member kinds a plugin supplies (#2524 D3, #2535, ws-031). A package publishes this file as data at its ./workspace-kinds subpath. chant reads it with the file system and never imports the package, so reading kinds never loads a lexicon.",
6
+ "type": "object",
7
+ "required": [
8
+ "schema",
9
+ "kinds"
10
+ ],
11
+ "properties": {
12
+ "$schema": {
13
+ "type": "string",
14
+ "description": "An editor hint. chant ignores it."
15
+ },
16
+ "$comment": {
17
+ "type": "string",
18
+ "description": "A note for people. chant ignores it."
19
+ },
20
+ "schema": {
21
+ "const": 1,
22
+ "description": "The kinds format version."
23
+ },
24
+ "kinds": {
25
+ "type": "array",
26
+ "minItems": 1,
27
+ "items": {
28
+ "$ref": "#/$defs/kind"
29
+ }
30
+ }
31
+ },
32
+ "patternProperties": {
33
+ "^x-": true
34
+ },
35
+ "additionalProperties": false,
36
+ "$defs": {
37
+ "kind": {
38
+ "type": "object",
39
+ "required": [
40
+ "name",
41
+ "description",
42
+ "precedence",
43
+ "probe"
44
+ ],
45
+ "properties": {
46
+ "name": {
47
+ "type": "string",
48
+ "pattern": "^[a-z][a-z0-9-]{0,39}$",
49
+ "description": "The kind's name, as a member's kind field names it. chant, workspace, other and examples are built in and can't be supplied."
50
+ },
51
+ "description": {
52
+ "type": "string",
53
+ "minLength": 1,
54
+ "description": "One line saying what the member's directory holds, for listings and error messages."
55
+ },
56
+ "precedence": {
57
+ "type": "integer",
58
+ "minimum": 1,
59
+ "maximum": 999,
60
+ "description": "When the probes of several kinds claim one directory, the highest precedence decides. Two claiming kinds with the same highest precedence are a tie, and a tie fails. The built-in chant kind has 500 and workspace has 1000."
61
+ },
62
+ "probe": {
63
+ "$ref": "#/$defs/probe"
64
+ },
65
+ "outputs": {
66
+ "type": "array",
67
+ "uniqueItems": true,
68
+ "description": "The outputs every member of this kind exposes as link targets (#2524 D6, #2539). A member entry of the kind may list more in its own outputs field. Without this field the kind exposes only what each entry lists.",
69
+ "items": {
70
+ "type": "string",
71
+ "minLength": 1,
72
+ "maxLength": 255,
73
+ "pattern": "^[^\\u0000-\\u001f]+$"
74
+ }
75
+ }
76
+ },
77
+ "patternProperties": {
78
+ "^x-": true
79
+ },
80
+ "additionalProperties": false
81
+ },
82
+ "probe": {
83
+ "type": "object",
84
+ "description": "What the probe checks in a member's directory. It runs no code. It passes when any of its clauses does, and it needs at least one.",
85
+ "minProperties": 1,
86
+ "properties": {
87
+ "anyFile": {
88
+ "type": "array",
89
+ "minItems": 1,
90
+ "uniqueItems": true,
91
+ "description": "The directory passes when a file matching one of these names sits directly in it. A * in a name matches any run of characters, so *.tf matches every Terraform file.",
92
+ "items": {
93
+ "$ref": "#/$defs/fileName"
94
+ }
95
+ },
96
+ "anyBlock": {
97
+ "type": "object",
98
+ "description": "The directory passes when a file directly in it, whose name matches one of in, has a line opening an unlabelled block named in blocks: leading space, the name, optional space, then {. No file is parsed.",
99
+ "required": [
100
+ "in",
101
+ "blocks"
102
+ ],
103
+ "properties": {
104
+ "in": {
105
+ "type": "array",
106
+ "minItems": 1,
107
+ "uniqueItems": true,
108
+ "items": {
109
+ "$ref": "#/$defs/fileName"
110
+ }
111
+ },
112
+ "blocks": {
113
+ "type": "array",
114
+ "minItems": 1,
115
+ "uniqueItems": true,
116
+ "items": {
117
+ "type": "string",
118
+ "pattern": "^[A-Za-z_][A-Za-z0-9_-]{0,63}$"
119
+ }
120
+ }
121
+ },
122
+ "additionalProperties": false
123
+ }
124
+ },
125
+ "additionalProperties": false
126
+ },
127
+ "fileName": {
128
+ "type": "string",
129
+ "description": "A file name, with no directory part. A * matches any run of characters.",
130
+ "pattern": "^(?!\\.\\.?$)[^/\\\\]+$"
131
+ }
132
+ }
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.80.0",
3
+ "version": "0.81.0",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -49,6 +49,7 @@
49
49
  "types": "./dist/cli/index.d.ts",
50
50
  "default": "./src/cli/index.ts"
51
51
  },
52
+ "./workspace/*.schema.json": "./src/workspace/*.schema.json",
52
53
  "./cli/*": {
53
54
  "development": "./src/cli/*.ts",
54
55
  "types": "./dist/cli/*.d.ts",
@@ -34,12 +34,15 @@
34
34
  * normalized to a JSON string here.
35
35
  */
36
36
 
37
- import { readdirSync, readFileSync, statSync } from "fs";
37
+ import { readFileSync, statSync } from "fs";
38
38
  import { basename, join, relative, resolve } from "path";
39
39
  import { parseYAML } from "../yaml";
40
40
  import type { LexiconPlugin } from "../lexicon";
41
41
  import type { AuditInput, AuditLexicon } from "./core";
42
42
  import { isNginxConfigPath } from "./nginx";
43
+ import { walkDiscovery } from "../discovery/walk";
44
+ import { findProjectConfig } from "../project-root";
45
+ import type { DiscoveryGlobs } from "../config";
43
46
  import { gitignoreCoversTerraformState, isTerraformStatePath, nestedGitignoreCovering } from "./terraform-state";
44
47
 
45
48
  /** Lexicons the auditor knows how to detect and run checks for. */
@@ -69,11 +72,8 @@ export async function loadAuditPlugins(names: readonly string[] = AUDIT_LEXICONS
69
72
  return plugins;
70
73
  }
71
74
 
72
- const WALK_SKIP = new Set(["node_modules", ".git", "dist"]);
73
75
  /** Terraform's local working directory: recorded by the walk, never entered (TF023). */
74
76
  const TERRAFORM_WORK_DIR = ".terraform";
75
- /** Dot-directories the walk descends into anyway (CI lives here). */
76
- const WALK_DOT_DIRS = new Set([".github", ".forgejo"]);
77
77
  /**
78
78
  * How many files the local walk takes before it stops. Every file it reaches
79
79
  * counts, candidate or not, so a tree with a large `docs/` or `assets/` can hit
@@ -83,53 +83,54 @@ export const DEFAULT_MAX_WALK_FILES = 1000;
83
83
  /** Skip absurdly large files before parsing — mirrors the fetch layer's caps. */
84
84
  const MAX_FILE_BYTES = 2 * 1024 * 1024;
85
85
 
86
- /** The walk's limit, and whether it left anything behind because of it. */
87
- interface WalkState {
88
- limit: number;
89
- truncated: boolean;
86
+ /**
87
+ * The project's `exclude`/`include` globs for the audit walk, read from a
88
+ * `chant.config.json` only. The audit reads repositories it has no reason to
89
+ * trust, so it never runs a `chant.config.ts` to find them.
90
+ */
91
+ function auditGlobs(root: string): DiscoveryGlobs | undefined {
92
+ const { dir, configPath } = findProjectConfig(root);
93
+ if (!configPath?.endsWith(".json")) return undefined;
94
+ let raw: unknown;
95
+ try {
96
+ raw = JSON.parse(readFileSync(configPath, "utf-8"));
97
+ } catch {
98
+ return undefined;
99
+ }
100
+ const list = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string" && x !== "") : []);
101
+ const exclude = list((raw as Record<string, unknown>)?.exclude);
102
+ const include = list((raw as Record<string, unknown>)?.include);
103
+ return exclude.length === 0 && include.length === 0 ? undefined : { root: dir, exclude, include };
90
104
  }
91
105
 
92
106
  /**
93
- * Recursively collect file paths under a root, skipping noise dirs.
107
+ * Collect file paths under a root through the one discovery walk
108
+ * (`../discovery/walk.ts`, #2527), sorted per directory.
109
+ *
110
+ * The audit's own departures from the other walkers: `.github` and `.forgejo`
111
+ * are entered, since they hold the CI files it audits; `.terraform/` is
112
+ * recorded but never entered, since it is machine-generated, routinely
113
+ * hundreds of megabytes of provider binaries, and its presence is itself the
114
+ * finding (TF023); and Terraform state paths are left to TF023's own
115
+ * `.gitignore` reading rather than dropped for being git-ignored.
94
116
  *
95
- * Stops at `state.limit` paths. Once full it keeps looking only until it meets
117
+ * Stops at `state.max` paths. Once full it keeps looking only until it meets
96
118
  * one more path it would have taken, which is what sets `truncated`: a tree of
97
- * exactly `limit` files, or one whose remaining entries are all skipped
98
- * directories, was not truncated. The paths it returns are the same first
99
- * `limit` either way.
119
+ * exactly `max` files, or one whose remaining entries are all skipped
120
+ * directories, was not truncated.
100
121
  */
101
- function walkFiles(dir: string, out: string[], state: WalkState): void {
102
- if (state.truncated) return;
103
- let entries;
104
- try {
105
- entries = readdirSync(dir, { withFileTypes: true });
106
- } catch {
107
- return;
108
- }
109
- const take = (path: string): boolean => {
110
- if (out.length >= state.limit) {
111
- state.truncated = true;
112
- return false;
113
- }
114
- out.push(path);
115
- return true;
116
- };
117
- for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
118
- if (state.truncated) return;
119
- if (e.name.startsWith(".") && e.isDirectory() && !WALK_DOT_DIRS.has(e.name)) {
120
- // `.terraform/` is never descended into: it is machine-generated,
121
- // routinely hundreds of megabytes of provider binaries, and nothing in
122
- // it is worth reading. Its presence is itself the finding (TF023), so
123
- // the directory path alone is recorded and `collectCandidates` hands it
124
- // on with no content.
125
- if (e.name === TERRAFORM_WORK_DIR && !take(join(dir, e.name))) return;
126
- continue;
127
- }
128
- if (WALK_SKIP.has(e.name)) continue;
129
- const full = join(dir, e.name);
130
- if (e.isDirectory()) walkFiles(full, out, state);
131
- else if (!take(full)) return;
132
- }
122
+ function walkFiles(root: string, state: { max: number; truncated: boolean }, excludeDirs?: readonly string[]): string[] {
123
+ return walkDiscovery({
124
+ walker: "audit",
125
+ root,
126
+ excludeDirs,
127
+ globs: auditGlobs(root),
128
+ sorted: true,
129
+ limit: state,
130
+ takeSkippedDir: (name) => name === TERRAFORM_WORK_DIR,
131
+ keepIgnored: (full) => isTerraformStatePath(relative(root, full)),
132
+ accept: () => true,
133
+ });
133
134
  }
134
135
 
135
136
  function readSafe(full: string): string | undefined {
@@ -625,18 +626,12 @@ export function discoverByDetection(root: string, plugins: DetectPlugin[]): Audi
625
626
  export interface CandidateWalkOptions {
626
627
  /** Files the walk takes before it stops, counting every file. Defaults to {@link DEFAULT_MAX_WALK_FILES}. */
627
628
  maxFiles?: number;
628
- }
629
-
630
- /**
631
- * A TF023 path the root `.gitignore` does not cover but a `.gitignore` in a
632
- * directory between it and the scan root does (#2528). TF023 reports it today;
633
- * from the next release, which reads every such `.gitignore`, it will not.
634
- */
635
- export interface NestedGitignoreChange {
636
- /** The TF023 path, relative to the scan root. */
637
- path: string;
638
- /** The nested `.gitignore` that ignores it, relative to the scan root. */
639
- gitignore: string;
629
+ /**
630
+ * Directories the walk leaves out, absolute: the workspace members and
631
+ * example-group matches below the root, from `workspaceMemberDirs`. The
632
+ * caller resolves them, since that is async and this walk is not.
633
+ */
634
+ excludeDirs?: readonly string[];
640
635
  }
641
636
 
642
637
  /** What the local walk read, plus what it knows about its own limits. */
@@ -646,8 +641,6 @@ export interface CandidateWalk {
646
641
  maxFiles: number;
647
642
  /** True when the walk stopped at `maxFiles` with files left unread. */
648
643
  truncated: boolean;
649
- /** TF023 paths whose finding changes once nested `.gitignore` files are read. */
650
- nestedGitignore: NestedGitignoreChange[];
651
644
  }
652
645
 
653
646
  /** The walk half of `discoverByDetection`: candidate files read into memory, paths relative to the root. */
@@ -656,39 +649,34 @@ export function collectCandidates(root: string): RepoFile[] {
656
649
  }
657
650
 
658
651
  /**
659
- * `collectCandidates` with the facts the audit CLI warns about: whether the
660
- * walk was truncated, and which TF023 paths a nested `.gitignore` would drop.
661
- * Neither changes `files`; the CLI prints them on stderr (#2528).
652
+ * `collectCandidates` plus whether the walk stopped at its limit with files
653
+ * left unread, which the audit report states (#2528).
662
654
  */
663
655
  export function walkCandidates(root: string, opts: CandidateWalkOptions = {}): CandidateWalk {
664
- const all: string[] = [];
665
- const state: WalkState = { limit: opts.maxFiles ?? DEFAULT_MAX_WALK_FILES, truncated: false };
666
- walkFiles(root, all, state);
656
+ const state = { max: opts.maxFiles ?? DEFAULT_MAX_WALK_FILES, truncated: false };
657
+ const all = walkFiles(root, state, opts.excludeDirs);
667
658
  // TF023 reads paths, not content: a `.tfstate` can be tens of megabytes of
668
659
  // machine-generated JSON, and the finding is that it is in the repository at
669
- // all. The root `.gitignore` is what separates "committed" from "merely on
670
- // disk" here — a Terraform working tree almost always has an ignored
660
+ // all. The `.gitignore` files are what separate "committed" from "merely on
661
+ // disk" here. A Terraform working tree almost always has an ignored
671
662
  // `.terraform/` in it, and reporting that would make TF023 fire on every
672
- // local audit. A fetched repository has no such ambiguity (its file list is
673
- // the tracked files) and is not filtered.
663
+ // local audit. Every `.gitignore` from the scan root down to the file's own
664
+ // directory is read (#2528), so auditing from the root and from a
665
+ // subdirectory agree. A fetched repository has no such ambiguity (its file
666
+ // list is the tracked files) and is not filtered.
674
667
  const gitignore = readSafe(join(root, ".gitignore")) ?? "";
675
- // Only the root one decides, for now. The nested ones are read to say which
676
- // findings the next release, which honours them, will stop reporting.
677
668
  const nestedBodies = new Map<string, string | undefined>();
678
669
  const nestedGitignoreAt = (dir: string): string | undefined => {
679
670
  if (!nestedBodies.has(dir)) nestedBodies.set(dir, readSafe(join(root, dir, ".gitignore")));
680
671
  return nestedBodies.get(dir);
681
672
  };
682
673
  const files: RepoFile[] = [];
683
- const nestedGitignore: NestedGitignoreChange[] = [];
684
674
  for (const full of all) {
685
675
  const path = relative(root, full);
686
676
  if (isTerraformStatePath(path)) {
687
- if (!gitignoreCoversTerraformState(gitignore, path)) {
688
- files.push({ path, content: "" });
689
- const dir = nestedGitignoreCovering(path, nestedGitignoreAt);
690
- if (dir !== undefined) nestedGitignore.push({ path, gitignore: `${dir}/.gitignore` });
691
- }
677
+ const ignored =
678
+ gitignoreCoversTerraformState(gitignore, path) || nestedGitignoreCovering(path, nestedGitignoreAt) !== undefined;
679
+ if (!ignored) files.push({ path, content: "" });
692
680
  continue;
693
681
  }
694
682
  // `.tf` is read locally so `classifyTerraform` can bundle real content.
@@ -702,5 +690,5 @@ export function walkCandidates(root: string, opts: CandidateWalkOptions = {}): C
702
690
  const content = readSafe(full);
703
691
  if (content !== undefined) files.push({ path, content });
704
692
  }
705
- return { files, maxFiles: state.limit, truncated: state.truncated, nestedGitignore };
693
+ return { files, maxFiles: state.max, truncated: state.truncated };
706
694
  }
@@ -31,6 +31,8 @@ export interface RenderHtmlOptions extends BuildModelOptions {
31
31
  /** Full template override; `{{title}} {{accent}} {{logo}} {{meta}} {{body}} {{footer}}` slots. */
32
32
  template?: string;
33
33
  notes?: string[];
34
+ /** A truncated local walk (#2528), carried into the embedded JSON report. */
35
+ truncated?: { limit: number; flag: string };
34
36
  }
35
37
 
36
38
  const DEFAULT_ACCENT = "#7c3aed";
@@ -194,7 +196,7 @@ export function renderHtml(findings: AuditFinding[], opts: RenderHtmlOptions = {
194
196
 
195
197
  // Machine-readable data embedded so the HTML report is also parseable.
196
198
  // `<` escaped so the JSON can't break out of the <script> element.
197
- const dataJson = JSON.stringify(buildReportJson(findings, { snapshot: opts.snapshot }), null, 0).replace(/</g, "\\u003c");
199
+ const dataJson = JSON.stringify(buildReportJson(findings, { snapshot: opts.snapshot, truncated: opts.truncated }), null, 0).replace(/</g, "\\u003c");
198
200
  const dataScript = `<script type="application/json" id="chant-audit-report">${dataJson}</script>`;
199
201
 
200
202
  let body: string;
@@ -129,6 +129,12 @@ export interface AuditReportJson {
129
129
  findings: SerializedFinding[];
130
130
  /** Candidate files that looked like they wanted a lexicon that is not installed. */
131
131
  unclaimed?: UnclaimedFile[];
132
+ /**
133
+ * Present when the local walk stopped at its file limit with files left
134
+ * unread (#2528): the limit, and the flag that raises it. Absent for a whole
135
+ * scan and for a repository URL.
136
+ */
137
+ truncated?: { limit: number; flag: string };
132
138
  }
133
139
 
134
140
  export function metaFor(id: string, catalog: Record<string, RuleMeta> = RULE_CATALOG): RuleMeta {
@@ -298,7 +304,13 @@ export function buildReportModel(findings: AuditFinding[], opts: BuildModelOptio
298
304
  /** Build the versioned, machine-readable JSON report (stable contract). */
299
305
  export function buildReportJson(
300
306
  findings: AuditFinding[],
301
- opts: { snapshot?: AuditSnapshot; toolVersion?: string; catalog?: Record<string, RuleMeta>; unclaimed?: UnclaimedFile[] } = {},
307
+ opts: {
308
+ snapshot?: AuditSnapshot;
309
+ toolVersion?: string;
310
+ catalog?: Record<string, RuleMeta>;
311
+ unclaimed?: UnclaimedFile[];
312
+ truncated?: { limit: number; flag: string };
313
+ } = {},
302
314
  ): AuditReportJson {
303
315
  const model = buildReportModel(findings, { catalog: opts.catalog });
304
316
  const version = opts.toolVersion ?? opts.snapshot?.toolVersion ?? "0.0.0";
@@ -328,5 +340,6 @@ export function buildReportJson(
328
340
  docUrl: ruleDocUrl(f.checkId),
329
341
  })),
330
342
  unclaimed: opts.unclaimed && opts.unclaimed.length > 0 ? opts.unclaimed : undefined,
343
+ truncated: opts.truncated,
331
344
  };
332
345
  }
@@ -196,22 +196,18 @@ describe("walkCandidates reports what the next release changes, and changes noth
196
196
  rmSync(dir, { recursive: true, force: true });
197
197
  });
198
198
 
199
- test("a state file a nested .gitignore ignores is still a candidate, and is named for the warning", () => {
199
+ test("a state file a nested .gitignore ignores is not a candidate (#2528)", () => {
200
200
  const dir = tmpRepo();
201
201
  mkdirSync(join(dir, "infra", ".terraform"), { recursive: true });
202
202
  writeFileSync(join(dir, "infra", ".gitignore"), "*.tfstate\n.terraform/\n");
203
203
  writeFileSync(join(dir, "infra", "terraform.tfstate"), "{}");
204
204
  const walk = walkCandidates(dir);
205
- expect(walk.files.map((f) => f.path).sort()).toEqual(["infra/.terraform", "infra/terraform.tfstate"]);
206
- expect(auditTerraformState(walk.files)).toHaveLength(2);
207
- expect(walk.nestedGitignore).toEqual([
208
- { path: "infra/.terraform", gitignore: "infra/.gitignore" },
209
- { path: "infra/terraform.tfstate", gitignore: "infra/.gitignore" },
210
- ]);
205
+ expect(walk.files.map((f) => f.path)).toEqual([]);
206
+ expect(auditTerraformState(walk.files)).toHaveLength(0);
211
207
  rmSync(dir, { recursive: true, force: true });
212
208
  });
213
209
 
214
- test("nothing is named when the root .gitignore already drops the path, or no nested one covers it", () => {
210
+ test("only the .gitignore files between the path and the root apply, not a sibling's", () => {
215
211
  const dir = tmpRepo();
216
212
  mkdirSync(join(dir, "a"), { recursive: true });
217
213
  mkdirSync(join(dir, "b"), { recursive: true });
@@ -223,7 +219,6 @@ describe("walkCandidates reports what the next release changes, and changes noth
223
219
  writeFileSync(join(dir, "b", ".gitignore"), "*.tfstate\n");
224
220
  const walk = walkCandidates(dir);
225
221
  expect(walk.files.map((f) => f.path)).toEqual(["a/terraform.tfstate"]);
226
- expect(walk.nestedGitignore).toEqual([]);
227
222
  rmSync(dir, { recursive: true, force: true });
228
223
  });
229
224
  });
@@ -22,8 +22,8 @@
22
22
  * walk that decides which paths reach here is `discover.ts`'s
23
23
  * `collectCandidates`, which is also where a locally ignored path is dropped:
24
24
  * see `isTerraformStatePath` there and `gitignoreCoversTerraformState` below.
25
- * Only the root `.gitignore` drops a path today; `nestedGitignoreCovering`
26
- * finds the ones a nested `.gitignore` would drop from the next release.
25
+ * The root `.gitignore` and every nested one on the way to the path can drop
26
+ * it (`nestedGitignoreCovering`, #2528).
27
27
  */
28
28
 
29
29
  import type { AuditFinding } from "./core";
@@ -92,9 +92,8 @@ export function gitignoreCoversTerraformState(gitignore: string, path: string):
92
92
  * the same narrow matcher as the root one. `readGitignore` takes a directory
93
93
  * relative to the scan root and returns its `.gitignore` body, if any.
94
94
  *
95
- * The walk does not act on this yet: today only the root `.gitignore` decides
96
- * TF023, and the audit CLI uses this to warn about the findings that go away
97
- * once every `.gitignore` is read (#2528).
95
+ * The local walk drops a TF023 path this returns a directory for, as it drops
96
+ * one the root `.gitignore` covers (#2528).
98
97
  */
99
98
  export function nestedGitignoreCovering(
100
99
  path: string,