@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,167 @@
1
+ import { afterEach, beforeAll, describe, expect, test, vi } from "vitest";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { auditCommand, printAuditResult, type AuditCommandOptions } from "./audit";
6
+ import { loadAuditPlugins } from "../../audit/discover";
7
+
8
+ /**
9
+ * #2528, one of the level-0 changes on #2525's exception list (warned in
10
+ * v0.80.0). A truncated local walk is stated in the report, with its limit and
11
+ * the flag that raises it, and TF023 honours every `.gitignore` between a path
12
+ * and the scan root.
13
+ */
14
+
15
+ const NOW = "2026-09-23T00:00:00.000Z";
16
+ const dirs: string[] = [];
17
+
18
+ function tmpRepo(): string {
19
+ const dir = mkdtempSync(join(tmpdir(), "chant-2528-"));
20
+ dirs.push(dir);
21
+ return dir;
22
+ }
23
+
24
+ afterEach(() => {
25
+ for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
26
+ vi.restoreAllMocks();
27
+ });
28
+
29
+ let plugins: Awaited<ReturnType<typeof loadAuditPlugins>>;
30
+
31
+ // Loading every audit lexicon is the slow part; once, outside any one test's timeout.
32
+ beforeAll(async () => {
33
+ plugins = await loadAuditPlugins();
34
+ }, 120_000);
35
+
36
+ function audit(path: string, extra: Partial<AuditCommandOptions> = {}) {
37
+ return auditCommand({ path, format: "json", now: NOW, toolVersion: "0.0.0", plugins, ...extra });
38
+ }
39
+
40
+ /** Print a result the way the CLI does, and split what reached each stream. */
41
+ function printed(result: Awaited<ReturnType<typeof auditCommand>>): { stdout: string; stderr: string } {
42
+ const out: string[] = [];
43
+ const err: string[] = [];
44
+ vi.spyOn(console, "log").mockImplementation((s: string) => void out.push(s));
45
+ vi.spyOn(console, "error").mockImplementation((s: string) => void err.push(s));
46
+ printAuditResult(result);
47
+ vi.restoreAllMocks();
48
+ return { stdout: out.join("\n"), stderr: err.join("\n") };
49
+ }
50
+
51
+ describe("a truncated walk is reported (#2528)", () => {
52
+ function treeWithTail(): string {
53
+ const dir = tmpRepo();
54
+ mkdirSync(join(dir, "a"), { recursive: true });
55
+ for (let i = 0; i < 5; i++) writeFileSync(join(dir, "a", `${i}.md`), "x");
56
+ writeFileSync(join(dir, "a", "terraform.tfstate"), "{}");
57
+ // Past the limit: a state file TF023 would report if the walk reached it.
58
+ mkdirSync(join(dir, "z"), { recursive: true });
59
+ writeFileSync(join(dir, "z", "terraform.tfstate"), "{}");
60
+ return dir;
61
+ }
62
+
63
+ test("JSON carries truncated with the limit and the flag, and nothing goes to stderr", async () => {
64
+ const dir = treeWithTail();
65
+ const result = await audit(dir, { maxFiles: 6 });
66
+ expect(JSON.parse(result.output).truncated).toEqual({ limit: 6, flag: "--max-files" });
67
+ expect(result.warnings).toBeUndefined();
68
+ const { stdout, stderr } = printed(result);
69
+ expect(stdout).toBe(result.output);
70
+ expect(stderr).toBe("");
71
+ expect(result.findings.map((f) => f.file)).toEqual(["a/terraform.tfstate"]);
72
+ });
73
+
74
+ test("the text report leads with a note naming the limit and --max-files", async () => {
75
+ const dir = treeWithTail();
76
+ const result = await audit(dir, { maxFiles: 6, format: "stylish" });
77
+ const first = result.output.split("\n")[0];
78
+ expect(first).toBe(
79
+ `Note: The scan of ${dir} stopped at 6 files, so part of the tree was not audited. ` +
80
+ "The limit counts every file the walk reaches, not only the ones it audits. Raise it with --max-files <n>.",
81
+ );
82
+ expect(result.warnings).toBeUndefined();
83
+ });
84
+
85
+ test("markdown and HTML carry the note, and HTML's embedded JSON carries the field", async () => {
86
+ const dir = treeWithTail();
87
+ const md = await audit(dir, { maxFiles: 6, format: "markdown" });
88
+ expect(md.output).toContain("stopped at 6 files");
89
+ const html = await audit(dir, { maxFiles: 6, format: "html" });
90
+ expect(html.output).toContain("stopped at 6 files");
91
+ expect(html.output).toContain('"truncated":{"limit":6,"flag":"--max-files"}');
92
+ });
93
+
94
+ test("SARIF has no slot for it, so the truncation goes to stderr there", async () => {
95
+ const dir = treeWithTail();
96
+ const result = await audit(dir, { maxFiles: 6, format: "sarif" });
97
+ const { stdout, stderr } = printed(result);
98
+ expect(stdout).toBe(result.output);
99
+ expect(stderr).toContain(`The scan of ${dir} stopped at 6 files`);
100
+ expect(stderr).toContain("--max-files <n>");
101
+ });
102
+
103
+ test("a whole scan reports nothing: no field, no note", async () => {
104
+ const dir = treeWithTail();
105
+ const raised = await audit(dir, { maxFiles: 7 });
106
+ expect(JSON.parse(raised.output)).not.toHaveProperty("truncated");
107
+ expect(raised.warnings).toBeUndefined();
108
+ expect(raised.findings.map((f) => f.file).sort()).toEqual(["a/terraform.tfstate", "z/terraform.tfstate"]);
109
+ const text = await audit(dir, { format: "stylish" });
110
+ expect(text.output).not.toContain("stopped at");
111
+ });
112
+
113
+ test("the no-lexicons report states it too", async () => {
114
+ const dir = treeWithTail();
115
+ const json = await audit(dir, { maxFiles: 6, plugins: [] });
116
+ expect(JSON.parse(json.output).truncated).toEqual({ limit: 6, flag: "--max-files" });
117
+ const text = await audit(dir, { maxFiles: 6, plugins: [], format: "stylish" });
118
+ expect(text.output).toContain("stopped at 6 files");
119
+ });
120
+
121
+ test("an audit with nothing auditable still says the scan was cut short", async () => {
122
+ const dir = tmpRepo();
123
+ for (let i = 0; i < 4; i++) writeFileSync(join(dir, `${i}.md`), "x");
124
+ const result = await audit(dir, { maxFiles: 2, format: "stylish" });
125
+ expect(result.output).toContain("No auditable files found");
126
+ expect(result.output).toContain("stopped at 2 files");
127
+ });
128
+ });
129
+
130
+ describe("TF023 honours every .gitignore between a path and the scan root (#2528)", () => {
131
+ function nestedTree(nested: string | undefined): string {
132
+ const dir = tmpRepo();
133
+ mkdirSync(join(dir, "infra", "prod"), { recursive: true });
134
+ writeFileSync(join(dir, ".gitignore"), "node_modules/\n");
135
+ writeFileSync(join(dir, "infra", "main.tf"), 'resource "null_resource" "a" {}\n');
136
+ writeFileSync(join(dir, "infra", "terraform.tfstate"), "{}");
137
+ writeFileSync(join(dir, "infra", "prod", "terraform.tfstate"), "{}");
138
+ if (nested !== undefined) writeFileSync(join(dir, "infra", ".gitignore"), nested);
139
+ return dir;
140
+ }
141
+ const tf023 = (r: Awaited<ReturnType<typeof audit>>) =>
142
+ r.findings.filter((f) => f.checkId === "TF023").map((f) => f.file).sort();
143
+
144
+ test("a nested .gitignore drops the paths it covers, at any depth below it", async () => {
145
+ const result = await audit(nestedTree("*.tfstate\n"));
146
+ expect(tf023(result)).toEqual([]);
147
+ expect(result.warnings).toBeUndefined();
148
+ });
149
+
150
+ test("a nested .gitignore that does not cover the file changes nothing", async () => {
151
+ expect(tf023(await audit(nestedTree("*.log\n")))).toEqual(["infra/prod/terraform.tfstate", "infra/terraform.tfstate"]);
152
+ });
153
+
154
+ test("a .gitignore deeper than the file does not apply to it", async () => {
155
+ const dir = nestedTree(undefined);
156
+ writeFileSync(join(dir, "infra", "prod", ".gitignore"), "*.tfstate\n");
157
+ expect(tf023(await audit(dir))).toEqual(["infra/terraform.tfstate"]);
158
+ });
159
+
160
+ test("auditing from the root and from the subdirectory agree", async () => {
161
+ const dir = nestedTree(undefined);
162
+ writeFileSync(join(dir, "infra", "prod", ".gitignore"), "*.tfstate\n");
163
+ const fromRoot = tf023(await audit(dir));
164
+ const fromInfra = tf023(await audit(join(dir, "infra"))).map((p) => `infra/${p}`);
165
+ expect(fromRoot).toEqual(fromInfra);
166
+ });
167
+ });
@@ -6,15 +6,15 @@
6
6
  */
7
7
 
8
8
  import { existsSync, readFileSync, statSync, writeFileSync } from "fs";
9
- import { join, resolve } from "path";
9
+ import { dirname, join, resolve } from "path";
10
10
  import { auditFiles, type AuditInput, type AuditFinding, type ChecksProvider, type SuppressionStats } from "../../audit/core";
11
11
  import { AUDIT_LEXICONS, classifyFiles, loadAuditPlugins, unclaimedFiles, walkCandidates, type CandidateWalk, type DetectPlugin, type RepoFile, type UnclaimedFile } from "../../audit/discover";
12
12
  import { RULE_CATALOG, resolveAuditCatalog, type RuleMeta } from "../../audit/catalog";
13
13
  import { scanForSecrets, parseSecretsConfig, type SecretsScanOptions } from "../../audit/secrets";
14
14
  import { auditWranglerConfigs } from "../../audit/wrangler";
15
15
  import { auditNginxConfigs } from "../../audit/nginx";
16
- import { auditTerraformState, isTerraformStatePath } from "../../audit/terraform-state";
17
- import { warnDiscoveryChanges } from "../../discovery/convergence";
16
+ import { auditTerraformState } from "../../audit/terraform-state";
17
+ import { workspaceMemberDirs } from "../../discovery/walk";
18
18
  import { renderMarkdown } from "../../audit/report";
19
19
  import { renderHtml, type ReportTheme } from "../../audit/report-html";
20
20
  import { buildReportJson, REPORT_SCHEMA_VERSION, type AuditSnapshot } from "../../audit/report-model";
@@ -23,7 +23,8 @@ import { extractUnpinnedActions, extractUnpinnedImages } from "../../audit/proof
23
23
  import type { ProveOptions } from "../../audit/proof";
24
24
  import type { Severity } from "../../lint/rule";
25
25
  import { formatWarning } from "../format";
26
- import { lexiconModulePath, lexiconPackagesToInstall, lexiconSourceLabel } from "../../lexicon-module";
26
+ import { lexiconPackagesToInstall, lexiconSourceLabel, pathLexiconMap, recordedLexiconModules } from "../../lexicon-module";
27
+ import { readLexiconDeclarationsStatically, unknownPathLexiconsNotice } from "../../config-static";
27
28
 
28
29
  export type AuditFormat = "stylish" | "json" | "sarif" | "markdown" | "html";
29
30
  export type AuditTier = "merge-worthy" | "all";
@@ -116,7 +117,7 @@ export interface AuditCommandResult {
116
117
  * summary line.
117
118
  */
118
119
  suppressedCount?: number;
119
- /** Run diagnostics for stderr (#2528). */
120
+ /** Run diagnostics for stderr: a truncated scan in a format that cannot carry it (#2528). */
120
121
  warnings?: AuditWarning[];
121
122
  }
122
123
 
@@ -129,16 +130,34 @@ export const NO_LEXICONS_EXIT_CODE = 2;
129
130
  * `chant.config.ts` names by path (#2520), which has no package to install
130
131
  * (chant#2578).
131
132
  */
132
- function missingLexiconRemedy(names: readonly string[]): string {
133
+ function missingLexiconRemedy(names: readonly string[], paths: ReadonlyMap<string, string>): string {
133
134
  const parts: string[] = [];
134
- const pkgs = lexiconPackagesToInstall(names);
135
+ const pkgs = lexiconPackagesToInstall(names, paths);
135
136
  if (pkgs.length > 0) parts.push(`npm i ${pkgs.join(" ")}`);
136
137
  for (const name of names) {
137
- if (lexiconModulePath(name) !== undefined) parts.push(`${name} is declared by path: ${lexiconSourceLabel(name)}`);
138
+ if (paths.has(name)) parts.push(`${name} is declared by path: ${lexiconSourceLabel(name, process.cwd(), paths)}`);
138
139
  }
139
140
  return parts.join("; ");
140
141
  }
141
142
 
143
+ /**
144
+ * chant#2589 — the lexicons a local target's chant.config declares by path,
145
+ * with any recorded earlier in this process. The config is read statically
146
+ * (../../config-static.ts) and never evaluated, and the paths are not
147
+ * recorded for the loaders: audit names them in its messages, but never
148
+ * imports a project's lexicon module. `notice` is set when the config's
149
+ * `lexicons` could not be read that way.
150
+ */
151
+ export function auditPathLexicons(target: string, isUrl: boolean): { paths: Map<string, string>; notice?: string } {
152
+ const paths = recordedLexiconModules();
153
+ if (isUrl) return { paths };
154
+ const read = readLexiconDeclarationsStatically(resolve(target));
155
+ if (read.status === "read") {
156
+ for (const [name, path] of pathLexiconMap(read.entries, dirname(read.configPath))) paths.set(name, path);
157
+ }
158
+ return { paths, notice: unknownPathLexiconsNotice(read) };
159
+ }
160
+
142
161
  /** Missing audit lexicons the unclaimed files pointed at, in first-seen order. */
143
162
  function wantedLexicons(unclaimed: UnclaimedFile[]): string[] {
144
163
  return [...new Set(unclaimed.map((u) => u.lexicon))];
@@ -149,26 +168,31 @@ function wantedLexicons(unclaimed: UnclaimedFile[]): string[] {
149
168
  * lexicons it needs. Every wanted lexicon is a `-p` package so npx puts all of
150
169
  * them on the same resolution path.
151
170
  */
152
- export function installLine(lexicons: string[], target: string): string {
171
+ export function installLine(
172
+ lexicons: string[],
173
+ target: string,
174
+ paths: ReadonlyMap<string, string> = recordedLexiconModules(),
175
+ ): string {
153
176
  // chant#2578 — a lexicon declared by path has no package for npx to fetch.
154
- const pkgs = ["@intentius/chant", ...lexiconPackagesToInstall(lexicons)];
177
+ const pkgs = ["@intentius/chant", ...lexiconPackagesToInstall(lexicons, paths)];
155
178
  return `npx ${pkgs.map((p) => `-p ${p}`).join(" ")} chant audit ${target}`;
156
179
  }
157
180
 
158
181
  /** One-line coverage hint for the partial case: some lexicons loaded, others wanted by files on disk. */
159
- function missingLexiconHint(unclaimed: UnclaimedFile[]): string | undefined {
182
+ function missingLexiconHint(unclaimed: UnclaimedFile[], paths: ReadonlyMap<string, string>): string | undefined {
160
183
  const wanted = wantedLexicons(unclaimed);
161
184
  if (wanted.length === 0) return undefined;
162
185
  const n = unclaimed.length;
163
186
  return `${n} file${n === 1 ? " looks" : "s look"} like ${wanted.join("/")} but ${wanted.length === 1 ? "that lexicon is" : "those lexicons are"} not installed, so ${n === 1 ? "it was" : "they were"} skipped` +
164
- ` (${missingLexiconRemedy(wanted)}).`;
187
+ ` (${missingLexiconRemedy(wanted, paths)}).`;
165
188
  }
166
189
 
167
190
  /** Human-readable diagnostic for the zero-lexicon case. */
168
- function renderNoLexicons(target: string, unclaimed: UnclaimedFile[]): string {
191
+ function renderNoLexicons(target: string, unclaimed: UnclaimedFile[], paths: ReadonlyMap<string, string>, truncated?: AuditTruncation): string {
169
192
  const lines: string[] = [];
170
193
  lines.push(`chant audit had nothing to look with: no audit lexicon is installed, so nothing under ${target} was inspected.`);
171
194
  lines.push("This is not a clean result. Detection and checks live in the lexicon packages.");
195
+ if (truncated) lines.push("", `Note: ${truncationNote(truncated, target)}`);
172
196
  const wanted = wantedLexicons(unclaimed);
173
197
  if (unclaimed.length > 0) {
174
198
  lines.push("", "Files that wanted a lexicon:");
@@ -179,12 +203,17 @@ function renderNoLexicons(target: string, unclaimed: UnclaimedFile[]): string {
179
203
  lines.push("", "No file under the target looked like CI, Kubernetes, Helm, Docker, CloudFormation, ARM, Config Connector, fountain, or Terraform either.");
180
204
  }
181
205
  lines.push("", "Run it with the lexicons those files need:");
182
- lines.push(` ${installLine(wanted.length > 0 ? wanted : [...AUDIT_LEXICONS], target)}`);
206
+ const lexicons = wanted.length > 0 ? wanted : [...AUDIT_LEXICONS];
207
+ lines.push(` ${installLine(lexicons, target, paths)}`);
208
+ // chant#2589 — a lexicon declared by path is left out of the line above.
209
+ for (const name of lexicons) {
210
+ if (paths.has(name)) lines.push(` (${name} is declared by path: ${lexiconSourceLabel(name, process.cwd(), paths)})`);
211
+ }
183
212
  return lines.join("\n");
184
213
  }
185
214
 
186
215
  /** Machine-readable form of the zero-lexicon diagnostic (`status: "no-lexicons"`). */
187
- function renderNoLexiconsJson(target: string, unclaimed: UnclaimedFile[]): string {
216
+ function renderNoLexiconsJson(target: string, unclaimed: UnclaimedFile[], paths: ReadonlyMap<string, string>, truncated?: AuditTruncation): string {
188
217
  const wanted = wantedLexicons(unclaimed);
189
218
  return JSON.stringify(
190
219
  {
@@ -196,7 +225,8 @@ function renderNoLexiconsJson(target: string, unclaimed: UnclaimedFile[]): strin
196
225
  findings: [],
197
226
  unclaimed,
198
227
  missingLexicons: wanted,
199
- install: installLine(wanted.length > 0 ? wanted : [...AUDIT_LEXICONS], target),
228
+ install: installLine(wanted.length > 0 ? wanted : [...AUDIT_LEXICONS], target, paths),
229
+ truncated,
200
230
  },
201
231
  null,
202
232
  2,
@@ -406,32 +436,26 @@ async function buildSnapshot(options: AuditCommandOptions, files: string[], isUr
406
436
  };
407
437
  }
408
438
 
409
- /**
410
- * Warnings for the two level-0 changes #2528 makes in the next release, one
411
- * release ahead of it (#2525). Neither changes today's report: a truncated walk
412
- * is still reported as if it were whole, and TF023 still reads only the root
413
- * `.gitignore`.
414
- */
415
- export function walkWarnings(walk: CandidateWalk, target: string): AuditWarning[] {
416
- const warnings: AuditWarning[] = [];
417
- if (walk.truncated) {
418
- warnings.push({
419
- message:
420
- `the scan of ${target} stopped at ${walk.maxFiles} files, so part of the tree was not audited. ` +
421
- "The limit counts every file the walk reaches, not only the ones it audits.",
422
- hint:
423
- "Raise it with --max-files <n>. From the next release, text and JSON output will also report a truncated scan.",
424
- });
425
- }
426
- for (const change of walk.nestedGitignore) {
427
- warnings.push({
428
- file: change.path,
429
- message: `TF023 reports this path because chant audit reads only the root .gitignore, but ${change.gitignore} ignores it.`,
430
- hint:
431
- "From the next release chant audit reads every .gitignore between a file and the scan root, and TF023 will no longer report this path.",
432
- });
433
- }
434
- return warnings;
439
+ /** The flag that raises the local walk's file limit, named wherever a truncated scan is reported. */
440
+ export const MAX_FILES_FLAG = "--max-files";
441
+
442
+ /** How a report states that the local walk stopped at its limit (#2528). JSON carries it as `truncated`. */
443
+ export interface AuditTruncation {
444
+ limit: number;
445
+ flag: typeof MAX_FILES_FLAG;
446
+ }
447
+
448
+ /** The truncation a walk reports, or undefined for a whole scan. */
449
+ export function walkTruncation(walk: CandidateWalk): AuditTruncation | undefined {
450
+ return walk.truncated ? { limit: walk.maxFiles, flag: MAX_FILES_FLAG } : undefined;
451
+ }
452
+
453
+ /** The text-report note for a truncated scan. */
454
+ export function truncationNote(t: AuditTruncation, target: string): string {
455
+ return (
456
+ `The scan of ${target} stopped at ${t.limit} files, so part of the tree was not audited. ` +
457
+ `The limit counts every file the walk reaches, not only the ones it audits. Raise it with ${t.flag} <n>.`
458
+ );
435
459
  }
436
460
 
437
461
  /** Run the audit and produce a rendered result. */
@@ -441,6 +465,8 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
441
465
  const failOn = options.failOn ?? "none";
442
466
 
443
467
  const isUrl = /^https?:\/\//.test(options.path);
468
+ // chant#2589 — path lexicons the target declares, read without running its config.
469
+ const pathLexicons = auditPathLexicons(options.path, isUrl);
444
470
 
445
471
  // Detection lives in the lexicon plugins (each one's `detectTemplate`), so a
446
472
  // lexicon that isn't installed can't claim its files. Every branch below
@@ -449,7 +475,7 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
449
475
  // render the same way (#1623).
450
476
  const plugins = options.plugins ?? (await loadAuditPlugins());
451
477
  let candidates: RepoFile[];
452
- let warnings: AuditWarning[] = [];
478
+ let truncated: AuditTruncation | undefined;
453
479
  if (isUrl) {
454
480
  try {
455
481
  // Fetch the whole repo's candidate files (all lexicons, not just CI) and
@@ -469,20 +495,18 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
469
495
  // One walk, plugin-delegated detection. CI (path), Dockerfiles (name), and
470
496
  // Helm charts (bundle) are special-cased by the classifier since content
471
497
  // shape alone can't disambiguate them.
472
- const walk = walkCandidates(options.path, { maxFiles: options.maxFiles });
498
+ // Workspace members leave the root's audit only when a declaration exists
499
+ // (#2525 rule 3); without one this is a few existsSync calls (#2527).
500
+ const excludeDirs = await workspaceMemberDirs(options.path);
501
+ const walk = walkCandidates(options.path, { maxFiles: options.maxFiles, excludeDirs });
473
502
  candidates = walk.files;
474
- warnings = walkWarnings(walk, options.path);
475
- // #2527's warning release: the converged walk skips git-ignored files and,
476
- // inside a project, child projects. Terraform state paths are TF023's own
477
- // question and stay out of it. `candidates` is unchanged.
478
- await warnDiscoveryChanges({
479
- walker: "audit",
480
- root: options.path,
481
- files: candidates.filter((f) => !isTerraformStatePath(f.path)).map((f) => resolve(options.path, f.path)),
482
- });
503
+ truncated = walkTruncation(walk);
483
504
  }
484
- // Only a non-empty list rides on the result, so a run with nothing to warn
485
- // about returns exactly the object it did before #2528.
505
+ // Every format but SARIF states a truncated scan in the report itself
506
+ // (#2528). SARIF has no slot chant fills for it, so there it goes to stderr.
507
+ const warnings: AuditWarning[] =
508
+ truncated && format === "sarif" ? [{ message: truncationNote(truncated, options.path) }] : [];
509
+ if (pathLexicons.notice !== undefined) warnings.push({ message: pathLexicons.notice });
486
510
  const withWarnings = warnings.length > 0 ? { warnings } : {};
487
511
  // A local target hands its directory to the classifier, so an input whose
488
512
  // path names a directory carries the directory itself (#2217): terraform's
@@ -514,14 +538,18 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
514
538
  const terraformStateFindings = auditTerraformState(candidates);
515
539
 
516
540
  if (plugins.length === 0) {
517
- const output = format === "json" ? renderNoLexiconsJson(options.path, unclaimed) : renderNoLexicons(options.path, unclaimed);
541
+ const output =
542
+ format === "json"
543
+ ? renderNoLexiconsJson(options.path, unclaimed, pathLexicons.paths, truncated)
544
+ : renderNoLexicons(options.path, unclaimed, pathLexicons.paths, truncated);
518
545
  return { success: true, status: "no-lexicons", output, findings: [...secretsFindings, ...wranglerFindings, ...nginxFindings, ...terraformStateFindings], scanned: [], unclaimed, exitCode: NO_LEXICONS_EXIT_CODE, stream: format === "json" ? "stdout" : "stderr", ...withWarnings };
519
546
  }
520
547
 
521
- const missingLexiconNote = missingLexiconHint(unclaimed);
548
+ const missingLexiconNote = missingLexiconHint(unclaimed, pathLexicons.paths);
522
549
 
523
550
  if (inputs.length === 0 && secretsFindings.length === 0 && wranglerFindings.length === 0 && nginxFindings.length === 0 && terraformStateFindings.length === 0) {
524
- const output = `No auditable files found under ${options.path}.${missingLexiconNote ? ` ${missingLexiconNote}` : ""}`;
551
+ let output = `No auditable files found under ${options.path}.${missingLexiconNote ? ` ${missingLexiconNote}` : ""}`;
552
+ if (truncated) output += `\n\nNote: ${truncationNote(truncated, options.path)}`;
525
553
  return { success: true, status: "ok", output, findings: [], scanned: [], unclaimed, exitCode: 0, ...withWarnings };
526
554
  }
527
555
 
@@ -548,6 +576,7 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
548
576
 
549
577
  if (tier === "merge-worthy") findings = findings.filter((f) => isMergeWorthy(f, catalog));
550
578
  const notes = coverageNotes(inputs);
579
+ if (truncated) notes.unshift(truncationNote(truncated, options.path));
551
580
  if (missingLexiconNote) notes.push(missingLexiconNote);
552
581
  if (suppressionStats.count > 0) {
553
582
  notes.push(
@@ -596,7 +625,7 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
596
625
  switch (format) {
597
626
  case "json": {
598
627
  const snapshot = await buildSnapshot(options, scanned, isUrl);
599
- output = JSON.stringify(buildReportJson(findings, { snapshot, toolVersion: options.toolVersion, catalog, unclaimed }), null, 2);
628
+ output = JSON.stringify(buildReportJson(findings, { snapshot, toolVersion: options.toolVersion, catalog, unclaimed, truncated }), null, 2);
600
629
  break;
601
630
  }
602
631
  case "sarif":
@@ -607,7 +636,7 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
607
636
  break;
608
637
  case "html": {
609
638
  const snapshot = await buildSnapshot(options, scanned, isUrl);
610
- output = renderHtml(findings, { files, resolveSha, resolveDigest, notes, snapshot, theme: options.theme, template: options.template, catalog });
639
+ output = renderHtml(findings, { files, resolveSha, resolveDigest, notes, snapshot, theme: options.theme, template: options.template, catalog, truncated });
611
640
  break;
612
641
  }
613
642
  default:
@@ -106,6 +106,28 @@ export async function coverageReportCheck(plugin: LexiconPlugin | undefined): Pr
106
106
  };
107
107
  }
108
108
 
109
+ /**
110
+ * #2535 — a lexicon that supplies member kinds publishes them as data at a
111
+ * `./workspace-kinds` subpath (ws-031). The file must be JSON, validate as
112
+ * kind data, and ship in the package. A lexicon without the subpath passes
113
+ * vacuously, like `coverageReport()` above. The workspace module loads only
114
+ * here, when the check runs.
115
+ */
116
+ export async function workspaceKindsCheck(dir: string): Promise<CheckItem> {
117
+ const { readPackageKinds, kindsFileShips } = await import("../../workspace/kinds");
118
+ const read = readPackageKinds(dir);
119
+ const problems = [...read.problems];
120
+ if (read.file && problems.length === 0 && !kindsFileShips(dir, read.file)) {
121
+ problems.push(`${basename(read.file)} is not covered by package.json "files", so the published package would not carry it`);
122
+ }
123
+ return {
124
+ name: "Any ./workspace-kinds subpath holds valid kind data",
125
+ tier: 1,
126
+ pass: problems.length === 0,
127
+ detail: problems.length > 0 ? problems.join("; ") : read.file ? `${read.kinds.length} kind(s): ${read.kinds.map((k) => k.name).join(", ")}` : undefined,
128
+ };
129
+ }
130
+
109
131
  // ── Check runner ─────────────────────────────────────────────────────
110
132
 
111
133
  /**
@@ -434,6 +456,8 @@ export async function checkLexicon(dir: string): Promise<CheckResult> {
434
456
  // now states the fact directly via `coverageReport()`.
435
457
  items.push(await coverageReportCheck(plugin));
436
458
 
459
+ items.push(await workspaceKindsCheck(dir));
460
+
437
461
  // ── Tier 2: Recommended ────────────────────────────────────────
438
462
 
439
463
  const pluginContent = readOr(join(dir, "src/plugin.ts"));
@@ -396,3 +396,33 @@ describe("package-type-module (#1421)", () => {
396
396
  });
397
397
  });
398
398
  });
399
+
400
+ describe("doctorCommand in a declared workspace (#2535)", () => {
401
+ test("shows a member of kind other as a warning, and adds nothing outside a workspace", async () => {
402
+ await withTestDir(async (testDir) => {
403
+ mkdirSync(join(testDir, ".git"));
404
+ mkdirSync(join(testDir, "app"));
405
+ mkdirSync(join(testDir, "docs"));
406
+ writeFileSync(join(testDir, "app", "chant.config.json"), "{}");
407
+ const before = await doctorCommand(join(testDir, "app"));
408
+ expect(before.checks.filter((c) => c.name.startsWith("workspace-"))).toEqual([]);
409
+
410
+ writeFileSync(
411
+ join(testDir, "chant.workspace.json"),
412
+ JSON.stringify({
413
+ name: "acme",
414
+ schema: 1,
415
+ members: [
416
+ { name: "app", dir: "app", kind: "chant" },
417
+ { name: "docs", dir: "docs", kind: "other", because: "a static site" },
418
+ ],
419
+ }),
420
+ );
421
+ const report = await doctorCommand(join(testDir, "app"));
422
+ const workspace = report.checks.filter((c) => c.name.startsWith("workspace-"));
423
+ expect(workspace).toEqual([
424
+ { name: "workspace-WSP009", status: "warn", message: expect.stringMatching(/^member docs \(docs\) is kind other, which chant does not read: a static site \(.*chant\.workspace\.json:1\)$/) },
425
+ ]);
426
+ });
427
+ });
428
+ });
@@ -8,6 +8,7 @@ import { loadPlugins, resolveProjectLexicons } from "../plugins";
8
8
  import { MCP_CONFIG_FILENAME, MCP_SETUP_COMMAND, mcpConfigPath } from "../mcp-config";
9
9
  import { loadCapabilityPlugin } from "../../components/capability-plugin-loader";
10
10
  import { isCapabilityPlugin } from "../../components/capability-plugin";
11
+ import { findWorkspaceRoot } from "../../project-root";
11
12
 
12
13
  export interface DoctorCheck {
13
14
  name: string;
@@ -322,6 +323,23 @@ export async function doctorCommand(path: string): Promise<DoctorReport> {
322
323
  }
323
324
  }
324
325
 
326
+ // #2535: in a declared workspace, doctor shows the declaration checks, so
327
+ // a member of kind other shows as a warning. The walk up only tests
328
+ // whether files exist; nothing under workspace/ loads without a
329
+ // declaration, so level 0 is unchanged (#2525).
330
+ const workspace = findWorkspaceRoot(projectPath);
331
+ if (workspace) {
332
+ const { runDeclarationChecks } = await import("../../workspace/checks");
333
+ // The declaration and kind checks only: `chant workspace check` runs the
334
+ // member checks, which read every member's files.
335
+ const report = await runDeclarationChecks(workspace.dir, (file) => join(workspace.dir, file), { gather: false });
336
+ const shown = report.diagnostics.filter((d) => d.severity !== "info");
337
+ for (const d of shown) {
338
+ checks.push({ name: `workspace-${d.ruleId}`, status: d.severity === "error" ? "fail" : "warn", message: `${d.message} (${d.file}:${d.line})` });
339
+ }
340
+ if (shown.length === 0) checks.push({ name: "workspace-declaration", status: "pass", message: report.file });
341
+ }
342
+
325
343
  return {
326
344
  checks,
327
345
  success: checks.every(c => c.status !== "fail"),
@@ -23,7 +23,7 @@ import { scanAgentConfigs } from "../../agents/discover";
23
23
  import type { AgentRuntime, AgentScope } from "../../agents/types";
24
24
  import type { AgentImportOutcome } from "../../agents/importer";
25
25
  import type { LexiconPlugin } from "../../lexicon";
26
- import { loadPlugin, recordProjectLexicons } from "../plugins";
26
+ import { loadPlugin, recordProjectLexicons, unknownPathLexiconsNotice } from "../plugins";
27
27
  import { lexiconModulePath, lexiconSourceLabel } from "../../lexicon-module";
28
28
 
29
29
  /** The lexicon used when `--lexicon` isn't given: the one that models agent workloads. */
@@ -94,7 +94,11 @@ export async function importAgentsCommand(opts: ImportAgentsOptions = {}): Promi
94
94
 
95
95
  // chant#2578 — a lexicon the project's chant.config declares by path loads
96
96
  // from that module, so read the declarations before loading the plugin.
97
- if (!opts.pluginLoader) await recordProjectLexicons(process.cwd());
97
+ // chant#2591 — read without running chant.config.ts.
98
+ if (!opts.pluginLoader) {
99
+ const notice = unknownPathLexiconsNotice(recordProjectLexicons(process.cwd()));
100
+ if (notice !== undefined) warnings.push(notice);
101
+ }
98
102
 
99
103
  let plugin: LexiconPlugin;
100
104
  try {
@@ -3,7 +3,7 @@ import { join, resolve, dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { createInterface } from "readline";
5
5
  import { formatSuccess, formatWarning } from "../format";
6
- import { loadPlugin, recordProjectLexicons } from "../plugins";
6
+ import { loadPlugin, recordProjectLexicons, unknownPathLexiconsNotice } from "../plugins";
7
7
  import { lexiconModulePath, lexiconSourceLabel } from "../../lexicon-module";
8
8
  import { MCP_CONFIG_FILENAME, detectPackageManager, generateMcpConfig, mcpConfigPath } from "../mcp-config";
9
9
 
@@ -266,10 +266,13 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
266
266
 
267
267
  // chant#2578 — `--force` into an existing project keeps its chant.config.
268
268
  // When that config declares the lexicon by path, it loads from the module
269
- // and there is no package to depend on or install.
269
+ // and there is no package to depend on or install. chant#2591 — the config
270
+ // is read without running it; when that fails, init says so and treats the
271
+ // lexicon as a package.
270
272
  let lexiconModule: string | undefined;
271
273
  if (existsSync(join(targetDir, "chant.config.ts")) || existsSync(join(targetDir, "chant.config.json"))) {
272
- await recordProjectLexicons(targetDir);
274
+ const notice = unknownPathLexiconsNotice(recordProjectLexicons(targetDir));
275
+ if (notice !== undefined) warnings.push(notice);
273
276
  if (lexiconModulePath(options.lexicon) !== undefined) {
274
277
  lexiconModule = lexiconSourceLabel(options.lexicon, targetDir);
275
278
  }
@@ -447,6 +450,22 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
447
450
  // Skills are optional — don't fail init if plugin isn't installed yet
448
451
  }
449
452
 
453
+ // `--template` records where the files came from (#2540, ws-047). Plain
454
+ // init writes no lock and never loads the lineage module.
455
+ if (options.template && templateSet) {
456
+ const { writeTemplateLock } = await import("../../workspace/lineage-init");
457
+ const lock = writeTemplateLock({
458
+ targetDir,
459
+ lexicon: options.lexicon,
460
+ template: options.template,
461
+ createdFiles,
462
+ chantVersion: getChantVersion(),
463
+ lexiconModule,
464
+ });
465
+ if (lock) createdFiles.push(lock);
466
+ else warnings.push(".chant/workspace.lock.json already exists, skipping");
467
+ }
468
+
450
469
  return {
451
470
  success: true,
452
471
  createdFiles,