@intentius/chant 0.46.0 → 0.50.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 (377) hide show
  1. package/dist/audit/catalog.d.ts +13 -3
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +30 -3
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +9 -2
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/fetch.d.ts.map +1 -1
  8. package/dist/audit/report-html.d.ts.map +1 -1
  9. package/dist/audit/report-model.d.ts +6 -0
  10. package/dist/audit/report-model.d.ts.map +1 -1
  11. package/dist/audit/report.d.ts.map +1 -1
  12. package/dist/audit/rules-doc.d.ts.map +1 -1
  13. package/dist/audit/secrets.d.ts +95 -0
  14. package/dist/audit/secrets.d.ts.map +1 -0
  15. package/dist/audit/wrangler.d.ts +33 -0
  16. package/dist/audit/wrangler.d.ts.map +1 -0
  17. package/dist/build.d.ts +3 -3
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/cli/commands/audit.d.ts +7 -0
  20. package/dist/cli/commands/audit.d.ts.map +1 -1
  21. package/dist/cli/commands/build.d.ts +23 -0
  22. package/dist/cli/commands/build.d.ts.map +1 -1
  23. package/dist/cli/commands/lint.d.ts.map +1 -1
  24. package/dist/cli/handlers/build.d.ts.map +1 -1
  25. package/dist/cli/handlers/components.d.ts +31 -0
  26. package/dist/cli/handlers/components.d.ts.map +1 -1
  27. package/dist/cli/handlers/lifecycle.d.ts +12 -1
  28. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  29. package/dist/cli/handlers/operator.d.ts +32 -0
  30. package/dist/cli/handlers/operator.d.ts.map +1 -0
  31. package/dist/cli/handlers/scenario.d.ts +39 -0
  32. package/dist/cli/handlers/scenario.d.ts.map +1 -0
  33. package/dist/cli/main.d.ts.map +1 -1
  34. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  35. package/dist/cli/mcp/server.d.ts +35 -2
  36. package/dist/cli/mcp/server.d.ts.map +1 -1
  37. package/dist/cli/mcp/tools/explain.d.ts +6 -0
  38. package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
  39. package/dist/cli/mcp/types.d.ts +29 -1
  40. package/dist/cli/mcp/types.d.ts.map +1 -1
  41. package/dist/cli/plugins.d.ts +1 -1
  42. package/dist/cli/plugins.d.ts.map +1 -1
  43. package/dist/cli/registry.d.ts +14 -2
  44. package/dist/cli/registry.d.ts.map +1 -1
  45. package/dist/cli/reporters/stylish.d.ts +15 -1
  46. package/dist/cli/reporters/stylish.d.ts.map +1 -1
  47. package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
  48. package/dist/components/auto-release.d.ts +4 -0
  49. package/dist/components/auto-release.d.ts.map +1 -1
  50. package/dist/components/capability.d.ts +17 -2
  51. package/dist/components/capability.d.ts.map +1 -1
  52. package/dist/components/cli-support.d.ts +7 -0
  53. package/dist/components/cli-support.d.ts.map +1 -1
  54. package/dist/components/component.d.ts +15 -0
  55. package/dist/components/component.d.ts.map +1 -1
  56. package/dist/components/driver.d.ts.map +1 -1
  57. package/dist/components/starter-plugin.d.ts +2 -0
  58. package/dist/components/starter-plugin.d.ts.map +1 -1
  59. package/dist/components/verbs/ensure-secret.d.ts +50 -0
  60. package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
  61. package/dist/components/verbs/index.d.ts +13 -0
  62. package/dist/components/verbs/index.d.ts.map +1 -1
  63. package/dist/components/verbs/r2-sync.d.ts +76 -0
  64. package/dist/components/verbs/r2-sync.d.ts.map +1 -0
  65. package/dist/components/verbs/run-agent.d.ts +499 -0
  66. package/dist/components/verbs/run-agent.d.ts.map +1 -0
  67. package/dist/components/verbs/sign.d.ts +30 -0
  68. package/dist/components/verbs/sign.d.ts.map +1 -1
  69. package/dist/components/verbs/wrangler.d.ts +108 -0
  70. package/dist/components/verbs/wrangler.d.ts.map +1 -0
  71. package/dist/composite.d.ts +6 -1
  72. package/dist/composite.d.ts.map +1 -1
  73. package/dist/config.d.ts +26 -0
  74. package/dist/config.d.ts.map +1 -1
  75. package/dist/deep-observation.d.ts +14 -0
  76. package/dist/deep-observation.d.ts.map +1 -1
  77. package/dist/discovery/collect.d.ts.map +1 -1
  78. package/dist/discovery/fold-import.d.ts +15 -1
  79. package/dist/discovery/fold-import.d.ts.map +1 -1
  80. package/dist/discovery/fold-rank.d.ts +66 -0
  81. package/dist/discovery/fold-rank.d.ts.map +1 -0
  82. package/dist/discovery/index.d.ts +15 -0
  83. package/dist/discovery/index.d.ts.map +1 -1
  84. package/dist/discovery/param-deps.d.ts +17 -0
  85. package/dist/discovery/param-deps.d.ts.map +1 -0
  86. package/dist/effect-receipt.d.ts +177 -0
  87. package/dist/effect-receipt.d.ts.map +1 -0
  88. package/dist/fold/fold.d.ts +55 -2
  89. package/dist/fold/fold.d.ts.map +1 -1
  90. package/dist/fold/subset.d.ts +20 -0
  91. package/dist/fold/subset.d.ts.map +1 -1
  92. package/dist/index.d.ts +4 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/lexicon-schema.d.ts +2 -0
  95. package/dist/lexicon-schema.d.ts.map +1 -1
  96. package/dist/lexicon.d.ts +137 -3
  97. package/dist/lexicon.d.ts.map +1 -1
  98. package/dist/lifecycle/change-set.d.ts +33 -5
  99. package/dist/lifecycle/change-set.d.ts.map +1 -1
  100. package/dist/lifecycle/converge-ledger.d.ts +90 -0
  101. package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
  102. package/dist/lifecycle/deep-diff.d.ts +18 -0
  103. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  104. package/dist/lifecycle/deep-observe.d.ts +9 -1
  105. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  106. package/dist/lifecycle/gate-ledger.d.ts +33 -0
  107. package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
  108. package/dist/lifecycle/git.d.ts +145 -21
  109. package/dist/lifecycle/git.d.ts.map +1 -1
  110. package/dist/lifecycle/index.d.ts +6 -0
  111. package/dist/lifecycle/index.d.ts.map +1 -1
  112. package/dist/lifecycle/lease.d.ts +113 -0
  113. package/dist/lifecycle/lease.d.ts.map +1 -0
  114. package/dist/lifecycle/observation-baseline.d.ts +21 -3
  115. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  116. package/dist/lifecycle/receipt-plan.d.ts +62 -0
  117. package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
  118. package/dist/lifecycle/release-ledger.d.ts +20 -0
  119. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  120. package/dist/lifecycle/scenario-eval.d.ts +42 -0
  121. package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
  122. package/dist/lifecycle/scenario.d.ts +163 -0
  123. package/dist/lifecycle/scenario.d.ts.map +1 -0
  124. package/dist/lifecycle/symptoms.d.ts +63 -0
  125. package/dist/lifecycle/symptoms.d.ts.map +1 -0
  126. package/dist/lifecycle/teardown.d.ts +6 -4
  127. package/dist/lifecycle/teardown.d.ts.map +1 -1
  128. package/dist/lifecycle/unobserved-gate.d.ts +67 -0
  129. package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
  130. package/dist/lint/knowledge-checks.d.ts +48 -0
  131. package/dist/lint/knowledge-checks.d.ts.map +1 -0
  132. package/dist/lint/output-checks.d.ts +5 -0
  133. package/dist/lint/output-checks.d.ts.map +1 -0
  134. package/dist/lint/output-docs.d.ts +94 -0
  135. package/dist/lint/output-docs.d.ts.map +1 -0
  136. package/dist/lint/pipeline-change-gate.d.ts +101 -0
  137. package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
  138. package/dist/lint/post-synth.d.ts +41 -0
  139. package/dist/lint/post-synth.d.ts.map +1 -1
  140. package/dist/lint/receipt-checks.d.ts +9 -0
  141. package/dist/lint/receipt-checks.d.ts.map +1 -0
  142. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
  143. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
  144. package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
  145. package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
  146. package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
  147. package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
  148. package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
  149. package/dist/lint/rules/index.d.ts +3 -1
  150. package/dist/lint/rules/index.d.ts.map +1 -1
  151. package/dist/lsp/lexicon-providers.d.ts +7 -0
  152. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  153. package/dist/okf-read.d.ts +78 -0
  154. package/dist/okf-read.d.ts.map +1 -0
  155. package/dist/op/activity-contract.d.ts +139 -0
  156. package/dist/op/activity-contract.d.ts.map +1 -0
  157. package/dist/op/builders.d.ts +140 -3
  158. package/dist/op/builders.d.ts.map +1 -1
  159. package/dist/op/converge-rule.d.ts +161 -0
  160. package/dist/op/converge-rule.d.ts.map +1 -0
  161. package/dist/op/generate-pipeline.d.ts +39 -0
  162. package/dist/op/generate-pipeline.d.ts.map +1 -0
  163. package/dist/op/index.d.ts +18 -2
  164. package/dist/op/index.d.ts.map +1 -1
  165. package/dist/op/local-executor.d.ts +2 -1
  166. package/dist/op/local-executor.d.ts.map +1 -1
  167. package/dist/op/op-verb-class.d.ts +42 -0
  168. package/dist/op/op-verb-class.d.ts.map +1 -0
  169. package/dist/op/operator.d.ts +128 -0
  170. package/dist/op/operator.d.ts.map +1 -0
  171. package/dist/op/receipt-store.d.ts +138 -0
  172. package/dist/op/receipt-store.d.ts.map +1 -0
  173. package/dist/op/step-output-ref.d.ts +187 -0
  174. package/dist/op/step-output-ref.d.ts.map +1 -0
  175. package/dist/op/types.d.ts +49 -2
  176. package/dist/op/types.d.ts.map +1 -1
  177. package/dist/provenance.d.ts +73 -3
  178. package/dist/provenance.d.ts.map +1 -1
  179. package/dist/runtime-adapter.d.ts +7 -1
  180. package/dist/runtime-adapter.d.ts.map +1 -1
  181. package/dist/secret-materialization.d.ts +138 -0
  182. package/dist/secret-materialization.d.ts.map +1 -0
  183. package/dist/secret-provenance.d.ts +218 -0
  184. package/dist/secret-provenance.d.ts.map +1 -0
  185. package/dist/serializer.d.ts +29 -0
  186. package/dist/serializer.d.ts.map +1 -1
  187. package/dist/toml.d.ts +40 -5
  188. package/dist/toml.d.ts.map +1 -1
  189. package/dist/yaml.d.ts.map +1 -1
  190. package/package.json +4 -1
  191. package/src/audit/catalog.test.ts +1 -1
  192. package/src/audit/catalog.ts +75 -3
  193. package/src/audit/core.test.ts +57 -0
  194. package/src/audit/core.ts +0 -0
  195. package/src/audit/detect-bundle.test.ts +1 -1
  196. package/src/audit/discover.test.ts +24 -0
  197. package/src/audit/discover.ts +40 -4
  198. package/src/audit/fetch.test.ts +216 -3
  199. package/src/audit/fetch.ts +270 -59
  200. package/src/audit/report-html.ts +5 -2
  201. package/src/audit/report-model.ts +9 -0
  202. package/src/audit/report.test.ts +22 -0
  203. package/src/audit/report.ts +3 -2
  204. package/src/audit/rules-doc.ts +13 -1
  205. package/src/audit/secrets.test.ts +303 -0
  206. package/src/audit/secrets.ts +406 -0
  207. package/src/audit/wrangler.test.ts +230 -0
  208. package/src/audit/wrangler.ts +290 -0
  209. package/src/build.test.ts +41 -0
  210. package/src/build.ts +39 -6
  211. package/src/cli/command-group.ts +1 -1
  212. package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
  213. package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
  214. package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
  215. package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
  216. package/src/cli/commands/audit.test.ts +268 -1
  217. package/src/cli/commands/audit.ts +87 -18
  218. package/src/cli/commands/build.test.ts +245 -0
  219. package/src/cli/commands/build.ts +210 -21
  220. package/src/cli/commands/lint.ts +15 -3
  221. package/src/cli/handlers/build.ts +2 -0
  222. package/src/cli/handlers/components.test.ts +199 -1
  223. package/src/cli/handlers/components.ts +160 -3
  224. package/src/cli/handlers/explain.test.ts +70 -1
  225. package/src/cli/handlers/graph.test.ts +20 -0
  226. package/src/cli/handlers/graph.ts +12 -3
  227. package/src/cli/handlers/lifecycle.test.ts +115 -1
  228. package/src/cli/handlers/lifecycle.ts +96 -15
  229. package/src/cli/handlers/operator.test.ts +255 -0
  230. package/src/cli/handlers/operator.ts +240 -0
  231. package/src/cli/handlers/scenario.test.ts +456 -0
  232. package/src/cli/handlers/scenario.ts +330 -0
  233. package/src/cli/main.test.ts +23 -0
  234. package/src/cli/main.ts +72 -1
  235. package/src/cli/mcp/resource-handlers.ts +38 -1
  236. package/src/cli/mcp/server.test.ts +323 -3
  237. package/src/cli/mcp/server.ts +84 -7
  238. package/src/cli/mcp/tools/explain.ts +51 -2
  239. package/src/cli/mcp/types.ts +27 -1
  240. package/src/cli/plugins.ts +4 -2
  241. package/src/cli/registry.ts +14 -2
  242. package/src/cli/reporters/stylish.test.ts +154 -0
  243. package/src/cli/reporters/stylish.ts +154 -33
  244. package/src/codegen/docs-rule-scanning.test.ts +42 -0
  245. package/src/codegen/docs-rule-scanning.ts +25 -2
  246. package/src/components/README.md +7 -0
  247. package/src/components/auto-release.ts +6 -0
  248. package/src/components/capability.ts +17 -2
  249. package/src/components/cli-support.test.ts +17 -0
  250. package/src/components/cli-support.ts +13 -1
  251. package/src/components/component-schema.test.ts +32 -0
  252. package/src/components/component.schema.json +6 -0
  253. package/src/components/component.test.ts +21 -0
  254. package/src/components/component.ts +15 -0
  255. package/src/components/driver.ts +12 -4
  256. package/src/components/registry.test.ts +7 -2
  257. package/src/components/starter-plugin.ts +17 -0
  258. package/src/components/verbs/ensure-secret.test.ts +130 -0
  259. package/src/components/verbs/ensure-secret.ts +79 -0
  260. package/src/components/verbs/index.ts +13 -0
  261. package/src/components/verbs/r2-sync.test.ts +107 -0
  262. package/src/components/verbs/r2-sync.ts +124 -0
  263. package/src/components/verbs/run-agent.test.ts +683 -0
  264. package/src/components/verbs/run-agent.ts +786 -0
  265. package/src/components/verbs/sign.test.ts +19 -0
  266. package/src/components/verbs/sign.ts +34 -2
  267. package/src/components/verbs/wrangler.test.ts +170 -0
  268. package/src/components/verbs/wrangler.ts +241 -0
  269. package/src/composite.ts +31 -2
  270. package/src/config.test.ts +15 -0
  271. package/src/config.ts +30 -0
  272. package/src/deep-observation.test.ts +19 -0
  273. package/src/deep-observation.ts +17 -0
  274. package/src/discovery/collect.ts +11 -2
  275. package/src/discovery/fold-import.test.ts +54 -0
  276. package/src/discovery/fold-import.ts +178 -38
  277. package/src/discovery/fold-rank.test.ts +197 -0
  278. package/src/discovery/fold-rank.ts +346 -0
  279. package/src/discovery/index.ts +16 -1
  280. package/src/discovery/param-deps.test.ts +118 -0
  281. package/src/discovery/param-deps.ts +170 -0
  282. package/src/effect-receipt-exclusion.test.ts +190 -0
  283. package/src/effect-receipt.test.ts +419 -0
  284. package/src/effect-receipt.ts +412 -0
  285. package/src/fold/fold.test.ts +6 -2
  286. package/src/fold/fold.ts +184 -3
  287. package/src/fold/subset.test.ts +95 -6
  288. package/src/fold/subset.ts +66 -2
  289. package/src/index.ts +4 -0
  290. package/src/lexicon-schema.ts +3 -0
  291. package/src/lexicon.ts +151 -5
  292. package/src/lifecycle/change-set.ts +46 -7
  293. package/src/lifecycle/converge-ledger.test.ts +199 -0
  294. package/src/lifecycle/converge-ledger.ts +179 -0
  295. package/src/lifecycle/deep-diff.test.ts +79 -1
  296. package/src/lifecycle/deep-diff.ts +23 -0
  297. package/src/lifecycle/deep-observe.ts +13 -2
  298. package/src/lifecycle/gate-ledger.test.ts +103 -0
  299. package/src/lifecycle/gate-ledger.ts +140 -0
  300. package/src/lifecycle/git.test.ts +430 -0
  301. package/src/lifecycle/git.ts +446 -84
  302. package/src/lifecycle/index.ts +6 -0
  303. package/src/lifecycle/lease.test.ts +343 -0
  304. package/src/lifecycle/lease.ts +270 -0
  305. package/src/lifecycle/observation-baseline.test.ts +46 -0
  306. package/src/lifecycle/observation-baseline.ts +33 -1
  307. package/src/lifecycle/receipt-plan.test.ts +250 -0
  308. package/src/lifecycle/receipt-plan.ts +249 -0
  309. package/src/lifecycle/release-ledger.ts +20 -0
  310. package/src/lifecycle/scenario-eval.test.ts +199 -0
  311. package/src/lifecycle/scenario-eval.ts +158 -0
  312. package/src/lifecycle/scenario.test.ts +195 -0
  313. package/src/lifecycle/scenario.ts +321 -0
  314. package/src/lifecycle/symptoms.test.ts +116 -0
  315. package/src/lifecycle/symptoms.ts +126 -0
  316. package/src/lifecycle/teardown.test.ts +31 -0
  317. package/src/lifecycle/teardown.ts +6 -4
  318. package/src/lifecycle/unobserved-gate.test.ts +109 -0
  319. package/src/lifecycle/unobserved-gate.ts +102 -0
  320. package/src/lint/knowledge-checks.test.ts +80 -0
  321. package/src/lint/knowledge-checks.ts +74 -0
  322. package/src/lint/output-checks.test.ts +85 -0
  323. package/src/lint/output-checks.ts +99 -0
  324. package/src/lint/output-docs.test.ts +220 -0
  325. package/src/lint/output-docs.ts +204 -0
  326. package/src/lint/pipeline-change-gate.test.ts +144 -0
  327. package/src/lint/pipeline-change-gate.ts +153 -0
  328. package/src/lint/post-synth.test.ts +97 -0
  329. package/src/lint/post-synth.ts +60 -0
  330. package/src/lint/receipt-checks.test.ts +101 -0
  331. package/src/lint/receipt-checks.ts +93 -0
  332. package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
  333. package/src/lint/rules/comp/comp.test.ts +49 -1
  334. package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
  335. package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
  336. package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
  337. package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
  338. package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
  339. package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
  340. package/src/lint/rules/index.ts +7 -1
  341. package/src/lsp/lexicon-providers.test.ts +44 -0
  342. package/src/lsp/lexicon-providers.ts +11 -1
  343. package/src/okf-read.test.ts +149 -0
  344. package/src/okf-read.ts +197 -0
  345. package/src/op/activity-contract.test.ts +180 -0
  346. package/src/op/activity-contract.ts +278 -0
  347. package/src/op/builders-exports.test.ts +17 -1
  348. package/src/op/builders.ts +198 -6
  349. package/src/op/converge-rule.test.ts +179 -0
  350. package/src/op/converge-rule.ts +311 -0
  351. package/src/op/effect-step.test.ts +311 -0
  352. package/src/op/generate-pipeline.test.ts +53 -0
  353. package/src/op/generate-pipeline.ts +99 -0
  354. package/src/op/index.ts +40 -3
  355. package/src/op/local-executor.test.ts +92 -0
  356. package/src/op/local-executor.ts +212 -29
  357. package/src/op/op-verb-class.test.ts +126 -0
  358. package/src/op/op-verb-class.ts +115 -0
  359. package/src/op/op.test.ts +25 -2
  360. package/src/op/operator.test.ts +346 -0
  361. package/src/op/operator.ts +213 -0
  362. package/src/op/receipt-store.ts +211 -0
  363. package/src/op/step-output-ref.test.ts +334 -0
  364. package/src/op/step-output-ref.ts +453 -0
  365. package/src/op/types.ts +51 -2
  366. package/src/provenance.test.ts +151 -4
  367. package/src/provenance.ts +118 -4
  368. package/src/runtime-adapter.ts +31 -10
  369. package/src/secret-materialization.test.ts +199 -0
  370. package/src/secret-materialization.ts +235 -0
  371. package/src/secret-provenance.test.ts +388 -0
  372. package/src/secret-provenance.ts +475 -0
  373. package/src/serializer.ts +30 -0
  374. package/src/toml.test.ts +157 -384
  375. package/src/toml.ts +371 -5
  376. package/src/yaml.test.ts +88 -0
  377. package/src/yaml.ts +76 -6
@@ -184,6 +184,71 @@ export const testEntity = {
184
184
  }
185
185
  });
186
186
 
187
+ test("#1083 — --fold-rank ranks blockers and, given a path, writes a collapsed-format export", async () => {
188
+ const thisDir = dirname(fileURLToPath(import.meta.url));
189
+ const runtimePath = resolvePath(thisDir, "../../runtime");
190
+
191
+ // A pair of files that fold fine (same shape as the #1022 test above),
192
+ // so the build produces output and the "no output" guard doesn't fire —
193
+ // unrelated to the ranking below.
194
+ await writeFile(
195
+ join(testDir, "resources.ts"),
196
+ `
197
+ import { createResource } from ${JSON.stringify(runtimePath)};
198
+ export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
199
+ `,
200
+ );
201
+ await writeFile(
202
+ join(testDir, "main.ts"),
203
+ `
204
+ import { Bucket } from "./resources";
205
+ export const bucket = new Bucket({ name: "my-bucket" });
206
+ `,
207
+ );
208
+ // a.ts fails directly (process.env); b.ts fails because it imports a.ts.
209
+ await writeFile(join(testDir, "a.ts"), `export const a = process.env.SOMETHING;`);
210
+ await writeFile(
211
+ join(testDir, "b.ts"),
212
+ `
213
+ import { a } from "./a";
214
+ export const b = a;
215
+ `,
216
+ );
217
+
218
+ const awsSerializer: Serializer = {
219
+ name: "aws",
220
+ rulePrefix: "TEST",
221
+ serialize: (entities) => JSON.stringify([...entities.keys()]),
222
+ };
223
+
224
+ const collapsedPath = join(testDir, "out", "fold.collapsed");
225
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
226
+ try {
227
+ const result = await buildCommand({
228
+ path: testDir,
229
+ format: "json",
230
+ serializers: [awsSerializer],
231
+ fold: true,
232
+ foldRank: true,
233
+ foldRankCollapsedFile: collapsedPath,
234
+ });
235
+
236
+ expect(result.success).toBe(true);
237
+
238
+ const lines = errorSpy.mock.calls.map((call) => String(call[0]));
239
+ const rankingLine = lines.find((l) => l.includes("fold-rank:") && l.includes("blocker(s)"));
240
+ expect(rankingLine).toBeDefined();
241
+ const aLine = lines.find((l) => l.includes("retained 2") && l.includes("a.ts"));
242
+ expect(aLine).toBeDefined();
243
+
244
+ expect(existsSync(collapsedPath)).toBe(true);
245
+ const collapsed = readFileSync(collapsedPath, "utf-8").trim().split("\n");
246
+ expect(collapsed.sort()).toEqual(["fold;a.ts 1", "fold;a.ts;b.ts 1"].sort());
247
+ } finally {
248
+ errorSpy.mockRestore();
249
+ }
250
+ });
251
+
187
252
  describe("#1396 — ownership.env resolves from the env build parameter", () => {
188
253
  const thisDir = dirname(fileURLToPath(import.meta.url));
189
254
  const runtimePath = resolvePath(thisDir, "../../runtime");
@@ -705,6 +770,186 @@ export const testEntity = {
705
770
  expect(existsSync(join(testDir, "dist", "ops", "alb-deploy", "worker.ts"))).toBe(true);
706
771
  });
707
772
 
773
+ describe("committed-encrypted secret sidecars", () => {
774
+ const CIPHERTEXT = 'apiVersion: v1\nkind: Secret\nstringData:\n T: ENC[AES256_GCM,data:xx]\nsops:\n version: 3.9.4\n';
775
+
776
+ /** A serializer standing in for the k8s one: a sidecar plus a primary doc. */
777
+ const sidecarSerializer: Serializer = {
778
+ name: "multi",
779
+ rulePrefix: "MULTI",
780
+ serialize: () => ({
781
+ primary: "kind: Deployment\n",
782
+ files: { "db.sops.yaml": CIPHERTEXT },
783
+ verbatimFiles: ["db.sops.yaml"],
784
+ }),
785
+ };
786
+
787
+ async function writeProject(fileValue: string): Promise<void> {
788
+ const modulePath = resolvePath(
789
+ dirname(fileURLToPath(import.meta.url)),
790
+ "..",
791
+ "..",
792
+ "secret-provenance",
793
+ );
794
+ await writeFile(
795
+ join(testDir, "infra.ts"),
796
+ `import { declareSecret } from ${JSON.stringify(modulePath)};
797
+ export const dbCredentials = declareSecret({ name: "db-credentials", provenance: "committed-encrypted", file: ${JSON.stringify(fileValue)} });
798
+ export const x = { [Symbol.for("chant.declarable")]: true, entityType: "X", lexicon: "multi", kind: "resource", props: {}, attributes: {} };`,
799
+ );
800
+ }
801
+
802
+ test("a build with no --output refuses, naming the flag", async () => {
803
+ await writeProject("secrets/db.sops.yaml");
804
+
805
+ const result = await buildCommand({
806
+ path: testDir,
807
+ format: "yaml",
808
+ serializers: [sidecarSerializer],
809
+ // no `output` — the sidecar would be echoed to stderr and lost
810
+ } as BuildOptions);
811
+
812
+ expect(result.success).toBe(false);
813
+ expect(result.errors.join("\n")).toContain("--output");
814
+ expect(result.errors.join("\n")).toContain("db.sops.yaml");
815
+ });
816
+
817
+ test("with --output the sidecar lands beside the primary, byte for byte", async () => {
818
+ await writeProject("secrets/db.sops.yaml");
819
+ const outputPath = join(testDir, "dist", "manifests.yaml");
820
+
821
+ const result = await buildCommand({
822
+ path: testDir,
823
+ output: outputPath,
824
+ format: "yaml",
825
+ serializers: [sidecarSerializer],
826
+ });
827
+
828
+ expect(result.errors).toEqual([]);
829
+ expect(readFileSync(join(testDir, "dist", "db.sops.yaml"), "utf-8")).toBe(CIPHERTEXT);
830
+ });
831
+
832
+ test("a declared sidecar is never round-tripped through JSON.parse", async () => {
833
+ // YAML is a JSON superset, so a ciphertext file that happens to parse as
834
+ // JSON would otherwise be rewritten key-sorted (or re-emitted as YAML),
835
+ // breaking the `sops` MAC. Declared files skip that path entirely.
836
+ const jsonish = '{"b": 1, "a": 2}\n';
837
+ const jsonSidecar: Serializer = {
838
+ name: "multi",
839
+ rulePrefix: "MULTI",
840
+ serialize: () => ({
841
+ primary: "kind: Deployment\n",
842
+ files: { "db.sops.yaml": jsonish },
843
+ verbatimFiles: ["db.sops.yaml"],
844
+ }),
845
+ };
846
+ await writeProject("secrets/db.sops.yaml");
847
+ const outputPath = join(testDir, "dist", "manifests.yaml");
848
+
849
+ const result = await buildCommand({
850
+ path: testDir,
851
+ output: outputPath,
852
+ format: "yaml",
853
+ serializers: [jsonSidecar],
854
+ });
855
+
856
+ expect(result.errors).toEqual([]);
857
+ expect(readFileSync(join(testDir, "dist", "db.sops.yaml"), "utf-8")).toBe(jsonish);
858
+ });
859
+ });
860
+
861
+ describe("SerializerResult.verbatimFiles — general channel (#1937)", () => {
862
+ test("a verbatim file whose content happens to be valid JSON survives byte-identical", async () => {
863
+ // Deliberately not key-sorted or 2-space indented — content the JSON
864
+ // round trip below would reformat. The old workaround (skip the round
865
+ // trip only because a file happens to fail JSON.parse, true for the
866
+ // committed-encrypted `.sops.yaml` case but never a real guarantee)
867
+ // could not protect this case. `verbatimFiles` can, structurally.
868
+ const raw = '{"b":1,"a":2, "nested":{"z":true}}';
869
+ const verbatimSerializer: Serializer = {
870
+ name: "multi",
871
+ rulePrefix: "MULTI",
872
+ serialize: () => ({
873
+ primary: "{}",
874
+ files: { "raw.json": raw },
875
+ verbatimFiles: ["raw.json"],
876
+ }),
877
+ };
878
+ await writeFile(
879
+ join(testDir, "infra.ts"),
880
+ `export const x = { [Symbol.for("chant.declarable")]: true, entityType: "X", lexicon: "multi", kind: "resource", props: {}, attributes: {} };`,
881
+ );
882
+ const outputPath = join(testDir, "dist", "manifest.json");
883
+
884
+ const result = await buildCommand({
885
+ path: testDir,
886
+ output: outputPath,
887
+ format: "json",
888
+ serializers: [verbatimSerializer],
889
+ });
890
+
891
+ expect(result.errors).toEqual([]);
892
+ expect(readFileSync(join(testDir, "dist", "raw.json"), "utf-8")).toBe(raw);
893
+ });
894
+ });
895
+
896
+ describe("additional-file basename collisions across lexicons (#1937)", () => {
897
+ const lexASerializer = (content: string): Serializer => ({
898
+ name: "lexa",
899
+ rulePrefix: "LEXA",
900
+ serialize: () => ({ primary: "{}", files: { "shared.yaml": content } }),
901
+ });
902
+ const lexBSerializer = (content: string): Serializer => ({
903
+ name: "lexb",
904
+ rulePrefix: "LEXB",
905
+ serialize: () => ({ primary: "{}", files: { "shared.yaml": content } }),
906
+ });
907
+
908
+ async function writeTwoLexiconProject(): Promise<void> {
909
+ await writeFile(
910
+ join(testDir, "infra.ts"),
911
+ [
912
+ `export const a = { [Symbol.for("chant.declarable")]: true, entityType: "A", lexicon: "lexa", kind: "resource", props: {}, attributes: {} };`,
913
+ `export const b = { [Symbol.for("chant.declarable")]: true, entityType: "B", lexicon: "lexb", kind: "resource", props: {}, attributes: {} };`,
914
+ ].join("\n"),
915
+ );
916
+ }
917
+
918
+ test("differing content at the same basename fails the build, naming the basename and both sources", async () => {
919
+ await writeTwoLexiconProject();
920
+ const outputPath = join(testDir, "dist", "manifest.json");
921
+
922
+ const result = await buildCommand({
923
+ path: testDir,
924
+ output: outputPath,
925
+ format: "json",
926
+ serializers: [lexASerializer("from lexa\n"), lexBSerializer("from lexb\n")],
927
+ });
928
+
929
+ expect(result.success).toBe(false);
930
+ const message = result.errors.join("\n");
931
+ expect(message).toContain("shared.yaml");
932
+ expect(message).toContain("lexa");
933
+ expect(message).toContain("lexb");
934
+ });
935
+
936
+ test("identical content at the same basename passes", async () => {
937
+ await writeTwoLexiconProject();
938
+ const outputPath = join(testDir, "dist", "manifest.json");
939
+ const shared = "same content for both\n";
940
+
941
+ const result = await buildCommand({
942
+ path: testDir,
943
+ output: outputPath,
944
+ format: "json",
945
+ serializers: [lexASerializer(shared), lexBSerializer(shared)],
946
+ });
947
+
948
+ expect(result.errors).toEqual([]);
949
+ expect(readFileSync(join(testDir, "dist", "shared.yaml"), "utf-8")).toBe(shared);
950
+ });
951
+ });
952
+
708
953
  test("op worker files go to <project>/dist/ops even with no --output (#878)", async () => {
709
954
  // The generated Op worker must land where `chant run <op> --temporal` reads it
710
955
  // (`<project>/dist/ops/<name>/worker.ts`) even when the build has no --output
@@ -7,7 +7,9 @@ import {
7
7
  isOwnershipParamRef,
8
8
  resolveFoldEnabled,
9
9
  resolveSandboxEnabled,
10
+ resolveKnowledgeDir,
10
11
  } from "../../config";
12
+ import { loadOkfBundle } from "../../okf-read";
11
13
  import { unknownEnvError } from "../../env";
12
14
  import type { OwnershipMarker } from "../../ownership";
13
15
  import { resolveCliBuildParams } from "../build-params-cli";
@@ -15,10 +17,14 @@ import type { Serializer, SerializerResult } from "../../serializer";
15
17
  import type { LexiconPlugin } from "../../lexicon";
16
18
  import { resolveLexiconVersions, collectBuildRootContributors } from "../plugins";
17
19
  import { runPostSynthChecks } from "../../lint/post-synth";
20
+ import { coreReceiptChecks } from "../../lint/receipt-checks";
21
+ import { coreOutputChecks } from "../../lint/output-checks";
22
+ import { coreKnowledgeChecks } from "../../lint/knowledge-checks";
18
23
  import { applyConfiguredSeverity } from "../../lint/config";
19
24
  import { loadPolicyChecks } from "../../lint/policy";
20
25
  import { armSandboxPolicyExecution, runProjectPolicies } from "../../lint/policy-sandbox";
21
26
  import { sortedJsonReplacer } from "../../utils";
27
+ import { rankFoldBlockers, toCollapsedFormat, type FoldRankResult } from "../../discovery/fold-rank";
22
28
  import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
23
29
  import { writeFileSync, mkdirSync } from "fs";
24
30
  import { resolve, dirname, join, relative } from "path";
@@ -79,6 +85,23 @@ export interface BuildOptions {
79
85
  * after {@link params}.
80
86
  */
81
87
  paramsFile?: string;
88
+
89
+ /**
90
+ * chant #1083 — `chant build --fold --fold-rank`: after the build, rank
91
+ * `"run"`-mode files by dominator retained-count over the forward
92
+ * import-failure graph (see `../../discovery/fold-rank.ts`), and report
93
+ * the reverse-taint bucket (#1044) separately. Printed to stderr,
94
+ * independent of `--verbose`'s per-file listing. No-op without `fold`.
95
+ */
96
+ foldRank?: boolean;
97
+
98
+ /**
99
+ * chant #1083 — `chant build --fold --fold-rank <path>`: as {@link
100
+ * foldRank}, and ALSO writes the ranking as a Brendan Gregg collapsed
101
+ * stack export (weighted by retained count) to this path, so it opens in
102
+ * any flame/icicle viewer with no chant-specific tooling.
103
+ */
104
+ foldRankCollapsedFile?: string;
82
105
  }
83
106
 
84
107
  /**
@@ -341,6 +364,23 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
341
364
  }
342
365
  }
343
366
 
367
+ // chant #1083 — rank fold blockers by dominator retained-count over the
368
+ // forward import-failure graph, and report the reverse-taint bucket
369
+ // (#1044) separately. Opt-in (`--fold-rank`) and independent of
370
+ // `--verbose`: the per-file listing above says WHAT ran; this says WHICH
371
+ // of those files is worth fixing first.
372
+ if (fold && (options.foldRank || options.foldRankCollapsedFile) && result.foldDecisions.length > 0) {
373
+ const ranking = await rankFoldBlockers(result.foldDecisions);
374
+ console.error(formatInfo(formatFoldRanking(ranking, infraPath)));
375
+ if (options.foldRankCollapsedFile) {
376
+ const collapsedPath = resolve(options.foldRankCollapsedFile);
377
+ mkdirSync(dirname(collapsedPath), { recursive: true });
378
+ const lines = toCollapsedFormat(ranking, { relativeTo: infraPath });
379
+ writeFileSync(collapsedPath, lines.length > 0 ? `${lines.join("\n")}\n` : "");
380
+ console.error(formatInfo(`[fold-rank] collapsed-format export: ${collapsedPath} (${lines.length} line(s))`));
381
+ }
382
+ }
383
+
344
384
  // Format errors — grouped by message, because one refusal can surface once
345
385
  // per resource file. A project whose params module throws (a guard like
346
386
  // "this seam cannot back that tier") throws in EVERY file that imports it,
@@ -382,6 +422,66 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
382
422
  // finding just as it suppresses a pre-synth one. A finding it suppresses is
383
423
  // counted, not dropped silently — see `suppressedPostSynthCount` below.
384
424
  let suppressedPostSynthCount = 0;
425
+
426
+ // Core-owned post-synth checks over effect receipts (#1833). Receipts are
427
+ // recognized by marker, lexicon-independently, so this set runs over the
428
+ // FULL build result — unscoped, and regardless of which plugins loaded.
429
+ // Same severity-resolution funnel as every other post-synth finding.
430
+ if (result.errors.length === 0) {
431
+ const receiptDiags = runPostSynthChecks(coreReceiptChecks(), result, env);
432
+ const { diagnostics: activeDiags, suppressed } = applyConfiguredSeverity(receiptDiags, config.lint?.rules);
433
+ suppressedPostSynthCount += suppressed.length;
434
+ for (const diag of activeDiags) {
435
+ const prefix = diag.entity ? `[${diag.entity}] ` : "";
436
+ const lexiconSuffix = diag.lexicon ? ` (${diag.lexicon})` : "";
437
+ if (diag.severity === "error") {
438
+ errors.push(formatError({ message: `${prefix}${diag.message}${lexiconSuffix}` }));
439
+ } else {
440
+ warnings.push(formatWarning({ message: `${prefix}${diag.message}${lexiconSuffix}` }));
441
+ }
442
+ }
443
+ }
444
+
445
+ // Core-owned post-synth check over serialized output text (#1526). The
446
+ // "[object Object]" marker means a reference got stringified instead of
447
+ // resolved; it can appear in any lexicon's emitted text, so this runs over
448
+ // the FULL build result the same way the receipt checks above do.
449
+ if (result.errors.length === 0) {
450
+ const outputDiags = runPostSynthChecks(coreOutputChecks(), result, env);
451
+ const { diagnostics: activeDiags, suppressed } = applyConfiguredSeverity(outputDiags, config.lint?.rules);
452
+ suppressedPostSynthCount += suppressed.length;
453
+ for (const diag of activeDiags) {
454
+ const prefix = diag.entity ? `[${diag.entity}] ` : "";
455
+ const lexiconSuffix = diag.lexicon ? ` (${diag.lexicon})` : "";
456
+ if (diag.severity === "error") {
457
+ errors.push(formatError({ message: `${prefix}${diag.message}${lexiconSuffix}` }));
458
+ } else {
459
+ warnings.push(formatWarning({ message: `${prefix}${diag.message}${lexiconSuffix}` }));
460
+ }
461
+ }
462
+ }
463
+
464
+ // Core-owned post-synth check over the project's OKF knowledge bundle
465
+ // (#1865, design #1059): a concept's `binds` naming no discovered entity is
466
+ // a stale binding, surfaced as a COR026 warning. `resolveKnowledgeDir` +
467
+ // `loadOkfBundle` never fail — a missing/absent `knowledge/` directory
468
+ // loads an empty bundle, so this is a no-op for a project that has none.
469
+ if (result.errors.length === 0) {
470
+ const bundle = await loadOkfBundle(resolveKnowledgeDir(config, configDir));
471
+ const knowledgeDiags = runPostSynthChecks(coreKnowledgeChecks(bundle), result, env);
472
+ const { diagnostics: activeDiags, suppressed } = applyConfiguredSeverity(knowledgeDiags, config.lint?.rules);
473
+ suppressedPostSynthCount += suppressed.length;
474
+ for (const diag of activeDiags) {
475
+ const prefix = diag.entity ? `[${diag.entity}] ` : "";
476
+ const lexiconSuffix = diag.lexicon ? ` (${diag.lexicon})` : "";
477
+ if (diag.severity === "error") {
478
+ errors.push(formatError({ message: `${prefix}${diag.message}${lexiconSuffix}` }));
479
+ } else {
480
+ warnings.push(formatWarning({ message: `${prefix}${diag.message}${lexiconSuffix}` }));
481
+ }
482
+ }
483
+ }
484
+
385
485
  if (result.errors.length === 0 && options.plugins) {
386
486
  for (const plugin of options.plugins) {
387
487
  if (!plugin.postSynthChecks) continue;
@@ -479,14 +579,55 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
479
579
 
480
580
  // Handle output
481
581
  if (result.errors.length === 0 && errors.length === 0) {
482
- // Extract primary content and collect additional files from SerializerResult
483
- const additionalFiles = new Map<string, string>();
582
+ // Extract primary content and collect additional files from SerializerResult.
583
+ // Each entry remembers which lexicon produced it, so a same-basename
584
+ // collision between two sources (below) can name both.
585
+ const additionalFiles = new Map<string, { content: string; source: string }>();
586
+
587
+ // Basenames (from some SerializerResult.files) that must reach disk
588
+ // BYTE-FOR-BYTE — e.g. committed ciphertext
589
+ // (`declareSecret({ provenance: "committed-encrypted", file })`). The
590
+ // additional-file writer below round-trips anything JSON.parse accepts,
591
+ // key-sorting it and possibly re-emitting it as YAML, which would break
592
+ // e.g. the `sops` MAC on committed ciphertext. A serializer opts a file
593
+ // out of that round trip structurally, via `SerializerResult.verbatimFiles`
594
+ // (chant#1937) — "the parse happens to fail" is not a safety guarantee.
595
+ const verbatimFiles = new Set<string>();
596
+
597
+ // A same basename emitted by two sources with different content would
598
+ // otherwise silently overwrite (last-writer-wins, since `additionalFiles`
599
+ // is keyed by basename alone). Identical content from two sources is
600
+ // fine — dedup — but differing content is refused, naming both sources,
601
+ // the same way the committed-encrypted resolution stage refuses a
602
+ // basename collision among its own declarations.
603
+ function addAdditionalFile(filename: string, content: string, source: string): void {
604
+ const existing = additionalFiles.get(filename);
605
+ if (existing === undefined) {
606
+ additionalFiles.set(filename, { content, source });
607
+ return;
608
+ }
609
+ if (existing.content === content) return;
610
+ errors.push(
611
+ formatError({
612
+ message:
613
+ `Additional file "${filename}" collides: "${existing.source}" and "${source}" ` +
614
+ `both emit it with different content — additional files share one flat ` +
615
+ `namespace, so two sources whose filenames match would otherwise silently ` +
616
+ `overwrite each other.`,
617
+ }),
618
+ );
619
+ }
484
620
 
485
- function getPrimaryContent(raw: string | SerializerResult): string {
621
+ function getPrimaryContent(raw: string | SerializerResult, source: string): string {
486
622
  if (typeof raw === "string") return raw;
487
623
  if (raw.files) {
488
624
  for (const [filename, content] of Object.entries(raw.files)) {
489
- additionalFiles.set(filename, content);
625
+ addAdditionalFile(filename, content, source);
626
+ }
627
+ }
628
+ if (raw.verbatimFiles) {
629
+ for (const filename of raw.verbatimFiles) {
630
+ verbatimFiles.add(filename);
490
631
  }
491
632
  }
492
633
  return raw.primary;
@@ -505,8 +646,8 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
505
646
  // Multiple lexicons: wrap in lexicon keys
506
647
  let output: string = "{}";
507
648
  if (result.outputs.size === 1) {
508
- const [, raw] = [...result.outputs.entries()][0];
509
- const content = getPrimaryContent(raw);
649
+ const [lexiconName, raw] = [...result.outputs.entries()][0];
650
+ const content = getPrimaryContent(raw, lexiconName);
510
651
  const parsed = tryParseJson(content);
511
652
  if ("json" in parsed) {
512
653
  output = JSON.stringify(parsed.json, sortedJsonReplacer, 2);
@@ -523,7 +664,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
523
664
  const nonJsonSections: string[] = [];
524
665
  const sortedLexiconNames = [...result.outputs.keys()].sort();
525
666
  for (const lexiconName of sortedLexiconNames) {
526
- const content = getPrimaryContent(result.outputs.get(lexiconName)!);
667
+ const content = getPrimaryContent(result.outputs.get(lexiconName)!, lexiconName);
527
668
  const parsed = tryParseJson(content);
528
669
  if ("json" in parsed) {
529
670
  combined[lexiconName] = parsed.json;
@@ -554,13 +695,13 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
554
695
  // `foo.yaml`, so the durable-run worker was never where `run --temporal` looks.
555
696
  const projectDist = resolve(options.path ?? ".", "dist");
556
697
  let opsWritten = 0;
557
- for (const [filename, content] of [...additionalFiles]) {
698
+ for (const [filename, entry] of [...additionalFiles]) {
558
699
  if (!filename.startsWith("ops/")) continue;
559
700
  additionalFiles.delete(filename);
560
701
  try {
561
702
  const targetPath = join(projectDist, filename);
562
703
  mkdirSync(dirname(targetPath), { recursive: true });
563
- writeFileSync(targetPath, content);
704
+ writeFileSync(targetPath, entry.content);
564
705
  opsWritten += 1;
565
706
  } catch (err) {
566
707
  errors.push(
@@ -585,17 +726,20 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
585
726
  // Write additional files (e.g. nested stack templates) alongside the primary output
586
727
  if (additionalFiles.size > 0) {
587
728
  const outputDir = dirname(outputPath);
588
- for (const [filename, content] of additionalFiles) {
589
- let fileContent = content;
590
- // Format additional files consistently
591
- try {
592
- const fileParsed = JSON.parse(content);
593
- fileContent = JSON.stringify(fileParsed, sortedJsonReplacer, 2);
594
- if (options.format === "yaml") {
595
- fileContent = jsonToYaml(JSON.parse(fileContent));
729
+ for (const [filename, entry] of additionalFiles) {
730
+ let fileContent = entry.content;
731
+ // Format additional files consistently — except the ones declared
732
+ // verbatim, which are copied exactly as they were committed.
733
+ if (!verbatimFiles.has(filename)) {
734
+ try {
735
+ const fileParsed = JSON.parse(entry.content);
736
+ fileContent = JSON.stringify(fileParsed, sortedJsonReplacer, 2);
737
+ if (options.format === "yaml") {
738
+ fileContent = jsonToYaml(JSON.parse(fileContent));
739
+ }
740
+ } catch {
741
+ // If not JSON, write as-is
596
742
  }
597
- } catch {
598
- // If not JSON, write as-is
599
743
  }
600
744
  const targetPath = join(outputDir, filename);
601
745
  mkdirSync(dirname(targetPath), { recursive: true });
@@ -609,13 +753,28 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
609
753
  })
610
754
  );
611
755
  }
756
+ } else if (verbatimFiles.size > 0) {
757
+ // A build carrying committed ciphertext and no --output has nowhere to
758
+ // put the sidecar. Echoing it to a terminal is not useful and dropping
759
+ // it silently is worse: the primary output would reference a Secret
760
+ // whose file never got written, and the miss would surface as a pod
761
+ // failing to start, far from its cause. Refuse, naming the flag.
762
+ const names = [...verbatimFiles].sort().join(", ");
763
+ errors.push(
764
+ formatError({
765
+ message:
766
+ `This build emits committed-encrypted secret file(s) (${names}) as sidecars, ` +
767
+ `which need a directory to be written into. Re-run with --output <path> ` +
768
+ `(the sidecars land beside it).`,
769
+ }),
770
+ );
612
771
  } else {
613
772
  // Print to stdout
614
773
  console.log(output);
615
774
  // Log additional files to stderr if any
616
- for (const [filename, content] of additionalFiles) {
775
+ for (const [filename, entry] of additionalFiles) {
617
776
  console.error(`\n--- ${filename} ---`);
618
- console.error(content);
777
+ console.error(entry.content);
619
778
  }
620
779
  }
621
780
  }
@@ -659,6 +818,36 @@ export function summarizeFoldDecisions(decisions: readonly { mode: string }[]):
659
818
  return `fold: ${files(folded)} folded, ${ran} ran${hint}`;
660
819
  }
661
820
 
821
+ /**
822
+ * `chant build --fold --fold-rank`'s text report (chant #1083): blockers
823
+ * ranked by dominator retained-count, highest first, then the reverse-taint
824
+ * bucket (#1044) called out separately since the tree can't express that
825
+ * edge (see `../../discovery/fold-rank.ts`'s module doc).
826
+ */
827
+ export function formatFoldRanking(ranking: FoldRankResult, infraPath: string): string {
828
+ const rel = (file: string): string => relative(infraPath, file) || file;
829
+ const lines: string[] = [];
830
+ if (ranking.blockers.length === 0) {
831
+ lines.push("fold-rank: no run-mode files to rank");
832
+ } else {
833
+ lines.push(`fold-rank: ${ranking.blockers.length} blocker(s), ${ranking.totalBlocked} file(s) blocked`);
834
+ for (const b of ranking.blockers) {
835
+ const via = b.topLevel ? "" : ` (behind ${rel(b.dominatedBy!)})`;
836
+ const reason = b.reason ? ` — ${b.reason}` : "";
837
+ lines.push(` retained ${b.retained}: ${rel(b.file)}${via}${reason}`);
838
+ }
839
+ }
840
+ if (ranking.reverseTainted.length > 0) {
841
+ lines.push(
842
+ `fold-rank: ${ranking.reverseTainted.length} file(s) held back only by the reverse rule (#1044) — would fold in isolation, blocked by a run-mode importer, not modeled in the tree above:`,
843
+ );
844
+ for (const t of ranking.reverseTainted) {
845
+ lines.push(` ${rel(t.file)}`);
846
+ }
847
+ }
848
+ return lines.join("\n");
849
+ }
850
+
662
851
  /**
663
852
  * Simple JSON to YAML converter
664
853
  */
@@ -20,8 +20,9 @@ import { GENERATED_MARKER } from "../../discovery/files";
20
20
 
21
21
  // Import config loader
22
22
  import { loadConfig, resolveRulesForFile, resolveConfiguredSeverity, findProjectRoot } from "../../lint/config";
23
- import { loadChantConfig } from "../../config";
23
+ import { loadChantConfig, resolveKnowledgeDir } from "../../config";
24
24
  import type { LintProjectConfig } from "../../lint/rule";
25
+ import { loadOkfBundle, type OkfBundle } from "../../okf-read";
25
26
 
26
27
  /**
27
28
  * Type guard to check if a value conforms to the LintRule interface.
@@ -454,10 +455,21 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
454
455
  // runLint() call below via LintContext.projectConfig. Best-effort: a
455
456
  // directory with no project config lints with those rules silent.
456
457
  let projectConfig: LintProjectConfig | undefined;
458
+ // chant #1866 — the loaded OKF knowledge bundle, threaded into
459
+ // `formatStylish` below so its suppressed section can resolve `okf:`
460
+ // citations. Best-effort like `projectConfig` above (same
461
+ // `loadChantConfig` call): a directory with no chant config, or no
462
+ // `knowledge/` bundle, lints with citations rendering unresolved rather
463
+ // than a failure — `loadOkfBundle` already treats a missing directory as
464
+ // an empty bundle.
465
+ let knowledgeBundle: OkfBundle | undefined;
457
466
  try {
458
- projectConfig = (await loadChantConfig(projectRoot)).config as LintProjectConfig;
467
+ const chantConfig = (await loadChantConfig(projectRoot)).config;
468
+ projectConfig = chantConfig as LintProjectConfig;
469
+ knowledgeBundle = await loadOkfBundle(resolveKnowledgeDir(chantConfig, projectRoot));
459
470
  } catch {
460
471
  projectConfig = undefined;
472
+ knowledgeBundle = undefined;
461
473
  }
462
474
 
463
475
  // Load all rules from lexicon plugins (core "chant" + lexicon-specific)
@@ -588,7 +600,7 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
588
600
  break;
589
601
  case "stylish":
590
602
  default:
591
- output = formatStylish(diagnostics);
603
+ output = formatStylish(diagnostics, suppressed, knowledgeBundle);
592
604
  break;
593
605
  }
594
606
 
@@ -148,6 +148,8 @@ export async function runBuild(ctx: CommandContext): Promise<number> {
148
148
  env: args.env,
149
149
  fold: args.fold,
150
150
  sandbox: args.sandbox,
151
+ foldRank: args.foldRank,
152
+ foldRankCollapsedFile: args.foldRankCollapsedFile,
151
153
  params,
152
154
  paramsFile: args.paramsFile,
153
155
  });