@intentius/chant 0.57.0 → 0.58.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 (504) hide show
  1. package/dist/audit/catalog.d.ts +51 -1
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +20 -1
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +14 -8
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/lineage.d.ts +10 -0
  8. package/dist/audit/lineage.d.ts.map +1 -0
  9. package/dist/audit/prior-art.d.ts +385 -0
  10. package/dist/audit/prior-art.d.ts.map +1 -0
  11. package/dist/audit/proof.d.ts +16 -0
  12. package/dist/audit/proof.d.ts.map +1 -1
  13. package/dist/audit/report-html.d.ts.map +1 -1
  14. package/dist/audit/report-model.d.ts +8 -1
  15. package/dist/audit/report-model.d.ts.map +1 -1
  16. package/dist/audit/report.d.ts.map +1 -1
  17. package/dist/audit/rules-doc.d.ts +3 -0
  18. package/dist/audit/rules-doc.d.ts.map +1 -1
  19. package/dist/audit/secrets.d.ts +15 -0
  20. package/dist/audit/secrets.d.ts.map +1 -1
  21. package/dist/audit/terraform-state.d.ts +46 -0
  22. package/dist/audit/terraform-state.d.ts.map +1 -0
  23. package/dist/build.d.ts +17 -1
  24. package/dist/build.d.ts.map +1 -1
  25. package/dist/claimed-fields.d.ts +161 -0
  26. package/dist/claimed-fields.d.ts.map +1 -0
  27. package/dist/cli/build-params-cli.d.ts +2 -2
  28. package/dist/cli/command-group.d.ts +14 -0
  29. package/dist/cli/command-group.d.ts.map +1 -1
  30. package/dist/cli/commands/audit.d.ts +17 -1
  31. package/dist/cli/commands/audit.d.ts.map +1 -1
  32. package/dist/cli/commands/build.d.ts.map +1 -1
  33. package/dist/cli/commands/check-lexicon-docs.d.ts +1 -1
  34. package/dist/cli/commands/check-lexicon-examples.d.ts +16 -6
  35. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  36. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts +1 -0
  37. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  38. package/dist/cli/commands/init-lexicon/templates/project.d.ts +7 -0
  39. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  40. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  41. package/dist/cli/commands/init.d.ts.map +1 -1
  42. package/dist/cli/commands/lint.d.ts.map +1 -1
  43. package/dist/cli/commands/onboard.d.ts +2 -0
  44. package/dist/cli/commands/onboard.d.ts.map +1 -1
  45. package/dist/cli/handlers/build.d.ts.map +1 -1
  46. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  47. package/dist/cli/handlers/operator.d.ts +82 -16
  48. package/dist/cli/handlers/operator.d.ts.map +1 -1
  49. package/dist/cli/handlers/run.d.ts +49 -42
  50. package/dist/cli/handlers/run.d.ts.map +1 -1
  51. package/dist/cli/main.d.ts +28 -1
  52. package/dist/cli/main.d.ts.map +1 -1
  53. package/dist/cli/mcp/op-tools.d.ts +16 -1
  54. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  55. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  56. package/dist/cli/mcp/resources/context.d.ts.map +1 -1
  57. package/dist/cli/registry.d.ts +26 -9
  58. package/dist/cli/registry.d.ts.map +1 -1
  59. package/dist/codegen/generate-post-synth-barrel.d.ts +1 -1
  60. package/dist/components/auto-release.d.ts +10 -20
  61. package/dist/components/auto-release.d.ts.map +1 -1
  62. package/dist/components/capability.d.ts +4 -4
  63. package/dist/components/cli-support.d.ts +20 -21
  64. package/dist/components/cli-support.d.ts.map +1 -1
  65. package/dist/components/component.d.ts +3 -3
  66. package/dist/components/component.d.ts.map +1 -1
  67. package/dist/components/driver-output.d.ts.map +1 -1
  68. package/dist/components/driver.d.ts +66 -36
  69. package/dist/components/driver.d.ts.map +1 -1
  70. package/dist/components/index.d.ts +1 -2
  71. package/dist/components/index.d.ts.map +1 -1
  72. package/dist/components/manifest-persistence.d.ts +1 -9
  73. package/dist/components/manifest-persistence.d.ts.map +1 -1
  74. package/dist/components/run-progress.d.ts +6 -6
  75. package/dist/components/run-progress.d.ts.map +1 -1
  76. package/dist/components/verbs/run-agent.d.ts +9 -15
  77. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  78. package/dist/components/verbs/verify.d.ts +2 -2
  79. package/dist/deep-observation.d.ts +17 -0
  80. package/dist/deep-observation.d.ts.map +1 -1
  81. package/dist/discovery/files.d.ts +4 -4
  82. package/dist/discovery/fold-import.d.ts.map +1 -1
  83. package/dist/discovery/param-deps.d.ts +33 -0
  84. package/dist/discovery/param-deps.d.ts.map +1 -1
  85. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/config-wire.d.ts +2 -2
  87. package/dist/fold/foldable-helpers.d.ts +5 -1
  88. package/dist/fold/foldable-helpers.d.ts.map +1 -1
  89. package/dist/fold-provenance.d.ts +236 -0
  90. package/dist/fold-provenance.d.ts.map +1 -0
  91. package/dist/graph-ops.d.ts +1 -1
  92. package/dist/held-elsewhere.d.ts +85 -0
  93. package/dist/held-elsewhere.d.ts.map +1 -0
  94. package/dist/index.d.ts +3 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/kubectl-context.d.ts +3 -3
  97. package/dist/lexicon-config.d.ts +4 -4
  98. package/dist/lexicon-plugin-helpers.d.ts +1 -1
  99. package/dist/lexicon.d.ts +104 -14
  100. package/dist/lexicon.d.ts.map +1 -1
  101. package/dist/lifecycle/change-set.d.ts +13 -0
  102. package/dist/lifecycle/change-set.d.ts.map +1 -1
  103. package/dist/lifecycle/deep-diff.d.ts +133 -6
  104. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  105. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  106. package/dist/lifecycle/gate-ledger.d.ts +71 -3
  107. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  108. package/dist/lifecycle/index.d.ts +1 -0
  109. package/dist/lifecycle/index.d.ts.map +1 -1
  110. package/dist/lifecycle/release-ledger.d.ts +2 -2
  111. package/dist/lifecycle/run-ledger.d.ts +53 -0
  112. package/dist/lifecycle/run-ledger.d.ts.map +1 -0
  113. package/dist/lifecycle/symptoms.d.ts +3 -3
  114. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  115. package/dist/lint/config.d.ts +81 -2
  116. package/dist/lint/config.d.ts.map +1 -1
  117. package/dist/lint/post-synth.d.ts +55 -2
  118. package/dist/lint/post-synth.d.ts.map +1 -1
  119. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +1 -1
  120. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -1
  121. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +1 -1
  122. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -1
  123. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts +44 -0
  124. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -0
  125. package/dist/lint/rules/comp/index.d.ts +1 -1
  126. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  127. package/dist/lint/rules/op/index.d.ts +16 -0
  128. package/dist/lint/rules/op/index.d.ts.map +1 -0
  129. package/dist/lint/rules/op/ops012-activity-contract.d.ts +27 -0
  130. package/dist/lint/rules/op/ops012-activity-contract.d.ts.map +1 -0
  131. package/dist/lint/rules/op/ops013-step-output-ref.d.ts +61 -0
  132. package/dist/lint/rules/op/ops013-step-output-ref.d.ts.map +1 -0
  133. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts +55 -0
  134. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -0
  135. package/dist/lint/rules/op/support.d.ts +12 -0
  136. package/dist/lint/rules/op/support.d.ts.map +1 -0
  137. package/dist/lint/suppressions.d.ts +108 -0
  138. package/dist/lint/suppressions.d.ts.map +1 -0
  139. package/dist/live-endpoint.d.ts +1 -1
  140. package/dist/op/activities/activity-contracts.d.ts +54 -0
  141. package/dist/op/activities/activity-contracts.d.ts.map +1 -0
  142. package/dist/op/activities/apply.d.ts +262 -0
  143. package/dist/op/activities/apply.d.ts.map +1 -0
  144. package/dist/op/activities/build.d.ts +13 -0
  145. package/dist/op/activities/build.d.ts.map +1 -0
  146. package/dist/op/activities/converge.d.ts +110 -0
  147. package/dist/op/activities/converge.d.ts.map +1 -0
  148. package/dist/op/activities/env-teardown.d.ts +76 -0
  149. package/dist/op/activities/env-teardown.d.ts.map +1 -0
  150. package/dist/op/activities/guard-validate.d.ts +64 -0
  151. package/dist/op/activities/guard-validate.d.ts.map +1 -0
  152. package/dist/op/activities/http-check.d.ts +34 -0
  153. package/dist/op/activities/http-check.d.ts.map +1 -0
  154. package/dist/op/activities/index.d.ts +31 -0
  155. package/dist/op/activities/index.d.ts.map +1 -0
  156. package/dist/op/activities/lexicon-upgrade.d.ts +182 -0
  157. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -0
  158. package/dist/op/activities/lifecycle.d.ts +42 -0
  159. package/dist/op/activities/lifecycle.d.ts.map +1 -0
  160. package/dist/op/activities/pipeline-audit.d.ts +84 -0
  161. package/dist/op/activities/pipeline-audit.d.ts.map +1 -0
  162. package/dist/op/activities/policy.d.ts +17 -0
  163. package/dist/op/activities/policy.d.ts.map +1 -0
  164. package/dist/op/activities/reconcile.d.ts +89 -0
  165. package/dist/op/activities/reconcile.d.ts.map +1 -0
  166. package/dist/op/activities/shell.d.ts +13 -0
  167. package/dist/op/activities/shell.d.ts.map +1 -0
  168. package/dist/op/activities/teardown.d.ts +10 -0
  169. package/dist/op/activities/teardown.d.ts.map +1 -0
  170. package/dist/op/activities/util.d.ts +7 -0
  171. package/dist/op/activities/util.d.ts.map +1 -0
  172. package/dist/op/activities/wait.d.ts +28 -0
  173. package/dist/op/activities/wait.d.ts.map +1 -0
  174. package/dist/op/activities/workflow-audit.d.ts +92 -0
  175. package/dist/op/activities/workflow-audit.d.ts.map +1 -0
  176. package/dist/op/activity-contract-registry.d.ts +76 -0
  177. package/dist/op/activity-contract-registry.d.ts.map +1 -0
  178. package/dist/op/activity-contract.d.ts +25 -18
  179. package/dist/op/activity-contract.d.ts.map +1 -1
  180. package/dist/op/activity-failure.d.ts +28 -0
  181. package/dist/op/activity-failure.d.ts.map +1 -0
  182. package/dist/op/activity-profiles.d.ts +118 -0
  183. package/dist/op/activity-profiles.d.ts.map +1 -0
  184. package/dist/op/activity-registry.d.ts +22 -38
  185. package/dist/op/activity-registry.d.ts.map +1 -1
  186. package/dist/op/activity-runtime.d.ts +3 -14
  187. package/dist/op/activity-runtime.d.ts.map +1 -1
  188. package/dist/op/builders.d.ts +84 -29
  189. package/dist/op/builders.d.ts.map +1 -1
  190. package/dist/op/composites/apply-op.d.ts +113 -0
  191. package/dist/op/composites/apply-op.d.ts.map +1 -0
  192. package/dist/op/composites/converge-op.d.ts +120 -0
  193. package/dist/op/composites/converge-op.d.ts.map +1 -0
  194. package/dist/op/composites/index.d.ts +26 -0
  195. package/dist/op/composites/index.d.ts.map +1 -0
  196. package/dist/op/composites/lexicon-upgrade-op.d.ts +78 -0
  197. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -0
  198. package/dist/op/composites/pipeline-audit-op.d.ts +50 -0
  199. package/dist/op/composites/pipeline-audit-op.d.ts.map +1 -0
  200. package/dist/op/composites/reconcile-op.d.ts +57 -0
  201. package/dist/op/composites/reconcile-op.d.ts.map +1 -0
  202. package/dist/op/composites/watch-op.d.ts +60 -0
  203. package/dist/op/composites/watch-op.d.ts.map +1 -0
  204. package/dist/op/composites/workflow-audit-op.d.ts +57 -0
  205. package/dist/op/composites/workflow-audit-op.d.ts.map +1 -0
  206. package/dist/op/converge-rule.d.ts +12 -10
  207. package/dist/op/converge-rule.d.ts.map +1 -1
  208. package/dist/op/cron.d.ts +51 -0
  209. package/dist/op/cron.d.ts.map +1 -0
  210. package/dist/op/discover.d.ts +9 -0
  211. package/dist/op/discover.d.ts.map +1 -1
  212. package/dist/op/duration.d.ts +11 -0
  213. package/dist/op/duration.d.ts.map +1 -0
  214. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  215. package/dist/op/gate.d.ts +90 -0
  216. package/dist/op/gate.d.ts.map +1 -0
  217. package/dist/op/generate-pipeline.d.ts +9 -0
  218. package/dist/op/generate-pipeline.d.ts.map +1 -1
  219. package/dist/op/index.d.ts +23 -7
  220. package/dist/op/index.d.ts.map +1 -1
  221. package/dist/op/local-executor.d.ts +114 -28
  222. package/dist/op/local-executor.d.ts.map +1 -1
  223. package/dist/op/local-output.d.ts +11 -1
  224. package/dist/op/local-output.d.ts.map +1 -1
  225. package/dist/op/op-ir.d.ts +184 -0
  226. package/dist/op/op-ir.d.ts.map +1 -0
  227. package/dist/op/op-verb-class.d.ts +10 -2
  228. package/dist/op/op-verb-class.d.ts.map +1 -1
  229. package/dist/op/operator.d.ts +30 -5
  230. package/dist/op/operator.d.ts.map +1 -1
  231. package/dist/op/receipt-store.d.ts +1 -1
  232. package/dist/op/receipt-store.d.ts.map +1 -1
  233. package/dist/op/resource.d.ts +15 -1
  234. package/dist/op/resource.d.ts.map +1 -1
  235. package/dist/op/runtime.d.ts +200 -0
  236. package/dist/op/runtime.d.ts.map +1 -0
  237. package/dist/op/runtimes/local.d.ts +29 -0
  238. package/dist/op/runtimes/local.d.ts.map +1 -0
  239. package/dist/op/step-output-ref.d.ts +15 -13
  240. package/dist/op/step-output-ref.d.ts.map +1 -1
  241. package/dist/op/types.d.ts +79 -27
  242. package/dist/op/types.d.ts.map +1 -1
  243. package/dist/ownership.d.ts +1 -1
  244. package/dist/provenance.d.ts +21 -0
  245. package/dist/provenance.d.ts.map +1 -1
  246. package/dist/serializer-walker.d.ts.map +1 -1
  247. package/package.json +6 -1
  248. package/src/audit/catalog.test.ts +79 -3
  249. package/src/audit/catalog.ts +111 -13
  250. package/src/audit/core.test.ts +26 -0
  251. package/src/audit/core.ts +61 -17
  252. package/src/audit/discover.test.ts +39 -0
  253. package/src/audit/discover.ts +91 -15
  254. package/src/audit/lineage.ts +100 -0
  255. package/src/audit/prior-art.ts +153 -0
  256. package/src/audit/proof.test.ts +67 -0
  257. package/src/audit/proof.ts +70 -0
  258. package/src/audit/report-html.ts +9 -4
  259. package/src/audit/report-model.ts +16 -2
  260. package/src/audit/report.test.ts +33 -0
  261. package/src/audit/report.ts +5 -3
  262. package/src/audit/rules-doc.test.ts +34 -2
  263. package/src/audit/rules-doc.ts +82 -9
  264. package/src/audit/secrets.ts +15 -3
  265. package/src/audit/terraform-state.test.ts +144 -0
  266. package/src/audit/terraform-state.ts +131 -0
  267. package/src/build.ts +31 -1
  268. package/src/claimed-fields.test.ts +145 -0
  269. package/src/claimed-fields.ts +208 -0
  270. package/src/cli/build-params-cli.ts +2 -2
  271. package/src/cli/command-group.ts +14 -0
  272. package/src/cli/commands/__fixtures__/audit-terraform/infra/.terraform/providers/lock.txt +1 -0
  273. package/src/cli/commands/__fixtures__/audit-terraform/infra/main.tf +1 -0
  274. package/src/cli/commands/__fixtures__/audit-terraform/infra/modules/foo/main.tf +1 -0
  275. package/src/cli/commands/__fixtures__/audit-terraform/infra/variables.tf +3 -0
  276. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +13 -4
  277. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs-cli.ts +4 -0
  278. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +1 -0
  279. package/src/cli/commands/__fixtures__/init-lexicon-output/src/package-cli.ts +21 -0
  280. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +1 -0
  281. package/src/cli/commands/audit.test.ts +189 -11
  282. package/src/cli/commands/audit.ts +73 -27
  283. package/src/cli/commands/build.test.ts +292 -9
  284. package/src/cli/commands/build.ts +156 -24
  285. package/src/cli/commands/check-lexicon-docs.ts +1 -1
  286. package/src/cli/commands/check-lexicon-examples.test.ts +29 -1
  287. package/src/cli/commands/check-lexicon-examples.ts +42 -10
  288. package/src/cli/commands/init-lexicon/templates/codegen.ts +9 -0
  289. package/src/cli/commands/init-lexicon/templates/project.ts +44 -4
  290. package/src/cli/commands/init-lexicon.test.ts +97 -2
  291. package/src/cli/commands/init-lexicon.ts +4 -2
  292. package/src/cli/commands/init.test.ts +35 -1
  293. package/src/cli/commands/init.ts +14 -3
  294. package/src/cli/commands/lint.test.ts +48 -0
  295. package/src/cli/commands/lint.ts +123 -0
  296. package/src/cli/commands/onboard.test.ts +75 -0
  297. package/src/cli/commands/onboard.ts +54 -23
  298. package/src/cli/handlers/build.test.ts +81 -1
  299. package/src/cli/handlers/build.ts +15 -1
  300. package/src/cli/handlers/graph.test.ts +3 -3
  301. package/src/cli/handlers/lifecycle.test.ts +195 -12
  302. package/src/cli/handlers/lifecycle.ts +123 -6
  303. package/src/cli/handlers/misc.ts +1 -1
  304. package/src/cli/handlers/operator.test.ts +183 -11
  305. package/src/cli/handlers/operator.ts +260 -55
  306. package/src/cli/handlers/run.test.ts +431 -1287
  307. package/src/cli/handlers/run.ts +365 -915
  308. package/src/cli/main.test.ts +23 -15
  309. package/src/cli/main.ts +88 -33
  310. package/src/cli/mcp/op-tools.ts +147 -88
  311. package/src/cli/mcp/resource-handlers.ts +10 -37
  312. package/src/cli/mcp/resources/context.ts +11 -12
  313. package/src/cli/mcp/server.test.ts +25 -27
  314. package/src/cli/mcp/server.ts +2 -2
  315. package/src/cli/registry.ts +26 -9
  316. package/src/codegen/docs.ts +1 -1
  317. package/src/codegen/generate-post-synth-barrel.test.ts +1 -1
  318. package/src/codegen/generate-post-synth-barrel.ts +1 -1
  319. package/src/components/README.md +3 -2
  320. package/src/components/auto-release.test.ts +4 -20
  321. package/src/components/auto-release.ts +17 -42
  322. package/src/components/capability.ts +4 -4
  323. package/src/components/cli-support.test.ts +10 -5
  324. package/src/components/cli-support.ts +52 -44
  325. package/src/components/component.schema.json +2 -2
  326. package/src/components/component.ts +3 -3
  327. package/src/components/driver-output.ts +14 -2
  328. package/src/components/driver.test.ts +92 -22
  329. package/src/components/driver.ts +193 -83
  330. package/src/components/index.ts +1 -6
  331. package/src/components/manifest-persistence.test.ts +2 -20
  332. package/src/components/manifest-persistence.ts +4 -24
  333. package/src/components/pilots/pilots-lint-clean.test.ts +11 -10
  334. package/src/components/run-progress.ts +6 -6
  335. package/src/components/verbs/run-agent.ts +11 -17
  336. package/src/components/verbs/verify.ts +2 -2
  337. package/src/deep-observation.test.ts +75 -0
  338. package/src/deep-observation.ts +48 -0
  339. package/src/discovery/collect.test.ts +3 -3
  340. package/src/discovery/files.ts +4 -4
  341. package/src/discovery/fold-import.ts +105 -2
  342. package/src/discovery/param-deps.test.ts +112 -1
  343. package/src/discovery/param-deps.ts +193 -0
  344. package/src/discovery/sandbox/bundle.ts +15 -1
  345. package/src/discovery/sandbox/config-wire.test.ts +2 -2
  346. package/src/discovery/sandbox/config-wire.ts +2 -2
  347. package/src/fold/fold.test.ts +56 -0
  348. package/src/fold/foldable-helpers.ts +81 -1
  349. package/src/fold-provenance-exclusion.test.ts +186 -0
  350. package/src/fold-provenance.test.ts +495 -0
  351. package/src/fold-provenance.ts +441 -0
  352. package/src/graph-ops.test.ts +2 -2
  353. package/src/graph-ops.ts +1 -1
  354. package/src/held-elsewhere.test.ts +102 -0
  355. package/src/held-elsewhere.ts +114 -0
  356. package/src/index.ts +3 -0
  357. package/src/kubectl-context.ts +3 -3
  358. package/src/lexicon-config.test.ts +5 -5
  359. package/src/lexicon-config.ts +4 -4
  360. package/src/lexicon-plugin-helpers.ts +1 -1
  361. package/src/lexicon.ts +120 -14
  362. package/src/lifecycle/change-set.test.ts +49 -0
  363. package/src/lifecycle/change-set.ts +67 -0
  364. package/src/lifecycle/converge-ledger.ts +1 -1
  365. package/src/lifecycle/deep-diff.test.ts +267 -14
  366. package/src/lifecycle/deep-diff.ts +217 -14
  367. package/src/lifecycle/deep-observe.test.ts +3 -1
  368. package/src/lifecycle/deep-observe.ts +16 -6
  369. package/src/lifecycle/gate-ledger.test.ts +74 -2
  370. package/src/lifecycle/gate-ledger.ts +143 -36
  371. package/src/lifecycle/index.ts +1 -0
  372. package/src/lifecycle/release-ledger.ts +2 -2
  373. package/src/lifecycle/run-ledger.test.ts +268 -0
  374. package/src/lifecycle/run-ledger.ts +201 -0
  375. package/src/lifecycle/symptoms.ts +3 -3
  376. package/src/lint/config.test.ts +82 -1
  377. package/src/lint/config.ts +112 -3
  378. package/src/lint/policy.test.ts +1 -1
  379. package/src/lint/post-synth.test.ts +31 -0
  380. package/src/lint/post-synth.ts +54 -1
  381. package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +2 -2
  382. package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +1 -1
  383. package/src/lint/rules/comp/comp.test.ts +1 -1
  384. package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +74 -0
  385. package/src/lint/rules/comp/index.ts +3 -3
  386. package/src/lint/rules/op/index.ts +23 -0
  387. package/src/lint/rules/op/ops012-activity-contract.test.ts +161 -0
  388. package/src/lint/rules/op/ops012-activity-contract.ts +64 -0
  389. package/src/lint/rules/op/ops013-step-output-ref.test.ts +237 -0
  390. package/src/lint/rules/op/ops013-step-output-ref.ts +98 -0
  391. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +263 -0
  392. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +179 -0
  393. package/src/lint/rules/op/support.ts +11 -0
  394. package/src/lint/suppressions.test.ts +163 -0
  395. package/src/lint/suppressions.ts +204 -0
  396. package/src/live-endpoint.ts +1 -1
  397. package/src/op/activities/activity-contracts.ts +66 -0
  398. package/src/op/activities/apply.test.ts +567 -0
  399. package/src/op/activities/apply.ts +662 -0
  400. package/src/op/activities/build.ts +26 -0
  401. package/src/op/activities/converge.test.ts +330 -0
  402. package/src/op/activities/converge.ts +449 -0
  403. package/src/op/activities/env-teardown.test.ts +187 -0
  404. package/src/op/activities/env-teardown.ts +171 -0
  405. package/src/op/activities/guard-validate.test.ts +139 -0
  406. package/src/op/activities/guard-validate.ts +223 -0
  407. package/src/op/activities/http-check.test.ts +48 -0
  408. package/src/op/activities/http-check.ts +55 -0
  409. package/src/op/activities/index.ts +119 -0
  410. package/src/op/activities/lexicon-upgrade.test.ts +492 -0
  411. package/src/op/activities/lexicon-upgrade.ts +673 -0
  412. package/src/op/activities/lifecycle.ts +85 -0
  413. package/src/op/activities/pipeline-audit.test.ts +67 -0
  414. package/src/op/activities/pipeline-audit.ts +201 -0
  415. package/src/op/activities/policy.test.ts +19 -0
  416. package/src/op/activities/policy.ts +37 -0
  417. package/src/op/activities/reconcile.test.ts +81 -0
  418. package/src/op/activities/reconcile.ts +187 -0
  419. package/src/op/activities/shell.ts +26 -0
  420. package/src/op/activities/teardown.ts +22 -0
  421. package/src/op/activities/util.ts +6 -0
  422. package/src/op/activities/wait.ts +69 -0
  423. package/src/op/activities/workflow-audit.test.ts +90 -0
  424. package/src/op/activities/workflow-audit.ts +203 -0
  425. package/src/op/activity-contract-registry.test.ts +98 -0
  426. package/src/op/activity-contract-registry.ts +114 -0
  427. package/src/op/activity-contract.test.ts +1 -1
  428. package/src/op/activity-contract.ts +37 -33
  429. package/src/op/activity-failure.ts +34 -0
  430. package/src/op/activity-profiles.test.ts +56 -0
  431. package/src/op/activity-profiles.ts +116 -0
  432. package/src/op/activity-registry.test.ts +37 -28
  433. package/src/op/activity-registry.ts +27 -56
  434. package/src/op/activity-runtime.ts +3 -47
  435. package/src/op/builders-exports.test.ts +0 -49
  436. package/src/op/builders-typed.test.ts +164 -0
  437. package/src/op/builders.ts +123 -39
  438. package/src/op/composites/apply-op.ts +208 -0
  439. package/src/op/composites/composites.test.ts +366 -0
  440. package/src/op/composites/converge-op.test.ts +168 -0
  441. package/src/op/composites/converge-op.ts +214 -0
  442. package/src/op/composites/index.ts +26 -0
  443. package/src/op/composites/lexicon-upgrade-op.test.ts +68 -0
  444. package/src/op/composites/lexicon-upgrade-op.ts +134 -0
  445. package/src/op/composites/pipeline-audit-op.test.ts +25 -0
  446. package/src/op/composites/pipeline-audit-op.ts +78 -0
  447. package/src/op/composites/reconcile-op.test.ts +51 -0
  448. package/src/op/composites/reconcile-op.ts +107 -0
  449. package/src/op/composites/watch-op.ts +108 -0
  450. package/src/op/composites/workflow-audit-op.test.ts +25 -0
  451. package/src/op/composites/workflow-audit-op.ts +88 -0
  452. package/src/op/converge-rule.ts +12 -10
  453. package/src/op/cron.test.ts +128 -0
  454. package/src/op/cron.ts +149 -0
  455. package/src/op/discover-root.test.ts +105 -1
  456. package/src/op/discover.test.ts +6 -2
  457. package/src/op/discover.ts +84 -16
  458. package/src/op/duration.ts +21 -0
  459. package/src/op/effect-step.test.ts +26 -11
  460. package/src/op/emulator-lifecycle.ts +1 -2
  461. package/src/op/gate.ts +155 -0
  462. package/src/op/generate-pipeline.test.ts +65 -3
  463. package/src/op/generate-pipeline.ts +19 -2
  464. package/src/op/index.ts +39 -6
  465. package/src/op/local-executor.test.ts +163 -34
  466. package/src/op/local-executor.ts +370 -102
  467. package/src/op/local-output.test.ts +95 -11
  468. package/src/op/local-output.ts +33 -4
  469. package/src/op/op-ir.test.ts +281 -0
  470. package/src/op/op-ir.ts +417 -0
  471. package/src/op/op-verb-class.ts +19 -5
  472. package/src/op/op.test.ts +26 -5
  473. package/src/op/operator.test.ts +175 -6
  474. package/src/op/operator.ts +63 -16
  475. package/src/op/receipt-store.ts +1 -1
  476. package/src/op/resource.ts +20 -2
  477. package/src/op/runtime.ts +203 -0
  478. package/src/op/runtimes/local.test.ts +181 -0
  479. package/src/op/runtimes/local.ts +212 -0
  480. package/src/op/step-output-ref.ts +15 -13
  481. package/src/op/types.ts +84 -24
  482. package/src/ownership.ts +1 -1
  483. package/src/provenance.test.ts +6 -0
  484. package/src/provenance.ts +21 -1
  485. package/src/serializer-walker.test.ts +42 -0
  486. package/src/serializer-walker.ts +21 -2
  487. package/dist/cli/handlers/op-progress.d.ts +0 -90
  488. package/dist/cli/handlers/op-progress.d.ts.map +0 -1
  489. package/dist/cli/handlers/run-client.d.ts +0 -127
  490. package/dist/cli/handlers/run-client.d.ts.map +0 -1
  491. package/dist/cli/handlers/run-report.d.ts +0 -5
  492. package/dist/cli/handlers/run-report.d.ts.map +0 -1
  493. package/dist/components/temporal-codegen-loader.d.ts +0 -27
  494. package/dist/components/temporal-codegen-loader.d.ts.map +0 -1
  495. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +0 -37
  496. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +0 -1
  497. package/src/cli/handlers/op-progress.test.ts +0 -268
  498. package/src/cli/handlers/op-progress.ts +0 -248
  499. package/src/cli/handlers/run-client.test.ts +0 -82
  500. package/src/cli/handlers/run-client.ts +0 -219
  501. package/src/cli/handlers/run-report.test.ts +0 -62
  502. package/src/cli/handlers/run-report.ts +0 -122
  503. package/src/components/temporal-codegen-loader.ts +0 -41
  504. package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +0 -66
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Prior art for chant's audit rules.
3
+ *
4
+ * Most of what `chant audit` checks has been checked before by a dedicated
5
+ * open-source tool: zizmor and poutine for GitHub Actions, hadolint for
6
+ * Dockerfiles, cfn-lint and cfn_nag for CloudFormation, kube-linter and
7
+ * kube-score for Kubernetes manifests, gitleaks for secrets, and so on. chant's
8
+ * rules were written independently against the emitted output, but the ideas
9
+ * are shared, and the people who wrote them down first deserve the credit.
10
+ *
11
+ * A rule declares its lineage as `Lineage[]` on its `RuleMeta` entry: which
12
+ * tool, which upstream rule, where it is documented, and how the two relate.
13
+ * `PRIOR_ART` is the registry those entries key into, so a tool is named,
14
+ * linked and licensed once. The audit rules reference renders both, the
15
+ * markdown and HTML reports credit the tool beside the finding, and the SARIF
16
+ * and JSON outputs carry the entries verbatim for consumers.
17
+ *
18
+ * Lineage is credit, not authority. `authority` (./catalog.ts) is a standard
19
+ * or vendor document that says a finding matters; lineage is a tool that
20
+ * checks the same condition. The two are independent: a rule may have either,
21
+ * both, or neither, and lineage never changes a rule's category or tier.
22
+ */
23
+
24
+ /** How a chant rule relates to the upstream check it credits. */
25
+ export type LineageRelation =
26
+ /** Same condition, same scope. */
27
+ | "equivalent"
28
+ /** The core condition is shared; one side is narrower or broader, or a different dialect of the same control. */
29
+ | "overlaps"
30
+ /** chant checks a strict superset of the upstream check. */
31
+ | "extends";
32
+
33
+ /** One credit: an upstream tool's rule that checks what this chant rule checks. */
34
+ export interface Lineage {
35
+ /** A key of {@link PRIOR_ART}. */
36
+ tool: PriorArtTool;
37
+ /** The upstream rule id or check name, in the tool's own vocabulary (e.g. `template-injection`, `DL3007`, `CKV_AWS_18`). Omitted when the tool has no per-rule ids. */
38
+ rule?: string;
39
+ /** Where the upstream rule is documented. */
40
+ url: string;
41
+ relation: LineageRelation;
42
+ }
43
+
44
+ /** A tool (or, for a few families, a vendor validator) that chant's rules credit. */
45
+ export interface PriorArtEntry {
46
+ name: string;
47
+ url: string;
48
+ /** SPDX identifier where confirmed, else "unverified" or "n/a" for documentation. */
49
+ license: string;
50
+ /** What kind of thing this is, so the rendered credit is honest about it. */
51
+ kind: "scanner" | "vendor-validator" | "specification";
52
+ }
53
+
54
+ /**
55
+ * The registry of prior art. Keys are the slugs `Lineage.tool` uses. Populated
56
+ * from the per-family research recorded in the PR that introduced lineage; the
57
+ * prior-art sweep (scripts/prior-art-sweep.ts) reads the same table.
58
+ */
59
+ export const PRIOR_ART = {
60
+ "checkov": { name: "Checkov", url: "https://www.checkov.io/", license: "Apache-2.0", kind: "scanner" },
61
+ "kics": { name: "KICS", url: "https://docs.kics.io/", license: "Apache-2.0", kind: "scanner" },
62
+ "zizmor": { name: "zizmor", url: "https://docs.zizmor.sh/", license: "MIT", kind: "scanner" },
63
+ "cfn-nag": { name: "cfn_nag", url: "https://github.com/stelligent/cfn_nag", license: "MIT", kind: "scanner" },
64
+ "gitlab-docs": { name: "GitLab CI/CD YAML reference", url: "https://docs.gitlab.com/ci/yaml/", license: "CC-BY-SA-4.0", kind: "specification" },
65
+ "guard-rules-registry": { name: "AWS Guard Rules Registry", url: "https://github.com/aws-cloudformation/aws-guard-rules-registry", license: "Apache-2.0", kind: "scanner" },
66
+ "polaris": { name: "Fairwinds Polaris", url: "https://polaris.docs.fairwinds.com/", license: "Apache-2.0", kind: "scanner" },
67
+ "kube-linter": { name: "KubeLinter", url: "https://github.com/stackrox/kube-linter", license: "Apache-2.0", kind: "scanner" },
68
+ "kube-score": { name: "kube-score", url: "https://github.com/zegl/kube-score", license: "MIT", kind: "scanner" },
69
+ "psrule-azure": { name: "PSRule for Azure", url: "https://azure.github.io/PSRule.Rules.Azure/", license: "MIT", kind: "scanner" },
70
+ "gcp-policy-library": { name: "Google Cloud Policy Library", url: "https://github.com/GoogleCloudPlatform/policy-library", license: "Apache-2.0", kind: "scanner" },
71
+ "poutine": { name: "poutine", url: "https://boostsecurityio.github.io/poutine/", license: "Apache-2.0", kind: "scanner" },
72
+ "scorecard": { name: "OpenSSF Scorecard", url: "https://github.com/ossf/scorecard", license: "Apache-2.0", kind: "scanner" },
73
+ "cfn-lint": { name: "cfn-lint", url: "https://github.com/aws-cloudformation/cfn-lint", license: "MIT-0", kind: "scanner" },
74
+ "controlplane-docs": { name: "Control Plane reference documentation", url: "https://docs.controlplane.com", license: "n/a", kind: "specification" },
75
+ "gitleaks": { name: "gitleaks", url: "https://github.com/gitleaks/gitleaks", license: "MIT", kind: "scanner" },
76
+ "octoscan": { name: "octoscan", url: "https://github.com/synacktiv/octoscan", license: "GPL-3.0", kind: "scanner" },
77
+ "detect-secrets": { name: "detect-secrets", url: "https://github.com/Yelp/detect-secrets", license: "Apache-2.0", kind: "scanner" },
78
+ "actionlint": { name: "actionlint", url: "https://github.com/rhysd/actionlint", license: "MIT", kind: "scanner" },
79
+ "trufflehog": { name: "TruffleHog", url: "https://github.com/trufflesecurity/trufflehog", license: "AGPL-3.0", kind: "scanner" },
80
+ "gixy-ng": { name: "gixy-ng (maintained Gixy fork)", url: "https://github.com/dvershinin/gixy", license: "MPL-2.0", kind: "scanner" },
81
+ "kubesec": { name: "kubesec", url: "https://kubesec.io/", license: "Apache-2.0", kind: "scanner" },
82
+ "datree": { name: "Datree", url: "https://hub.datree.io/built-in-rules", license: "Apache-2.0", kind: "scanner" },
83
+ "helm-lint": { name: "helm lint", url: "https://helm.sh/docs/helm/helm_lint/", license: "Apache-2.0", kind: "vendor-validator" },
84
+ "cedar-validator": { name: "Cedar policy validator", url: "https://docs.cedarpolicy.com/policies/validation.html", license: "Apache-2.0", kind: "vendor-validator" },
85
+ "flux-docs": { name: "Flux Kustomization API reference", url: "https://fluxcd.io/flux/components/kustomize/kustomizations/", license: "n/a", kind: "specification" },
86
+ "hadolint": { name: "hadolint", url: "https://github.com/hadolint/hadolint", license: "GPL-3.0", kind: "scanner" },
87
+ "agent-audit": { name: "agent-audit", url: "https://github.com/piiiico/agent-audit", license: "MIT", kind: "scanner" },
88
+ "arm-ttk": { name: "ARM Template Toolkit (arm-ttk)", url: "https://github.com/Azure/arm-ttk", license: "MIT", kind: "scanner" },
89
+ "bicep-linter": { name: "Bicep linter", url: "https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/linter", license: "MIT", kind: "scanner" },
90
+ "cedar-cli": { name: "Cedar CLI", url: "https://github.com/cedar-policy/cedar/tree/main/cedar-policy-cli", license: "Apache-2.0", kind: "vendor-validator" },
91
+ "dockle": { name: "Dockle", url: "https://github.com/goodwithtech/dockle", license: "Apache-2.0", kind: "scanner" },
92
+ "mcp-audit": { name: "mcp-audit (APIsec)", url: "https://github.com/apisec-inc/mcp-audit", license: "MIT", kind: "scanner" },
93
+ "render-docs": { name: "Render Blueprint YAML reference", url: "https://render.com/docs/blueprint-spec", license: "n/a", kind: "specification" },
94
+ "agent-scan": { name: "Snyk Agent Scan (formerly mcp-scan)", url: "https://github.com/invariantlabs-ai/mcp-scan", license: "Apache-2.0", kind: "scanner" },
95
+ "cedar-docs": { name: "Cedar policy language reference", url: "https://docs.cedarpolicy.com/policies/json-format.html", license: "n/a", kind: "specification" },
96
+ "cedar-policy-crate": { name: "cedar-policy crate (PolicySet loader)", url: "https://docs.rs/cedar-policy/latest/cedar_policy/", license: "Apache-2.0", kind: "vendor-validator" },
97
+ "chart-testing": { name: "chart-testing (ct)", url: "https://github.com/helm/chart-testing", license: "Apache-2.0", kind: "vendor-validator" },
98
+ "fly-docs": { name: "Fly Machines API reference", url: "https://fly.io/docs/machines/api/machines-resource/", license: "n/a", kind: "specification" },
99
+ "gixy": { name: "Gixy", url: "https://github.com/yandex/gixy", license: "MPL-2.0", kind: "scanner" },
100
+ "kube-bench": { name: "kube-bench", url: "https://github.com/aquasecurity/kube-bench", license: "Apache-2.0", kind: "scanner" },
101
+ "pluto": { name: "Pluto", url: "https://github.com/FairwindsOps/pluto", license: "Apache-2.0", kind: "scanner" },
102
+ "choudoufu": { name: "choudoufu", url: "https://github.com/INTENTIUS/choudoufu", license: "MPL-2.0", kind: "scanner" },
103
+ // Terraform lint family (#2107). Registry entries recorded in
104
+ // lexicons/terraform/src/lint/audit-lineage.ts's header comment; added here
105
+ // the first time a shipped rule cites them (#2109).
106
+ "tflint-ruleset-terraform": { name: "tflint-ruleset-terraform", url: "https://github.com/terraform-linters/tflint-ruleset-terraform", license: "MPL-2.0", kind: "scanner" },
107
+ "terraform-sentinel-policies": { name: "HashiCorp reference Sentinel policies", url: "https://github.com/hashicorp/terraform-sentinel-policies", license: "MPL-2.0", kind: "specification" },
108
+ "hashicorp-style-guide": { name: "HashiCorp Terraform style guide", url: "https://developer.hashicorp.com/terraform/language/style", license: "n/a", kind: "specification" },
109
+ "aws-terraform-prescriptive-guidance": { name: "AWS prescriptive guidance for Terraform", url: "https://docs.aws.amazon.com/prescriptive-guidance/latest/terraform-aws-provider-best-practices/", license: "n/a", kind: "specification" },
110
+ // A multi-page series; a credit cites the specific page and fragment, not
111
+ // this landing URL. First cited by TF014 and TF015 (#2112).
112
+ "gcp-terraform-best-practices": { name: "Google Cloud Terraform best-practices series", url: "https://docs.cloud.google.com/docs/terraform/best-practices/general-style-structure", license: "n/a", kind: "specification" },
113
+ "tflint-ruleset-redeploy": { name: "tflint-ruleset-redeploy", url: "https://github.com/RedeployAB/tflint-ruleset-redeploy", license: "0BSD", kind: "scanner" },
114
+ "tflint-ruleset-avm": { name: "tflint-ruleset-avm (Azure Verified Modules)", url: "https://github.com/Azure/tflint-ruleset-avm", license: "MIT", kind: "scanner" },
115
+ // tfsec is frozen rather than archived, and its docs site 404s above v0.61.x,
116
+ // so its credits cite pinned repository paths (a tag or `master`), never the
117
+ // docs site. The three `general/secrets` checks chant credits live only here:
118
+ // trivy dropped them when it absorbed tfsec.
119
+ "tfsec": { name: "tfsec", url: "https://github.com/aquasecurity/tfsec", license: "MIT", kind: "scanner" },
120
+ // Semgrep's engine is LGPL-2.1 but its rules are not: they ship under the
121
+ // proprietary Semgrep Rules License v1.0, which forbids redistribution. So
122
+ // this entry carries no SPDX id, and no semgrep rule text is ever copied into
123
+ // this repository — a credit by name and link is the whole of it (#2107).
124
+ "semgrep": { name: "Semgrep Registry rules", url: "https://github.com/semgrep/semgrep-rules", license: "Semgrep Rules License v1.0 (proprietary, not an SPDX id)", kind: "scanner" },
125
+ } as const satisfies Record<string, PriorArtEntry>;
126
+
127
+ export type PriorArtTool = keyof typeof PRIOR_ART;
128
+
129
+ /**
130
+ * Attach lineage to a catalog in place. A lexicon keeps its lineage in a
131
+ * sibling `audit-lineage.ts` (one `Record<ruleId, Lineage[]>`) and calls this
132
+ * once at the bottom of its `audit-catalog.ts`, so the prose metadata and the
133
+ * credits stay next to each other but the credits can be regenerated by the
134
+ * prior-art sweep without touching the rule text. Throws at module load if the
135
+ * lineage names a rule the catalog does not have: a stale id is a bug, not a
136
+ * silent no-op.
137
+ */
138
+ export function applyLineage<T extends { id: string; lineage?: Lineage[] }>(
139
+ catalog: Record<string, T>,
140
+ lineage: Record<string, Lineage[]>,
141
+ ): void {
142
+ for (const [id, entries] of Object.entries(lineage)) {
143
+ const rule = catalog[id];
144
+ if (!rule) throw new Error(`audit lineage names ${id}, which this catalog does not define`);
145
+ if (entries.length > 0) rule.lineage = entries;
146
+ }
147
+ }
148
+
149
+ /** Render one lineage entry as "tool rule (relation)" for prose contexts. */
150
+ export function lineageLabel(l: Lineage): string {
151
+ const tool = (PRIOR_ART as Record<string, PriorArtEntry>)[l.tool]?.name ?? l.tool;
152
+ return l.rule ? `${tool} ${l.rule}` : tool;
153
+ }
@@ -124,6 +124,73 @@ describe("proveFix — guidance findings are not auto-fixed", () => {
124
124
  });
125
125
  });
126
126
 
127
+ /** A Terraform root with both of #2110's deterministic findings in it. */
128
+ const TF_ROOT = `variable "log_level" {
129
+ type = string
130
+ sensitive = false
131
+ }
132
+
133
+ resource "aws_s3_bucket" "assets" {
134
+ bucket = "\${var.assets_bucket}"
135
+ tags = {
136
+ name = "\${var.project}-assets"
137
+ }
138
+
139
+ lifecycle {
140
+ prevent_destroy = false
141
+ create_before_destroy = var.replace_first
142
+ }
143
+ }
144
+ `;
145
+
146
+ describe("proveFix — TF019, delete a meta-argument set to its default false", () => {
147
+ test("drops both redundant lines and leaves the expression alone", () => {
148
+ const res = proveFix("TF019", TF_ROOT);
149
+ expect(res.applied).toBe(true);
150
+ expect(res.patched).not.toContain("sensitive = false");
151
+ expect(res.patched).not.toContain("prevent_destroy = false");
152
+ expect(res.patched).toContain("create_before_destroy = var.replace_first");
153
+ expect(res.diff).toContain("- sensitive = false");
154
+ expect(res.diff).toContain("- prevent_destroy = false");
155
+ // Deletions only: nothing is added.
156
+ expect((res.diff ?? "").split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++"))).toEqual([]);
157
+ });
158
+
159
+ test("a root with no redundant default is a no-op, not a patch", () => {
160
+ const res = proveFix("TF019", 'resource "aws_s3_bucket" "assets" {\n bucket = var.assets_bucket\n}\n');
161
+ expect(res.applied).toBe(false);
162
+ expect(res.reason).toBe("noop");
163
+ });
164
+
165
+ test("a line with a trailing comment is left alone: deleting it would delete the comment", () => {
166
+ const res = proveFix("TF019", " sensitive = false # deliberate, see #123\n");
167
+ expect(res.applied).toBe(false);
168
+ });
169
+ });
170
+
171
+ describe("proveFix — TF016, unwrap an interpolation-only value", () => {
172
+ test("unquotes the single interpolation and leaves a real template alone", () => {
173
+ const res = proveFix("TF016", TF_ROOT);
174
+ expect(res.applied).toBe(true);
175
+ expect(res.patched).toContain("bucket = var.assets_bucket");
176
+ expect(res.patched).toContain('name = "${var.project}-assets"');
177
+ expect(res.diff).toContain("+ bucket = var.assets_bucket");
178
+ });
179
+
180
+ test("the two fixes combine into one patch, as a quick-win diff does", () => {
181
+ const first = proveFix("TF016", TF_ROOT);
182
+ const second = proveFix("TF019", first.patched ?? TF_ROOT);
183
+ expect(second.patched).toContain("bucket = var.assets_bucket");
184
+ expect(second.patched).not.toContain("prevent_destroy");
185
+ });
186
+
187
+ test("a root written the modern way is a no-op", () => {
188
+ const res = proveFix("TF016", 'output "arn" {\n value = aws_s3_bucket.assets.arn\n}\n');
189
+ expect(res.applied).toBe(false);
190
+ expect(res.reason).toBe("noop");
191
+ });
192
+ });
193
+
127
194
  describe("unifiedDiff", () => {
128
195
  test("identical input produces an empty diff", () => {
129
196
  expect(unifiedDiff(WF, WF)).toBe("");
@@ -147,6 +147,70 @@ function narrowWriteAll(content: string): { patched: string; changed: boolean }
147
147
  return { patched: content.replace(re, "permissions:\n contents: read"), changed: true };
148
148
  }
149
149
 
150
+ /**
151
+ * TF019 — drop a meta-argument explicitly set to its own default `false`
152
+ * (`sensitive`, `ephemeral`, `prevent_destroy`, `create_before_destroy`). The
153
+ * fix is the deletion of the whole line, which is what tflint-ruleset-redeploy's
154
+ * own autofix for `terraform_redundant_default` does. Only a literal `false` is
155
+ * touched: `prevent_destroy = var.protect` is left alone, and so is a line with
156
+ * a trailing comment, where deleting the line would delete prose too.
157
+ */
158
+ const REDUNDANT_DEFAULT_RE = /^[ \t]*(?:sensitive|ephemeral|prevent_destroy|create_before_destroy)[ \t]*=[ \t]*false[ \t]*$/;
159
+
160
+ function dropRedundantDefaults(content: string): { patched: string; changed: boolean } {
161
+ const kept = content.split("\n").filter((line) => !REDUNDANT_DEFAULT_RE.test(line));
162
+ const patched = kept.join("\n");
163
+ return { patched, changed: patched !== content };
164
+ }
165
+
166
+ /**
167
+ * TF016 — unwrap an attribute whose whole value is one interpolation
168
+ * (`x = "${var.y}"` becomes `x = var.y`), the deprecated pre-0.12 style
169
+ * tflint's `terraform_deprecated_interpolation` reports. Anchored on the
170
+ * source text rather than the parsed body for the reason TF016's own module
171
+ * gives: `@cdktf/hcl2json` renders a bare reference and a quoted interpolation
172
+ * identically, so the quotes only exist here.
173
+ */
174
+ const INTERPOLATION_ONLY_RE = /^([ \t]*)([A-Za-z_][A-Za-z0-9_-]*)([ \t]*=[ \t]*)"\$\{([^"]+)\}"([ \t]*(?:#.*)?)$/;
175
+
176
+ /** One `name = "${expr}"` line, split into the parts the fix reassembles. */
177
+ export interface InterpolationOnlyLine {
178
+ /** The attribute's name. */
179
+ name: string;
180
+ /** The expression inside the interpolation, without the `${` and `}`. */
181
+ expr: string;
182
+ /** The same line with the quotes and the `${}` taken off. */
183
+ unwrapped: string;
184
+ }
185
+
186
+ /**
187
+ * Parse a line whose whole value is one interpolation. Exported because a rule
188
+ * and its fix have to agree on what counts: the terraform lexicon's TF016
189
+ * check calls this over a block's source text, and `unwrapInterpolations`
190
+ * below rewrites exactly the lines it accepts.
191
+ */
192
+ export function interpolationOnlyLine(line: string): InterpolationOnlyLine | undefined {
193
+ const m = INTERPOLATION_ONLY_RE.exec(line);
194
+ if (!m) return undefined;
195
+ const expr = m[4];
196
+ // A nested `${` means the template is not one interpolation (it embeds
197
+ // another), and a `}` before the end means the interpolation closed early,
198
+ // so the quotes are carrying literal text as well.
199
+ if (expr.includes("${") || expr.includes("}")) return undefined;
200
+ return { name: m[2], expr, unwrapped: `${m[1]}${m[2]}${m[3]}${expr}${m[5]}` };
201
+ }
202
+
203
+ function unwrapInterpolations(content: string): { patched: string; changed: boolean } {
204
+ let changed = false;
205
+ const lines = content.split("\n").map((line) => {
206
+ const parsed = interpolationOnlyLine(line);
207
+ if (!parsed) return line;
208
+ changed = true;
209
+ return parsed.unwrapped;
210
+ });
211
+ return { patched: lines.join("\n"), changed };
212
+ }
213
+
150
214
  /**
151
215
  * Produce a deterministic fix + diff for a finding, if one is mechanical.
152
216
  * Returns `applied: false` with guidance for non-deterministic findings.
@@ -181,6 +245,12 @@ export function proveFix(checkId: string, content: string, opts: ProveOptions =
181
245
  case "GHA033":
182
246
  result = narrowWriteAll(content);
183
247
  break;
248
+ case "TF016":
249
+ result = unwrapInterpolations(content);
250
+ break;
251
+ case "TF019":
252
+ result = dropRedundantDefaults(content);
253
+ break;
184
254
  default:
185
255
  return notApplied(checkId, "needs-input", cat?.remediation || "No deterministic fix implemented for this rule yet.");
186
256
  }
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { AuditFinding } from "./core";
12
- import { ruleDocUrl, type RuleMeta } from "./catalog";
12
+ import { ruleDocUrl, type RuleMeta, lineageLabel } from "./catalog";
13
13
  import { buildReportModel, buildReportJson, type AuditSnapshot, type BuildModelOptions, type EnrichedFinding, type GuidanceCluster, type QuickWinFile, type ReportCounts } from "./report-model";
14
14
 
15
15
  export type { AuditSnapshot } from "./report-model";
@@ -48,9 +48,14 @@ function ruleLink(id: string): string {
48
48
 
49
49
  /** External backing for a merge-worthy rule (OSSF Scorecard, CIS, vendor docs). */
50
50
  function authorityLinks(meta: RuleMeta): string {
51
- if (!meta.authority?.length) return "";
52
- const links = meta.authority.map((a) => `<a href="${esc(a.url)}">${esc(a.name)}</a>`).join(", ");
53
- return ` <span class="muted">per ${links}</span>`;
51
+ const parts: string[] = [];
52
+ if (meta.authority?.length) {
53
+ parts.push(`per ${meta.authority.map((a) => `<a href="${esc(a.url)}">${esc(a.name)}</a>`).join(", ")}`);
54
+ }
55
+ if (meta.lineage?.length) {
56
+ parts.push(`prior art: ${meta.lineage.map((l) => `<a href="${esc(l.url)}">${esc(lineageLabel(l))}</a>`).join(", ")}`);
57
+ }
58
+ return parts.length ? ` <span class="muted">${parts.join(" · ")}</span>` : "";
54
59
  }
55
60
 
56
61
  function renderDiff(diff: string): string {
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { AuditFinding } from "./core";
9
9
  import type { UnclaimedFile } from "./discover";
10
- import { RULE_CATALOG, ruleDocUrl, type Authority, type Category, type FixKind, type RuleMeta, type Tier } from "./catalog";
10
+ import { RULE_CATALOG, ruleDocUrl, type Authority, type Category, type FixKind, type Lineage, type RuleMeta, type Tier } from "./catalog";
11
11
  import { proveFix, unifiedDiff, type ProveOptions } from "./proof";
12
12
  import type { Severity } from "../lint/rule";
13
13
 
@@ -98,6 +98,8 @@ export interface SerializedFinding {
98
98
  line?: number;
99
99
  /** Redaction-safe fingerprint of a flagged value, when applicable (see `secrets.ts`). */
100
100
  fingerprint?: string;
101
+ /** Set when the finding is an absence rather than a defect — see `PostSynthDiagnostic.missing` (chant #2113). */
102
+ missing?: { kind: string; scope: string };
101
103
  lexicon: string;
102
104
  tier: Tier;
103
105
  fixKind: FixKind;
@@ -106,6 +108,8 @@ export interface SerializedFinding {
106
108
  title: string;
107
109
  remediation: string;
108
110
  authority: Authority[];
111
+ /** Prior art: upstream tools whose rules check the same condition (credit, not authority). */
112
+ lineage: Lineage[];
109
113
  /** Link to this rule's entry in the audit rules reference. */
110
114
  docUrl: string;
111
115
  }
@@ -246,7 +250,13 @@ export function buildReportModel(findings: AuditFinding[], opts: BuildModelOptio
246
250
  const contents = new Map((opts.files ?? []).map((f) => [f.path, f.content]));
247
251
 
248
252
  const mergeWorthy = enriched.filter((f) => f.meta.tier === "merge-worthy");
249
- const quickWinFindings = mergeWorthy.filter((f) => f.meta.fixKind === "deterministic");
253
+ // A deterministic fix is a quick win whatever the finding's tier. Every
254
+ // deterministic rule was merge-worthy until terraform's TF016 and TF019
255
+ // (#2110): both are hygiene, and both delete or unquote exactly the flagged
256
+ // line, which is the same safe mechanical patch a merge-worthy quick win
257
+ // offers. Filing them under "needs a judgement call" or in the hygiene table
258
+ // would hide a ready-to-apply diff.
259
+ const quickWinFindings = enriched.filter((f) => f.meta.fixKind === "deterministic");
250
260
  const needsReviewFindings = mergeWorthy.filter((f) => f.meta.fixKind === "guidance");
251
261
 
252
262
  // De-noise: drop a report-only finding only when a specific merge-worthy
@@ -254,6 +264,8 @@ export function buildReportModel(findings: AuditFinding[], opts: BuildModelOptio
254
264
  const mwOnEntity = new Set(mergeWorthy.filter((f) => f.entity).map((f) => `${f.file}:${f.entity}:${f.checkId}`));
255
265
  const reportOnly = enriched.filter((f) => {
256
266
  if (f.meta.tier !== "report-only") return false;
267
+ // Counted and rendered as a quick win instead, so the three buckets stay disjoint.
268
+ if (f.meta.fixKind === "deterministic") return false;
257
269
  const supers = SUPERSEDED_BY[f.checkId];
258
270
  if (supers && f.entity && supers.some((id) => mwOnEntity.has(`${f.file}:${f.entity}:${id}`))) return false;
259
271
  return true;
@@ -304,6 +316,7 @@ export function buildReportJson(
304
316
  entity: f.entity,
305
317
  line: f.line,
306
318
  fingerprint: f.fingerprint,
319
+ missing: f.missing,
307
320
  lexicon: f.lexicon,
308
321
  tier: f.meta.tier,
309
322
  fixKind: f.meta.fixKind,
@@ -311,6 +324,7 @@ export function buildReportJson(
311
324
  title: f.meta.title,
312
325
  remediation: f.meta.remediation,
313
326
  authority: f.meta.authority ?? [],
327
+ lineage: f.meta.lineage ?? [],
314
328
  docUrl: ruleDocUrl(f.checkId),
315
329
  })),
316
330
  unclaimed: opts.unclaimed && opts.unclaimed.length > 0 ? opts.unclaimed : undefined,
@@ -149,6 +149,39 @@ describe("efficiency dimension (#444)", () => {
149
149
  });
150
150
  });
151
151
 
152
+ describe("a report-only rule with a deterministic fix (#2110)", () => {
153
+ const TF_ROOT = `variable "log_level" {
154
+ type = string
155
+ sensitive = false
156
+ }
157
+ `;
158
+ const findings: AuditFinding[] = [
159
+ {
160
+ checkId: "TF019",
161
+ severity: "info",
162
+ message: "sensitive = false is already the default.",
163
+ file: "infra/prod",
164
+ lexicon: "terraform",
165
+ entity: "infra/prod/var.log_level.sensitive",
166
+ },
167
+ ];
168
+
169
+ test("renders in quick wins with its diff, not in the hygiene table", async () => {
170
+ const catalog = await resolveAuditCatalog(["terraform"]);
171
+ const out = renderMarkdown(findings, { catalog, files: [{ path: "infra/prod", content: TF_ROOT }] });
172
+ expect(out).toContain("## Quick wins (deterministic)");
173
+ expect(out).toContain("- sensitive = false");
174
+ expect(out).toContain("1 finding — 1 quick-win, 0 needs-review, 0 report-only");
175
+ expect(out).not.toContain("Report-only (hygiene)");
176
+ });
177
+
178
+ test("the credit rides beside the fix", async () => {
179
+ const catalog = await resolveAuditCatalog(["terraform"]);
180
+ const out = renderMarkdown(findings, { catalog, files: [{ path: "infra/prod", content: TF_ROOT }] });
181
+ expect(out).toContain("prior art: [tflint-ruleset-redeploy terraform_redundant_default]");
182
+ });
183
+ });
184
+
152
185
  describe("catalog threading (#687)", () => {
153
186
  test("buildReportModel uses a passed-in catalog over core's static one", async () => {
154
187
  const { buildReportModel } = await import("./report-model");
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import type { AuditFinding } from "./core";
14
- import { ruleDocUrl, type RuleMeta } from "./catalog";
14
+ import { lineageLabel, ruleDocUrl, type RuleMeta } from "./catalog";
15
15
  import type { ProveOptions } from "./proof";
16
16
  import { buildReportModel, type EnrichedFinding, type GuidanceCluster, type QuickWinFile } from "./report-model";
17
17
 
@@ -22,8 +22,10 @@ function ruleLink(id: string): string {
22
22
 
23
23
  /** External backing for a merge-worthy rule (OSSF Scorecard, CIS, vendor docs). */
24
24
  function authorityLinks(meta: RuleMeta): string {
25
- if (!meta.authority?.length) return "";
26
- return ` — per ${meta.authority.map((a) => `[${a.name}](${a.url})`).join(", ")}`;
25
+ const authority = meta.authority?.length ? ` — per ${meta.authority.map((a) => `[${a.name}](${a.url})`).join(", ")}` : "";
26
+ // Prior art rides beside the authority: the tool that checked this first.
27
+ const lineage = meta.lineage?.length ? ` · prior art: ${meta.lineage.map((l) => `[${lineageLabel(l)}](${l.url})`).join(", ")}` : "";
28
+ return authority + lineage;
27
29
  }
28
30
 
29
31
  export interface RenderOptions {
@@ -1,8 +1,8 @@
1
1
  import { describe, test, expect } from "vitest";
2
2
  import { readFileSync } from "fs";
3
3
  import { fileURLToPath } from "url";
4
- import { renderRulesReference } from "./rules-doc";
5
- import { resolveAuditCatalog, ruleDocUrl } from "./catalog";
4
+ import { renderRulesReference, ruleBlock } from "./rules-doc";
5
+ import { resolveAuditCatalog, ruleDocUrl, auditRule } from "./catalog";
6
6
  import { AUDIT_LEXICONS } from "./discover";
7
7
 
8
8
  const PAGE = fileURLToPath(new URL("../../../../docs/src/content/docs/lint-rules/audit-rules.mdx", import.meta.url));
@@ -23,3 +23,35 @@ describe("audit rules reference", () => {
23
23
  }
24
24
  });
25
25
  });
26
+
27
+ describe("ruleBlock, aliases and deprecated (chant #2113)", () => {
28
+ test("renders aliases as a list", () => {
29
+ const m = { ...auditRule("NEW001", "report-only", "guidance", "T", "r"), aliases: ["OLD001", "LEGACY_OLD"] };
30
+ const block = ruleBlock(m);
31
+ expect(block).toContain("Also known as: `OLD001`, `LEGACY_OLD`.");
32
+ });
33
+
34
+ test("greys a deprecated rule (tags line, not the heading; the heading is the anchor `ruleDocUrl` targets)", () => {
35
+ const m = { ...auditRule("OLD002", "report-only", "guidance", "T", "r") };
36
+ m.deprecated = "superseded by NEW002";
37
+ const block = ruleBlock(m);
38
+ expect(block).toContain("### OLD002"); // heading unchanged: `ruleDocUrl("OLD002")` must still resolve
39
+ expect(block).toContain("~~deprecated~~");
40
+ expect(block).toContain(":::note[Deprecated]");
41
+ expect(block).toContain("superseded by NEW002");
42
+ });
43
+
44
+ test("a deprecated rule with no string reason gets a generic note", () => {
45
+ const m = { ...auditRule("OLD003", "report-only", "guidance", "T", "r") };
46
+ m.deprecated = true;
47
+ expect(ruleBlock(m)).toContain("This rule no longer runs.");
48
+ });
49
+
50
+ test("a rule with neither aliases nor deprecated renders unchanged from before #2113", () => {
51
+ const m = auditRule("PLAIN001", "report-only", "guidance", "T", "r");
52
+ const block = ruleBlock(m);
53
+ expect(block).not.toContain("Also known as");
54
+ expect(block).not.toContain(":::note[Deprecated]");
55
+ expect(block).not.toContain("~~deprecated~~");
56
+ });
57
+ });
@@ -7,7 +7,7 @@
7
7
  * (lowercased) — the anchor `ruleDocUrl()` links to.
8
8
  */
9
9
 
10
- import { resolveAuditCatalog, type RuleMeta } from "./catalog";
10
+ import { PRIOR_ART, resolveAuditCatalog, type Lineage, type PriorArtEntry, type RuleMeta } from "./catalog";
11
11
  import { AUDIT_LEXICONS } from "./discover";
12
12
 
13
13
  const GROUPS: Array<{ heading: string; prefixes: string[]; blurb: string }> = [
@@ -21,6 +21,7 @@ const GROUPS: Array<{ heading: string; prefixes: string[]; blurb: string }> = [
21
21
  { heading: "GCP Config Connector (WGC)", prefixes: ["WGC"], blurb: "Run against Config Connector (cnrm.cloud.google.com) manifests." },
22
22
  { heading: "Helm (WHM)", prefixes: ["WHM"], blurb: "Run against Helm charts (Chart.yaml + templates)." },
23
23
  { heading: "fountain (FTN)", prefixes: ["FTN"], blurb: "Run against fountain manifests (`apiVersion: fountain.dev/v1`) — standalone `fountain apply` YAML is parsed back into the entity graph, so the same rules fire on `chant build` and `chant audit`." },
24
+ { heading: "Terraform (TF)", prefixes: ["TF"], blurb: "Run against Terraform root modules (a directory of `.tf` files) parsed into the entity graph, so the same rules fire on `chant build` over `terraform.roots` and on `chant audit` (#2083)." },
24
25
  { heading: "Secrets & credentials (SEC)", prefixes: ["SEC"], blurb: "Lexicon-independent — scans the raw text of every scanned file for likely credentials, regardless of which audit lexicons are installed. Matched values are always redacted; see [suppressing false positives](/chant/cli/audit/#suppressing-a-secrets-finding)." },
25
26
  { heading: "Wrangler config (WRG)", prefixes: ["WRG"], blurb: "Lexicon-independent, audit-only (#446) — scans `wrangler.toml`, Cloudflare Workers' native deploy config, which the engine cannot otherwise parse (it is not YAML/JSON). No authoring surface: chant does not write Wrangler config, it only reads it for these checks." },
26
27
  { heading: "nginx config (NGX)", prefixes: ["NGX"], blurb: "Lexicon-independent, audit-only (#1979) — parses nginx's native directive/block config (`nginx.conf` and `.conf` files under nginx-ish directories, confirmed by content). No authoring surface: chant does not write nginx config, it only reads it for these checks." },
@@ -41,12 +42,84 @@ function mdxEscape(text: string): string {
41
42
  return text.replace(/\{/g, "\\{").replace(/</g, "\\<");
42
43
  }
43
44
 
44
- function ruleBlock(m: RuleMeta): string {
45
- const tags = `${m.tier} · ${m.fixKind}`;
46
- const authority = m.authority?.length
47
- ? `\n\nAuthority: ${m.authority.map((a) => `[${a.name}](${a.url})`).join(" · ")}`
48
- : "";
49
- return `### ${m.id}\n\n**${mdxEscape(m.title)}** — ${tags}\n\n${mdxEscape(m.remediation)}${authority}`;
45
+ function lineageLink(l: Lineage): string {
46
+ const tool = (PRIOR_ART as Record<string, PriorArtEntry>)[l.tool]?.name ?? l.tool;
47
+ const label = l.rule ? `${tool} \`${l.rule}\`` : tool;
48
+ return `[${mdxEscape(label)}](${l.url}) (${l.relation})`;
49
+ }
50
+
51
+ /**
52
+ * A rule's backing, as a two-column table: the authority that says the finding
53
+ * matters, and the prior art that checked it first. A table rather than two
54
+ * "Label: ..." lines so that 379 rules do not read as 379 repeated sentences.
55
+ */
56
+ function backingTable(m: RuleMeta): string {
57
+ const rows: string[] = [];
58
+ if (m.authority?.length) rows.push(`| Authority | ${m.authority.map((a) => `[${a.name}](${a.url})`).join(" · ")} |`);
59
+ if (m.lineage?.length) rows.push(`| Prior art | ${m.lineage.map(lineageLink).join(" · ")} |`);
60
+ return rows.length ? `\n\n| | |\n|---|---|\n${rows.join("\n")}` : "";
61
+ }
62
+
63
+ /**
64
+ * A rule kept the day it retires (chant #2113) rather than dropped: still
65
+ * gets a heading (so old links and `lint.rules`/suppression entries that
66
+ * name it still land somewhere), but greyed via a `:::note` aside instead of
67
+ * reading like a live rule, and with the `deprecated` string, when one was
68
+ * given, saying what replaced it.
69
+ */
70
+ function deprecatedNote(deprecated: boolean | string): string {
71
+ const reason = typeof deprecated === "string" ? mdxEscape(deprecated) : "This rule no longer runs.";
72
+ return `\n\n:::note[Deprecated]\n${reason} The id is kept so a reference to it still resolves.\n:::`;
73
+ }
74
+
75
+ function aliasesLine(aliases: string[]): string {
76
+ return `\n\nAlso known as: ${aliases.map((a) => `\`${mdxEscape(a)}\``).join(", ")}.`;
77
+ }
78
+
79
+ /** Exported for `rules-doc.test.ts` to exercise the alias/deprecated rendering directly against a synthetic `RuleMeta`, without needing a real catalog entry to carry either (chant #2113). */
80
+ export function ruleBlock(m: RuleMeta): string {
81
+ // The heading text is what Starlight slugifies into `#<id>`, the anchor
82
+ // `ruleDocUrl()` links to (`./catalog.ts`), so it stays exactly `### <ID>`
83
+ // even for a deprecated rule. The "greyed out" treatment goes below the
84
+ // heading instead, so an old link/alias reference still lands on the id.
85
+ const tags = m.deprecated ? `${m.tier} · ${m.fixKind} · ~~deprecated~~` : `${m.tier} · ${m.fixKind}`;
86
+ return (
87
+ `### ${m.id}\n\n**${mdxEscape(m.title)}** — ${tags}\n\n${mdxEscape(m.remediation)}` +
88
+ `${backingTable(m)}` +
89
+ `${m.aliases?.length ? aliasesLine(m.aliases) : ""}` +
90
+ `${m.deprecated ? deprecatedNote(m.deprecated) : ""}`
91
+ );
92
+ }
93
+
94
+ /**
95
+ * The closing section: every tool the rules credit, with its licence and how
96
+ * many rules point at it. Rendered from the same registry the entries key into,
97
+ * so a tool cannot be credited on a rule without appearing here.
98
+ */
99
+ function priorArtSection(catalog: Record<string, RuleMeta>): string {
100
+ const counts = new Map<string, number>();
101
+ for (const m of Object.values(catalog)) {
102
+ for (const l of m.lineage ?? []) counts.set(l.tool, (counts.get(l.tool) ?? 0) + 1);
103
+ }
104
+ const rows = (Object.entries(PRIOR_ART) as Array<[string, PriorArtEntry]>)
105
+ .filter(([key]) => counts.has(key))
106
+ .sort(([a], [b]) => (counts.get(b) ?? 0) - (counts.get(a) ?? 0) || a.localeCompare(b))
107
+ .map(([key, t]) => `| [${t.name}](${t.url}) | ${t.kind} | ${t.license} | ${counts.get(key)} |`);
108
+ if (rows.length === 0) return "";
109
+ return `## Prior art
110
+
111
+ chant's rules were written against the output chant emits, but much of what they check was checked first by a dedicated tool. Each rule above that has a counterpart names it under **Prior art**, with the upstream rule id and how the two relate.
112
+
113
+ - \`equivalent\` is the same condition at the same scope.
114
+ - \`overlaps\` shares the core condition, with one side narrower or broader, or expressed for a different dialect of the same control.
115
+ - \`extends\` means chant checks a strict superset of the upstream rule.
116
+
117
+ This is credit rather than authority, and it never changes a rule's tier or category. A credit counts once per upstream rule cited, so a chant rule that names two zizmor audits adds two.
118
+
119
+ | Tool | Kind | Licence | Credits |
120
+ |---|---|---|---|
121
+ ${rows.join("\n")}
122
+ `;
50
123
  }
51
124
 
52
125
  /**
@@ -73,8 +146,8 @@ diataxis: reference
73
146
 
74
147
  This is the reference for every rule [\`chant audit\`](/chant/cli/audit/) can report. Each finding in a report links to its rule here.
75
148
 
76
- Each rule is tagged with its **tier** — \`merge-worthy\` (a security or correctness issue worth a PR) or \`report-only\` (hygiene) — and its **fix kind** — \`deterministic\` (a safe mechanical fix the report can apply as a diff) or \`guidance\` (needs a judgement call).
149
+ Each rule is tagged with its **tier** — \`merge-worthy\` (a security or correctness issue worth a PR) or \`report-only\` (hygiene) — and its **fix kind** — \`deterministic\` (a safe mechanical fix the report can apply as a diff) or \`guidance\` (needs a judgement call). Where an open-source tool checks the same thing, the rule says so under **Prior art**; the [Prior art](#prior-art) section at the end lists every tool the rules on this page credit.
77
150
 
78
- ${sections.join("\n\n")}
151
+ ${[...sections, priorArtSection(catalog)].filter(Boolean).join("\n\n")}
79
152
  `;
80
153
  }