code-auditor-mcp 3.10.0 → 4.0.1

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 (358) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +197 -0
  3. package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.d.ts.map +1 -1
  4. package/dist/__tests__/fixtures/spec-52/item-05-upsert-forms.d.ts +4 -4
  5. package/dist/__tests__/fixtures/spec-52/item-05-upsert-forms.d.ts.map +1 -1
  6. package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
  7. package/dist/analyzers/analyzerUtils.js +2 -2
  8. package/dist/analyzers/analyzerUtils.js.map +1 -1
  9. package/dist/analyzers/applicability.d.ts.map +1 -1
  10. package/dist/analyzers/applicability.js +1 -0
  11. package/dist/analyzers/applicability.js.map +1 -1
  12. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  13. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +1 -1
  14. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  15. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +3 -3
  16. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  17. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  18. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  19. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +2 -2
  20. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  21. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +62 -17
  22. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  23. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  24. package/dist/analyzers/documentationAnalyzer.js +14 -16
  25. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  26. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  27. package/dist/analyzers/invariantsAnalyzer.js +30 -25
  28. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  29. package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
  30. package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
  31. package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
  32. package/dist/analyzers/provenance.d.ts +10 -1
  33. package/dist/analyzers/provenance.d.ts.map +1 -1
  34. package/dist/analyzers/provenance.js +138 -1
  35. package/dist/analyzers/provenance.js.map +1 -1
  36. package/dist/analyzers/reactAnalyzer.d.ts +1 -1
  37. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  38. package/dist/analyzers/reactAnalyzer.js +34 -116
  39. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  40. package/dist/analyzers/ruleRegistry.d.ts +5 -3
  41. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  42. package/dist/analyzers/ruleRegistry.js +199 -55
  43. package/dist/analyzers/ruleRegistry.js.map +1 -1
  44. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +2 -2
  45. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  46. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +10 -17
  47. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  48. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +6 -6
  49. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  50. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +13 -13
  51. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  52. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  53. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +266 -101
  54. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  55. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -2
  56. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  57. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +14 -13
  58. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  59. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  60. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +11 -11
  61. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  62. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +1 -1
  63. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  64. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +19 -7
  65. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  66. package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts.map +1 -1
  67. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -2
  68. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  69. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +93 -46
  70. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  71. package/dist/analyzers/universal/schema/codeAnalysis.d.ts +58 -34
  72. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
  73. package/dist/analyzers/universal/schema/codeAnalysis.js +239 -97
  74. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
  75. package/dist/analyzers/universal/schema/config.d.ts +6 -6
  76. package/dist/analyzers/universal/schema/config.d.ts.map +1 -1
  77. package/dist/analyzers/universal/schema/jsonSchema.js +1 -1
  78. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
  79. package/dist/analyzers/universal/schema/types.d.ts +10 -0
  80. package/dist/analyzers/universal/schema/types.d.ts.map +1 -1
  81. package/dist/analyzers/universal/schema/violations.d.ts +1 -1
  82. package/dist/analyzers/universal/schema/violations.d.ts.map +1 -1
  83. package/dist/auditRouter.js +7 -7
  84. package/dist/auditRouter.js.map +1 -1
  85. package/dist/auditRunner.d.ts +1 -1
  86. package/dist/auditRunner.d.ts.map +1 -1
  87. package/dist/auditRunner.js +53 -8
  88. package/dist/auditRunner.js.map +1 -1
  89. package/dist/churn/churnExtractor.js.map +1 -1
  90. package/dist/cli.js +52 -24
  91. package/dist/cli.js.map +1 -1
  92. package/dist/codeIndexDB.d.ts +1 -1
  93. package/dist/codeIndexDB.d.ts.map +1 -1
  94. package/dist/codeIndexDB.js +164 -18
  95. package/dist/codeIndexDB.js.map +1 -1
  96. package/dist/codeIndexService.d.ts.map +1 -1
  97. package/dist/codeIndexService.js.map +1 -1
  98. package/dist/componentScanner.d.ts.map +1 -1
  99. package/dist/componentScanner.js +89 -28
  100. package/dist/componentScanner.js.map +1 -1
  101. package/dist/config/configLoader.js +1 -1
  102. package/dist/config/configLoader.js.map +1 -1
  103. package/dist/config/defaults.d.ts +2 -2
  104. package/dist/config/defaults.d.ts.map +1 -1
  105. package/dist/config/defaults.js +10 -3
  106. package/dist/config/defaults.js.map +1 -1
  107. package/dist/constants.d.ts +1 -1
  108. package/dist/constants.d.ts.map +1 -1
  109. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  110. package/dist/conventions/conventionMiner.js +3 -10
  111. package/dist/conventions/conventionMiner.js.map +1 -1
  112. package/dist/coverage/lcovParser.js.map +1 -1
  113. package/dist/daemon/args.js.map +1 -1
  114. package/dist/daemon/core.d.ts.map +1 -1
  115. package/dist/daemon/core.js +50 -46
  116. package/dist/daemon/core.js.map +1 -1
  117. package/dist/daemon/lspServer.d.ts.map +1 -1
  118. package/dist/daemon/lspServer.js +1 -1
  119. package/dist/daemon/lspServer.js.map +1 -1
  120. package/dist/daemon/main.d.ts.map +1 -1
  121. package/dist/daemon/main.js +14 -4
  122. package/dist/daemon/main.js.map +1 -1
  123. package/dist/daemon/resolve.js.map +1 -1
  124. package/dist/daemon/socketServer.d.ts.map +1 -1
  125. package/dist/enforcement/gate.js +2 -2
  126. package/dist/functionScanner.d.ts.map +1 -1
  127. package/dist/functionScanner.js +47 -42
  128. package/dist/functionScanner.js.map +1 -1
  129. package/dist/generators/AWSQConfigGenerator.d.ts.map +1 -1
  130. package/dist/generators/AiderConfigGenerator.d.ts.map +1 -1
  131. package/dist/generators/BaseConfigGenerator.d.ts.map +1 -1
  132. package/dist/generators/BaseConfigGenerator.js.map +1 -1
  133. package/dist/generators/ClaudeConfigGenerator.d.ts.map +1 -1
  134. package/dist/generators/ClineConfigGenerator.d.ts.map +1 -1
  135. package/dist/generators/CodeiumConfigGenerator.d.ts.map +1 -1
  136. package/dist/generators/CodexConfigGenerator.d.ts.map +1 -1
  137. package/dist/generators/ConfigGeneratorFactory.d.ts.map +1 -1
  138. package/dist/generators/ConfigGeneratorFactory.js.map +1 -1
  139. package/dist/generators/ContinueConfigGenerator.d.ts.map +1 -1
  140. package/dist/generators/CopilotConfigGenerator.d.ts.map +1 -1
  141. package/dist/generators/CursorConfigGenerator.d.ts.map +1 -1
  142. package/dist/generators/GeminiConfigGenerator.d.ts.map +1 -1
  143. package/dist/generators/JetBrainsConfigGenerator.d.ts.map +1 -1
  144. package/dist/generators/VSCodeConfigGenerator.d.ts.map +1 -1
  145. package/dist/generators/ZCodeConfigGenerator.d.ts.map +1 -1
  146. package/dist/graph/importClassification.d.ts +114 -0
  147. package/dist/graph/importClassification.d.ts.map +1 -0
  148. package/dist/graph/importClassification.js +393 -0
  149. package/dist/graph/importClassification.js.map +1 -0
  150. package/dist/graph/importGraph.d.ts +1 -0
  151. package/dist/graph/importGraph.d.ts.map +1 -1
  152. package/dist/graph/importGraph.js +1 -1
  153. package/dist/graph/importGraph.js.map +1 -1
  154. package/dist/graph/outputFormatter.js.map +1 -1
  155. package/dist/hooks/codex.d.ts.map +1 -1
  156. package/dist/hooks/codex.js +1 -1
  157. package/dist/hooks/codex.js.map +1 -1
  158. package/dist/hooks/core.d.ts +1 -1
  159. package/dist/hooks/core.d.ts.map +1 -1
  160. package/dist/hooks/core.js +4 -4
  161. package/dist/hooks/core.js.map +1 -1
  162. package/dist/hooks/cursor.d.ts.map +1 -1
  163. package/dist/hooks/cursor.js +2 -2
  164. package/dist/hooks/cursor.js.map +1 -1
  165. package/dist/index.d.ts +1 -1
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js.map +1 -1
  168. package/dist/installer.d.ts +13 -7
  169. package/dist/installer.d.ts.map +1 -1
  170. package/dist/installer.js +25 -15
  171. package/dist/installer.js.map +1 -1
  172. package/dist/invariants/invariant-rules.schema.json +1 -1
  173. package/dist/invariants/ruleEngine.js.map +1 -1
  174. package/dist/invariants/ruleValidator.js +1 -1
  175. package/dist/invariants/ruleValidator.js.map +1 -1
  176. package/dist/invariants/types.d.ts +1 -1
  177. package/dist/invariants/types.d.ts.map +1 -1
  178. package/dist/languages/LanguageOrchestrator.d.ts +3 -2
  179. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  180. package/dist/languages/LanguageOrchestrator.js +1 -1
  181. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  182. package/dist/languages/LanguageRegistry.d.ts.map +1 -1
  183. package/dist/languages/RuntimeManager.d.ts +20 -11
  184. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  185. package/dist/languages/RuntimeManager.js +151 -51
  186. package/dist/languages/RuntimeManager.js.map +1 -1
  187. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  188. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  189. package/dist/languages/adapterBridge.d.ts +10 -0
  190. package/dist/languages/adapterBridge.d.ts.map +1 -1
  191. package/dist/languages/adapterBridge.js +30 -7
  192. package/dist/languages/adapterBridge.js.map +1 -1
  193. package/dist/languages/go/GoAdapter.d.ts +1 -19
  194. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  195. package/dist/languages/go/GoAdapter.js +33 -116
  196. package/dist/languages/go/GoAdapter.js.map +1 -1
  197. package/dist/languages/go/analyzer +0 -0
  198. package/dist/languages/go/analyzer-src/analyzer.go +8 -8
  199. package/dist/languages/go/analyzer-src/solid.go +5 -5
  200. package/dist/languages/goBinary.d.ts +46 -0
  201. package/dist/languages/goBinary.d.ts.map +1 -0
  202. package/dist/languages/goBinary.js +169 -0
  203. package/dist/languages/goBinary.js.map +1 -0
  204. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +1 -28
  205. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
  206. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +3 -42
  207. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
  208. package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
  209. package/dist/languages/tree-sitter/converter.js +2 -1
  210. package/dist/languages/tree-sitter/converter.js.map +1 -1
  211. package/dist/languages/tree-sitter/parser.js.map +1 -1
  212. package/dist/languages/tree-sitter/rawNode.d.ts +31 -0
  213. package/dist/languages/tree-sitter/rawNode.d.ts.map +1 -0
  214. package/dist/languages/tree-sitter/rawNode.js +38 -0
  215. package/dist/languages/tree-sitter/rawNode.js.map +1 -0
  216. package/dist/languages/types.d.ts +15 -21
  217. package/dist/languages/types.d.ts.map +1 -1
  218. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +12 -32
  219. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  220. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +103 -203
  221. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  222. package/dist/ledger.d.ts +3 -0
  223. package/dist/ledger.d.ts.map +1 -1
  224. package/dist/ledger.js +45 -9
  225. package/dist/ledger.js.map +1 -1
  226. package/dist/mcp-tools/projectTasks.d.ts +37 -37
  227. package/dist/mcp-tools/projectTasks.js +2 -2
  228. package/dist/mcp-tools/projectTasks.js.map +1 -1
  229. package/dist/mcp-tools/whitelistTools.d.ts +17 -17
  230. package/dist/mcp-tools/whitelistTools.d.ts.map +1 -1
  231. package/dist/mcp-tools/workflowGuide.js +3 -3
  232. package/dist/mcp-tools/workflowGuide.js.map +1 -1
  233. package/dist/mcp-tools-shared.d.ts.map +1 -1
  234. package/dist/mcp-tools-shared.js +13 -13
  235. package/dist/mcp-tools-shared.js.map +1 -1
  236. package/dist/mcp-ui-simple.js +7 -7
  237. package/dist/mcp-ui-simple.js.map +1 -1
  238. package/dist/mcp.js +14 -14
  239. package/dist/mcp.js.map +1 -1
  240. package/dist/mcpAuditJobs.js +7 -7
  241. package/dist/mcpAuditJobs.js.map +1 -1
  242. package/dist/mcpAutoIndex.js +1 -1
  243. package/dist/mcpAutoIndex.js.map +1 -1
  244. package/dist/mcpToolErrors.d.ts.map +1 -1
  245. package/dist/mcpToolErrors.js.map +1 -1
  246. package/dist/native-bootstrap.js.map +1 -1
  247. package/dist/nextFile.d.ts +2 -2
  248. package/dist/nextFile.d.ts.map +1 -1
  249. package/dist/nextFile.js +19 -5
  250. package/dist/nextFile.js.map +1 -1
  251. package/dist/nextFileIncremental.d.ts +10 -2
  252. package/dist/nextFileIncremental.d.ts.map +1 -1
  253. package/dist/nextFileIncremental.js +45 -16
  254. package/dist/nextFileIncremental.js.map +1 -1
  255. package/dist/pipeline.d.ts +2 -2
  256. package/dist/pipeline.d.ts.map +1 -1
  257. package/dist/pipeline.js +35 -0
  258. package/dist/pipeline.js.map +1 -1
  259. package/dist/pipelineAdapters.d.ts +17 -0
  260. package/dist/pipelineAdapters.d.ts.map +1 -1
  261. package/dist/pipelineAdapters.js +356 -197
  262. package/dist/pipelineAdapters.js.map +1 -1
  263. package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
  264. package/dist/reporting/csvReportGenerator.js +6 -6
  265. package/dist/reporting/csvReportGenerator.js.map +1 -1
  266. package/dist/reporting/htmlReportGenerator.d.ts.map +1 -1
  267. package/dist/reporting/htmlReportGenerator.js +11 -11
  268. package/dist/reporting/htmlReportGenerator.js.map +1 -1
  269. package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
  270. package/dist/reporting/jsonReportGenerator.js +2 -2
  271. package/dist/reporting/jsonReportGenerator.js.map +1 -1
  272. package/dist/reporting/reportGenerator.d.ts.map +1 -1
  273. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  274. package/dist/reporting/sarifReportGenerator.js +1 -1
  275. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  276. package/dist/scripts/benchmarkWorkers.js +1 -1
  277. package/dist/scripts/benchmarkWorkers.js.map +1 -1
  278. package/dist/search/QueryParser.d.ts.map +1 -1
  279. package/dist/search/QueryParser.js.map +1 -1
  280. package/dist/services/CodeMapGenerator.d.ts.map +1 -1
  281. package/dist/services/CodeMapGenerator.js.map +1 -1
  282. package/dist/services/ProjectTaskRepository.d.ts.map +1 -1
  283. package/dist/services/ProjectTaskRepository.js.map +1 -1
  284. package/dist/services/auditJobService.js.map +1 -1
  285. package/dist/services/fileAccounting.d.ts.map +1 -1
  286. package/dist/services/projectTaskHelpers.js.map +1 -1
  287. package/dist/services/whitelistService.d.ts.map +1 -1
  288. package/dist/sqlite/betterSqlite3.d.ts.map +1 -1
  289. package/dist/sqlite/betterSqlite3.js.map +1 -1
  290. package/dist/sqlite/nodeSqlite.d.ts.map +1 -1
  291. package/dist/sqlite/nodeSqlite.js.map +1 -1
  292. package/dist/structuralSignature.js.map +1 -1
  293. package/dist/styles/cssAstExtractor.d.ts +2 -2
  294. package/dist/styles/cssAstExtractor.d.ts.map +1 -1
  295. package/dist/styles/cssAstExtractor.js +67 -111
  296. package/dist/styles/cssAstExtractor.js.map +1 -1
  297. package/dist/styles/styleExtractor.js.map +1 -1
  298. package/dist/styles/styleIndexer.d.ts.map +1 -1
  299. package/dist/styles/styleIndexer.js +19 -6
  300. package/dist/styles/styleIndexer.js.map +1 -1
  301. package/dist/styles/tailwindConfigLoader.js.map +1 -1
  302. package/dist/styles/tailwindProbe.d.ts.map +1 -1
  303. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  304. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  305. package/dist/telemetry.d.ts +1 -1
  306. package/dist/telemetry.d.ts.map +1 -1
  307. package/dist/telemetry.js.map +1 -1
  308. package/dist/tool-registry.d.ts.map +1 -1
  309. package/dist/types/crossLanguage.d.ts +1 -1
  310. package/dist/types/crossLanguage.d.ts.map +1 -1
  311. package/dist/types.d.ts +67 -19
  312. package/dist/types.d.ts.map +1 -1
  313. package/dist/types.js +6 -5
  314. package/dist/types.js.map +1 -1
  315. package/dist/ui/InteractivePrompts.d.ts.map +1 -1
  316. package/dist/ui/InteractivePrompts.js.map +1 -1
  317. package/dist/utils/astParser.d.ts +1 -17
  318. package/dist/utils/astParser.d.ts.map +1 -1
  319. package/dist/utils/astParser.js +18 -60
  320. package/dist/utils/astParser.js.map +1 -1
  321. package/dist/utils/astUtils.d.ts +12 -12
  322. package/dist/utils/astUtils.d.ts.map +1 -1
  323. package/dist/utils/astUtils.js +83 -82
  324. package/dist/utils/astUtils.js.map +1 -1
  325. package/dist/utils/componentPatterns.js.map +1 -1
  326. package/dist/utils/componentResponsibility.d.ts +8 -8
  327. package/dist/utils/componentResponsibility.d.ts.map +1 -1
  328. package/dist/utils/componentResponsibility.js +33 -38
  329. package/dist/utils/componentResponsibility.js.map +1 -1
  330. package/dist/utils/dependencyExtractor.d.ts +3 -3
  331. package/dist/utils/dependencyExtractor.d.ts.map +1 -1
  332. package/dist/utils/dependencyExtractor.js +26 -30
  333. package/dist/utils/dependencyExtractor.js.map +1 -1
  334. package/dist/utils/fileDiscovery.d.ts.map +1 -1
  335. package/dist/utils/fileDiscovery.js +65 -0
  336. package/dist/utils/fileDiscovery.js.map +1 -1
  337. package/dist/utils/jsDocParser.d.ts +6 -6
  338. package/dist/utils/jsDocParser.d.ts.map +1 -1
  339. package/dist/utils/jsDocParser.js +18 -21
  340. package/dist/utils/jsDocParser.js.map +1 -1
  341. package/dist/utils/reactDetection.d.ts +7 -7
  342. package/dist/utils/reactDetection.d.ts.map +1 -1
  343. package/dist/utils/reactDetection.js +42 -49
  344. package/dist/utils/reactDetection.js.map +1 -1
  345. package/dist/version.generated.d.ts +1 -1
  346. package/dist/version.generated.d.ts.map +1 -1
  347. package/dist/version.generated.js +1 -1
  348. package/dist/version.generated.js.map +1 -1
  349. package/dist/workers/auditWorker.js.map +1 -1
  350. package/package.json +4 -3
  351. package/plugin/.claude-plugin/plugin.json +1 -1
  352. package/plugin/README.md +5 -4
  353. package/plugin/hooks/hooks.json +11 -0
  354. package/plugin/scripts/hook-common.sh +85 -31
  355. package/plugin/scripts/hook-warm.sh +44 -0
  356. package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +28 -21
  357. package/plugin/skills/code-auditor/SKILL.md +10 -10
  358. package/server.json +2 -2
@@ -5,9 +5,9 @@
5
5
  */
6
6
  import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
7
7
  import { withRuleTiming } from '../ruleTiming.js';
8
- import { buildProvenanceContext, isDBProvenanced, DB_CALL_METHODS, } from '../provenance.js';
8
+ import { buildProvenanceContext, isDBProvenanced, getCallExpressionCallee, DB_CALL_METHODS, ORM_METHODS, } from '../provenance.js';
9
9
  import { DB_RECEIVER_NAMES, DB_CALL_METHOD_NAMES, DB_BINDING_NAMES, DB_WRAPPER_NAMES, } from './UniversalSchemaAnalyzer.js';
10
- import { isSqlKeyword } from './schema/codeAnalysis.js';
10
+ import { isSqlKeyword, findEnclosingFunctionIdentity, functionIdentityLabel } from './schema/codeAnalysis.js';
11
11
  import { isTestOrSpecPath } from '../../languages/testConventions.js';
12
12
  /**
13
13
  * SQL keywords recognized as evidence that a string is a SQL query.
@@ -209,7 +209,8 @@ function buildDatabaseCall(node, ast, scan) {
209
209
  hasFilter,
210
210
  hasParameterizedQuery: security.parameterized,
211
211
  hasSqlInjectionRisk: security.injectionRisk,
212
- enclosingFunction: findEnclosingFunctionName(node, adapter),
212
+ sqlEscaped: security.escaped,
213
+ enclosingFunction: enclosingIdentity(node, adapter, ast.filePath),
213
214
  };
214
215
  }
215
216
  /**
@@ -307,36 +308,57 @@ function checkViolations(call, analysis, ctx) {
307
308
  const violations = [];
308
309
  const symbol = nextSymbol(call.enclosingFunction ?? 'top-level', call.method, symbolOrdinals);
309
310
  const push = (message, opts) => violations.push(makeViolation(filePath, { line: call.line, column: call.column }, message, { ...opts, symbol }));
310
- // Security: SQL injection — string-interpolated SQL from input is exploitable
311
- // now, so `critical`; manual quote-escaping is not sanitization.
311
+ // Security: SQL injection. Raw unescaped interpolation of input is a live
312
+ // vulnerability now → `critical`. Manual quote-escaping
313
+ // (`.replace(/'/g, "''")`) is *defended* — single-quote doubling handles only
314
+ // the single-quote vector, not backslash escapes or unicode quote variants —
315
+ // so it downgrades to `advisory` ("verify escaping") rather than asserting a
316
+ // certified vulnerability.
312
317
  if (config.checkSQLInjection && call.hasSqlInjectionRisk) {
313
- push(`Potential SQL injection risk in ${call.method}. Use parameterized queries.`, {
314
- severity: 'critical',
315
- rule: 'sql-injection-risk',
316
- resolution: {
317
- action: 'parameterize',
318
- summary: `Replace the string-interpolated SQL in ${call.method} with a parameterized query — bind values via the driver's placeholder form (\`?\`, \`$1\`, or \`:name\`) instead of concatenating them into the statement.`,
319
- symbols: [call.method],
320
- files: [filePath],
321
- lines: [call.line],
322
- },
323
- });
318
+ if (call.sqlEscaped) {
319
+ push(`Interpolated SQL in ${call.method} — verify escaping is sufficient. Use parameterized queries.`, {
320
+ severity: 'advisory',
321
+ rule: 'sql-injection-risk',
322
+ resolution: {
323
+ action: 'parameterize',
324
+ summary: `The SQL in ${call.method} interpolates a manually quote-escaped value. Quote-doubling defends only the single-quote case (not backslash escapes, unicode quote variants, or numeric/identifier positions) — replace the interpolation with a parameterized query (\`?\`, \`$1\`, or \`:name\`) for a full guarantee.`,
325
+ symbols: [call.method],
326
+ files: [filePath],
327
+ lines: [call.line],
328
+ },
329
+ });
330
+ }
331
+ else {
332
+ push(`Potential SQL injection risk in ${call.method}. Use parameterized queries.`, {
333
+ severity: 'critical',
334
+ rule: 'sql-injection-risk',
335
+ resolution: {
336
+ action: 'parameterize',
337
+ summary: `Replace the string-interpolated SQL in ${call.method} with a parameterized query — bind values via the driver's placeholder form (\`?\`, \`$1\`, or \`:name\`) instead of concatenating them into the statement.`,
338
+ symbols: [call.method],
339
+ files: [filePath],
340
+ lines: [call.line],
341
+ },
342
+ });
343
+ }
324
344
  }
325
- // Security: Missing Organization Filter
345
+ // Security: Missing Organization Filter — a query on a tenant-scoped table with
346
+ // no org/tenant filter is a live data-isolation breach: one tenant reads
347
+ // another's rows. `critical` (already wrong in production), not `severe`.
326
348
  if (config.checkOrgFilters && !call.hasOrganizationFilter && call.tables.length > 0 && requiresOrgFilter(call.tables, config)) {
327
- push(`Query on ${call.tables.join(', ')} missing organization/tenant filter`, { severity: 'severe', rule: 'missing-org-filter' });
349
+ push(`Query on ${call.tables.join(', ')} missing organization/tenant filter`, { severity: 'critical', rule: 'missing-org-filter' });
328
350
  }
329
351
  // Performance: Complex Query — a join-heavy query (many tables). Spec 55 R5:
330
352
  // a subquery alone is no longer "complex" — it is an ordinary SQLite/D1 idiom.
331
353
  if (analysis.performanceRisk === 'high') {
332
- push(`Query references ${call.tables.length} tables`, { severity: 'high', rule: 'complex-query' });
354
+ push(`Query references ${call.tables.length} tables`, { severity: 'severe', rule: 'complex-query' });
333
355
  }
334
356
  // Performance: Unfiltered Query — an unfiltered write (DELETE/UPDATE with no
335
357
  // row-limiting clause), the classic mass-mutation foot-gun. Spec 55 R3: it is
336
358
  // a query-shape rule excluded from test files; R5: it is about writes now, not
337
359
  // reads (an unfiltered SELECT is often an intentional full-set load).
338
360
  if (!skipTestRules && analysis.performanceRisk === 'medium') {
339
- push(`Unfiltered write on ${call.tables.join(', ')} has no WHERE/HAVING/LIMIT`, { severity: 'high', rule: 'unfiltered-query' });
361
+ push(`Unfiltered write on ${call.tables.join(', ')} has no WHERE/HAVING/LIMIT`, { severity: 'severe', rule: 'unfiltered-query' });
340
362
  }
341
363
  return violations;
342
364
  }
@@ -360,7 +382,7 @@ function checkGeneralPatterns(ast, adapter, sourceCode, config) {
360
382
  for (const node of stringNodes) {
361
383
  const text = adapter.getNodeText(node, sourceCode);
362
384
  if (isConnectionString(text)) {
363
- const fnName = findEnclosingFunctionName(node, adapter);
385
+ const fnName = enclosingIdentity(node, adapter, ast.filePath);
364
386
  const baseSym = `${fnName}:hardcoded-connection`;
365
387
  const count = (hardcodedOrdinals.get(baseSym) ?? 0) + 1;
366
388
  hardcodedOrdinals.set(baseSym, count);
@@ -660,6 +682,64 @@ function isStatementConstructionOnly(node, adapter, sourceCode) {
660
682
  const method = dbCallMethodName(node, adapter, sourceCode);
661
683
  return (method === 'prepare' || method === 'bind') && !hasEagerMethodInCallChain(node, adapter, sourceCode);
662
684
  }
685
+ /**
686
+ * True when a db-call node is SQL-string construction inside a query compiler:
687
+ * an ORM *builder* method (e.g. `aggregate`, `select`, `where` — never an eager
688
+ * I/O method) invoked on an unprovenanced `this` receiver whose result is
689
+ * accumulated into an array via `.push(...)`. knex's query compilers build SQL
690
+ * exactly this way — `sql.push(...this.aggregate(stmt))` inside a
691
+ * column-iteration loop — and the call returns a string fragment, not a query
692
+ * result, so the loop is not an N+1 (Spec 52 R1 extension). Eager methods
693
+ * (`run`/`all`/`exec`/`query`/…) are excluded, so a genuine
694
+ * `results.push(db.query(...))` still fires.
695
+ */
696
+ function isSqlStringConstruction(node, adapter, sourceCode) {
697
+ const method = dbCallMethodName(node, adapter, sourceCode);
698
+ if (!method || EAGER_DB_METHODS.has(method))
699
+ return false;
700
+ if (!ORM_METHODS.has(method))
701
+ return false;
702
+ const call = resolveDbCallNode(node, adapter);
703
+ if (!call)
704
+ return false;
705
+ const memberExpr = findMemberCallee(call, adapter);
706
+ if (!memberExpr)
707
+ return false;
708
+ // The receiver must be an unprovenanced `this`/`super` — the query-compiler
709
+ // object itself. `this.db.aggregate(...)` (receiver `this.db`) would be a real
710
+ // ORM query on a provenanced `db`, not a compiler fragment.
711
+ const objectText = memberObjectText(memberExpr, adapter, sourceCode);
712
+ if (objectText !== 'this' && objectText !== 'super')
713
+ return false;
714
+ return isConsumedByArrayPush(call, adapter, sourceCode);
715
+ }
716
+ /** The object text of a member_expression (`this.aggregate` → `this`). */
717
+ function memberObjectText(node, adapter, sourceCode) {
718
+ const object = adapter.getChildren(node).find((c) => {
719
+ const t = adapter.getNodeType(c);
720
+ return t !== 'property_identifier' && t !== 'field_identifier' && t !== '.';
721
+ });
722
+ return object ? adapter.getNodeText(object, sourceCode) : null;
723
+ }
724
+ /**
725
+ * True when a call's result is consumed by an array `.push(...)` — the call is
726
+ * an argument (possibly spread) of a `push` invocation. The accumulation
727
+ * signature (`sql.push(...this.aggregate(stmt))`) marks the value as a string
728
+ * fragment being collected, not a query result being read.
729
+ */
730
+ function isConsumedByArrayPush(call, adapter, sourceCode) {
731
+ let cur = adapter.getParent(call);
732
+ if (cur && adapter.getNodeType(cur) === 'spread_element') {
733
+ cur = adapter.getParent(cur);
734
+ }
735
+ if (!cur || adapter.getNodeType(cur) !== 'arguments')
736
+ return false;
737
+ const pushCall = adapter.getParent(cur);
738
+ if (!pushCall || adapter.getNodeType(pushCall) !== 'call_expression')
739
+ return false;
740
+ const pushMember = findMemberCallee(pushCall, adapter);
741
+ return !!pushMember && memberPropertyName(pushMember, adapter, sourceCode) === 'push';
742
+ }
663
743
  /** True when a call's arguments contain a spread_element (…binds). */
664
744
  function hasSpreadArgument(call, adapter) {
665
745
  const args = adapter.getChildren(call).find(c => adapter.getNodeType(c) === 'arguments');
@@ -754,10 +834,12 @@ function isWrapperFunctionWithBindParams(node, adapter, sourceCode, wrapperNames
754
834
  if (!call || adapter.getNodeType(call) !== 'call_expression')
755
835
  return false;
756
836
  // Check if the callee is a simple identifier (not member expression)
757
- // matching one of the wrapper names.
758
- const children = adapter.getChildren(call);
759
- const callee = children.find(c => adapter.getNodeType(c) === 'identifier');
760
- if (!callee)
837
+ // matching one of the wrapper names. Use the shared callee extractor so an
838
+ // `await fn<T>(...)` call — whose callee is an `await_expression` wrapping the
839
+ // identifier, plus a `type_arguments` child — resolves the same way the
840
+ // provenance detector does. A naive `children.find(identifier)` misses it.
841
+ const callee = getCallExpressionCallee(call, adapter);
842
+ if (!callee || adapter.getNodeType(callee) !== 'identifier')
761
843
  return false;
762
844
  const calleeName = adapter.getNodeText(callee, sourceCode);
763
845
  if (!wrapperNames.includes(calleeName))
@@ -1048,17 +1130,35 @@ function isUnfilteredWrite(call) {
1048
1130
  * wrapper function with bind params). All four short-circuit checkQuerySecurity
1049
1131
  * to a "safe" result.
1050
1132
  */
1051
- function isParameterizedByChain(node, adapter, sourceCode, config) {
1133
+ function isParameterizedByChain(node, adapter, sourceCode, config, wrapperNames) {
1052
1134
  if (isInPrepareBindChain(node, adapter, sourceCode))
1053
1135
  return true;
1054
1136
  if (isInExecChain(node, adapter, sourceCode))
1055
1137
  return true;
1056
1138
  if (isD1ConvenienceCall(node, adapter, sourceCode))
1057
1139
  return true;
1058
- if (isWrapperFunctionWithBindParams(node, adapter, sourceCode, config.dbWrapperNames ?? []))
1140
+ if (isWrapperFunctionWithBindParams(node, adapter, sourceCode, wrapperNames))
1059
1141
  return true;
1060
1142
  return false;
1061
1143
  }
1144
+ /**
1145
+ * The effective set of DB-wrapper names the FP guards recognize as
1146
+ * parameterized. This is the union of the static `config.dbWrapperNames`
1147
+ * (d1Query, d1Exec) and the names the provenance detector *learned* at
1148
+ * file-scan time (`reason: 'wrapper'` — e.g. a bare `d1(sql, params)` D1 REST
1149
+ * helper). Keeping the two in sync is what prevents a wrapper the detector
1150
+ * already treats as DB-bound from being re-flagged as raw interpolation by
1151
+ * `checkQuerySecurity`.
1152
+ */
1153
+ function effectiveWrapperNames(scan) {
1154
+ const staticNames = scan.config.dbWrapperNames ?? [];
1155
+ const learned = scan.provenanceContext
1156
+ ? [...scan.provenanceContext.dbProvenanced.values()]
1157
+ .filter(ev => ev.reason === 'wrapper')
1158
+ .map(ev => ev.identifier)
1159
+ : [];
1160
+ return [...new Set([...staticNames, ...learned])];
1161
+ }
1062
1162
  /**
1063
1163
  * True when a single dynamic interpolation part is provably safe — via a
1064
1164
  * config-driven sanitizer allowlist, the adapter's cross-function safety
@@ -1104,30 +1204,33 @@ function checkQuerySecurity(node, text, ast, scan) {
1104
1204
  const { adapter, sourceCode, config } = scan;
1105
1205
  // Parameterized chains (.prepare().bind(), .exec() spread, D1 convenience,
1106
1206
  // DB wrappers) and explicit parameterization are always safe.
1107
- if (isParameterizedByChain(node, adapter, sourceCode, config)) {
1108
- return { parameterized: true, injectionRisk: false };
1207
+ if (isParameterizedByChain(node, adapter, sourceCode, config, effectiveWrapperNames(scan))) {
1208
+ return { parameterized: true, injectionRisk: false, escaped: false };
1109
1209
  }
1110
1210
  if ((config.securityPatterns?.parameterizedQueries || []).some(p => text.includes(p))) {
1111
- return { parameterized: true, injectionRisk: false };
1211
+ return { parameterized: true, injectionRisk: false, escaped: false };
1112
1212
  }
1113
1213
  // No dynamic-string capability → can't prove unsafe; err quiet.
1114
1214
  if (!adapter.isDynamicStringConstruction || !adapter.getDynamicParts) {
1115
- return { parameterized: false, injectionRisk: false };
1215
+ return { parameterized: false, injectionRisk: false, escaped: false };
1116
1216
  }
1117
1217
  // Only a dynamically-constructed string carrying SQL keywords can inject.
1118
1218
  if (!adapter.isDynamicStringConstruction(node) || !containsSQLKeywords(text)) {
1119
- return { parameterized: false, injectionRisk: false };
1219
+ return { parameterized: false, injectionRisk: false, escaped: false };
1120
1220
  }
1121
1221
  const unresolved = adapter.getDynamicParts(node, sourceCode)
1122
- .filter(part => !isSafeDynamicPart(part, ast, scan))
1123
- .map(part => part.text);
1222
+ .filter(part => !isSafeDynamicPart(part, ast, scan));
1124
1223
  if (unresolved.length === 0) {
1125
- return { parameterized: false, injectionRisk: false };
1224
+ return { parameterized: false, injectionRisk: false, escaped: false };
1126
1225
  }
1226
+ // Manual quote-escaping (`.replace(/'/g, "''")`) is defended, not raw — every
1227
+ // unresolved part being quote-escaped downgrades the finding to advisory.
1228
+ const escaped = unresolved.every(part => !!part.node && !!adapter.isEscapedInterpolation && adapter.isEscapedInterpolation(part.node, ast, sourceCode));
1127
1229
  return {
1128
1230
  parameterized: false,
1129
1231
  injectionRisk: true,
1130
- message: `Cannot protect interpolated content: ${unresolved.map(id => '${' + id + '}').join(', ')}`,
1232
+ escaped,
1233
+ message: `Cannot protect interpolated content: ${unresolved.map(part => '${' + part.text + '}').join(', ')}`,
1131
1234
  };
1132
1235
  }
1133
1236
  function extractCallExpressionMethod(callExpr, adapter, sourceCode) {
@@ -1137,11 +1240,10 @@ function extractCallExpressionMethod(callExpr, adapter, sourceCode) {
1137
1240
  // keywords (COUNT, JOIN, WHERE, ...) that appear inside template literals
1138
1241
  // in the call arguments, which a regex scan of the full call-expression
1139
1242
  // text would incorrectly match.
1140
- const children = adapter.getChildren(callExpr);
1141
- const callee = children.find(c => {
1142
- const t = adapter.getNodeType(c);
1143
- return t === 'member_expression' || t === 'identifier';
1144
- });
1243
+ // Use the shared callee extractor: it recurses through `await_expression` and
1244
+ // skips `type_arguments`, so `await d1<{...}>(...)` resolves to `d1` rather
1245
+ // than falling through to the "unknown" regex fallback.
1246
+ const callee = getCallExpressionCallee(callExpr, adapter);
1145
1247
  if (callee) {
1146
1248
  const calleeType = adapter.getNodeType(callee);
1147
1249
  if (calleeType === 'member_expression') {
@@ -1244,6 +1346,31 @@ function isConnectionString(text) {
1244
1346
  return patterns.some(pattern => pattern.test(text));
1245
1347
  }
1246
1348
  // ── R4.1: Loop-query detection ──────────────────────────────────────
1349
+ /**
1350
+ * LLM provider / SDK namespaces. A callee whose text carries one of these is
1351
+ * an unambiguous LLM/agent invocation (e.g. `gatewayDeepseekModel`,
1352
+ * `anthropic.messages.create`, `openai.chat`).
1353
+ */
1354
+ const LLM_PROVIDER_RE = /(?:anthropic|openai|deepseek|claude|gpt|gemini|cohere|mistral|bedrock|vertex|ollama|llm|gateway)/i;
1355
+ /**
1356
+ * LLM action verbs — AI SDK methods and the wrapper names that survive the
1357
+ * provider-token check (e.g. `aiEmbed`, `chatCompletion`, `thesisEngine`).
1358
+ * Deliberately narrower than `generate`/`extract` (those collide with ordinary
1359
+ * helpers like `insertSingleGeneratedBuild`); the provider namespace and the
1360
+ * model-client argument signal cover the rest.
1361
+ */
1362
+ const LLM_ACTION_RE = /(?:embed|reembed|completion|chat|prompt|synthesize|thesis|agent|classify|summarize|translate)/i;
1363
+ /**
1364
+ * Identifier names that denote an LLM/model client when they appear as a call
1365
+ * argument (e.g. `extractBuildsFromCorpus(db, model, …)`) or as the object of a
1366
+ * member callee (e.g. `model.chat(…)`). Deliberately excludes `client`/`ai`
1367
+ * (ambiguous — a DB client is also a `client`) so we never suppress a real N+1
1368
+ * on a generic variable name.
1369
+ */
1370
+ const LLM_CLIENT_ARG_NAMES = new Set([
1371
+ 'model', 'llm', 'embedder', 'gateway',
1372
+ 'anthropic', 'openai', 'deepseek', 'claude', 'gpt', 'gemini', 'cohere', 'mistral', 'bedrock',
1373
+ ]);
1247
1374
  /**
1248
1375
  * R4.1: Find database queries inside loops and flag them as N+1 risks.
1249
1376
  * Each finding carries the query call location (never line 1).
@@ -1258,6 +1385,14 @@ function checkLoopQueries(ast, scan) {
1258
1385
  const reported = new Set();
1259
1386
  const loopOrdinals = new Map();
1260
1387
  for (const node of dbNodes) {
1388
+ // A DB call and its template-literal SQL argument both satisfy isDbCallNode —
1389
+ // the call via provenance, the literal via the template-literal branch — so one
1390
+ // query site (`db.prepare(`…`)`) would otherwise emit two findings, one at the
1391
+ // call line and one at the SQL line. Collapse to the call node: it is always
1392
+ // present when its literal is (isDbCallNode only admits a literal whose
1393
+ // enclosing call is itself provenanced), so skipping the literal loses nothing.
1394
+ if (isTemplateLiteral(node, adapter))
1395
+ continue;
1261
1396
  const nodeText = adapter.getNodeText(node, sourceCode);
1262
1397
  if (!nodeText || nodeText.trim().length < 10)
1263
1398
  continue;
@@ -1265,16 +1400,33 @@ function checkLoopQueries(ast, scan) {
1265
1400
  // an eager call or a prepare chained into one still fires.
1266
1401
  if (isStatementConstructionOnly(node, adapter, sourceCode))
1267
1402
  continue;
1403
+ // Spec 52 R1 (extension) — skip SQL-string construction inside a query
1404
+ // compiler: an ORM builder method (e.g. `aggregate`) on an unprovenanced
1405
+ // `this` whose result is accumulated via `.push(...)` returns a string
1406
+ // fragment, not a query result (knex `sql.push(...this.aggregate(stmt))`).
1407
+ if (isSqlStringConstruction(node, adapter, sourceCode))
1408
+ continue;
1268
1409
  const loopInfo = findEnclosingLoop(node, adapter);
1269
1410
  if (!loopInfo)
1270
1411
  continue;
1271
- // R4.1: query node's actual location (never line 1) + runtime dedup.
1272
- const queryLine = node.location.start.line;
1273
- const dedupKey = `${queryLine}:${loopInfo.loopNode.location.start.line}`;
1412
+ // R4.1 (Spec 46): LLM-pipeline discriminator. A loop whose body invokes an
1413
+ // LLM/agent (embedding, model completion, corpus extraction) is an intentional
1414
+ // *sequential pipeline* — its per-item DB calls are persistence steps gated by
1415
+ // rate limits and per-item crash-recovery, not a batchable N+1. Batching or
1416
+ // joining would regress those semantics, so suppress the finding rather than
1417
+ // emit an unfixable `severe`.
1418
+ if (loopBodyContainsLlmCall(loopInfo.loopNode, adapter, sourceCode))
1419
+ continue;
1420
+ // R4.1: one finding per *loop*, not per query — an N+1 is a property of the
1421
+ // loop, so a loop issuing several queries is still one violation (defect #51).
1422
+ // Key on the loop node's start byte offset so distinct loops (including
1423
+ // nested ones) never collapse, while every query in the same loop dedups to
1424
+ // the first. The finding anchors to that first query's location below.
1425
+ const dedupKey = String(loopInfo.loopNode.range[0]);
1274
1426
  if (reported.has(dedupKey))
1275
1427
  continue;
1276
1428
  reported.add(dedupKey);
1277
- const sym = nextSymbol(findEnclosingFunctionName(node, adapter), 'loop-query', loopOrdinals);
1429
+ const sym = nextSymbol(enclosingIdentity(node, adapter, ast.filePath), 'loop-query', loopOrdinals);
1278
1430
  // R4.2: Nested-loop attribution.
1279
1431
  const depthMsg = loopInfo.depth > 1 ? ` (nested ${loopInfo.depth} levels deep)` : '';
1280
1432
  violations.push(makeViolation(ast.filePath, node.location.start, `Database query inside loop${depthMsg} ` +
@@ -1283,6 +1435,71 @@ function checkLoopQueries(ast, scan) {
1283
1435
  }
1284
1436
  return violations;
1285
1437
  }
1438
+ /**
1439
+ * R4.1 (Spec 46): True when the loop's subtree contains an LLM/agent invocation.
1440
+ * Used as the discriminator between a batchable N+1 (no LLM call — the queries
1441
+ * should be batched/joined) and an intentional sequential pipeline (an LLM call
1442
+ * — the per-item DB calls are persistence steps behind rate limits and
1443
+ * crash-recovery). Walking the whole loop subtree (header + body) is safe: a
1444
+ * loop header rarely hosts an LLM call, and a body-level one is exactly the
1445
+ * signal we want.
1446
+ */
1447
+ function loopBodyContainsLlmCall(loopNode, adapter, sourceCode) {
1448
+ let found = false;
1449
+ walkSubtree(loopNode, adapter, (node) => {
1450
+ if (found || adapter.getNodeType(node) !== 'call_expression')
1451
+ return;
1452
+ if (isLlmCallNode(node, adapter, sourceCode))
1453
+ found = true;
1454
+ });
1455
+ return found;
1456
+ }
1457
+ /** Depth-first walk over an ASTNode subtree (children only, no parent links). */
1458
+ function walkSubtree(node, adapter, visitor) {
1459
+ visitor(node);
1460
+ for (const child of adapter.getChildren(node)) {
1461
+ walkSubtree(child, adapter, visitor);
1462
+ }
1463
+ }
1464
+ /**
1465
+ * Classify a call_expression as an LLM/agent invocation via three unambiguous
1466
+ * signals: a provider/SDK namespace in the callee text, an LLM action verb in the
1467
+ * callee text, or a model-client identifier passed as an argument / used as the
1468
+ * member-callee object.
1469
+ */
1470
+ function isLlmCallNode(node, adapter, sourceCode) {
1471
+ const children = adapter.getChildren(node);
1472
+ const callee = children.find(c => adapter.getNodeType(c) !== 'arguments');
1473
+ if (callee) {
1474
+ const calleeText = adapter.getNodeText(callee, sourceCode);
1475
+ if (LLM_PROVIDER_RE.test(calleeText) || LLM_ACTION_RE.test(calleeText))
1476
+ return true;
1477
+ // Member callee whose object is an LLM client: `model.chat(…)`, `llm.invoke(…)`.
1478
+ if (adapter.getNodeType(callee) === 'member_expression') {
1479
+ const obj = adapter.getChildren(callee).find(c => adapter.getNodeType(c) === 'identifier');
1480
+ if (obj && LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(obj, sourceCode)))
1481
+ return true;
1482
+ }
1483
+ }
1484
+ // Argument signal: an LLM client identifier passed into the call, e.g.
1485
+ // `extractBuildsFromCorpus(db, model, …)`.
1486
+ const args = children.find(c => adapter.getNodeType(c) === 'arguments');
1487
+ if (args) {
1488
+ for (const arg of adapter.getChildren(args)) {
1489
+ if (adapter.getNodeType(arg) === 'identifier' &&
1490
+ LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(arg, sourceCode))) {
1491
+ return true;
1492
+ }
1493
+ // Member access argument (`client.messages`) — check its object identifier.
1494
+ if (adapter.getNodeType(arg) === 'member_expression') {
1495
+ const obj = adapter.getChildren(arg).find(c => adapter.getNodeType(c) === 'identifier');
1496
+ if (obj && LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(obj, sourceCode)))
1497
+ return true;
1498
+ }
1499
+ }
1500
+ }
1501
+ return false;
1502
+ }
1286
1503
  /** True when `node` is a function call whose callee is DB-provenanced. */
1287
1504
  function isDBProvenancedFunctionCall(node, adapter, sourceCode, provenanceContext) {
1288
1505
  return isFunctionCall(node, adapter) &&
@@ -1410,63 +1627,11 @@ function getPropertyName(node, adapter) {
1410
1627
  return '';
1411
1628
  }
1412
1629
  /**
1413
- * Walk the parent chain to find the enclosing function/method/arrow name.
1414
- * Returns 'top-level' if no enclosing function is found.
1415
- *
1416
- * Used for stable fingerprint symbols — the enclosing function name is
1417
- * immune to line drift (Spec 18 Gap 2).
1418
- */
1419
- function findEnclosingFunctionName(node, adapter) {
1420
- let current = adapter.getParent(node);
1421
- while (current) {
1422
- const type = adapter.getNodeType(current);
1423
- // Arrow functions, function declarations, function expressions
1424
- if (type === 'arrow_function' ||
1425
- type === 'function_declaration' ||
1426
- type === 'function_expression' ||
1427
- type === 'generator_function_declaration' ||
1428
- type === 'generator_function_expression') {
1429
- const name = getNodeName(current, adapter);
1430
- if (name)
1431
- return name;
1432
- }
1433
- // Method definitions on classes/objects
1434
- if (type === 'method_definition' || adapter.isMethod(current)) {
1435
- const name = getNodeName(current, adapter);
1436
- if (name)
1437
- return name;
1438
- }
1439
- current = adapter.getParent(current);
1440
- }
1441
- return 'top-level';
1442
- }
1443
- /**
1444
- * Extract the name/identifier from a function or method AST node.
1630
+ * The enclosing-function identity used for stable fingerprint symbols. The
1631
+ * coordinate (start line + column) keeps anonymous handlers distinct; the single
1632
+ * definition lives in codeAnalysis.ts (Spec 61 Amendment A).
1445
1633
  */
1446
- function getNodeName(node, adapter) {
1447
- // Try named children first
1448
- if (node.name && typeof node.name === 'string')
1449
- return node.name;
1450
- if (node.text && typeof node.text === 'string')
1451
- return node.text;
1452
- // Fall back to the raw tree-sitter node's text content (leaf identifiers etc.)
1453
- const rawText = node.raw?.text;
1454
- if (typeof rawText === 'string' && rawText.length > 0)
1455
- return rawText;
1456
- // Walk children for identifier / property_identifier
1457
- if (node.children) {
1458
- for (const child of node.children) {
1459
- const childType = adapter.getNodeType(child);
1460
- if (childType === 'identifier' ||
1461
- childType === 'property_identifier') {
1462
- const name = getNodeName(child, adapter);
1463
- if (name)
1464
- return name;
1465
- }
1466
- }
1467
- }
1468
- return '';
1469
- }
1634
+ const enclosingIdentity = (node, adapter, filePath) => functionIdentityLabel(findEnclosingFunctionIdentity(node, adapter, filePath));
1470
1635
  /**
1471
1636
  * Universal data access analyzer.
1472
1637
  */