@intentius/chant 0.57.0 → 0.59.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 (558) 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 +36 -1
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +26 -9
  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/carve-bridge.d.ts.map +1 -1
  34. package/dist/cli/commands/check-lexicon-docs.d.ts +1 -1
  35. package/dist/cli/commands/check-lexicon-examples.d.ts +16 -6
  36. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  37. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts +1 -0
  38. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  39. package/dist/cli/commands/init-lexicon/templates/project.d.ts +7 -0
  40. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  41. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  42. package/dist/cli/commands/init.d.ts.map +1 -1
  43. package/dist/cli/commands/lint.d.ts +13 -0
  44. package/dist/cli/commands/lint.d.ts.map +1 -1
  45. package/dist/cli/commands/onboard.d.ts +2 -0
  46. package/dist/cli/commands/onboard.d.ts.map +1 -1
  47. package/dist/cli/handlers/build.d.ts.map +1 -1
  48. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  49. package/dist/cli/handlers/operator.d.ts +82 -16
  50. package/dist/cli/handlers/operator.d.ts.map +1 -1
  51. package/dist/cli/handlers/run.d.ts +49 -42
  52. package/dist/cli/handlers/run.d.ts.map +1 -1
  53. package/dist/cli/main.d.ts +12 -1
  54. package/dist/cli/main.d.ts.map +1 -1
  55. package/dist/cli/mcp/op-tools.d.ts +16 -1
  56. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  57. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  58. package/dist/cli/mcp/resources/context.d.ts.map +1 -1
  59. package/dist/cli/plugins.d.ts +22 -0
  60. package/dist/cli/plugins.d.ts.map +1 -1
  61. package/dist/cli/registry.d.ts +40 -9
  62. package/dist/cli/registry.d.ts.map +1 -1
  63. package/dist/codegen/generate-post-synth-barrel.d.ts +1 -1
  64. package/dist/components/auto-release.d.ts +10 -20
  65. package/dist/components/auto-release.d.ts.map +1 -1
  66. package/dist/components/capability.d.ts +4 -4
  67. package/dist/components/cli-support.d.ts +24 -22
  68. package/dist/components/cli-support.d.ts.map +1 -1
  69. package/dist/components/component.d.ts +21 -6
  70. package/dist/components/component.d.ts.map +1 -1
  71. package/dist/components/driver-output.d.ts.map +1 -1
  72. package/dist/components/driver.d.ts +73 -37
  73. package/dist/components/driver.d.ts.map +1 -1
  74. package/dist/components/index.d.ts +1 -2
  75. package/dist/components/index.d.ts.map +1 -1
  76. package/dist/components/manifest-persistence.d.ts +1 -9
  77. package/dist/components/manifest-persistence.d.ts.map +1 -1
  78. package/dist/components/run-progress.d.ts +6 -6
  79. package/dist/components/run-progress.d.ts.map +1 -1
  80. package/dist/components/verbs/run-agent.d.ts +10 -22
  81. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  82. package/dist/components/verbs/verify.d.ts +2 -2
  83. package/dist/deep-observation.d.ts +17 -0
  84. package/dist/deep-observation.d.ts.map +1 -1
  85. package/dist/detectLexicon.d.ts +13 -0
  86. package/dist/detectLexicon.d.ts.map +1 -1
  87. package/dist/discovery/files.d.ts +4 -4
  88. package/dist/discovery/fold-import.d.ts.map +1 -1
  89. package/dist/discovery/param-deps.d.ts +33 -0
  90. package/dist/discovery/param-deps.d.ts.map +1 -1
  91. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  92. package/dist/discovery/sandbox/config-wire.d.ts +2 -2
  93. package/dist/fold/foldable-helpers.d.ts +5 -1
  94. package/dist/fold/foldable-helpers.d.ts.map +1 -1
  95. package/dist/fold-provenance.d.ts +236 -0
  96. package/dist/fold-provenance.d.ts.map +1 -0
  97. package/dist/graph-ops.d.ts +1 -1
  98. package/dist/held-elsewhere.d.ts +85 -0
  99. package/dist/held-elsewhere.d.ts.map +1 -0
  100. package/dist/index.d.ts +3 -0
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/kubectl-context.d.ts +3 -3
  103. package/dist/lexicon-config.d.ts +4 -4
  104. package/dist/lexicon-plugin-helpers.d.ts +1 -1
  105. package/dist/lexicon.d.ts +273 -16
  106. package/dist/lexicon.d.ts.map +1 -1
  107. package/dist/lifecycle/change-set.d.ts +13 -0
  108. package/dist/lifecycle/change-set.d.ts.map +1 -1
  109. package/dist/lifecycle/deep-diff.d.ts +133 -6
  110. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  111. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  112. package/dist/lifecycle/gate-ledger.d.ts +79 -3
  113. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  114. package/dist/lifecycle/index.d.ts +1 -0
  115. package/dist/lifecycle/index.d.ts.map +1 -1
  116. package/dist/lifecycle/release-ledger.d.ts +2 -2
  117. package/dist/lifecycle/run-ledger.d.ts +53 -0
  118. package/dist/lifecycle/run-ledger.d.ts.map +1 -0
  119. package/dist/lifecycle/symptoms.d.ts +3 -3
  120. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  121. package/dist/lint/config.d.ts +81 -2
  122. package/dist/lint/config.d.ts.map +1 -1
  123. package/dist/lint/post-synth.d.ts +55 -2
  124. package/dist/lint/post-synth.d.ts.map +1 -1
  125. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +1 -1
  126. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -1
  127. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +1 -1
  128. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -1
  129. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts +44 -0
  130. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -0
  131. package/dist/lint/rules/comp/index.d.ts +1 -1
  132. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  133. package/dist/lint/rules/op/index.d.ts +16 -0
  134. package/dist/lint/rules/op/index.d.ts.map +1 -0
  135. package/dist/lint/rules/op/ops012-activity-contract.d.ts +27 -0
  136. package/dist/lint/rules/op/ops012-activity-contract.d.ts.map +1 -0
  137. package/dist/lint/rules/op/ops013-step-output-ref.d.ts +61 -0
  138. package/dist/lint/rules/op/ops013-step-output-ref.d.ts.map +1 -0
  139. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts +55 -0
  140. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -0
  141. package/dist/lint/rules/op/support.d.ts +12 -0
  142. package/dist/lint/rules/op/support.d.ts.map +1 -0
  143. package/dist/lint/suppressions.d.ts +108 -0
  144. package/dist/lint/suppressions.d.ts.map +1 -0
  145. package/dist/live-endpoint.d.ts +1 -1
  146. package/dist/op/activities/activity-contracts.d.ts +54 -0
  147. package/dist/op/activities/activity-contracts.d.ts.map +1 -0
  148. package/dist/op/activities/apply.d.ts +262 -0
  149. package/dist/op/activities/apply.d.ts.map +1 -0
  150. package/dist/op/activities/build.d.ts +13 -0
  151. package/dist/op/activities/build.d.ts.map +1 -0
  152. package/dist/op/activities/converge.d.ts +110 -0
  153. package/dist/op/activities/converge.d.ts.map +1 -0
  154. package/dist/op/activities/env-teardown.d.ts +76 -0
  155. package/dist/op/activities/env-teardown.d.ts.map +1 -0
  156. package/dist/op/activities/guard-validate.d.ts +64 -0
  157. package/dist/op/activities/guard-validate.d.ts.map +1 -0
  158. package/dist/op/activities/http-check.d.ts +34 -0
  159. package/dist/op/activities/http-check.d.ts.map +1 -0
  160. package/dist/op/activities/index.d.ts +31 -0
  161. package/dist/op/activities/index.d.ts.map +1 -0
  162. package/dist/op/activities/lexicon-upgrade.d.ts +182 -0
  163. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -0
  164. package/dist/op/activities/lifecycle.d.ts +42 -0
  165. package/dist/op/activities/lifecycle.d.ts.map +1 -0
  166. package/dist/op/activities/pipeline-audit.d.ts +84 -0
  167. package/dist/op/activities/pipeline-audit.d.ts.map +1 -0
  168. package/dist/op/activities/policy.d.ts +17 -0
  169. package/dist/op/activities/policy.d.ts.map +1 -0
  170. package/dist/op/activities/reconcile.d.ts +149 -0
  171. package/dist/op/activities/reconcile.d.ts.map +1 -0
  172. package/dist/op/activities/shell.d.ts +13 -0
  173. package/dist/op/activities/shell.d.ts.map +1 -0
  174. package/dist/op/activities/teardown.d.ts +10 -0
  175. package/dist/op/activities/teardown.d.ts.map +1 -0
  176. package/dist/op/activities/util.d.ts +7 -0
  177. package/dist/op/activities/util.d.ts.map +1 -0
  178. package/dist/op/activities/wait.d.ts +28 -0
  179. package/dist/op/activities/wait.d.ts.map +1 -0
  180. package/dist/op/activities/workflow-audit.d.ts +92 -0
  181. package/dist/op/activities/workflow-audit.d.ts.map +1 -0
  182. package/dist/op/activity-contract-registry.d.ts +76 -0
  183. package/dist/op/activity-contract-registry.d.ts.map +1 -0
  184. package/dist/op/activity-contract.d.ts +25 -18
  185. package/dist/op/activity-contract.d.ts.map +1 -1
  186. package/dist/op/activity-failure.d.ts +28 -0
  187. package/dist/op/activity-failure.d.ts.map +1 -0
  188. package/dist/op/activity-profiles.d.ts +118 -0
  189. package/dist/op/activity-profiles.d.ts.map +1 -0
  190. package/dist/op/activity-registry.d.ts +22 -38
  191. package/dist/op/activity-registry.d.ts.map +1 -1
  192. package/dist/op/activity-runtime.d.ts +3 -14
  193. package/dist/op/activity-runtime.d.ts.map +1 -1
  194. package/dist/op/builders.d.ts +85 -30
  195. package/dist/op/builders.d.ts.map +1 -1
  196. package/dist/op/change-signal.d.ts +91 -0
  197. package/dist/op/change-signal.d.ts.map +1 -0
  198. package/dist/op/composites/apply-op.d.ts +118 -0
  199. package/dist/op/composites/apply-op.d.ts.map +1 -0
  200. package/dist/op/composites/converge-op.d.ts +120 -0
  201. package/dist/op/composites/converge-op.d.ts.map +1 -0
  202. package/dist/op/composites/index.d.ts +26 -0
  203. package/dist/op/composites/index.d.ts.map +1 -0
  204. package/dist/op/composites/lexicon-upgrade-op.d.ts +78 -0
  205. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -0
  206. package/dist/op/composites/pipeline-audit-op.d.ts +50 -0
  207. package/dist/op/composites/pipeline-audit-op.d.ts.map +1 -0
  208. package/dist/op/composites/reconcile-op.d.ts +57 -0
  209. package/dist/op/composites/reconcile-op.d.ts.map +1 -0
  210. package/dist/op/composites/watch-op.d.ts +60 -0
  211. package/dist/op/composites/watch-op.d.ts.map +1 -0
  212. package/dist/op/composites/workflow-audit-op.d.ts +57 -0
  213. package/dist/op/composites/workflow-audit-op.d.ts.map +1 -0
  214. package/dist/op/converge-rule.d.ts +12 -10
  215. package/dist/op/converge-rule.d.ts.map +1 -1
  216. package/dist/op/cron.d.ts +51 -0
  217. package/dist/op/cron.d.ts.map +1 -0
  218. package/dist/op/discover.d.ts +9 -0
  219. package/dist/op/discover.d.ts.map +1 -1
  220. package/dist/op/duration.d.ts +11 -0
  221. package/dist/op/duration.d.ts.map +1 -0
  222. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  223. package/dist/op/gate-name.d.ts +40 -0
  224. package/dist/op/gate-name.d.ts.map +1 -0
  225. package/dist/op/gate-summary.d.ts +43 -0
  226. package/dist/op/gate-summary.d.ts.map +1 -0
  227. package/dist/op/gate.d.ts +90 -0
  228. package/dist/op/gate.d.ts.map +1 -0
  229. package/dist/op/generate-pipeline.d.ts +9 -0
  230. package/dist/op/generate-pipeline.d.ts.map +1 -1
  231. package/dist/op/index.d.ts +29 -9
  232. package/dist/op/index.d.ts.map +1 -1
  233. package/dist/op/local-executor.d.ts +114 -28
  234. package/dist/op/local-executor.d.ts.map +1 -1
  235. package/dist/op/local-output.d.ts +11 -1
  236. package/dist/op/local-output.d.ts.map +1 -1
  237. package/dist/op/op-ir.d.ts +185 -0
  238. package/dist/op/op-ir.d.ts.map +1 -0
  239. package/dist/op/op-verb-class.d.ts +10 -2
  240. package/dist/op/op-verb-class.d.ts.map +1 -1
  241. package/dist/op/operator.d.ts +93 -5
  242. package/dist/op/operator.d.ts.map +1 -1
  243. package/dist/op/receipt-store.d.ts +1 -1
  244. package/dist/op/receipt-store.d.ts.map +1 -1
  245. package/dist/op/resource.d.ts +15 -1
  246. package/dist/op/resource.d.ts.map +1 -1
  247. package/dist/op/runtime.d.ts +200 -0
  248. package/dist/op/runtime.d.ts.map +1 -0
  249. package/dist/op/runtimes/local.d.ts +29 -0
  250. package/dist/op/runtimes/local.d.ts.map +1 -0
  251. package/dist/op/step-output-ref.d.ts +15 -13
  252. package/dist/op/step-output-ref.d.ts.map +1 -1
  253. package/dist/op/types.d.ts +96 -29
  254. package/dist/op/types.d.ts.map +1 -1
  255. package/dist/ownership.d.ts +1 -1
  256. package/dist/provenance.d.ts +21 -0
  257. package/dist/provenance.d.ts.map +1 -1
  258. package/dist/serializer-walker.d.ts.map +1 -1
  259. package/dist/terraform/bridge.d.ts +26 -7
  260. package/dist/terraform/bridge.d.ts.map +1 -1
  261. package/dist/terraform/carve-provider.d.ts +28 -2
  262. package/dist/terraform/carve-provider.d.ts.map +1 -1
  263. package/dist/terraform/data-source-shape.d.ts +77 -0
  264. package/dist/terraform/data-source-shape.d.ts.map +1 -0
  265. package/dist/terraform/graph.d.ts.map +1 -1
  266. package/dist/terraform/providers/kubernetes.d.ts +5 -0
  267. package/dist/terraform/providers/kubernetes.d.ts.map +1 -1
  268. package/dist/terraform/tier-map.d.ts +16 -4
  269. package/dist/terraform/tier-map.d.ts.map +1 -1
  270. package/dist/terraform/types.d.ts +8 -0
  271. package/dist/terraform/types.d.ts.map +1 -1
  272. package/package.json +6 -1
  273. package/src/audit/catalog.test.ts +79 -3
  274. package/src/audit/catalog.ts +111 -13
  275. package/src/audit/core.test.ts +26 -0
  276. package/src/audit/core.ts +84 -18
  277. package/src/audit/discover.test.ts +39 -0
  278. package/src/audit/discover.ts +202 -18
  279. package/src/audit/lineage.ts +100 -0
  280. package/src/audit/prior-art.ts +153 -0
  281. package/src/audit/proof.test.ts +67 -0
  282. package/src/audit/proof.ts +70 -0
  283. package/src/audit/report-html.ts +9 -4
  284. package/src/audit/report-model.ts +16 -2
  285. package/src/audit/report.test.ts +33 -0
  286. package/src/audit/report.ts +5 -3
  287. package/src/audit/rules-doc.test.ts +34 -2
  288. package/src/audit/rules-doc.ts +82 -9
  289. package/src/audit/secrets.ts +15 -3
  290. package/src/audit/terraform-state.test.ts +144 -0
  291. package/src/audit/terraform-state.ts +131 -0
  292. package/src/build.ts +31 -1
  293. package/src/claimed-fields.test.ts +145 -0
  294. package/src/claimed-fields.ts +208 -0
  295. package/src/cli/build-params-cli.ts +2 -2
  296. package/src/cli/command-group.ts +14 -0
  297. package/src/cli/commands/__fixtures__/audit-terraform/infra/.terraform/providers/lock.txt +1 -0
  298. package/src/cli/commands/__fixtures__/audit-terraform/infra/main.tf +1 -0
  299. package/src/cli/commands/__fixtures__/audit-terraform/infra/modules/foo/main.tf +1 -0
  300. package/src/cli/commands/__fixtures__/audit-terraform/infra/variables.tf +3 -0
  301. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +13 -4
  302. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs-cli.ts +4 -0
  303. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +1 -0
  304. package/src/cli/commands/__fixtures__/init-lexicon-output/src/package-cli.ts +21 -0
  305. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +1 -0
  306. package/src/cli/commands/audit.test.ts +191 -11
  307. package/src/cli/commands/audit.ts +78 -28
  308. package/src/cli/commands/build.test.ts +330 -9
  309. package/src/cli/commands/build.ts +170 -24
  310. package/src/cli/commands/carve-bridge.test.ts +237 -14
  311. package/src/cli/commands/carve-bridge.ts +21 -17
  312. package/src/cli/commands/carve-emit-k8s.test.ts +17 -10
  313. package/src/cli/commands/check-lexicon-docs.ts +1 -1
  314. package/src/cli/commands/check-lexicon-examples.test.ts +29 -1
  315. package/src/cli/commands/check-lexicon-examples.ts +42 -10
  316. package/src/cli/commands/init-lexicon/templates/codegen.ts +9 -0
  317. package/src/cli/commands/init-lexicon/templates/project.ts +44 -4
  318. package/src/cli/commands/init-lexicon.test.ts +97 -2
  319. package/src/cli/commands/init-lexicon.ts +4 -2
  320. package/src/cli/commands/init.test.ts +35 -1
  321. package/src/cli/commands/init.ts +14 -3
  322. package/src/cli/commands/lint.test.ts +194 -1
  323. package/src/cli/commands/lint.ts +205 -7
  324. package/src/cli/commands/onboard.test.ts +75 -0
  325. package/src/cli/commands/onboard.ts +54 -23
  326. package/src/cli/handlers/build.test.ts +81 -1
  327. package/src/cli/handlers/build.ts +15 -1
  328. package/src/cli/handlers/graph.test.ts +3 -3
  329. package/src/cli/handlers/lifecycle.test.ts +195 -12
  330. package/src/cli/handlers/lifecycle.ts +123 -6
  331. package/src/cli/handlers/misc.ts +1 -1
  332. package/src/cli/handlers/operator.test.ts +183 -11
  333. package/src/cli/handlers/operator.ts +341 -55
  334. package/src/cli/handlers/run.test.ts +568 -1295
  335. package/src/cli/handlers/run.ts +446 -912
  336. package/src/cli/main.test.ts +22 -16
  337. package/src/cli/main.ts +66 -33
  338. package/src/cli/mcp/op-tools.ts +147 -88
  339. package/src/cli/mcp/resource-handlers.ts +10 -37
  340. package/src/cli/mcp/resources/context.ts +11 -12
  341. package/src/cli/mcp/server.test.ts +25 -27
  342. package/src/cli/mcp/server.ts +2 -2
  343. package/src/cli/plugins.test.ts +68 -2
  344. package/src/cli/plugins.ts +39 -0
  345. package/src/cli/registry.ts +40 -9
  346. package/src/codegen/docs.ts +1 -1
  347. package/src/codegen/generate-post-synth-barrel.test.ts +1 -1
  348. package/src/codegen/generate-post-synth-barrel.ts +1 -1
  349. package/src/components/README.md +4 -3
  350. package/src/components/__fixtures__/neo4j-fanout.json +1 -1
  351. package/src/components/auto-release.test.ts +4 -20
  352. package/src/components/auto-release.ts +17 -42
  353. package/src/components/capability.ts +4 -4
  354. package/src/components/cli-support.test.ts +28 -12
  355. package/src/components/cli-support.ts +60 -47
  356. package/src/components/component-schema.test.ts +18 -2
  357. package/src/components/component.schema.json +18 -5
  358. package/src/components/component.test.ts +2 -2
  359. package/src/components/component.ts +27 -7
  360. package/src/components/config-defaults.test.ts +2 -2
  361. package/src/components/driver-output.ts +14 -2
  362. package/src/components/driver.test.ts +109 -20
  363. package/src/components/driver.ts +201 -84
  364. package/src/components/index.ts +1 -6
  365. package/src/components/manifest-persistence.test.ts +2 -20
  366. package/src/components/manifest-persistence.ts +4 -24
  367. package/src/components/pilots/neo4j-fanout.pilot.ts +3 -3
  368. package/src/components/pilots/pilots-lint-clean.test.ts +11 -10
  369. package/src/components/run-progress.ts +6 -6
  370. package/src/components/verbs/run-agent.test.ts +19 -0
  371. package/src/components/verbs/run-agent.ts +12 -24
  372. package/src/components/verbs/verify.ts +2 -2
  373. package/src/deep-observation.test.ts +75 -0
  374. package/src/deep-observation.ts +48 -0
  375. package/src/detectLexicon.ts +18 -1
  376. package/src/discovery/collect.test.ts +3 -3
  377. package/src/discovery/files.ts +4 -4
  378. package/src/discovery/fold-import.test.ts +1 -1
  379. package/src/discovery/fold-import.ts +105 -2
  380. package/src/discovery/param-deps.test.ts +112 -1
  381. package/src/discovery/param-deps.ts +193 -0
  382. package/src/discovery/sandbox/bundle.ts +15 -1
  383. package/src/discovery/sandbox/config-wire.test.ts +2 -2
  384. package/src/discovery/sandbox/config-wire.ts +2 -2
  385. package/src/fold/fold.test.ts +56 -0
  386. package/src/fold/foldable-helpers.ts +82 -2
  387. package/src/fold-provenance-exclusion.test.ts +186 -0
  388. package/src/fold-provenance.test.ts +495 -0
  389. package/src/fold-provenance.ts +441 -0
  390. package/src/graph-ops.test.ts +3 -3
  391. package/src/graph-ops.ts +1 -1
  392. package/src/held-elsewhere.test.ts +102 -0
  393. package/src/held-elsewhere.ts +114 -0
  394. package/src/index.ts +3 -0
  395. package/src/kubectl-context.ts +3 -3
  396. package/src/lexicon-config.test.ts +5 -5
  397. package/src/lexicon-config.ts +4 -4
  398. package/src/lexicon-plugin-helpers.ts +1 -1
  399. package/src/lexicon.ts +296 -16
  400. package/src/lifecycle/change-set.test.ts +49 -0
  401. package/src/lifecycle/change-set.ts +67 -0
  402. package/src/lifecycle/converge-ledger.ts +1 -1
  403. package/src/lifecycle/deep-diff.test.ts +267 -14
  404. package/src/lifecycle/deep-diff.ts +217 -14
  405. package/src/lifecycle/deep-observe.test.ts +3 -1
  406. package/src/lifecycle/deep-observe.ts +16 -6
  407. package/src/lifecycle/gate-ledger.test.ts +74 -2
  408. package/src/lifecycle/gate-ledger.ts +154 -36
  409. package/src/lifecycle/index.ts +1 -0
  410. package/src/lifecycle/release-ledger.ts +2 -2
  411. package/src/lifecycle/run-ledger.test.ts +268 -0
  412. package/src/lifecycle/run-ledger.ts +201 -0
  413. package/src/lifecycle/symptoms.ts +3 -3
  414. package/src/lint/config.test.ts +82 -1
  415. package/src/lint/config.ts +112 -3
  416. package/src/lint/pipeline-change-gate.test.ts +2 -2
  417. package/src/lint/policy.test.ts +1 -1
  418. package/src/lint/post-synth.test.ts +31 -0
  419. package/src/lint/post-synth.ts +54 -1
  420. package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +2 -2
  421. package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +1 -1
  422. package/src/lint/rules/comp/comp.test.ts +27 -1
  423. package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +76 -0
  424. package/src/lint/rules/comp/index.ts +3 -3
  425. package/src/lint/rules/op/index.ts +23 -0
  426. package/src/lint/rules/op/ops012-activity-contract.test.ts +161 -0
  427. package/src/lint/rules/op/ops012-activity-contract.ts +64 -0
  428. package/src/lint/rules/op/ops013-step-output-ref.test.ts +237 -0
  429. package/src/lint/rules/op/ops013-step-output-ref.ts +98 -0
  430. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +263 -0
  431. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +179 -0
  432. package/src/lint/rules/op/support.ts +11 -0
  433. package/src/lint/suppressions.test.ts +163 -0
  434. package/src/lint/suppressions.ts +204 -0
  435. package/src/live-endpoint.ts +1 -1
  436. package/src/op/activities/activity-contracts.ts +66 -0
  437. package/src/op/activities/apply.test.ts +567 -0
  438. package/src/op/activities/apply.ts +662 -0
  439. package/src/op/activities/build.ts +26 -0
  440. package/src/op/activities/converge.test.ts +330 -0
  441. package/src/op/activities/converge.ts +449 -0
  442. package/src/op/activities/env-teardown.test.ts +187 -0
  443. package/src/op/activities/env-teardown.ts +171 -0
  444. package/src/op/activities/guard-validate.test.ts +139 -0
  445. package/src/op/activities/guard-validate.ts +223 -0
  446. package/src/op/activities/http-check.test.ts +48 -0
  447. package/src/op/activities/http-check.ts +55 -0
  448. package/src/op/activities/index.ts +119 -0
  449. package/src/op/activities/lexicon-upgrade.test.ts +492 -0
  450. package/src/op/activities/lexicon-upgrade.ts +673 -0
  451. package/src/op/activities/lifecycle.ts +85 -0
  452. package/src/op/activities/pipeline-audit.test.ts +67 -0
  453. package/src/op/activities/pipeline-audit.ts +201 -0
  454. package/src/op/activities/policy.test.ts +19 -0
  455. package/src/op/activities/policy.ts +37 -0
  456. package/src/op/activities/reconcile.test.ts +161 -0
  457. package/src/op/activities/reconcile.ts +354 -0
  458. package/src/op/activities/shell.ts +26 -0
  459. package/src/op/activities/teardown.ts +22 -0
  460. package/src/op/activities/util.ts +6 -0
  461. package/src/op/activities/wait.ts +69 -0
  462. package/src/op/activities/workflow-audit.test.ts +90 -0
  463. package/src/op/activities/workflow-audit.ts +203 -0
  464. package/src/op/activity-contract-registry.test.ts +98 -0
  465. package/src/op/activity-contract-registry.ts +114 -0
  466. package/src/op/activity-contract.test.ts +1 -1
  467. package/src/op/activity-contract.ts +37 -33
  468. package/src/op/activity-failure.ts +34 -0
  469. package/src/op/activity-profiles.test.ts +56 -0
  470. package/src/op/activity-profiles.ts +116 -0
  471. package/src/op/activity-registry.test.ts +37 -28
  472. package/src/op/activity-registry.ts +27 -56
  473. package/src/op/activity-runtime.ts +3 -47
  474. package/src/op/builders-exports.test.ts +0 -49
  475. package/src/op/builders-typed.test.ts +164 -0
  476. package/src/op/builders.ts +125 -41
  477. package/src/op/change-signal.test.ts +117 -0
  478. package/src/op/change-signal.ts +169 -0
  479. package/src/op/composites/apply-op.ts +218 -0
  480. package/src/op/composites/composites.test.ts +379 -0
  481. package/src/op/composites/converge-op.test.ts +168 -0
  482. package/src/op/composites/converge-op.ts +214 -0
  483. package/src/op/composites/index.ts +26 -0
  484. package/src/op/composites/lexicon-upgrade-op.test.ts +68 -0
  485. package/src/op/composites/lexicon-upgrade-op.ts +134 -0
  486. package/src/op/composites/pipeline-audit-op.test.ts +25 -0
  487. package/src/op/composites/pipeline-audit-op.ts +78 -0
  488. package/src/op/composites/reconcile-op.test.ts +51 -0
  489. package/src/op/composites/reconcile-op.ts +110 -0
  490. package/src/op/composites/watch-op.ts +108 -0
  491. package/src/op/composites/workflow-audit-op.test.ts +25 -0
  492. package/src/op/composites/workflow-audit-op.ts +88 -0
  493. package/src/op/converge-rule.ts +12 -10
  494. package/src/op/cron.test.ts +128 -0
  495. package/src/op/cron.ts +149 -0
  496. package/src/op/discover-root.test.ts +105 -1
  497. package/src/op/discover.test.ts +6 -2
  498. package/src/op/discover.ts +84 -16
  499. package/src/op/duration.ts +21 -0
  500. package/src/op/effect-step.test.ts +29 -14
  501. package/src/op/emulator-lifecycle.ts +1 -2
  502. package/src/op/gate-name.test.ts +65 -0
  503. package/src/op/gate-name.ts +60 -0
  504. package/src/op/gate-summary.ts +84 -0
  505. package/src/op/gate.ts +155 -0
  506. package/src/op/generate-pipeline.test.ts +65 -3
  507. package/src/op/generate-pipeline.ts +19 -2
  508. package/src/op/index.ts +48 -8
  509. package/src/op/local-executor.test.ts +178 -34
  510. package/src/op/local-executor.ts +371 -102
  511. package/src/op/local-output.test.ts +95 -11
  512. package/src/op/local-output.ts +33 -4
  513. package/src/op/op-ir.test.ts +292 -0
  514. package/src/op/op-ir.ts +419 -0
  515. package/src/op/op-verb-class.test.ts +2 -2
  516. package/src/op/op-verb-class.ts +19 -5
  517. package/src/op/op.test.ts +28 -7
  518. package/src/op/operator.test.ts +543 -6
  519. package/src/op/operator.ts +203 -20
  520. package/src/op/receipt-store.ts +1 -1
  521. package/src/op/resource.ts +20 -2
  522. package/src/op/runtime.ts +203 -0
  523. package/src/op/runtimes/local.test.ts +181 -0
  524. package/src/op/runtimes/local.ts +212 -0
  525. package/src/op/step-output-ref.ts +15 -13
  526. package/src/op/types.ts +106 -26
  527. package/src/ownership.ts +1 -1
  528. package/src/provenance.test.ts +6 -0
  529. package/src/provenance.ts +21 -1
  530. package/src/serializer-walker.test.ts +42 -0
  531. package/src/serializer-walker.ts +21 -2
  532. package/src/terraform/aws-resources.test.ts +13 -4
  533. package/src/terraform/bridge.test.ts +22 -9
  534. package/src/terraform/bridge.ts +89 -28
  535. package/src/terraform/carve-provider.ts +38 -2
  536. package/src/terraform/data-source-shape.ts +95 -0
  537. package/src/terraform/graph.ts +35 -7
  538. package/src/terraform/providers/kubernetes.ts +48 -2
  539. package/src/terraform/tier-map.ts +21 -6
  540. package/src/terraform/types.ts +8 -0
  541. package/dist/cli/handlers/op-progress.d.ts +0 -90
  542. package/dist/cli/handlers/op-progress.d.ts.map +0 -1
  543. package/dist/cli/handlers/run-client.d.ts +0 -127
  544. package/dist/cli/handlers/run-client.d.ts.map +0 -1
  545. package/dist/cli/handlers/run-report.d.ts +0 -5
  546. package/dist/cli/handlers/run-report.d.ts.map +0 -1
  547. package/dist/components/temporal-codegen-loader.d.ts +0 -27
  548. package/dist/components/temporal-codegen-loader.d.ts.map +0 -1
  549. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +0 -37
  550. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +0 -1
  551. package/src/cli/handlers/op-progress.test.ts +0 -268
  552. package/src/cli/handlers/op-progress.ts +0 -248
  553. package/src/cli/handlers/run-client.test.ts +0 -82
  554. package/src/cli/handlers/run-client.ts +0 -219
  555. package/src/cli/handlers/run-report.test.ts +0 -62
  556. package/src/cli/handlers/run-report.ts +0 -122
  557. package/src/components/temporal-codegen-loader.ts +0 -41
  558. package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +0 -66
@@ -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
  }
@@ -173,7 +173,13 @@ export function parseSecretsConfig(content: string): SecretsScanOptions {
173
173
 
174
174
  // ── Shannon entropy ─────────────────────────────────────────────────────
175
175
 
176
- function shannonEntropy(s: string): number {
176
+ /**
177
+ * Shannon entropy in bits per character. Exported so a lexicon's own
178
+ * secret-shape heuristic can score a candidate on the same scale this
179
+ * module's SEC010 catch-all uses (the terraform lexicon's
180
+ * `lint/secret-shape.ts`, #2110) instead of inventing a second one.
181
+ */
182
+ export function shannonEntropy(s: string): number {
177
183
  const freq = new Map<string, number>();
178
184
  for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
179
185
  let entropy = 0;
@@ -188,8 +194,14 @@ function shannonEntropy(s: string): number {
188
194
 
189
195
  const PLACEHOLDER_RE = /example|placeholder|dummy|changeme|change-me|redacted|sample|fixme|todo|xxxxxxxx|your[-_]?(api[-_]?)?(key|token|secret)|fake[-_]?(key|token|secret)|test[-_]?(key|token|secret)/i;
190
196
 
191
- /** Cheap, format-agnostic "this doesn't look like a real secret" filter. */
192
- function looksLikePlaceholder(value: string): boolean {
197
+ /**
198
+ * Cheap, format-agnostic "this doesn't look like a real secret" filter.
199
+ * Exported for the same reason {@link shannonEntropy} is: the placeholder
200
+ * vocabulary a scanner has to ignore (`changeme`, `example`, `your-api-key`,
201
+ * a run of one repeated character) is the same wherever the candidate came
202
+ * from, and a second copy of this list would drift from this one.
203
+ */
204
+ export function looksLikePlaceholder(value: string): boolean {
193
205
  if (PLACEHOLDER_RE.test(value)) return true;
194
206
  const uniq = new Set(value.toLowerCase()).size;
195
207
  if (value.length > 8 && uniq <= 2) return true; // e.g. "0000000000000000"
@@ -0,0 +1,144 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { describe, expect, test } from "vitest";
5
+ import {
6
+ auditTerraformState,
7
+ gitignoreCoversTerraformState,
8
+ isTerraformStateFile,
9
+ isTerraformStatePath,
10
+ isTerraformWorkDir,
11
+ type ScannableFile,
12
+ } from "./terraform-state";
13
+ import { collectCandidates } from "./discover";
14
+ import { RULE_CATALOG, RULE_CATEGORY } from "./catalog";
15
+
16
+ const file = (path: string): ScannableFile => ({ path, content: "" });
17
+
18
+ function tmpRepo(): string {
19
+ return mkdtempSync(join(tmpdir(), "chant-tf023-"));
20
+ }
21
+
22
+ describe("the TF023 path detectors", () => {
23
+ test.each([
24
+ "terraform.tfstate",
25
+ "terraform.tfstate.backup",
26
+ "infra/prod/terraform.tfstate",
27
+ "infra/prod.tfstate",
28
+ "infra/prod.tfstate.backup",
29
+ ])("%s is state", (path) => {
30
+ expect(isTerraformStateFile(path)).toBe(true);
31
+ expect(isTerraformStatePath(path)).toBe(true);
32
+ });
33
+
34
+ test.each(["main.tf", "terraform.tfvars", "state.json", "docs/tfstate.md"])("%s is not", (path) => {
35
+ expect(isTerraformStateFile(path)).toBe(false);
36
+ });
37
+
38
+ test("the working directory is matched as a path segment, at any depth", () => {
39
+ expect(isTerraformWorkDir(".terraform")).toBe(true);
40
+ expect(isTerraformWorkDir(".terraform/providers/registry.terraform.io/hashicorp/aws/5.0.0")).toBe(true);
41
+ expect(isTerraformWorkDir("infra/prod/.terraform")).toBe(true);
42
+ expect(isTerraformWorkDir("infra/terraform/main.tf")).toBe(false);
43
+ });
44
+ });
45
+
46
+ describe("auditTerraformState", () => {
47
+ test("reports a committed state file as an error, without reading it", () => {
48
+ const findings = auditTerraformState([file("infra/prod/terraform.tfstate")]);
49
+ expect(findings).toHaveLength(1);
50
+ expect(findings[0].checkId).toBe("TF023");
51
+ expect(findings[0].severity).toBe("error");
52
+ expect(findings[0].lexicon).toBe("terraform");
53
+ expect(findings[0].file).toBe("infra/prod/terraform.tfstate");
54
+ expect(findings[0].message).toContain("secret store");
55
+ });
56
+
57
+ test("reports a backup state file too", () => {
58
+ expect(auditTerraformState([file("terraform.tfstate.backup")])).toHaveLength(1);
59
+ });
60
+
61
+ test("reports a `.terraform` directory once, not once per file inside it", () => {
62
+ const findings = auditTerraformState([
63
+ file(".terraform"),
64
+ file(".terraform/modules/modules.json"),
65
+ file(".terraform/providers/registry.terraform.io/hashicorp/aws/5.0.0/terraform-provider-aws"),
66
+ ]);
67
+ expect(findings).toHaveLength(1);
68
+ expect(findings[0].file).toBe(".terraform");
69
+ expect(findings[0].entity).toBe(".terraform");
70
+ });
71
+
72
+ test("reports each root's own working directory separately", () => {
73
+ const findings = auditTerraformState([file("infra/prod/.terraform"), file("infra/staging/.terraform")]);
74
+ expect(findings.map((f) => f.file)).toEqual(["infra/prod/.terraform", "infra/staging/.terraform"]);
75
+ });
76
+
77
+ test("says nothing about a repository with neither", () => {
78
+ expect(auditTerraformState([{ path: "main.tf", content: 'resource "null_resource" "a" {}' }])).toEqual([]);
79
+ });
80
+
81
+ test("is catalogued in core, as a merge-worthy security rule", () => {
82
+ expect(RULE_CATALOG.TF023.tier).toBe("merge-worthy");
83
+ expect(RULE_CATALOG.TF023.category).toBe("security");
84
+ expect(RULE_CATEGORY.TF023).toBe("security");
85
+ expect(RULE_CATALOG.TF023.authority?.[0].url).toContain("#gitignore");
86
+ });
87
+ });
88
+
89
+ describe("gitignoreCoversTerraformState", () => {
90
+ test.each([
91
+ ["*.tfstate", "terraform.tfstate"],
92
+ ["*.tfstate.*", "terraform.tfstate.backup"],
93
+ [".terraform/", ".terraform"],
94
+ [".terraform*", ".terraform"],
95
+ ["/terraform.tfstate", "terraform.tfstate"],
96
+ ["**/.terraform", "infra/prod/.terraform"],
97
+ ["# comment\n\n*.tfstate\n", "infra/prod/terraform.tfstate"],
98
+ ])("%s covers %s", (gitignore, path) => {
99
+ expect(gitignoreCoversTerraformState(gitignore, path)).toBe(true);
100
+ });
101
+
102
+ test.each([
103
+ ["node_modules/\ndist/", "terraform.tfstate"],
104
+ ["*.tfvars", "terraform.tfstate"],
105
+ ["!terraform.tfstate", "terraform.tfstate"],
106
+ ])("%s does not cover %s", (gitignore, path) => {
107
+ expect(gitignoreCoversTerraformState(gitignore, path)).toBe(false);
108
+ });
109
+ });
110
+
111
+ describe("discovery hands TF023 its paths, and nothing else changes", () => {
112
+ test("a state file becomes a candidate with no content, so nothing reads it", () => {
113
+ const dir = tmpRepo();
114
+ writeFileSync(join(dir, "main.tf"), 'resource "null_resource" "a" {}\n');
115
+ writeFileSync(join(dir, "terraform.tfstate"), JSON.stringify({ version: 4, resources: [] }));
116
+ const candidates = collectCandidates(dir);
117
+ const state = candidates.find((c) => c.path === "terraform.tfstate");
118
+ expect(state).toBeDefined();
119
+ expect(state!.content).toBe("");
120
+ expect(auditTerraformState(candidates)).toHaveLength(1);
121
+ rmSync(dir, { recursive: true, force: true });
122
+ });
123
+
124
+ test("the walk records `.terraform` without descending into it", () => {
125
+ const dir = tmpRepo();
126
+ writeFileSync(join(dir, "main.tf"), 'resource "null_resource" "a" {}\n');
127
+ mkdirSync(join(dir, ".terraform", "providers"), { recursive: true });
128
+ writeFileSync(join(dir, ".terraform", "providers", "huge.json"), "{}");
129
+ const candidates = collectCandidates(dir);
130
+ expect(candidates.map((c) => c.path).sort()).toEqual([".terraform", "main.tf"]);
131
+ expect(auditTerraformState(candidates)).toHaveLength(1);
132
+ rmSync(dir, { recursive: true, force: true });
133
+ });
134
+
135
+ test("a gitignored state file is on disk, not in the repository, so it is dropped", () => {
136
+ const dir = tmpRepo();
137
+ writeFileSync(join(dir, ".gitignore"), ".terraform/\n*.tfstate\n*.tfstate.*\n");
138
+ writeFileSync(join(dir, "main.tf"), 'resource "null_resource" "a" {}\n');
139
+ writeFileSync(join(dir, "terraform.tfstate"), "{}");
140
+ mkdirSync(join(dir, ".terraform"), { recursive: true });
141
+ expect(auditTerraformState(collectCandidates(dir))).toEqual([]);
142
+ rmSync(dir, { recursive: true, force: true });
143
+ });
144
+ });