code-auditor-mcp 4.1.0 → 5.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 (1086) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +374 -0
  3. package/README.md +4 -4
  4. package/configs/hhra-compat.json +0 -8
  5. package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.d.ts +1 -1
  6. package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.d.ts.map +1 -1
  7. package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.js +7 -1
  8. package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.js.map +1 -1
  9. package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.d.ts +2 -8
  10. package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.d.ts.map +1 -1
  11. package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.js +4 -4
  12. package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.js.map +1 -1
  13. package/dist/__tests__/fixtures/spec-52/item-06-chained-prepare-run-in-loop.d.ts +1 -1
  14. package/dist/__tests__/fixtures/spec-52/item-06-chained-prepare-run-in-loop.d.ts.map +1 -1
  15. package/dist/__tests__/fixtures/spec-52/item-06-chained-prepare-run-in-loop.js.map +1 -1
  16. package/dist/__tests__/fixtures/spec-52/item-07-chained-prepare-bind-first-in-loop.d.ts +2 -2
  17. package/dist/__tests__/fixtures/spec-52/item-07-chained-prepare-bind-first-in-loop.d.ts.map +1 -1
  18. package/dist/__tests__/fixtures/spec-52/item-07-chained-prepare-bind-first-in-loop.js.map +1 -1
  19. package/dist/__tests__/fixtures/spec-52/item-10-promise-all-map-of-eager-all-in-loop.d.ts +1 -1
  20. package/dist/__tests__/fixtures/spec-52/item-10-promise-all-map-of-eager-all-in-loop.d.ts.map +1 -1
  21. package/dist/__tests__/fixtures/spec-52/item-10-promise-all-map-of-eager-all-in-loop.js.map +1 -1
  22. package/dist/analyzers/applicability.d.ts +42 -0
  23. package/dist/analyzers/applicability.d.ts.map +1 -1
  24. package/dist/analyzers/applicability.js +74 -0
  25. package/dist/analyzers/applicability.js.map +1 -1
  26. package/dist/analyzers/cross-language/APIContractAnalyzer.js +18 -24
  27. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  28. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +30 -23
  29. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  30. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +113 -76
  31. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  32. package/dist/analyzers/cross-language/SchemaValidator.d.ts +2 -0
  33. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  34. package/dist/analyzers/cross-language/SchemaValidator.js +8 -3
  35. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  36. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  37. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +210 -148
  38. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  39. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  40. package/dist/analyzers/documentationAnalyzer.js +23 -28
  41. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  42. package/dist/analyzers/handleIdentification.d.ts +356 -0
  43. package/dist/analyzers/handleIdentification.d.ts.map +1 -0
  44. package/dist/analyzers/handleIdentification.js +424 -0
  45. package/dist/analyzers/handleIdentification.js.map +1 -0
  46. package/dist/analyzers/manifestStaleness.d.ts +64 -0
  47. package/dist/analyzers/manifestStaleness.d.ts.map +1 -0
  48. package/dist/analyzers/manifestStaleness.js +115 -0
  49. package/dist/analyzers/manifestStaleness.js.map +1 -0
  50. package/dist/analyzers/orgFilterTiers.d.ts +73 -1
  51. package/dist/analyzers/orgFilterTiers.d.ts.map +1 -1
  52. package/dist/analyzers/orgFilterTiers.js +166 -1
  53. package/dist/analyzers/orgFilterTiers.js.map +1 -1
  54. package/dist/analyzers/provenance.d.ts +251 -57
  55. package/dist/analyzers/provenance.d.ts.map +1 -1
  56. package/dist/analyzers/provenance.js +1109 -429
  57. package/dist/analyzers/provenance.js.map +1 -1
  58. package/dist/analyzers/reactAnalyzer.d.ts +2 -0
  59. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  60. package/dist/analyzers/reactAnalyzer.js +2 -0
  61. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  62. package/dist/analyzers/receiverResolution.d.ts +183 -0
  63. package/dist/analyzers/receiverResolution.d.ts.map +1 -0
  64. package/dist/analyzers/receiverResolution.js +540 -0
  65. package/dist/analyzers/receiverResolution.js.map +1 -0
  66. package/dist/analyzers/receiverRoot.d.ts +328 -0
  67. package/dist/analyzers/receiverRoot.d.ts.map +1 -0
  68. package/dist/analyzers/receiverRoot.js +990 -0
  69. package/dist/analyzers/receiverRoot.js.map +1 -0
  70. package/dist/analyzers/ruleRegistry.d.ts +12 -68
  71. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  72. package/dist/analyzers/ruleRegistry.js +32 -342
  73. package/dist/analyzers/ruleRegistry.js.map +1 -1
  74. package/dist/analyzers/ruleTiming.d.ts +10 -0
  75. package/dist/analyzers/ruleTiming.d.ts.map +1 -1
  76. package/dist/analyzers/ruleTiming.js +20 -0
  77. package/dist/analyzers/ruleTiming.js.map +1 -1
  78. package/dist/analyzers/sqlLiteral.d.ts +76 -0
  79. package/dist/analyzers/sqlLiteral.d.ts.map +1 -0
  80. package/dist/analyzers/sqlLiteral.js +419 -0
  81. package/dist/analyzers/sqlLiteral.js.map +1 -0
  82. package/dist/analyzers/tsEcosystem.d.ts +115 -0
  83. package/dist/analyzers/tsEcosystem.d.ts.map +1 -0
  84. package/dist/analyzers/tsEcosystem.js +231 -0
  85. package/dist/analyzers/tsEcosystem.js.map +1 -0
  86. package/dist/analyzers/tsExpressionDescriptor.d.ts +156 -0
  87. package/dist/analyzers/tsExpressionDescriptor.d.ts.map +1 -0
  88. package/dist/analyzers/tsExpressionDescriptor.js +374 -0
  89. package/dist/analyzers/tsExpressionDescriptor.js.map +1 -0
  90. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +6 -0
  91. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  92. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +107 -44
  93. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  94. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +1 -12
  95. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  96. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +24 -101
  97. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  98. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +287 -49
  99. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  100. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +2130 -512
  101. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  102. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  103. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +2 -18
  104. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  105. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +0 -33
  106. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  107. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +20 -143
  108. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  109. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +4 -3
  110. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  111. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +36 -13
  112. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  113. package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts.map +1 -1
  114. package/dist/analyzers/universal/UniversalSecretsAnalyzer.js +6 -0
  115. package/dist/analyzers/universal/UniversalSecretsAnalyzer.js.map +1 -1
  116. package/dist/analyzers/universal/UniversalSecurityAnalyzer.d.ts +15 -0
  117. package/dist/analyzers/universal/UniversalSecurityAnalyzer.d.ts.map +1 -1
  118. package/dist/analyzers/universal/UniversalSecurityAnalyzer.js +25 -0
  119. package/dist/analyzers/universal/UniversalSecurityAnalyzer.js.map +1 -1
  120. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +21 -32
  121. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  122. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +166 -196
  123. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  124. package/dist/analyzers/universal/functionConcerns.d.ts +3 -0
  125. package/dist/analyzers/universal/functionConcerns.d.ts.map +1 -1
  126. package/dist/analyzers/universal/functionConcerns.js +4 -1
  127. package/dist/analyzers/universal/functionConcerns.js.map +1 -1
  128. package/dist/analyzers/universal/schema/codeAnalysis.d.ts +310 -69
  129. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
  130. package/dist/analyzers/universal/schema/codeAnalysis.js +709 -383
  131. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
  132. package/dist/analyzers/universal/schema/config.d.ts +3 -14
  133. package/dist/analyzers/universal/schema/config.d.ts.map +1 -1
  134. package/dist/analyzers/universal/schema/config.js +3 -16
  135. package/dist/analyzers/universal/schema/config.js.map +1 -1
  136. package/dist/analyzers/universal/schema/discovery.d.ts +22 -1
  137. package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -1
  138. package/dist/analyzers/universal/schema/discovery.js +36 -20
  139. package/dist/analyzers/universal/schema/discovery.js.map +1 -1
  140. package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -1
  141. package/dist/analyzers/universal/schema/jsonSchema.js +13 -3
  142. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
  143. package/dist/analyzers/universal/schema/migrations.d.ts +187 -27
  144. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -1
  145. package/dist/analyzers/universal/schema/migrations.js +443 -186
  146. package/dist/analyzers/universal/schema/migrations.js.map +1 -1
  147. package/dist/analyzers/universal/schema/types.d.ts +17 -3
  148. package/dist/analyzers/universal/schema/types.d.ts.map +1 -1
  149. package/dist/analyzers/universal/schema/violations.js +1 -1
  150. package/dist/analyzers/universal/schema/violations.js.map +1 -1
  151. package/dist/analyzers/universal/styleScale.d.ts +52 -0
  152. package/dist/analyzers/universal/styleScale.d.ts.map +1 -0
  153. package/dist/analyzers/universal/styleScale.js +95 -0
  154. package/dist/analyzers/universal/styleScale.js.map +1 -0
  155. package/dist/applyDataDirEnv.js +1 -1
  156. package/dist/applyDataDirEnv.js.map +1 -1
  157. package/dist/auditRouter.d.ts +26 -30
  158. package/dist/auditRouter.d.ts.map +1 -1
  159. package/dist/auditRouter.js +373 -143
  160. package/dist/auditRouter.js.map +1 -1
  161. package/dist/auditRunner.d.ts +64 -1
  162. package/dist/auditRunner.d.ts.map +1 -1
  163. package/dist/auditRunner.js +1537 -1051
  164. package/dist/auditRunner.js.map +1 -1
  165. package/dist/baseline.d.ts +30 -3
  166. package/dist/baseline.d.ts.map +1 -1
  167. package/dist/baseline.js +70 -9
  168. package/dist/baseline.js.map +1 -1
  169. package/dist/cli.js +997 -812
  170. package/dist/cli.js.map +1 -1
  171. package/dist/codeIndex/analyzerConfig.d.ts +60 -0
  172. package/dist/codeIndex/analyzerConfig.d.ts.map +1 -0
  173. package/dist/codeIndex/analyzerConfig.js +98 -0
  174. package/dist/codeIndex/analyzerConfig.js.map +1 -0
  175. package/dist/codeIndex/auditResults.d.ts +39 -0
  176. package/dist/codeIndex/auditResults.d.ts.map +1 -0
  177. package/dist/codeIndex/auditResults.js +99 -0
  178. package/dist/codeIndex/auditResults.js.map +1 -0
  179. package/dist/codeIndex/codeMap.d.ts +59 -0
  180. package/dist/codeIndex/codeMap.d.ts.map +1 -0
  181. package/dist/codeIndex/codeMap.js +72 -0
  182. package/dist/codeIndex/codeMap.js.map +1 -0
  183. package/dist/codeIndex/conventions.d.ts +33 -0
  184. package/dist/codeIndex/conventions.d.ts.map +1 -0
  185. package/dist/codeIndex/conventions.js +84 -0
  186. package/dist/codeIndex/conventions.js.map +1 -0
  187. package/dist/codeIndex/coverage.d.ts +78 -0
  188. package/dist/codeIndex/coverage.d.ts.map +1 -0
  189. package/dist/codeIndex/coverage.js +164 -0
  190. package/dist/codeIndex/coverage.js.map +1 -0
  191. package/dist/codeIndex/dependencyGraph.d.ts +97 -0
  192. package/dist/codeIndex/dependencyGraph.d.ts.map +1 -0
  193. package/dist/codeIndex/dependencyGraph.js +241 -0
  194. package/dist/codeIndex/dependencyGraph.js.map +1 -0
  195. package/dist/codeIndex/functionIndex.d.ts +111 -0
  196. package/dist/codeIndex/functionIndex.d.ts.map +1 -0
  197. package/dist/codeIndex/functionIndex.js +306 -0
  198. package/dist/codeIndex/functionIndex.js.map +1 -0
  199. package/dist/codeIndex/inferredReceivers.d.ts +32 -0
  200. package/dist/codeIndex/inferredReceivers.d.ts.map +1 -0
  201. package/dist/codeIndex/inferredReceivers.js +41 -0
  202. package/dist/codeIndex/inferredReceivers.js.map +1 -0
  203. package/dist/codeIndex/meta.d.ts +30 -0
  204. package/dist/codeIndex/meta.d.ts.map +1 -0
  205. package/dist/codeIndex/meta.js +37 -0
  206. package/dist/codeIndex/meta.js.map +1 -0
  207. package/dist/codeIndex/migrations.d.ts +96 -0
  208. package/dist/codeIndex/migrations.d.ts.map +1 -0
  209. package/dist/codeIndex/migrations.js +1302 -0
  210. package/dist/codeIndex/migrations.js.map +1 -0
  211. package/dist/codeIndex/projectTasks.d.ts +54 -0
  212. package/dist/codeIndex/projectTasks.d.ts.map +1 -0
  213. package/dist/codeIndex/projectTasks.js +81 -0
  214. package/dist/codeIndex/projectTasks.js.map +1 -0
  215. package/dist/codeIndex/provenance.d.ts +47 -0
  216. package/dist/codeIndex/provenance.d.ts.map +1 -0
  217. package/dist/codeIndex/provenance.js +48 -0
  218. package/dist/codeIndex/provenance.js.map +1 -0
  219. package/dist/codeIndex/rawSql.d.ts +54 -0
  220. package/dist/codeIndex/rawSql.d.ts.map +1 -0
  221. package/dist/codeIndex/rawSql.js +71 -0
  222. package/dist/codeIndex/rawSql.js.map +1 -0
  223. package/dist/codeIndex/schema.d.ts +99 -0
  224. package/dist/codeIndex/schema.d.ts.map +1 -0
  225. package/dist/codeIndex/schema.js +174 -0
  226. package/dist/codeIndex/schema.js.map +1 -0
  227. package/dist/codeIndex/search.d.ts +40 -0
  228. package/dist/codeIndex/search.d.ts.map +1 -0
  229. package/dist/codeIndex/search.js +316 -0
  230. package/dist/codeIndex/search.js.map +1 -0
  231. package/dist/codeIndex/shared.d.ts +27 -0
  232. package/dist/codeIndex/shared.d.ts.map +1 -0
  233. package/dist/codeIndex/shared.js +46 -0
  234. package/dist/codeIndex/shared.js.map +1 -0
  235. package/dist/codeIndex/sqliteCollection.d.ts +81 -0
  236. package/dist/codeIndex/sqliteCollection.d.ts.map +1 -0
  237. package/dist/codeIndex/sqliteCollection.js +250 -0
  238. package/dist/codeIndex/sqliteCollection.js.map +1 -0
  239. package/dist/codeIndex/whitelist.d.ts +59 -0
  240. package/dist/codeIndex/whitelist.d.ts.map +1 -0
  241. package/dist/codeIndex/whitelist.js +188 -0
  242. package/dist/codeIndex/whitelist.js.map +1 -0
  243. package/dist/codeIndexDB.d.ts +130 -277
  244. package/dist/codeIndexDB.d.ts.map +1 -1
  245. package/dist/codeIndexDB.js +364 -2748
  246. package/dist/codeIndexDB.js.map +1 -1
  247. package/dist/codeIndexService.d.ts +87 -12
  248. package/dist/codeIndexService.d.ts.map +1 -1
  249. package/dist/codeIndexService.js +110 -34
  250. package/dist/codeIndexService.js.map +1 -1
  251. package/dist/componentScanner.d.ts +35 -0
  252. package/dist/componentScanner.d.ts.map +1 -1
  253. package/dist/componentScanner.js +114 -80
  254. package/dist/componentScanner.js.map +1 -1
  255. package/dist/config/configGeneratePath.d.ts +4 -0
  256. package/dist/config/configGeneratePath.d.ts.map +1 -1
  257. package/dist/config/configGeneratePath.js +4 -0
  258. package/dist/config/configGeneratePath.js.map +1 -1
  259. package/dist/config/configLoader.d.ts +16 -1
  260. package/dist/config/configLoader.d.ts.map +1 -1
  261. package/dist/config/configLoader.js +26 -23
  262. package/dist/config/configLoader.js.map +1 -1
  263. package/dist/config/defaults.d.ts +11 -7
  264. package/dist/config/defaults.d.ts.map +1 -1
  265. package/dist/config/defaults.js +21 -20
  266. package/dist/config/defaults.js.map +1 -1
  267. package/dist/config/effectiveConfig.d.ts +17 -2
  268. package/dist/config/effectiveConfig.d.ts.map +1 -1
  269. package/dist/config/effectiveConfig.js +32 -25
  270. package/dist/config/effectiveConfig.js.map +1 -1
  271. package/dist/config/lintConfigReader.d.ts +6 -0
  272. package/dist/config/lintConfigReader.d.ts.map +1 -1
  273. package/dist/config/lintConfigReader.js +6 -0
  274. package/dist/config/lintConfigReader.js.map +1 -1
  275. package/dist/config/staticObjectExtract.d.ts +4 -0
  276. package/dist/config/staticObjectExtract.d.ts.map +1 -1
  277. package/dist/config/staticObjectExtract.js +7 -3
  278. package/dist/config/staticObjectExtract.js.map +1 -1
  279. package/dist/config/thresholdRationales.d.ts +2 -0
  280. package/dist/config/thresholdRationales.d.ts.map +1 -1
  281. package/dist/config/thresholdRationales.js +5 -2
  282. package/dist/config/thresholdRationales.js.map +1 -1
  283. package/dist/constants.d.ts +1 -1
  284. package/dist/constants.d.ts.map +1 -1
  285. package/dist/constants.js +10 -5
  286. package/dist/constants.js.map +1 -1
  287. package/dist/conventions/conventionMiner.d.ts +105 -2
  288. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  289. package/dist/conventions/conventionMiner.js +334 -259
  290. package/dist/conventions/conventionMiner.js.map +1 -1
  291. package/dist/coverage/coverageService.js +4 -4
  292. package/dist/coverage/coverageService.js.map +1 -1
  293. package/dist/daemon/args.d.ts +3 -0
  294. package/dist/daemon/args.d.ts.map +1 -1
  295. package/dist/daemon/args.js +3 -0
  296. package/dist/daemon/args.js.map +1 -1
  297. package/dist/daemon/core.d.ts +33 -27
  298. package/dist/daemon/core.d.ts.map +1 -1
  299. package/dist/daemon/core.js +198 -162
  300. package/dist/daemon/core.js.map +1 -1
  301. package/dist/daemon/lspServer.d.ts +8 -0
  302. package/dist/daemon/lspServer.d.ts.map +1 -1
  303. package/dist/daemon/lspServer.js +8 -0
  304. package/dist/daemon/lspServer.js.map +1 -1
  305. package/dist/daemon/main.d.ts +2 -0
  306. package/dist/daemon/main.d.ts.map +1 -1
  307. package/dist/daemon/main.js +2 -0
  308. package/dist/daemon/main.js.map +1 -1
  309. package/dist/daemon/resolve.d.ts +22 -7
  310. package/dist/daemon/resolve.d.ts.map +1 -1
  311. package/dist/daemon/resolve.js +29 -8
  312. package/dist/daemon/resolve.js.map +1 -1
  313. package/dist/daemon/socketClient.d.ts +10 -1
  314. package/dist/daemon/socketClient.d.ts.map +1 -1
  315. package/dist/daemon/socketClient.js +10 -1
  316. package/dist/daemon/socketClient.js.map +1 -1
  317. package/dist/daemon/socketServer.d.ts +13 -0
  318. package/dist/daemon/socketServer.d.ts.map +1 -1
  319. package/dist/daemon/socketServer.js +13 -0
  320. package/dist/daemon/socketServer.js.map +1 -1
  321. package/dist/dataPaths.d.ts +27 -8
  322. package/dist/dataPaths.d.ts.map +1 -1
  323. package/dist/dataPaths.js +28 -44
  324. package/dist/dataPaths.js.map +1 -1
  325. package/dist/dismissals.d.ts +22 -1
  326. package/dist/dismissals.d.ts.map +1 -1
  327. package/dist/dismissals.js +22 -1
  328. package/dist/dismissals.js.map +1 -1
  329. package/dist/enforcement/gate.d.ts +1 -0
  330. package/dist/enforcement/gate.d.ts.map +1 -1
  331. package/dist/enforcement/gate.js +1 -0
  332. package/dist/enforcement/gate.js.map +1 -1
  333. package/dist/fingerprint.d.ts +6 -0
  334. package/dist/fingerprint.d.ts.map +1 -1
  335. package/dist/fingerprint.js +46 -1
  336. package/dist/fingerprint.js.map +1 -1
  337. package/dist/functionScanner.d.ts +30 -1
  338. package/dist/functionScanner.d.ts.map +1 -1
  339. package/dist/functionScanner.js +258 -256
  340. package/dist/functionScanner.js.map +1 -1
  341. package/dist/generators/AWSQConfigGenerator.d.ts +23 -0
  342. package/dist/generators/AWSQConfigGenerator.d.ts.map +1 -1
  343. package/dist/generators/AWSQConfigGenerator.js +23 -0
  344. package/dist/generators/AWSQConfigGenerator.js.map +1 -1
  345. package/dist/generators/BaseConfigGenerator.d.ts +7 -0
  346. package/dist/generators/BaseConfigGenerator.d.ts.map +1 -1
  347. package/dist/generators/BaseConfigGenerator.js +7 -0
  348. package/dist/generators/BaseConfigGenerator.js.map +1 -1
  349. package/dist/generators/ClaudeConfigGenerator.d.ts +23 -0
  350. package/dist/generators/ClaudeConfigGenerator.d.ts.map +1 -1
  351. package/dist/generators/ClaudeConfigGenerator.js +23 -0
  352. package/dist/generators/ClaudeConfigGenerator.js.map +1 -1
  353. package/dist/generators/ClineConfigGenerator.d.ts +23 -0
  354. package/dist/generators/ClineConfigGenerator.d.ts.map +1 -1
  355. package/dist/generators/ClineConfigGenerator.js +23 -0
  356. package/dist/generators/ClineConfigGenerator.js.map +1 -1
  357. package/dist/generators/CodeiumConfigGenerator.d.ts +23 -0
  358. package/dist/generators/CodeiumConfigGenerator.d.ts.map +1 -1
  359. package/dist/generators/CodeiumConfigGenerator.js +23 -0
  360. package/dist/generators/CodeiumConfigGenerator.js.map +1 -1
  361. package/dist/generators/CodexConfigGenerator.d.ts +23 -0
  362. package/dist/generators/CodexConfigGenerator.d.ts.map +1 -1
  363. package/dist/generators/CodexConfigGenerator.js +23 -0
  364. package/dist/generators/CodexConfigGenerator.js.map +1 -1
  365. package/dist/generators/ConfigGeneratorFactory.d.ts +9 -0
  366. package/dist/generators/ConfigGeneratorFactory.d.ts.map +1 -1
  367. package/dist/generators/ConfigGeneratorFactory.js +9 -0
  368. package/dist/generators/ConfigGeneratorFactory.js.map +1 -1
  369. package/dist/generators/ContinueConfigGenerator.d.ts +23 -0
  370. package/dist/generators/ContinueConfigGenerator.d.ts.map +1 -1
  371. package/dist/generators/ContinueConfigGenerator.js +23 -0
  372. package/dist/generators/ContinueConfigGenerator.js.map +1 -1
  373. package/dist/generators/CopilotConfigGenerator.d.ts +23 -0
  374. package/dist/generators/CopilotConfigGenerator.d.ts.map +1 -1
  375. package/dist/generators/CopilotConfigGenerator.js +23 -0
  376. package/dist/generators/CopilotConfigGenerator.js.map +1 -1
  377. package/dist/generators/CursorConfigGenerator.d.ts +23 -0
  378. package/dist/generators/CursorConfigGenerator.d.ts.map +1 -1
  379. package/dist/generators/CursorConfigGenerator.js +23 -0
  380. package/dist/generators/CursorConfigGenerator.js.map +1 -1
  381. package/dist/generators/GeminiConfigGenerator.d.ts +23 -0
  382. package/dist/generators/GeminiConfigGenerator.d.ts.map +1 -1
  383. package/dist/generators/GeminiConfigGenerator.js +23 -0
  384. package/dist/generators/GeminiConfigGenerator.js.map +1 -1
  385. package/dist/generators/VSCodeConfigGenerator.d.ts +23 -0
  386. package/dist/generators/VSCodeConfigGenerator.d.ts.map +1 -1
  387. package/dist/generators/VSCodeConfigGenerator.js +23 -0
  388. package/dist/generators/VSCodeConfigGenerator.js.map +1 -1
  389. package/dist/generators/ZCodeConfigGenerator.d.ts +23 -0
  390. package/dist/generators/ZCodeConfigGenerator.d.ts.map +1 -1
  391. package/dist/generators/ZCodeConfigGenerator.js +23 -0
  392. package/dist/generators/ZCodeConfigGenerator.js.map +1 -1
  393. package/dist/graph/blastRadius.d.ts.map +1 -1
  394. package/dist/graph/blastRadius.js +12 -7
  395. package/dist/graph/blastRadius.js.map +1 -1
  396. package/dist/graph/callGraph.d.ts +31 -0
  397. package/dist/graph/callGraph.d.ts.map +1 -1
  398. package/dist/graph/callGraph.js +62 -32
  399. package/dist/graph/callGraph.js.map +1 -1
  400. package/dist/graph/importClassification.d.ts +28 -0
  401. package/dist/graph/importClassification.d.ts.map +1 -1
  402. package/dist/graph/importClassification.js +57 -0
  403. package/dist/graph/importClassification.js.map +1 -1
  404. package/dist/graph/importGraph.d.ts +38 -0
  405. package/dist/graph/importGraph.d.ts.map +1 -1
  406. package/dist/graph/importGraph.js +38 -0
  407. package/dist/graph/importGraph.js.map +1 -1
  408. package/dist/graph/outputFormatter.d.ts +12 -4
  409. package/dist/graph/outputFormatter.d.ts.map +1 -1
  410. package/dist/graph/outputFormatter.js +12 -62
  411. package/dist/graph/outputFormatter.js.map +1 -1
  412. package/dist/hooks/codex.d.ts +8 -1
  413. package/dist/hooks/codex.d.ts.map +1 -1
  414. package/dist/hooks/codex.js +7 -0
  415. package/dist/hooks/codex.js.map +1 -1
  416. package/dist/hooks/core.d.ts +6 -0
  417. package/dist/hooks/core.d.ts.map +1 -1
  418. package/dist/hooks/core.js +6 -1
  419. package/dist/hooks/core.js.map +1 -1
  420. package/dist/hooks/cursor.d.ts +11 -1
  421. package/dist/hooks/cursor.d.ts.map +1 -1
  422. package/dist/hooks/cursor.js +10 -0
  423. package/dist/hooks/cursor.js.map +1 -1
  424. package/dist/index.d.ts +4 -0
  425. package/dist/index.d.ts.map +1 -1
  426. package/dist/index.js +4 -0
  427. package/dist/index.js.map +1 -1
  428. package/dist/installConfig.d.ts +23 -5
  429. package/dist/installConfig.d.ts.map +1 -1
  430. package/dist/installConfig.js +23 -5
  431. package/dist/installConfig.js.map +1 -1
  432. package/dist/installer.d.ts +6 -0
  433. package/dist/installer.d.ts.map +1 -1
  434. package/dist/installer.js +6 -0
  435. package/dist/installer.js.map +1 -1
  436. package/dist/invariants/ruleEngine.d.ts +3 -0
  437. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  438. package/dist/invariants/ruleEngine.js +156 -119
  439. package/dist/invariants/ruleEngine.js.map +1 -1
  440. package/dist/invariants/ruleValidator.d.ts +3 -0
  441. package/dist/invariants/ruleValidator.d.ts.map +1 -1
  442. package/dist/invariants/ruleValidator.js +185 -178
  443. package/dist/invariants/ruleValidator.js.map +1 -1
  444. package/dist/languages/LanguageOrchestrator.d.ts +4 -1
  445. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  446. package/dist/languages/LanguageOrchestrator.js +8 -19
  447. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  448. package/dist/languages/RuntimeManager.d.ts +8 -0
  449. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  450. package/dist/languages/RuntimeManager.js +55 -5
  451. package/dist/languages/RuntimeManager.js.map +1 -1
  452. package/dist/languages/UniversalAnalyzer.js +1 -1
  453. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  454. package/dist/languages/adapterBridge.d.ts +4 -0
  455. package/dist/languages/adapterBridge.d.ts.map +1 -1
  456. package/dist/languages/adapterBridge.js +37 -3
  457. package/dist/languages/adapterBridge.js.map +1 -1
  458. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  459. package/dist/languages/go/GoAdapter.js +11 -14
  460. package/dist/languages/go/GoAdapter.js.map +1 -1
  461. package/dist/languages/go/analyzer-darwin-amd64 +0 -0
  462. package/dist/languages/go/analyzer-darwin-arm64 +0 -0
  463. package/dist/languages/go/analyzer-linux-amd64 +0 -0
  464. package/dist/languages/go/analyzer-linux-arm64 +0 -0
  465. package/dist/languages/go/analyzer-src/analyzer.go +2 -0
  466. package/dist/languages/go/analyzer-src/dataaccess.go +388 -0
  467. package/dist/languages/go/analyzer-src/types.go +8 -0
  468. package/dist/languages/go/analyzer-windows-amd64.exe +0 -0
  469. package/dist/languages/go/database-packages.json +8 -0
  470. package/dist/languages/go/goResolution.d.ts +229 -0
  471. package/dist/languages/go/goResolution.d.ts.map +1 -0
  472. package/dist/languages/go/goResolution.js +799 -0
  473. package/dist/languages/go/goResolution.js.map +1 -0
  474. package/dist/languages/go/stdlib-packages.json +353 -0
  475. package/dist/languages/go/tenantInputs.d.ts +37 -0
  476. package/dist/languages/go/tenantInputs.d.ts.map +1 -0
  477. package/dist/languages/go/tenantInputs.js +121 -0
  478. package/dist/languages/go/tenantInputs.js.map +1 -0
  479. package/dist/languages/index.d.ts +1 -0
  480. package/dist/languages/index.d.ts.map +1 -1
  481. package/dist/languages/index.js +5 -0
  482. package/dist/languages/index.js.map +1 -1
  483. package/dist/languages/json/JsonAdapter.d.ts +177 -0
  484. package/dist/languages/json/JsonAdapter.d.ts.map +1 -0
  485. package/dist/languages/json/JsonAdapter.js +531 -0
  486. package/dist/languages/json/JsonAdapter.js.map +1 -0
  487. package/dist/languages/locationIndex.d.ts +41 -0
  488. package/dist/languages/locationIndex.d.ts.map +1 -0
  489. package/dist/languages/locationIndex.js +42 -0
  490. package/dist/languages/locationIndex.js.map +1 -0
  491. package/dist/languages/sql/dialectDetection.d.ts +57 -0
  492. package/dist/languages/sql/dialectDetection.d.ts.map +1 -0
  493. package/dist/languages/sql/dialectDetection.js +135 -0
  494. package/dist/languages/sql/dialectDetection.js.map +1 -0
  495. package/dist/languages/sql/sqlAst.d.ts +494 -0
  496. package/dist/languages/sql/sqlAst.d.ts.map +1 -0
  497. package/dist/languages/sql/sqlAst.js +1817 -0
  498. package/dist/languages/sql/sqlAst.js.map +1 -0
  499. package/dist/languages/testConventions.d.ts +12 -1
  500. package/dist/languages/testConventions.d.ts.map +1 -1
  501. package/dist/languages/testConventions.js +13 -1
  502. package/dist/languages/testConventions.js.map +1 -1
  503. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +1 -1
  504. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
  505. package/dist/languages/tree-sitter/converter.js +2 -2
  506. package/dist/languages/tree-sitter/converter.js.map +1 -1
  507. package/dist/languages/tree-sitter/parser.d.ts +3 -4
  508. package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
  509. package/dist/languages/tree-sitter/parser.js +10 -4
  510. package/dist/languages/tree-sitter/parser.js.map +1 -1
  511. package/dist/languages/tree-sitter/rawNode.d.ts +3 -0
  512. package/dist/languages/tree-sitter/rawNode.d.ts.map +1 -1
  513. package/dist/languages/tree-sitter/rawNode.js +3 -0
  514. package/dist/languages/tree-sitter/rawNode.js.map +1 -1
  515. package/dist/languages/types.d.ts +7 -0
  516. package/dist/languages/types.d.ts.map +1 -1
  517. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +36 -0
  518. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  519. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +168 -45
  520. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  521. package/dist/languages/typescript/database-packages.json +19 -0
  522. package/dist/ledger.d.ts +118 -2
  523. package/dist/ledger.d.ts.map +1 -1
  524. package/dist/ledger.js +167 -45
  525. package/dist/ledger.js.map +1 -1
  526. package/dist/legacyIndexNotice.d.ts +32 -0
  527. package/dist/legacyIndexNotice.d.ts.map +1 -0
  528. package/dist/legacyIndexNotice.js +89 -0
  529. package/dist/legacyIndexNotice.js.map +1 -0
  530. package/dist/mcp-tools/discoveryQueries.d.ts +55 -0
  531. package/dist/mcp-tools/discoveryQueries.d.ts.map +1 -0
  532. package/dist/mcp-tools/discoveryQueries.js +157 -0
  533. package/dist/mcp-tools/discoveryQueries.js.map +1 -0
  534. package/dist/mcp-tools/projectTasks.d.ts +9 -123
  535. package/dist/mcp-tools/projectTasks.d.ts.map +1 -1
  536. package/dist/mcp-tools/projectTasks.js +180 -170
  537. package/dist/mcp-tools/projectTasks.js.map +1 -1
  538. package/dist/mcp-tools/whitelistTools.d.ts +20 -0
  539. package/dist/mcp-tools/whitelistTools.d.ts.map +1 -1
  540. package/dist/mcp-tools/whitelistTools.js +20 -0
  541. package/dist/mcp-tools/whitelistTools.js.map +1 -1
  542. package/dist/mcp-tools/workflowGuide.d.ts +9 -0
  543. package/dist/mcp-tools/workflowGuide.d.ts.map +1 -1
  544. package/dist/mcp-tools/workflowGuide.js +13 -5
  545. package/dist/mcp-tools/workflowGuide.js.map +1 -1
  546. package/dist/mcp-tools-shared.d.ts +78 -0
  547. package/dist/mcp-tools-shared.d.ts.map +1 -1
  548. package/dist/mcp-tools-shared.js +203 -340
  549. package/dist/mcp-tools-shared.js.map +1 -1
  550. package/dist/mcp-ui-simple.d.ts +2 -0
  551. package/dist/mcp-ui-simple.d.ts.map +1 -1
  552. package/dist/mcp-ui-simple.js +12 -5
  553. package/dist/mcp-ui-simple.js.map +1 -1
  554. package/dist/mcp.d.ts +6 -0
  555. package/dist/mcp.d.ts.map +1 -1
  556. package/dist/mcp.js +832 -851
  557. package/dist/mcp.js.map +1 -1
  558. package/dist/mcpAuditJobs.d.ts +29 -33
  559. package/dist/mcpAuditJobs.d.ts.map +1 -1
  560. package/dist/mcpAuditJobs.js +102 -791
  561. package/dist/mcpAuditJobs.js.map +1 -1
  562. package/dist/mcpAutoIndex.d.ts +5 -0
  563. package/dist/mcpAutoIndex.d.ts.map +1 -1
  564. package/dist/mcpAutoIndex.js +7 -2
  565. package/dist/mcpAutoIndex.js.map +1 -1
  566. package/dist/mcpDiagnostics.d.ts +7 -0
  567. package/dist/mcpDiagnostics.d.ts.map +1 -1
  568. package/dist/mcpDiagnostics.js +34 -9
  569. package/dist/mcpDiagnostics.js.map +1 -1
  570. package/dist/mcpToolErrors.d.ts +16 -0
  571. package/dist/mcpToolErrors.d.ts.map +1 -1
  572. package/dist/mcpToolErrors.js +16 -0
  573. package/dist/mcpToolErrors.js.map +1 -1
  574. package/dist/nextFile.d.ts +6 -0
  575. package/dist/nextFile.d.ts.map +1 -1
  576. package/dist/nextFile.js +6 -0
  577. package/dist/nextFile.js.map +1 -1
  578. package/dist/nextFileIncremental.d.ts +44 -4
  579. package/dist/nextFileIncremental.d.ts.map +1 -1
  580. package/dist/nextFileIncremental.js +47 -7
  581. package/dist/nextFileIncremental.js.map +1 -1
  582. package/dist/phase/batchFunctions.d.ts +38 -0
  583. package/dist/phase/batchFunctions.d.ts.map +1 -0
  584. package/dist/phase/batchFunctions.js +64 -0
  585. package/dist/phase/batchFunctions.js.map +1 -0
  586. package/dist/phase/checks.d.ts +39 -0
  587. package/dist/phase/checks.d.ts.map +1 -0
  588. package/dist/phase/checks.js +48 -0
  589. package/dist/phase/checks.js.map +1 -0
  590. package/dist/phase/codeBlocks.d.ts +25 -0
  591. package/dist/phase/codeBlocks.d.ts.map +1 -0
  592. package/dist/phase/codeBlocks.js +268 -0
  593. package/dist/phase/codeBlocks.js.map +1 -0
  594. package/dist/phase/colorMath.d.ts +41 -0
  595. package/dist/phase/colorMath.d.ts.map +1 -0
  596. package/dist/phase/colorMath.js +109 -0
  597. package/dist/phase/colorMath.js.map +1 -0
  598. package/dist/phase/config.d.ts +29 -0
  599. package/dist/phase/config.d.ts.map +1 -0
  600. package/dist/phase/config.js +54 -0
  601. package/dist/phase/config.js.map +1 -0
  602. package/dist/phase/consumed.d.ts +47 -0
  603. package/dist/phase/consumed.d.ts.map +1 -0
  604. package/dist/phase/consumed.js +22 -0
  605. package/dist/phase/consumed.js.map +1 -0
  606. package/dist/phase/conventionMining.d.ts +59 -0
  607. package/dist/phase/conventionMining.d.ts.map +1 -0
  608. package/dist/phase/conventionMining.js +152 -0
  609. package/dist/phase/conventionMining.js.map +1 -0
  610. package/dist/phase/coverage.d.ts +65 -0
  611. package/dist/phase/coverage.d.ts.map +1 -0
  612. package/dist/phase/coverage.js +121 -0
  613. package/dist/phase/coverage.js.map +1 -0
  614. package/dist/phase/dag.d.ts +49 -0
  615. package/dist/phase/dag.d.ts.map +1 -0
  616. package/dist/phase/dag.js +120 -0
  617. package/dist/phase/dag.js.map +1 -0
  618. package/dist/phase/dataAccessCalls.d.ts +33 -0
  619. package/dist/phase/dataAccessCalls.d.ts.map +1 -0
  620. package/dist/phase/dataAccessCalls.js +42 -0
  621. package/dist/phase/dataAccessCalls.js.map +1 -0
  622. package/dist/phase/dataAccessCallsCandidates.d.ts +25 -0
  623. package/dist/phase/dataAccessCallsCandidates.d.ts.map +1 -0
  624. package/dist/phase/dataAccessCallsCandidates.js +27 -0
  625. package/dist/phase/dataAccessCallsCandidates.js.map +1 -0
  626. package/dist/phase/dynamicSql.d.ts +18 -0
  627. package/dist/phase/dynamicSql.d.ts.map +1 -0
  628. package/dist/phase/dynamicSql.js +20 -0
  629. package/dist/phase/dynamicSql.js.map +1 -0
  630. package/dist/phase/exportForm.d.ts +23 -0
  631. package/dist/phase/exportForm.d.ts.map +1 -0
  632. package/dist/phase/exportForm.js +29 -0
  633. package/dist/phase/exportForm.js.map +1 -0
  634. package/dist/phase/exportSymbols.d.ts +21 -0
  635. package/dist/phase/exportSymbols.d.ts.map +1 -0
  636. package/dist/phase/exportSymbols.js +28 -0
  637. package/dist/phase/exportSymbols.js.map +1 -0
  638. package/dist/phase/fileHeader.d.ts +15 -0
  639. package/dist/phase/fileHeader.d.ts.map +1 -0
  640. package/dist/phase/fileHeader.js +33 -0
  641. package/dist/phase/fileHeader.js.map +1 -0
  642. package/dist/phase/fileImports.d.ts +39 -0
  643. package/dist/phase/fileImports.d.ts.map +1 -0
  644. package/dist/phase/fileImports.js +138 -0
  645. package/dist/phase/fileImports.js.map +1 -0
  646. package/dist/phase/fileSymbols.d.ts +25 -0
  647. package/dist/phase/fileSymbols.d.ts.map +1 -0
  648. package/dist/phase/fileSymbols.js +302 -0
  649. package/dist/phase/fileSymbols.js.map +1 -0
  650. package/dist/phase/functionBodies.d.ts +28 -0
  651. package/dist/phase/functionBodies.d.ts.map +1 -0
  652. package/dist/phase/functionBodies.js +67 -0
  653. package/dist/phase/functionBodies.js.map +1 -0
  654. package/dist/phase/functionIndex.d.ts +28 -0
  655. package/dist/phase/functionIndex.d.ts.map +1 -0
  656. package/dist/phase/functionIndex.js +152 -0
  657. package/dist/phase/functionIndex.js.map +1 -0
  658. package/dist/phase/goFunctionAnalysis.d.ts +47 -0
  659. package/dist/phase/goFunctionAnalysis.d.ts.map +1 -0
  660. package/dist/phase/goFunctionAnalysis.js +291 -0
  661. package/dist/phase/goFunctionAnalysis.js.map +1 -0
  662. package/dist/phase/goFunctions.d.ts +38 -0
  663. package/dist/phase/goFunctions.d.ts.map +1 -0
  664. package/dist/phase/goFunctions.js +154 -0
  665. package/dist/phase/goFunctions.js.map +1 -0
  666. package/dist/phase/goImports.d.ts +29 -0
  667. package/dist/phase/goImports.d.ts.map +1 -0
  668. package/dist/phase/goImports.js +68 -0
  669. package/dist/phase/goImports.js.map +1 -0
  670. package/dist/phase/goPackageBindings.d.ts +22 -0
  671. package/dist/phase/goPackageBindings.d.ts.map +1 -0
  672. package/dist/phase/goPackageBindings.js +30 -0
  673. package/dist/phase/goPackageBindings.js.map +1 -0
  674. package/dist/phase/goSwitches.d.ts +28 -0
  675. package/dist/phase/goSwitches.d.ts.map +1 -0
  676. package/dist/phase/goSwitches.js +52 -0
  677. package/dist/phase/goSwitches.js.map +1 -0
  678. package/dist/phase/importForm.d.ts +20 -0
  679. package/dist/phase/importForm.d.ts.map +1 -0
  680. package/dist/phase/importForm.js +27 -0
  681. package/dist/phase/importForm.js.map +1 -0
  682. package/dist/phase/importSpecifiers.d.ts +20 -0
  683. package/dist/phase/importSpecifiers.d.ts.map +1 -0
  684. package/dist/phase/importSpecifiers.js +30 -0
  685. package/dist/phase/importSpecifiers.js.map +1 -0
  686. package/dist/phase/imports.d.ts +18 -0
  687. package/dist/phase/imports.d.ts.map +1 -0
  688. package/dist/phase/imports.js +25 -0
  689. package/dist/phase/imports.js.map +1 -0
  690. package/dist/phase/jsonDocument.d.ts +26 -0
  691. package/dist/phase/jsonDocument.d.ts.map +1 -0
  692. package/dist/phase/jsonDocument.js +36 -0
  693. package/dist/phase/jsonDocument.js.map +1 -0
  694. package/dist/phase/localBinding.d.ts +33 -0
  695. package/dist/phase/localBinding.d.ts.map +1 -0
  696. package/dist/phase/localBinding.js +208 -0
  697. package/dist/phase/localBinding.js.map +1 -0
  698. package/dist/phase/loopQueries.d.ts +31 -0
  699. package/dist/phase/loopQueries.d.ts.map +1 -0
  700. package/dist/phase/loopQueries.js +44 -0
  701. package/dist/phase/loopQueries.js.map +1 -0
  702. package/dist/phase/loopQueryCandidates.d.ts +26 -0
  703. package/dist/phase/loopQueryCandidates.d.ts.map +1 -0
  704. package/dist/phase/loopQueryCandidates.js +28 -0
  705. package/dist/phase/loopQueryCandidates.js.map +1 -0
  706. package/dist/phase/oracles.d.ts +216 -0
  707. package/dist/phase/oracles.d.ts.map +1 -0
  708. package/dist/phase/oracles.js +484 -0
  709. package/dist/phase/oracles.js.map +1 -0
  710. package/dist/phase/phaseModel.d.ts +190 -0
  711. package/dist/phase/phaseModel.d.ts.map +1 -0
  712. package/dist/phase/phaseModel.js +468 -0
  713. package/dist/phase/phaseModel.js.map +1 -0
  714. package/dist/phase/producers.d.ts +450 -0
  715. package/dist/phase/producers.d.ts.map +1 -0
  716. package/dist/phase/producers.js +1027 -0
  717. package/dist/phase/producers.js.map +1 -0
  718. package/dist/phase/querySiteCandidates.d.ts +27 -0
  719. package/dist/phase/querySiteCandidates.d.ts.map +1 -0
  720. package/dist/phase/querySiteCandidates.js +66 -0
  721. package/dist/phase/querySiteCandidates.js.map +1 -0
  722. package/dist/phase/querySites.d.ts +30 -0
  723. package/dist/phase/querySites.d.ts.map +1 -0
  724. package/dist/phase/querySites.js +93 -0
  725. package/dist/phase/querySites.js.map +1 -0
  726. package/dist/phase/reachability.d.ts +61 -0
  727. package/dist/phase/reachability.d.ts.map +1 -0
  728. package/dist/phase/reachability.js +104 -0
  729. package/dist/phase/reachability.js.map +1 -0
  730. package/dist/phase/reactComponents.d.ts +33 -0
  731. package/dist/phase/reactComponents.d.ts.map +1 -0
  732. package/dist/phase/reactComponents.js +47 -0
  733. package/dist/phase/reactComponents.js.map +1 -0
  734. package/dist/phase/receiverActivity.d.ts +35 -0
  735. package/dist/phase/receiverActivity.d.ts.map +1 -0
  736. package/dist/phase/receiverActivity.js +66 -0
  737. package/dist/phase/receiverActivity.js.map +1 -0
  738. package/dist/phase/receiverConsumers.d.ts +200 -0
  739. package/dist/phase/receiverConsumers.d.ts.map +1 -0
  740. package/dist/phase/receiverConsumers.js +1025 -0
  741. package/dist/phase/receiverConsumers.js.map +1 -0
  742. package/dist/phase/receiverProvenance.d.ts +97 -0
  743. package/dist/phase/receiverProvenance.d.ts.map +1 -0
  744. package/dist/phase/receiverProvenance.js +337 -0
  745. package/dist/phase/receiverProvenance.js.map +1 -0
  746. package/dist/phase/routing.d.ts +47 -0
  747. package/dist/phase/routing.d.ts.map +1 -0
  748. package/dist/phase/routing.js +62 -0
  749. package/dist/phase/routing.js.map +1 -0
  750. package/dist/phase/rules/conventions.d.ts +61 -0
  751. package/dist/phase/rules/conventions.d.ts.map +1 -0
  752. package/dist/phase/rules/conventions.js +403 -0
  753. package/dist/phase/rules/conventions.js.map +1 -0
  754. package/dist/phase/rules/crossDomain.d.ts +102 -0
  755. package/dist/phase/rules/crossDomain.d.ts.map +1 -0
  756. package/dist/phase/rules/crossDomain.js +918 -0
  757. package/dist/phase/rules/crossDomain.js.map +1 -0
  758. package/dist/phase/rules/dataAccess.d.ts +62 -0
  759. package/dist/phase/rules/dataAccess.d.ts.map +1 -0
  760. package/dist/phase/rules/dataAccess.js +480 -0
  761. package/dist/phase/rules/dataAccess.js.map +1 -0
  762. package/dist/phase/rules/dependencyGraph.d.ts +44 -0
  763. package/dist/phase/rules/dependencyGraph.d.ts.map +1 -0
  764. package/dist/phase/rules/dependencyGraph.js +260 -0
  765. package/dist/phase/rules/dependencyGraph.js.map +1 -0
  766. package/dist/phase/rules/documentation.d.ts +45 -0
  767. package/dist/phase/rules/documentation.d.ts.map +1 -0
  768. package/dist/phase/rules/documentation.js +340 -0
  769. package/dist/phase/rules/documentation.js.map +1 -0
  770. package/dist/phase/rules/dry.d.ts +109 -0
  771. package/dist/phase/rules/dry.d.ts.map +1 -0
  772. package/dist/phase/rules/dry.js +803 -0
  773. package/dist/phase/rules/dry.js.map +1 -0
  774. package/dist/phase/rules/fileDocumentation.d.ts +36 -0
  775. package/dist/phase/rules/fileDocumentation.d.ts.map +1 -0
  776. package/dist/phase/rules/fileDocumentation.js +134 -0
  777. package/dist/phase/rules/fileDocumentation.js.map +1 -0
  778. package/dist/phase/rules/functionBodies.d.ts +33 -0
  779. package/dist/phase/rules/functionBodies.d.ts.map +1 -0
  780. package/dist/phase/rules/functionBodies.js +71 -0
  781. package/dist/phase/rules/functionBodies.js.map +1 -0
  782. package/dist/phase/rules/goRules.d.ts +49 -0
  783. package/dist/phase/rules/goRules.d.ts.map +1 -0
  784. package/dist/phase/rules/goRules.js +209 -0
  785. package/dist/phase/rules/goRules.js.map +1 -0
  786. package/dist/phase/rules/querySites.d.ts +29 -0
  787. package/dist/phase/rules/querySites.d.ts.map +1 -0
  788. package/dist/phase/rules/querySites.js +94 -0
  789. package/dist/phase/rules/querySites.js.map +1 -0
  790. package/dist/phase/rules/react.d.ts +38 -0
  791. package/dist/phase/rules/react.d.ts.map +1 -0
  792. package/dist/phase/rules/react.js +202 -0
  793. package/dist/phase/rules/react.js.map +1 -0
  794. package/dist/phase/rules/registry.d.ts +170 -0
  795. package/dist/phase/rules/registry.d.ts.map +1 -0
  796. package/dist/phase/rules/registry.js +103 -0
  797. package/dist/phase/rules/registry.js.map +1 -0
  798. package/dist/phase/rules/schema.d.ts +79 -0
  799. package/dist/phase/rules/schema.d.ts.map +1 -0
  800. package/dist/phase/rules/schema.js +273 -0
  801. package/dist/phase/rules/schema.js.map +1 -0
  802. package/dist/phase/rules/schemaJson.d.ts +32 -0
  803. package/dist/phase/rules/schemaJson.d.ts.map +1 -0
  804. package/dist/phase/rules/schemaJson.js +81 -0
  805. package/dist/phase/rules/schemaJson.js.map +1 -0
  806. package/dist/phase/rules/schemaValidator.d.ts +38 -0
  807. package/dist/phase/rules/schemaValidator.d.ts.map +1 -0
  808. package/dist/phase/rules/schemaValidator.js +107 -0
  809. package/dist/phase/rules/schemaValidator.js.map +1 -0
  810. package/dist/phase/rules/secrets.d.ts +34 -0
  811. package/dist/phase/rules/secrets.d.ts.map +1 -0
  812. package/dist/phase/rules/secrets.js +208 -0
  813. package/dist/phase/rules/secrets.js.map +1 -0
  814. package/dist/phase/rules/security.d.ts +30 -0
  815. package/dist/phase/rules/security.d.ts.map +1 -0
  816. package/dist/phase/rules/security.js +85 -0
  817. package/dist/phase/rules/security.js.map +1 -0
  818. package/dist/phase/rules/securityDefects.d.ts +37 -0
  819. package/dist/phase/rules/securityDefects.d.ts.map +1 -0
  820. package/dist/phase/rules/securityDefects.js +167 -0
  821. package/dist/phase/rules/securityDefects.js.map +1 -0
  822. package/dist/phase/rules/solid.d.ts +61 -0
  823. package/dist/phase/rules/solid.d.ts.map +1 -0
  824. package/dist/phase/rules/solid.js +562 -0
  825. package/dist/phase/rules/solid.js.map +1 -0
  826. package/dist/phase/rules/styles.d.ts +77 -0
  827. package/dist/phase/rules/styles.d.ts.map +1 -0
  828. package/dist/phase/rules/styles.js +868 -0
  829. package/dist/phase/rules/styles.js.map +1 -0
  830. package/dist/phase/rules/unreferencedModule.d.ts +26 -0
  831. package/dist/phase/rules/unreferencedModule.d.ts.map +1 -0
  832. package/dist/phase/rules/unreferencedModule.js +61 -0
  833. package/dist/phase/rules/unreferencedModule.js.map +1 -0
  834. package/dist/phase/runner.d.ts +549 -0
  835. package/dist/phase/runner.d.ts.map +1 -0
  836. package/dist/phase/runner.js +1098 -0
  837. package/dist/phase/runner.js.map +1 -0
  838. package/dist/phase/schemaCode.d.ts +34 -0
  839. package/dist/phase/schemaCode.d.ts.map +1 -0
  840. package/dist/phase/schemaCode.js +53 -0
  841. package/dist/phase/schemaCode.js.map +1 -0
  842. package/dist/phase/schemaObjects.d.ts +37 -0
  843. package/dist/phase/schemaObjects.d.ts.map +1 -0
  844. package/dist/phase/schemaObjects.js +139 -0
  845. package/dist/phase/schemaObjects.js.map +1 -0
  846. package/dist/phase/schemaUsage.d.ts +30 -0
  847. package/dist/phase/schemaUsage.d.ts.map +1 -0
  848. package/dist/phase/schemaUsage.js +99 -0
  849. package/dist/phase/schemaUsage.js.map +1 -0
  850. package/dist/phase/schemaUsageCandidates.d.ts +32 -0
  851. package/dist/phase/schemaUsageCandidates.d.ts.map +1 -0
  852. package/dist/phase/schemaUsageCandidates.js +165 -0
  853. package/dist/phase/schemaUsageCandidates.js.map +1 -0
  854. package/dist/phase/schemaValidations.d.ts +34 -0
  855. package/dist/phase/schemaValidations.d.ts.map +1 -0
  856. package/dist/phase/schemaValidations.js +53 -0
  857. package/dist/phase/schemaValidations.js.map +1 -0
  858. package/dist/phase/secretCandidates.d.ts +28 -0
  859. package/dist/phase/secretCandidates.d.ts.map +1 -0
  860. package/dist/phase/secretCandidates.js +132 -0
  861. package/dist/phase/secretCandidates.js.map +1 -0
  862. package/dist/phase/securityCandidates.d.ts +29 -0
  863. package/dist/phase/securityCandidates.d.ts.map +1 -0
  864. package/dist/phase/securityCandidates.js +469 -0
  865. package/dist/phase/securityCandidates.js.map +1 -0
  866. package/dist/phase/seeded-defects.d.ts +18 -0
  867. package/dist/phase/seeded-defects.d.ts.map +1 -0
  868. package/dist/phase/seeded-defects.js +61 -0
  869. package/dist/phase/seeded-defects.js.map +1 -0
  870. package/dist/phase/stringLiterals.d.ts +24 -0
  871. package/dist/phase/stringLiterals.d.ts.map +1 -0
  872. package/dist/phase/stringLiterals.js +35 -0
  873. package/dist/phase/stringLiterals.js.map +1 -0
  874. package/dist/phase/stylesCss.d.ts +25 -0
  875. package/dist/phase/stylesCss.d.ts.map +1 -0
  876. package/dist/phase/stylesCss.js +34 -0
  877. package/dist/phase/stylesCss.js.map +1 -0
  878. package/dist/phase/stylesMarkup.d.ts +31 -0
  879. package/dist/phase/stylesMarkup.d.ts.map +1 -0
  880. package/dist/phase/stylesMarkup.js +48 -0
  881. package/dist/phase/stylesMarkup.js.map +1 -0
  882. package/dist/phase/stylesSource.d.ts +36 -0
  883. package/dist/phase/stylesSource.d.ts.map +1 -0
  884. package/dist/phase/stylesSource.js +50 -0
  885. package/dist/phase/stylesSource.js.map +1 -0
  886. package/dist/phase/typeDeclarations.d.ts +33 -0
  887. package/dist/phase/typeDeclarations.d.ts.map +1 -0
  888. package/dist/phase/typeDeclarations.js +76 -0
  889. package/dist/phase/typeDeclarations.js.map +1 -0
  890. package/dist/phase/types.d.ts +2191 -0
  891. package/dist/phase/types.d.ts.map +1 -0
  892. package/dist/phase/types.js +27 -0
  893. package/dist/phase/types.js.map +1 -0
  894. package/dist/phase/withinFileProvenance.d.ts +50 -0
  895. package/dist/phase/withinFileProvenance.d.ts.map +1 -0
  896. package/dist/phase/withinFileProvenance.js +112 -0
  897. package/dist/phase/withinFileProvenance.js.map +1 -0
  898. package/dist/pipeline.d.ts +37 -30
  899. package/dist/pipeline.d.ts.map +1 -1
  900. package/dist/pipeline.js +131 -326
  901. package/dist/pipeline.js.map +1 -1
  902. package/dist/pipelineAdapters.d.ts +104 -21
  903. package/dist/pipelineAdapters.d.ts.map +1 -1
  904. package/dist/pipelineAdapters.js +1145 -988
  905. package/dist/pipelineAdapters.js.map +1 -1
  906. package/dist/pipelineTypes.d.ts +21 -0
  907. package/dist/pipelineTypes.d.ts.map +1 -1
  908. package/dist/pipelineTypes.js +21 -10
  909. package/dist/pipelineTypes.js.map +1 -1
  910. package/dist/presets/presets.d.ts +9 -2
  911. package/dist/presets/presets.d.ts.map +1 -1
  912. package/dist/presets/presets.js +15 -26
  913. package/dist/presets/presets.js.map +1 -1
  914. package/dist/reporting/csvReportGenerator.d.ts +14 -3
  915. package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
  916. package/dist/reporting/csvReportGenerator.js +14 -3
  917. package/dist/reporting/csvReportGenerator.js.map +1 -1
  918. package/dist/reporting/htmlReportGenerator.d.ts +5 -1
  919. package/dist/reporting/htmlReportGenerator.d.ts.map +1 -1
  920. package/dist/reporting/htmlReportGenerator.js +5 -1
  921. package/dist/reporting/htmlReportGenerator.js.map +1 -1
  922. package/dist/reporting/jsonReportGenerator.d.ts +9 -2
  923. package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
  924. package/dist/reporting/jsonReportGenerator.js +19 -5
  925. package/dist/reporting/jsonReportGenerator.js.map +1 -1
  926. package/dist/reporting/reportGenerator.d.ts +9 -2
  927. package/dist/reporting/reportGenerator.d.ts.map +1 -1
  928. package/dist/reporting/reportGenerator.js +9 -2
  929. package/dist/reporting/reportGenerator.js.map +1 -1
  930. package/dist/reporting/sarifReportGenerator.d.ts +14 -0
  931. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  932. package/dist/reporting/sarifReportGenerator.js +14 -0
  933. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  934. package/dist/reporting/sizeDistribution.d.ts +19 -2
  935. package/dist/reporting/sizeDistribution.d.ts.map +1 -1
  936. package/dist/reporting/sizeDistribution.js +23 -5
  937. package/dist/reporting/sizeDistribution.js.map +1 -1
  938. package/dist/ruleAliases.d.ts +6 -8
  939. package/dist/ruleAliases.d.ts.map +1 -1
  940. package/dist/ruleAliases.js +29 -4
  941. package/dist/ruleAliases.js.map +1 -1
  942. package/dist/search/QueryParser.d.ts +1 -0
  943. package/dist/search/QueryParser.d.ts.map +1 -1
  944. package/dist/search/QueryParser.js +32 -91
  945. package/dist/search/QueryParser.js.map +1 -1
  946. package/dist/services/CodeMapGenerator.d.ts +19 -20
  947. package/dist/services/CodeMapGenerator.d.ts.map +1 -1
  948. package/dist/services/CodeMapGenerator.js +214 -194
  949. package/dist/services/CodeMapGenerator.js.map +1 -1
  950. package/dist/services/ProjectTaskRepository.d.ts +65 -3
  951. package/dist/services/ProjectTaskRepository.d.ts.map +1 -1
  952. package/dist/services/ProjectTaskRepository.js +94 -25
  953. package/dist/services/ProjectTaskRepository.js.map +1 -1
  954. package/dist/services/auditJobService.d.ts +17 -1
  955. package/dist/services/auditJobService.d.ts.map +1 -1
  956. package/dist/services/auditJobService.js +16 -0
  957. package/dist/services/auditJobService.js.map +1 -1
  958. package/dist/services/fileAccounting.d.ts +43 -5
  959. package/dist/services/fileAccounting.d.ts.map +1 -1
  960. package/dist/services/fileAccounting.js +48 -9
  961. package/dist/services/fileAccounting.js.map +1 -1
  962. package/dist/services/projectTaskHelpers.d.ts +45 -0
  963. package/dist/services/projectTaskHelpers.d.ts.map +1 -1
  964. package/dist/services/projectTaskHelpers.js +45 -0
  965. package/dist/services/projectTaskHelpers.js.map +1 -1
  966. package/dist/services/whitelistService.d.ts +40 -5
  967. package/dist/services/whitelistService.d.ts.map +1 -1
  968. package/dist/services/whitelistService.js +87 -46
  969. package/dist/services/whitelistService.js.map +1 -1
  970. package/dist/sqlite/betterSqlite3.d.ts +35 -1
  971. package/dist/sqlite/betterSqlite3.d.ts.map +1 -1
  972. package/dist/sqlite/betterSqlite3.js +35 -1
  973. package/dist/sqlite/betterSqlite3.js.map +1 -1
  974. package/dist/sqlite/driver.d.ts +23 -0
  975. package/dist/sqlite/driver.d.ts.map +1 -1
  976. package/dist/sqlite/driver.js +23 -0
  977. package/dist/sqlite/driver.js.map +1 -1
  978. package/dist/sqlite/nodeSqlite.d.ts +35 -1
  979. package/dist/sqlite/nodeSqlite.d.ts.map +1 -1
  980. package/dist/sqlite/nodeSqlite.js +35 -1
  981. package/dist/sqlite/nodeSqlite.js.map +1 -1
  982. package/dist/structuralSignature.d.ts +9 -0
  983. package/dist/structuralSignature.d.ts.map +1 -1
  984. package/dist/structuralSignature.js +9 -0
  985. package/dist/structuralSignature.js.map +1 -1
  986. package/dist/styles/cssAstExtractor.d.ts +15 -0
  987. package/dist/styles/cssAstExtractor.d.ts.map +1 -1
  988. package/dist/styles/cssAstExtractor.js +15 -0
  989. package/dist/styles/cssAstExtractor.js.map +1 -1
  990. package/dist/styles/normalizer.d.ts +6 -0
  991. package/dist/styles/normalizer.d.ts.map +1 -1
  992. package/dist/styles/normalizer.js +6 -0
  993. package/dist/styles/normalizer.js.map +1 -1
  994. package/dist/styles/styleExtractor.d.ts +29 -2
  995. package/dist/styles/styleExtractor.d.ts.map +1 -1
  996. package/dist/styles/styleExtractor.js +67 -48
  997. package/dist/styles/styleExtractor.js.map +1 -1
  998. package/dist/styles/styleIndexer.d.ts +8 -0
  999. package/dist/styles/styleIndexer.d.ts.map +1 -1
  1000. package/dist/styles/styleIndexer.js +135 -138
  1001. package/dist/styles/styleIndexer.js.map +1 -1
  1002. package/dist/styles/tailwindConfigLoader.d.ts +12 -0
  1003. package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
  1004. package/dist/styles/tailwindConfigLoader.js +12 -0
  1005. package/dist/styles/tailwindConfigLoader.js.map +1 -1
  1006. package/dist/styles/tailwindExpander.d.ts +3 -0
  1007. package/dist/styles/tailwindExpander.d.ts.map +1 -1
  1008. package/dist/styles/tailwindExpander.js +8 -4
  1009. package/dist/styles/tailwindExpander.js.map +1 -1
  1010. package/dist/styles/tailwindProbe.d.ts +9 -0
  1011. package/dist/styles/tailwindProbe.d.ts.map +1 -1
  1012. package/dist/styles/tailwindProbe.js +181 -166
  1013. package/dist/styles/tailwindProbe.js.map +1 -1
  1014. package/dist/styles/tailwindUtilityExpander.d.ts +13 -0
  1015. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  1016. package/dist/styles/tailwindUtilityExpander.js +13 -0
  1017. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  1018. package/dist/symbols.d.ts +13 -11
  1019. package/dist/symbols.d.ts.map +1 -1
  1020. package/dist/symbols.js +14 -21
  1021. package/dist/symbols.js.map +1 -1
  1022. package/dist/telemetry.d.ts +31 -3
  1023. package/dist/telemetry.d.ts.map +1 -1
  1024. package/dist/telemetry.js +31 -3
  1025. package/dist/telemetry.js.map +1 -1
  1026. package/dist/tool-registry.d.ts +37 -1
  1027. package/dist/tool-registry.d.ts.map +1 -1
  1028. package/dist/tool-registry.js +37 -1
  1029. package/dist/tool-registry.js.map +1 -1
  1030. package/dist/types.d.ts +106 -92
  1031. package/dist/types.d.ts.map +1 -1
  1032. package/dist/types.js +7 -16
  1033. package/dist/types.js.map +1 -1
  1034. package/dist/ui/InteractivePrompts.d.ts +12 -0
  1035. package/dist/ui/InteractivePrompts.d.ts.map +1 -1
  1036. package/dist/ui/InteractivePrompts.js +12 -0
  1037. package/dist/ui/InteractivePrompts.js.map +1 -1
  1038. package/dist/utils/astParser.d.ts +15 -0
  1039. package/dist/utils/astParser.d.ts.map +1 -1
  1040. package/dist/utils/astParser.js +15 -0
  1041. package/dist/utils/astParser.js.map +1 -1
  1042. package/dist/utils/astUtils.d.ts +60 -0
  1043. package/dist/utils/astUtils.d.ts.map +1 -1
  1044. package/dist/utils/astUtils.js +350 -362
  1045. package/dist/utils/astUtils.js.map +1 -1
  1046. package/dist/utils/contentHash.d.ts +22 -0
  1047. package/dist/utils/contentHash.d.ts.map +1 -0
  1048. package/dist/utils/contentHash.js +26 -0
  1049. package/dist/utils/contentHash.js.map +1 -0
  1050. package/dist/utils/dependencyExtractor.d.ts +28 -0
  1051. package/dist/utils/dependencyExtractor.d.ts.map +1 -1
  1052. package/dist/utils/dependencyExtractor.js +28 -1
  1053. package/dist/utils/dependencyExtractor.js.map +1 -1
  1054. package/dist/utils/errorMessage.d.ts +10 -0
  1055. package/dist/utils/errorMessage.d.ts.map +1 -0
  1056. package/dist/utils/errorMessage.js +12 -0
  1057. package/dist/utils/errorMessage.js.map +1 -0
  1058. package/dist/utils/fileDiscovery.d.ts +61 -2
  1059. package/dist/utils/fileDiscovery.d.ts.map +1 -1
  1060. package/dist/utils/fileDiscovery.js +131 -23
  1061. package/dist/utils/fileDiscovery.js.map +1 -1
  1062. package/dist/utils/reactDetection.d.ts +34 -5
  1063. package/dist/utils/reactDetection.d.ts.map +1 -1
  1064. package/dist/utils/reactDetection.js +73 -52
  1065. package/dist/utils/reactDetection.js.map +1 -1
  1066. package/dist/version.generated.d.ts +1 -1
  1067. package/dist/version.generated.js +1 -1
  1068. package/dist/workers/auditWorker.js +7 -3
  1069. package/dist/workers/auditWorker.js.map +1 -1
  1070. package/examples/.auditrc.example.json +0 -8
  1071. package/examples/.auditrc.json +0 -11
  1072. package/examples/.code-auditor.json +0 -2
  1073. package/examples/nextjs.auditrc.json +0 -8
  1074. package/examples/node-api.auditrc.json +0 -7
  1075. package/examples/react.auditrc.json +0 -6
  1076. package/examples/tolerant-solid.auditrc.json +0 -5
  1077. package/package.json +14 -8
  1078. package/plugin/.claude-plugin/manifest.json +16 -0
  1079. package/plugin/.claude-plugin/plugin.json +1 -1
  1080. package/plugin/hooks/hooks.json +6 -2
  1081. package/plugin/scripts/hook-audit.sh +41 -14
  1082. package/plugin/scripts/hook-common.sh +50 -5
  1083. package/plugin/scripts/hook-drift-check.sh +72 -0
  1084. package/plugin/scripts/hook-self-audit.sh +17 -12
  1085. package/plugin/skills/code-auditor/SKILL.md +1 -1
  1086. package/server.json +2 -2
@@ -3,21 +3,27 @@
3
3
  * Works across multiple programming languages using the adapter pattern
4
4
  * Analyzes database access patterns and data layer interactions
5
5
  */
6
- import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
7
6
  import { withRuleTiming } from '../ruleTiming.js';
8
- import { buildProvenanceContext, isDBProvenanced, getCallExpressionCallee, DB_CALL_METHODS, ORM_METHODS, } from '../provenance.js';
9
- import { DB_RECEIVER_NAMES, DB_CALL_METHOD_NAMES, DB_BINDING_NAMES, DB_WRAPPER_NAMES, } from './UniversalSchemaAnalyzer.js';
10
- import { isSqlKeyword, findEnclosingFunctionIdentity, functionIdentityLabel } from './schema/codeAnalysis.js';
11
- import { isTestOrSpecPath } from '../../languages/testConventions.js';
12
- import { buildOrgFilterTierSet, tableRequiresOrgFilter } from '../orgFilterTiers.js';
13
- /**
14
- * SQL keywords recognized as evidence that a string is a SQL query.
15
- * Includes DML verbs (SELECT/INSERT/UPDATE/DELETE) and DDL verbs
16
- * (CREATE/DROP/ALTER/TRUNCATE) so DDL injection — e.g. a raw schema
17
- * migration built by concatenating an interpolated identifier — is
18
- * recognized as SQL rather than silently passing the keyword gate.
19
- * Single source of truth for both containsSQLKeywords and
20
- * containsSQLStructure to prevent drift.
7
+ import { buildProvenanceContext, getCallExpressionCallee, getMemberExpressionReceiver, extractMemberExpressionProperty, resolveSiteDialect, } from '../provenance.js';
8
+ import { extractStaticSql, extractGoStaticSql, isTemplateLiteral, isTaggedTemplateSqlCall, stripSqlQuotes } from '../sqlLiteral.js';
9
+ import { isOrmMethod, handleTypesForPackage } from '../tsEcosystem.js';
10
+ import { identifyHandle } from '../handleIdentification.js';
11
+ import { buildBindingEnv, extractInterfaceFields, resolveReceiverRoot, isDbShapedRoot, findEnclosingClassHeritage, } from '../receiverRoot.js';
12
+ import { buildGoBindingEnv, buildGoImportMap, classifyGoRootIdentifier, } from '../../languages/go/goResolution.js';
13
+ import { DB_BINDING_NAMES, DB_WRAPPER_NAMES, SQL_TAG_NAMES, } from './UniversalSchemaAnalyzer.js';
14
+ import { findEnclosingFunctionIdentity, functionIdentityLabel } from './schema/codeAnalysis.js';
15
+ import { buildOrgFilterTierSet, tableRequiresOrgFilter, DEFAULT_ORG_PREDICATE_PATTERNS, hasOrganizationFilter, } from '../orgFilterTiers.js';
16
+ import { resolveWhereBinding } from '../../phase/localBinding.js';
17
+ import { parseSql, DEFAULT_SQL_DIALECT, extractTableNames, whereFacts, isWriteStatement, isMassWriteStatement, isUpsertStatement, whereColumnRefs, isDdlStatementKind, } from '../../languages/sql/sqlAst.js';
18
+ /**
19
+ * SQL keywords recognized as evidence that a *dynamically-constructed* string is
20
+ * an injection surface — the host-language dataflow gate in `checkQuerySecurity`
21
+ * (site #11, not converted by R2). This list is NOT the SQL-ness admission gate:
22
+ * that decision is provenance/shape/tag-based (see `isSqlPosition` in
23
+ * `buildDatabaseCall`), and a keyword name list must not decide whether a string
24
+ * is SQL — it would silently drop keyword-less SQL (`PRAGMA`, `VACUUM`,
25
+ * `ANALYZE`) that provenance already establishes as SQL. `containsSQLStructure`
26
+ * is gone (R2 site #4); this list now feeds only `checkQuerySecurity`.
21
27
  */
22
28
  const SQL_KEYWORDS = [
23
29
  'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'FROM', 'WHERE', 'JOIN',
@@ -27,39 +33,20 @@ export const DEFAULT_DATA_ACCESS_CONFIG = {
27
33
  checkOrgFilters: true,
28
34
  checkSQLInjection: true,
29
35
  skipTestFiles: true,
30
- databases: {
31
- 'primary': {
32
- name: 'Primary Database',
33
- importPatterns: ['/database/', '/db/', './db', './schema', 'drizzle', 'prisma', 'typeorm', 'knex', 'sequelize'],
34
- queryPatterns: ['select', 'insert', 'update', 'delete', 'query', 'execute'],
35
- ormPatterns: ['from', 'where', 'join', 'orderBy', 'groupBy']
36
- }
37
- },
38
- organizationPatterns: [
39
- 'organizationId',
40
- 'organization_id',
41
- 'orgId',
42
- 'org_id',
43
- 'tenantId',
44
- 'tenant_id',
45
- 'companyId',
46
- 'company_id'
47
- ],
36
+ organizationPatterns: [...DEFAULT_ORG_PREDICATE_PATTERNS],
48
37
  // Spec 21 R6.2: three-tier org-filter detection
49
38
  orgFilterTables: [], // Tier 1: empty — user must declare
50
39
  orgFilterColumns: ['org_id', 'tenant_id', 'organization_id', 'workspace_id'], // Tier 2
51
40
  schemas: [], // Tier 2: schema definitions for column-based inference
52
41
  tablePatterns: {
42
+ // Only the non-structural `table: 'users'` / `table = "users"` shape remains
43
+ // here as a user-tunable text pattern. The `.from(x)` / join / Kysely-verb
44
+ // shapes are read structurally by `collectOrmTableReferences`, so they are
45
+ // NOT duplicated as regexes — a `.from(…)` inside a string argument must not
46
+ // read as a table.
53
47
  orm: [
54
- /from\s*\(\s*["'`]?([\p{L}\p{N}_]+)["'`]?\s*\)/giu,
55
- /table\s*[:=]\s*["'`]?([\p{L}\p{N}_]+)["'`]?/giu,
56
- /\.from\s*\(\s*([\p{L}\p{N}_]+)\s*\)/giu, // Handle .from(users) where users is a variable
57
- /join\s*\(\s*([\p{L}\p{N}_]+)\s*,/giu, // Handle joins
58
- /leftJoin\s*\(\s*([\p{L}\p{N}_]+)\s*,/giu,
59
- /rightJoin\s*\(\s*([\p{L}\p{N}_]+)\s*,/giu,
60
- /innerJoin\s*\(\s*([\p{L}\p{N}_]+)\s*,/giu
48
+ /table\s*[:=]\s*["'`]?([\p{L}\p{N}_]+)["'`]?/giu
61
49
  ],
62
- sql: [/\b(?:INSERT(?:\s+OR\s+(?:IGNORE|REPLACE))?|REPLACE)\s+INTO\s+["'`]?([\p{L}\p{N}_]+)["'`]?/giu, /DELETE\s+FROM\s+["'`]?([\p{L}\p{N}_]+)["'`]?/giu, /FROM\s+["'`]?([\p{L}\p{N}_]+)["'`]?/giu, /JOIN\s+["'`]?([\p{L}\p{N}_]+)["'`]?/giu, /UPDATE\s+["'`]?([\p{L}\p{N}_]+)["'`]?/giu],
63
50
  queryBuilder: [/\.from\s*\(\s*["'`]?([\p{L}\p{N}_]+)["'`]?\s*\)/giu]
64
51
  },
65
52
  performanceThresholds: {
@@ -77,10 +64,12 @@ export const DEFAULT_DATA_ACCESS_CONFIG = {
77
64
  dbWrapperNames: [...DB_WRAPPER_NAMES],
78
65
  // DB detection patterns — values shared with the schema analyzer, but owned by
79
66
  // THIS analyzer's namespace (no cross-analyzer fallback in analyzeAST).
80
- dbReceiverNames: [...DB_RECEIVER_NAMES],
81
- dbCallMethods: [...DB_CALL_METHOD_NAMES],
82
67
  dbBindingNames: [...DB_BINDING_NAMES],
68
+ sqlTagNames: [...SQL_TAG_NAMES],
83
69
  detection: { mode: 'hybrid' },
70
+ // Spec 70 R1 — no dialect until the corpus names one; the SQL-content facts
71
+ // then `cannot-fire` rather than being regex-derived.
72
+ dialect: null,
84
73
  // SQL sanitizer functions — interpolation via escapeSql(x) is not raw.
85
74
  sanitizerNames: ['escapeSql'],
86
75
  };
@@ -101,47 +90,406 @@ function makeViolation(file, location, message, classification) {
101
90
  analyzer: 'data-access'
102
91
  };
103
92
  if (classification.symbol)
104
- v.functionName = classification.symbol;
93
+ v.symbol = classification.symbol;
105
94
  if (classification.resolution)
106
95
  v.resolution = classification.resolution;
107
96
  return v;
108
97
  }
109
98
  /**
110
- * Map imports to database types
99
+ * The four companion pairs of the query-builder chain grammar (Spec 68 Thing 2,
100
+ * #312). A verb admits a chain only when one of its required companions appears
101
+ * LATER in the same callee — this is the Drizzle/Kysely grammar and is pure
102
+ * syntax (no catalog, no name list).
103
+ */
104
+ const QUERY_BUILDER_COMPANIONS = [
105
+ // `.select` also admits Drizzle's `.selectDistinct(…)` / `.selectDistinctOn(…)`
106
+ // — the same select verb followed by `.from(`; `.select\b` alone would miss the
107
+ // `Distinct` suffix (a word character breaks the boundary).
108
+ { verb: /\.select(?:DistinctOn|Distinct)?\b/, companions: [/\.from\b/] },
109
+ { verb: /\.insert\b/, companions: [/\.values\b/] },
110
+ { verb: /\.update\b/, companions: [/\.set\b/, /\.where\b/] },
111
+ { verb: /\.delete\b/, companions: [/\.where\b/] },
112
+ ];
113
+ /**
114
+ * Admitter 1 — verb plus required companion, tested on the CALLEE text. The
115
+ * callee of the outermost call in a chain carries every earlier verb with its
116
+ * argument (e.g. `db.select({…}).from(users).where(…)` → callee text
117
+ * `db.select({…}).from(users).where`), so a single substring scan over the
118
+ * callee sees the whole grammar. The trailing verb appears without its paren
119
+ * (the arguments belong to the outermost call), so companions are matched with
120
+ * a word boundary, not `\(`. `map.delete(k)`, `createHash().update(b)`,
121
+ * `cookies.delete(n)` and `stripe.customers.update(id, data)` all lack the
122
+ * companion and fail.
123
+ */
124
+ /**
125
+ * Collect the method names of a callee's member chain, left-to-right (base
126
+ * first), reading each property_identifier node. String arguments and
127
+ * subscripts are structural — a `.from(…)` inside a string argument is NOT a
128
+ * chain member, so it is not collected.
111
129
  */
112
- function mapDatabaseImports(imports, config) {
113
- const dbImports = new Map();
114
- Object.entries(config.databases || {}).forEach(([dbType, dbConfig]) => {
115
- const hasImports = imports.some(imp => dbConfig.importPatterns.some(pattern => imp.source.includes(pattern)));
116
- dbImports.set(dbType, {
117
- hasImports,
118
- patterns: [...dbConfig.queryPatterns, ...(dbConfig.ormPatterns || [])]
130
+ function collectChainMethods(node, adapter, sourceCode, out = []) {
131
+ const t = adapter.getNodeType(node);
132
+ if (t === 'member_expression' || t === 'selector_expression') {
133
+ const children = adapter.getChildren(node);
134
+ const object = children.find((c) => {
135
+ const ct = adapter.getNodeType(c);
136
+ return ct !== '.' && ct !== 'property_identifier' && ct !== 'field_identifier';
119
137
  });
138
+ if (object)
139
+ collectChainMethods(object, adapter, sourceCode, out);
140
+ const prop = extractMemberExpressionProperty(node, adapter, sourceCode);
141
+ if (prop)
142
+ out.push(prop);
143
+ }
144
+ else if (t === 'call_expression') {
145
+ const callee = getCallExpressionCallee(node, adapter);
146
+ if (callee)
147
+ collectChainMethods(callee, adapter, sourceCode, out);
148
+ }
149
+ else if (t === 'subscript_expression') {
150
+ const children = adapter.getChildren(node);
151
+ const object = children.find((c) => {
152
+ const ct = adapter.getNodeType(c);
153
+ return ct !== '[' && ct !== ']';
154
+ });
155
+ if (object)
156
+ collectChainMethods(object, adapter, sourceCode, out);
157
+ }
158
+ return out;
159
+ }
160
+ function hasChainCompanion(methods) {
161
+ for (const { verb, companions } of QUERY_BUILDER_COMPANIONS) {
162
+ const verbIdx = methods.findIndex((m) => verb.test('.' + m));
163
+ if (verbIdx < 0)
164
+ continue;
165
+ const rest = methods.slice(verbIdx + 1);
166
+ if (rest.some((m) => companions.some((companion) => companion.test('.' + m))))
167
+ return true;
168
+ }
169
+ return false;
170
+ }
171
+ /** Prisma CRUD verbs admitted by the object-form shape (admitter 2). */
172
+ const PRISMA_VERBS = new Set([
173
+ 'create', 'createMany', 'update', 'updateMany', 'upsert',
174
+ 'delete', 'deleteMany', 'findUnique', 'findFirst', 'findMany',
175
+ 'findUniqueOrThrow', 'findFirstOrThrow', 'findRaw', 'count', 'groupBy', 'aggregate',
176
+ ]);
177
+ /** Collect the property keys of an object literal, including its nested objects. */
178
+ function collectObjectLiteralKeys(node, adapter, sourceCode) {
179
+ const keys = [];
180
+ const stack = [node];
181
+ while (stack.length) {
182
+ const n = stack.pop();
183
+ const t = adapter.getNodeType(n);
184
+ if (t === 'shorthand_property_identifier' || t === 'property_identifier') {
185
+ const txt = adapter.getNodeText(n, sourceCode)?.trim();
186
+ if (txt)
187
+ keys.push(txt);
188
+ continue;
189
+ }
190
+ for (const c of adapter.getChildren(n) ?? [])
191
+ stack.push(c);
192
+ }
193
+ return keys;
194
+ }
195
+ /**
196
+ * Admitter 2 — Prisma's object form: `<recv>.<Model>.<verb>({ … })` where the
197
+ * object-literal argument carries a `where` or `data` key. Option 1 (chained
198
+ * companions) misses this because the `where`/`data` are keys inside an object
199
+ * literal, not chained calls (blitz `prisma.user.update({ where, data })`).
200
+ */
201
+ function isPrismaObjectForm(node, adapter, sourceCode) {
202
+ const callee = getCallExpressionCallee(node, adapter);
203
+ if (!callee || adapter.getNodeType(callee) !== 'member_expression')
204
+ return false;
205
+ // The chain must be `<recv>.<Model>.<verb>` — at least two member segments
206
+ // (the Model and the verb) read from their property nodes. A subscript like
207
+ // `obj["a.b"]` is not a Model segment, so `obj["a.b"].update(…)` has only one
208
+ // segment and fails.
209
+ const methods = collectChainMethods(callee, adapter, sourceCode);
210
+ if (methods.length < 2)
211
+ return false;
212
+ if (!PRISMA_VERBS.has(methods[methods.length - 1]))
213
+ return false;
214
+ const args = adapter.getChildren(node).find((c) => adapter.getNodeType(c) === 'arguments');
215
+ if (!args)
216
+ return false;
217
+ for (const arg of adapter.getChildren(args)) {
218
+ const keys = collectObjectLiteralKeys(arg, adapter, sourceCode);
219
+ if (keys.includes('where') || keys.includes('data'))
220
+ return true;
221
+ }
222
+ return false;
223
+ }
224
+ /**
225
+ * Spec 68 Thing 2 (#312) — the query-builder shape test.
226
+ *
227
+ * Answers "is this a DB query?" structurally, from the call's own grammar,
228
+ * instead of by the receiver's name. A chain that shapes like a Drizzle/Kysely
229
+ * builder or a Prisma CRUD call is a query regardless of whether its receiver
230
+ * is named `db`, `appDb`, `tx`, or `prisma`. This replaces the receiver-name
231
+ * blind spot (`DB_RECEIVER_NAMES`) without growing the name list — the raw-SQL
232
+ * path (`db.exec(sql)`) still relies on receiver/import provenance because it
233
+ * has no chain grammar to lean on.
234
+ *
235
+ * Two admitting conditions, union (not intersection):
236
+ * 1. verb + companion chain grammar — {@link hasChainCompanion};
237
+ * 2. Prisma object form — {@link isPrismaObjectForm}.
238
+ *
239
+ * A third admitter — catalog-resolved argument (a bare `.from(table)` /
240
+ * `.query(table)` whose verb argument resolves to a `resolution` table) —
241
+ * was evaluated and REJECTED. The residual gap after admitter 1+2 contains no
242
+ * such chain: the real misses are *variable-split* builders (the companion verb
243
+ * lives in a prior statement, e.g. `const q = baseQuery.where(…)`), which
244
+ * catalog resolution does not address. Gating on an under-discovered catalog
245
+ * would just rebuild the receiver-name failure one layer up. See the
246
+ * extraction-completeness gate (scripts/verify-extraction-completeness.ts) for
247
+ * the pinned residual.
248
+ *
249
+ * @param node the candidate call node to classify
250
+ * @param adapter the language adapter used to read the callee
251
+ * @param sourceCode the file source text for reading callee text
252
+ * @returns true when the call is query-builder shaped (chain companion or Prisma object form)
253
+ */
254
+ export function isQueryBuilderShape(node, adapter, sourceCode) {
255
+ if (node.type !== 'call_expression')
256
+ return false;
257
+ const callee = getCallExpressionCallee(node, adapter);
258
+ if (!callee)
259
+ return false;
260
+ const methods = collectChainMethods(callee, adapter, sourceCode);
261
+ return hasChainCompanion(methods) || isPrismaObjectForm(node, adapter, sourceCode);
262
+ }
263
+ /**
264
+ * The single admission seam for a candidate node: resolve its handle-ness through
265
+ * `identifyHandle` (Spec 70 R1.2) and return the tri-state verdict, or `null`
266
+ * when the node is not a query-shaped DB/ORM call in the TypeScript family.
267
+ *
268
+ * The candidate *filter* (a DB/ORM method, or a provenanced wrapper name) is
269
+ * query-shape vocabulary — the same surface `isDBMethodCall` admitted — not a
270
+ * handle test. Handle-ness is decided once, here: a `handle` verdict admits the
271
+ * site (carrying `via`); `not-handle` rejects it; `unproven` admits it *as
272
+ * unproven* (carrying the reason) so downstream rules see the site and report
273
+ * `cannot-fire` rather than never seeing it. Admission is tri-state, not boolean.
274
+ */
275
+ function handleVerdictForCall(node, scan) {
276
+ const { adapter, sourceCode, config, provenanceContext, handleEnv } = scan;
277
+ if (!provenanceContext)
278
+ return null;
279
+ // Go: declaration resolution runs through Go's own binding/import environment
280
+ // (provenance seed + bindings + imports), routed through the same
281
+ // `identifyHandle` seam. `classifyGoRootIdentifier` checks the provenance seed
282
+ // first, so a cross-file-resolved `db *sql.DB` proves `handle`; the seed also
283
+ // carries the receiver's package, so an unrecognized import stays `unproven`
284
+ // (cannot-fire) rather than being guessed `not-handle` (Spec 70 R4).
285
+ if (adapter.name === 'go') {
286
+ return goHandleVerdictForCall(node, scan);
287
+ }
288
+ if (!handleEnv)
289
+ return null;
290
+ // A template literal is admitted via the call whose argument it is.
291
+ const callNode = isTemplateLiteral(node, adapter) ? enclosingCallOf(node, adapter) : node;
292
+ if (!callNode || !isFunctionCall(callNode, adapter))
293
+ return null;
294
+ // A constructor call (`new Pool()`) is an instantiation — how a handle is
295
+ // *created* — not a data-access call itself. Its provenanced constructor
296
+ // proves the *binding* (`const db = new Pool()`); the constructor expression
297
+ // is never a query site, so admit nothing here. (Spec 70 R4)
298
+ if (callNode.type === 'new_expression')
299
+ return null;
300
+ const callee = getCallExpressionCallee(callNode, adapter);
301
+ if (!callee)
302
+ return null;
303
+ const dialect = resolveSiteDialect(callNode, adapter, sourceCode, provenanceContext) ??
304
+ config.dialect ?? null;
305
+ const facts = () => ({
306
+ imports: new Map(),
307
+ typeAnnotations: new Map(),
308
+ bindings: new Map(),
309
+ withinFileProvenance: new Map(),
310
+ sqlDialect: dialect,
311
+ resolution: { dialect: 'ts', env: handleEnv },
312
+ });
313
+ // Bare-identifier call (`query(…)` / `d1(…)`): admit a provenanced wrapper or a
314
+ // type-annotated handle (`const query: D1Database = …`), and let `identifyHandle`
315
+ // decide handle / not-handle / unproven on the one entry point. A truly unbound
316
+ // name (an ambient global like `someFunction`) is rejected here — not because its
317
+ // name proves non-handle, but because its SQL argument must not be allowed to
318
+ // *prove* handle-ness (R3) on a name with no DB signal at all. The candidate
319
+ // filter is a DB/ORM *shape* test (provenance or an explicit type annotation),
320
+ // never a handle decision.
321
+ if (callee.type === 'identifier') {
322
+ const name = adapter.getNodeText(callee, sourceCode);
323
+ if (!name)
324
+ return null;
325
+ const binding = handleEnv.bindings.get(name);
326
+ const isProvenanced = provenanceContext.dbProvenanced.has(name);
327
+ const isTypeAnnotated = !!binding &&
328
+ (binding.kind === 'variable' || binding.kind === 'field' || binding.kind === 'parameter') &&
329
+ !!binding.typeText;
330
+ // A named import from a manifest DB package (`import { eq } from
331
+ // 'drizzle-orm'`) is a DB *signal* even when the name isn't a handle — it
332
+ // must reach `identifyHandle` so it can be proven `not-handle`, not dropped
333
+ // here on the negative for want of evidence (Spec 70 — a site that stops
334
+ // being reported has to be proven not-a-handle).
335
+ const isDbPackageImport = !!binding && binding.kind === 'import' && handleTypesForPackage(binding.source ?? '') !== undefined;
336
+ if (!isProvenanced && !isTypeAnnotated && !isDbPackageImport)
337
+ return null;
338
+ return identifyHandle({
339
+ format: 'typescript',
340
+ root: name,
341
+ receiver: name,
342
+ method: name,
343
+ sqlArgument: extractStaticSql(callNode, adapter, sourceCode),
344
+ thisField: false,
345
+ }, facts());
346
+ }
347
+ if (callee.type !== 'member_expression' && callee.type !== 'selector_expression') {
348
+ return null;
349
+ }
350
+ const method = extractMemberExpressionProperty(callee, adapter, sourceCode);
351
+ if (!method)
352
+ return null;
353
+ const root = resolveReceiverRoot(callee, adapter, sourceCode);
354
+ if (root === null)
355
+ return null;
356
+ const sqlArg = extractStaticSql(callNode, adapter, sourceCode);
357
+ const thisRooted = receiverIsThisRooted(callee, adapter);
358
+ // Candidacy is the package discriminant — the receiver root's disposition, not
359
+ // the method name (`join` is `Array.prototype.join` and also `SQL JOIN`). A
360
+ // call is query-shaped when its receiver resolves handle/unproven, OR its
361
+ // argument is a static SQL literal (which `identifyHandle`'s sql-argument arm
362
+ // can prove). A provably non-DB receiver (JS global, primitive, Node builtin)
363
+ // with no static SQL is rejected here. A `this.<field>` root is a field
364
+ // reference, not an ambient global, so it stays `unproven` (admitted) rather
365
+ // than dropped as a phantom global.
366
+ if (sqlArg === null && !isDbShapedRoot(root, handleEnv, { thisField: thisRooted }))
367
+ return null;
368
+ const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode) ?? root;
369
+ return identifyHandle({
370
+ format: 'typescript',
371
+ root,
372
+ receiver,
373
+ method,
374
+ sqlArgument: sqlArg,
375
+ thisField: thisRooted,
376
+ }, facts());
377
+ }
378
+ /**
379
+ * The Go arm of the admission seam: resolve a Go `selector_expression` call
380
+ * (`db.Query(…)`) through `identifyHandle` with Go's own resolution environment,
381
+ * instead of abstaining. Candidacy is a string-resolvable argument with a
382
+ * query-shaped-method backstop for the no-argument case; handle-ness is then
383
+ * decided once by the seam (`handle` via provenance seed, `unproven` via an
384
+ * unrecognized package, `not-handle` via a non-DB binding), never by a name list
385
+ * here.
386
+ */
387
+ function goHandleVerdictForCall(node, scan) {
388
+ const { adapter, sourceCode, config, goEnv } = scan;
389
+ if (!goEnv)
390
+ return null;
391
+ if (!isFunctionCall(node, adapter))
392
+ return null;
393
+ const callee = getCallExpressionCallee(node, adapter);
394
+ if (!callee || callee.type !== 'selector_expression')
395
+ return null;
396
+ const method = extractMemberExpressionProperty(callee, adapter, sourceCode);
397
+ if (!method)
398
+ return null;
399
+ const root = resolveReceiverRoot(callee, adapter, sourceCode);
400
+ if (root === null)
401
+ return null;
402
+ const sqlArg = extractGoStaticSql(node, adapter, sourceCode, goEnv);
403
+ // Candidacy is the package discriminant — the receiver root's disposition, not
404
+ // the method name. A call is query-shaped when its receiver resolves
405
+ // handle/unproven, OR its argument is a static SQL literal (which
406
+ // `identifyHandle`'s sql-argument arm can prove). A provably non-DB receiver
407
+ // (stdlib package, primitive, non-DB binding) with no static SQL is rejected.
408
+ if (sqlArg === null && classifyGoRootIdentifier(root, goEnv) === 'not-handle')
409
+ return null;
410
+ const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode) ?? root;
411
+ return identifyHandle({
412
+ format: 'go',
413
+ root,
414
+ receiver,
415
+ method,
416
+ sqlArgument: sqlArg,
417
+ thisField: false,
418
+ }, {
419
+ imports: new Map(),
420
+ typeAnnotations: new Map(),
421
+ bindings: new Map(),
422
+ withinFileProvenance: new Map(),
423
+ sqlDialect: config.dialect ?? null,
424
+ resolution: { dialect: 'go', env: goEnv },
120
425
  });
121
- return dbImports;
426
+ }
427
+ /** Walk parents until an enclosing call/new expression, or null. */
428
+ function enclosingCallOf(node, adapter) {
429
+ // A template literal is the SQL argument of a call only when it sits directly
430
+ // in that call's argument list (`db.prepare(\`…\`)`). A literal nested inside an
431
+ // arrow body, object literal, or array (`{ message: () => \`…is written (…)\` }`)
432
+ // is prose/config, not a query — walking past those boundaries attributes it to
433
+ // an unrelated enclosing call (`runUsageDirectionCheck(…, { message: … })`) and
434
+ // lets human-readable text reach the SQL parser. Only the immediate `arguments`
435
+ // admits the enclosing call; anything else is not a direct query argument.
436
+ let cur = adapter.getParent(node);
437
+ while (cur) {
438
+ const t = adapter.getNodeType(cur);
439
+ if (t === 'arguments') {
440
+ const call = adapter.getParent(cur);
441
+ return call && (adapter.getNodeType(call) === 'call_expression' || adapter.getNodeType(call) === 'new_expression')
442
+ ? call
443
+ : null;
444
+ }
445
+ // Tagged templates carry the literal as a direct child of the call (no
446
+ // `arguments`), but they are admitted separately by `isTaggedTemplateSqlCall`,
447
+ // never through this path — a literal not directly in `arguments` is not a
448
+ // positional SQL argument.
449
+ return null;
450
+ }
451
+ return null;
452
+ }
453
+ /** True when a receiver chain bottoms out at `this`/`super` (a field reference). */
454
+ function receiverIsThisRooted(callee, adapter) {
455
+ let current = callee;
456
+ while (current.type === 'member_expression' || current.type === 'selector_expression') {
457
+ const children = adapter.getChildren(current);
458
+ const firstChild = children.find((c) => c.type !== '.' && c.type !== 'property_identifier' && c.type !== 'field_identifier');
459
+ if (!firstChild)
460
+ return false;
461
+ current = firstChild;
462
+ }
463
+ return current.type === 'this' || current.type === 'super';
122
464
  }
123
465
  /**
124
466
  * Predicate for the node-discovery pass of extractDatabaseCalls. A node is a
125
- * candidate when it is a DB-provenanced function call, a template literal in a
126
- * DB-provenanced call's arguments, or a variable assignment holding SQL-shaped
127
- * text (Spec 17 R2 — content scanning is removed in favour of provenance).
467
+ * candidate when it is a DB/ORM-method call whose receiver is handle-or-unproven,
468
+ * a tagged-template SQL call, a variable assignment holding SQL-shaped text
469
+ * (Spec 17 R2 — content scanning is removed in favour of provenance), or a
470
+ * query-builder chain discovered by shape (Spec 68 Thing 2, #312).
128
471
  */
129
- function isDbCallCandidate(node, adapter, sourceCode, provenanceContext) {
130
- const nodeText = stripComments(adapter.getNodeText(node, sourceCode));
131
- // Check if it's a function call whose callee is DB-related
132
- if (provenanceContext && isDBProvenancedFunctionCall(node, adapter, sourceCode, provenanceContext)) {
472
+ function isDbCallCandidate(node, scan, tagNames = SQL_TAG_NAMES) {
473
+ const { adapter, sourceCode } = scan;
474
+ // Spec 70 R1.2: a DB/ORM-method call whose receiver resolves handle-or-unproven.
475
+ // `not-handle` (e.g. lodash `get`) and `null` (not query-shaped) are rejected.
476
+ const verdict = handleVerdictForCall(node, scan);
477
+ if (verdict !== null && verdict.kind !== 'not-handle')
478
+ return true;
479
+ // A query-builder chain is discovered by shape, not receiver name.
480
+ if (isQueryBuilderShape(node, adapter, sourceCode)) {
133
481
  return true;
134
482
  }
135
- // Spec 17 R2: Template literals are SQL candidates because of where they sit
136
- // (DB-provenanced call arguments), not what their body contains.
137
- if (isTemplateLiteral(node, adapter)) {
138
- return isTemplateInDBProvenancedCall(node, adapter, sourceCode, provenanceContext);
483
+ // Tagged-template SQL is a data-access call by tag name — no provenance needed.
484
+ if (isTaggedTemplateSqlCall(node, adapter, sourceCode, tagNames)) {
485
+ return true;
139
486
  }
140
- // Variable assignment with SQL structure — no child template check: the
141
- // template-literal path is now provenance-only (Spec 17 R2), so there is no
142
- // overlap risk from child template detection.
487
+ // Variable assignment whose RHS is a static string/template literal — the
488
+ // string parses as a statement or it does not (Spec 70 R2 site #4, obviated):
489
+ // the candidate check is now the presence of a static literal, and
490
+ // `buildDatabaseCall` parses it. No SQL-keyword content scan here.
143
491
  if (isVariableAssignment(node, adapter)) {
144
- return containsSQLStructure(nodeText);
492
+ return extractStaticSql(node, adapter, sourceCode) !== null;
145
493
  }
146
494
  return false;
147
495
  }
@@ -176,12 +524,60 @@ function dedupeCandidateNodes(nodes, adapter) {
176
524
  });
177
525
  return uniqueNodes;
178
526
  }
527
+ /** The explicit lowercased column list of a raw-SQL INSERT/REPLACE, or null for
528
+ * a positional INSERT (`INSERT INTO t VALUES (…)`, no column list). Mirrors
529
+ * sqlAst's `insertColumns` but preserves the null-vs-empty distinction the
530
+ * tenant-predicate rule needs.
531
+ * @param sqlAst - The parsed SQL statement to read the column list from.
532
+ * @returns The lowercased explicit column list, or `null` for a positional
533
+ * insert or a non-INSERT/REPLACE statement. */
534
+ export function rawInsertColumnsFromAst(sqlAst) {
535
+ if (sqlAst.type !== 'insert' && sqlAst.type !== 'replace')
536
+ return null;
537
+ const columns = sqlAst.columns;
538
+ if (!Array.isArray(columns))
539
+ return null;
540
+ return columns.filter((c) => typeof c === 'string').map((c) => c.toLowerCase());
541
+ }
542
+ /** The Kysely builder write verb carried as a camelCase method name — a
543
+ * host-language chain shape, not SQL. `selectFrom`/`selectAll` are reads.
544
+ * Read from the callee's member chain, not the source text: a `.insertInto(…)`
545
+ * inside a string argument (`db.raw(".insertInto(x)")`) is not a chain member.
546
+ *
547
+ * @param node the candidate call-expression node
548
+ * @param adapter the language adapter used to read the member chain
549
+ * @param sourceCode the file source text for reading node text
550
+ * @returns the Kysely write verb, or null when the chain is not a write */
551
+ export function builderWriteVerb(node, adapter, sourceCode) {
552
+ const methods = collectChainMethods(node, adapter, sourceCode);
553
+ if (methods.includes('insertInto'))
554
+ return 'insert';
555
+ if (methods.includes('updateTable'))
556
+ return 'update';
557
+ if (methods.includes('deleteFrom'))
558
+ return 'delete';
559
+ return null;
560
+ }
561
+ /** True when an ORM chain carries a row-limiting shape (`.where(...)`, `.having(...)`,
562
+ * `.limit(...)`, `.andWhere(...)`, `.orWhere(...)`) — the host-language analog of
563
+ * SQL's WHERE/HAVING/LIMIT that the AST walk cannot see because there is no SQL.
564
+ * Read from the member chain, not the source text: a `.where(…)` inside a string
565
+ * argument is a string, not a chain member.
566
+ *
567
+ * @param node the candidate call-expression node
568
+ * @param adapter the language adapter used to read the member chain
569
+ * @param sourceCode the file source text for reading node text
570
+ * @returns true when the chain carries a row-limiting shape */
571
+ export function hasOrmFilterShape(node, adapter, sourceCode) {
572
+ const methods = collectChainMethods(node, adapter, sourceCode);
573
+ return methods.some((m) => m === 'where' || m === 'andWhere' || m === 'orWhere' || m === 'having' || m === 'limit');
574
+ }
179
575
  /**
180
576
  * Classify a single candidate node into a DatabaseCall, or null when it does not
181
577
  * look like a DB-related query.
182
578
  */
183
579
  function buildDatabaseCall(node, ast, scan) {
184
- const { adapter, sourceCode, dbImports, config } = scan;
580
+ const { adapter, sourceCode, config, provenanceContext } = scan;
185
581
  const nodeText = stripComments(adapter.getNodeText(node, sourceCode));
186
582
  if (!nodeText || nodeText.trim().length < 10)
187
583
  return null;
@@ -189,15 +585,83 @@ function buildDatabaseCall(node, ast, scan) {
189
585
  // its template argument (path 2) — the template string is the precise target.
190
586
  if (shouldSkipCallForTemplateArg(node, adapter))
191
587
  return null;
192
- const isSqlQuery = containsSQLKeywords(nodeText);
588
+ // The candidate's static SQL argument (unquoted), or null when it has none
589
+ // or is dynamically interpolated.
590
+ const sqlArg = extractStaticSql(node, adapter, sourceCode);
591
+ // Spec 70 R2 — parse the static SQL once and derive every SQL-content fact from
592
+ // the AST. The dialect is per-call-site (Spec 70): `pool.query(…)` where `pool`
593
+ // resolves to `pg` parses as postgres even in a repo that also names `mysql2`;
594
+ // the repo-level detection result is only the fallback when the receiver
595
+ // doesn't resolve to a single-dialect package. R2 does not condition parsing on
596
+ // proving the dialect, so when none resolves the static SQL is still attempted
597
+ // under {@link DEFAULT_SQL_DIALECT}; a literal that parses yields its facts, and
598
+ // only a genuine parse failure (under the resolved dialect, or the default with
599
+ // the dialect undetermined) leaves the facts absent (`cannot-fire`).
600
+ const siteDialect = resolveSiteDialect(node, adapter, sourceCode, provenanceContext);
601
+ const dialect = siteDialect ?? config.dialect ?? null;
602
+ const parsed = sqlArg !== null ? parseSql(sqlArg, dialect ?? DEFAULT_SQL_DIALECT) : null;
603
+ const sqlOk = parsed && parsed.ok ? parsed : null;
604
+ // DDL statements (CREATE/ALTER/DROP/USE) are schema definition, not a
605
+ // row-level data-access query: they carry no rows to filter and no tenant
606
+ // predicate to check. A variable assignment holding a migration body
607
+ // (`const up = \`CREATE TABLE …\``) must not surface as a data-access call —
608
+ // schema is read separately by the `.sql` visitor and Durable Object DDL.
609
+ // Interpolated DDL still parses to null here, so this guard only excludes
610
+ // *static* schema definitions and leaves the injection surface untouched.
611
+ if (sqlOk && isDdlStatementKind(sqlOk.kind))
612
+ return null;
193
613
  const isOrmCall = isOrmPattern(nodeText);
614
+ const tagNames = config.sqlTagNames ?? SQL_TAG_NAMES;
615
+ // A tagged-template SQL call (`sql\`…\`` / `this.sql\`…\``) is a SQL query by
616
+ // tag name, whether its template is static or interpolated. Its static body
617
+ // parses normally; an interpolated body is `cannot-fire` on the SQL facts but
618
+ // is still the injection surface sql-injection-risk (site #11) reads.
619
+ const isTaggedSqlCall = isTaggedTemplateSqlCall(node, adapter, sourceCode, tagNames);
620
+ // A call is a SQL candidate when its static argument parsed as a statement, or
621
+ // it sits in a SQL position — a handle-or-unproven receiver, a query-builder
622
+ // chain, or a tagged template — regardless of whether its argument is static or
623
+ // dynamically interpolated. A parse-failed static argument in a SQL position is
624
+ // `cannot-fire` (facts absent), never silently dropped. (Spec 70 R2 — the
625
+ // keyword name-list gate `containsSQLKeywords` is deleted from this decision: it
626
+ // dropped keyword-less SQL like `PRAGMA`/`VACUUM`/`ANALYZE` that provenance
627
+ // already establishes as SQL, the opposite of honest `cannot-fire`.)
628
+ //
629
+ // Spec 70 R1.2 — handle-ness is decided once, by `identifyHandle`, and the site
630
+ // is admitted on the tri-state verdict: `handle` and `unproven` both admit (the
631
+ // latter carrying its reason, recorded on the call below); only `not-handle`
632
+ // rejects. `null` means "not a query-shaped DB/ORM call", not "not a handle" —
633
+ // a type-annotated `db: D1Database` with a dynamic template argument now stays
634
+ // visible instead of being dropped before the rules can see it.
635
+ const handleVerdict = handleVerdictForCall(node, scan);
636
+ const handleAdmits = handleVerdict !== null && handleVerdict.kind !== 'not-handle';
637
+ const isSqlPosition = handleAdmits || isQueryBuilderShape(node, adapter, sourceCode);
638
+ const isSqlQuery = sqlOk !== null || isSqlPosition || isTaggedSqlCall;
194
639
  if (!isSqlQuery && !isOrmCall)
195
640
  return null;
196
- const tables = extractTables(nodeText, config);
641
+ const sqlAstNode = sqlOk ? sqlOk.ast : null;
642
+ const builderVerb = sqlAstNode ? null : builderWriteVerb(node, adapter, sourceCode);
643
+ // Tables: SQL relations from the AST ∪ ORM-shaped references from the node.
644
+ const sqlTables = sqlAstNode ? extractTableNames(sqlAstNode) : [];
645
+ const ormTables = isOrmCall ? extractOrmTables(node, adapter, sourceCode, config) : [];
646
+ const tables = [...new Set([...sqlTables, ...ormTables])];
647
+ // Row-limiting filter: SQL WHERE/HAVING/LIMIT from the AST, or the ORM chain
648
+ // shape when there is no SQL to walk.
649
+ const facts = sqlAstNode ? whereFacts(sqlAstNode) : null;
650
+ const hasFilter = (facts
651
+ ? (facts.hasWhere && !facts.whereIsTautology) || facts.hasHaving || facts.hasLimit
652
+ : false)
653
+ || (isOrmCall && hasOrmFilterShape(node, adapter, sourceCode));
197
654
  const hasOrgFilter = hasOrganizationFilter(nodeText, config);
198
- const hasFilter = hasQueryFilter(nodeText);
199
655
  const security = withRuleTiming('sql-injection-risk', () => checkQuerySecurity(node, nodeText, ast, scan));
200
- const callType = classifyCallType(isSqlQuery, isOrmCall, dbImports);
656
+ // Spec 70 R4 — a sql-injection finding asserts the receiver is actually a DB
657
+ // handle. `unproven` (resolution reached a declaration whose origin it cannot
658
+ // tie to a database client, e.g. a Worker env binding like `D1Database`) is
659
+ // `cannot-fire`: the site stays visible but no vulnerability is asserted. Only
660
+ // a proven `handle` — a parsed SQL argument (R3) or a package in the manifest
661
+ // (R4) — fires. A type name proves nothing under criterion 9, so there is no
662
+ // type-annotation signal to consult.
663
+ const injectionRisk = security.injectionRisk && handleVerdict?.kind === 'handle';
664
+ const callType = classifyCallType(isSqlQuery, isOrmCall);
201
665
  return {
202
666
  type: callType,
203
667
  method: extractMethodName(node, adapter, sourceCode),
@@ -208,26 +672,33 @@ function buildDatabaseCall(node, ast, scan) {
208
672
  queryText: nodeText,
209
673
  hasOrganizationFilter: hasOrgFilter,
210
674
  hasFilter,
675
+ isWrite: sqlAstNode ? isWriteStatement(sqlAstNode) : builderVerb !== null,
676
+ isMassWrite: sqlAstNode ? isMassWriteStatement(sqlAstNode) : builderVerb === 'update',
677
+ isUpsert: sqlAstNode
678
+ ? isUpsertStatement(sqlAstNode) || sqlOk?.conflictClauseTruncated === true
679
+ : false,
680
+ isRawInsert: sqlAstNode ? (sqlAstNode.type === 'insert' || sqlAstNode.type === 'replace') : false,
681
+ insertColumns: sqlAstNode ? rawInsertColumnsFromAst(sqlAstNode) : null,
682
+ sqlWhereColumns: sqlAstNode ? [...whereColumnRefs(sqlAstNode)] : null,
211
683
  hasParameterizedQuery: security.parameterized,
212
- hasSqlInjectionRisk: security.injectionRisk,
684
+ hasSqlInjectionRisk: injectionRisk,
213
685
  sqlEscaped: security.escaped,
214
686
  enclosingFunction: enclosingIdentity(node, adapter, ast.filePath),
687
+ resolvedWhere: resolveWhereBinding(node, sourceCode, adapter) ?? undefined,
688
+ handleVerdict: handleVerdict ?? undefined,
215
689
  };
216
690
  }
217
691
  /**
218
- * Classify a SQL/ORM candidate as the concrete database call type: `sql` for a
219
- * raw SQL query, or the ORM type name (e.g. `knex`) resolved from the first
220
- * imported DB driver; `unknown` when neither applies.
692
+ * Classify a SQL/ORM candidate as a broad call-type label: `sql` for a raw SQL
693
+ * query, `orm` for a query-builder-shaped call, `unknown` otherwise. This is a
694
+ * non-decision label (no rule reads it); the receiver's DB provenance — resolved
695
+ * separately — is what decides whether the call is a data-access violation.
221
696
  */
222
- function classifyCallType(isSqlQuery, isOrmCall, dbImports) {
697
+ function classifyCallType(isSqlQuery, isOrmCall) {
223
698
  if (isSqlQuery)
224
699
  return 'sql';
225
- if (isOrmCall) {
226
- for (const [dbType, importInfo] of dbImports) {
227
- if (importInfo.hasImports)
228
- return dbType;
229
- }
230
- }
700
+ if (isOrmCall)
701
+ return 'orm';
231
702
  return 'unknown';
232
703
  }
233
704
  /**
@@ -246,9 +717,10 @@ function shouldSkipCallForTemplateArg(node, adapter) {
246
717
  * Extract database calls from AST
247
718
  */
248
719
  function extractDatabaseCalls(ast, scan) {
249
- const { adapter, sourceCode, provenanceContext } = scan;
720
+ const { adapter, config } = scan;
721
+ const tagNames = config.sqlTagNames ?? SQL_TAG_NAMES;
250
722
  const allNodes = adapter.findNodes(ast, {
251
- custom: (node) => isDbCallCandidate(node, adapter, sourceCode, provenanceContext),
723
+ custom: (node) => isDbCallCandidate(node, scan, tagNames),
252
724
  });
253
725
  const uniqueNodes = dedupeCandidateNodes(allNodes, adapter);
254
726
  const calls = [];
@@ -311,7 +783,7 @@ function analyzeQuery(call, config) {
311
783
  function checkViolations(call, analysis, ctx) {
312
784
  const { filePath, config, symbolOrdinals, skipTestRules } = ctx;
313
785
  const violations = [];
314
- const symbol = nextSymbol(call.enclosingFunction ?? 'top-level', call.method, symbolOrdinals);
786
+ const symbol = nextSymbol(call.enclosingFunction ?? 'top-level', call.method ?? '', symbolOrdinals);
315
787
  const push = (message, opts) => violations.push(makeViolation(filePath, { line: call.line, column: call.column }, message, { ...opts, symbol }));
316
788
  // Security: SQL injection. Raw unescaped interpolation of input is a live
317
789
  // vulnerability now → `critical`. Manual quote-escaping
@@ -327,7 +799,7 @@ function checkViolations(call, analysis, ctx) {
327
799
  resolution: {
328
800
  action: 'parameterize',
329
801
  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.`,
330
- symbols: [call.method],
802
+ symbols: [call.method ?? ''],
331
803
  files: [filePath],
332
804
  lines: [call.line],
333
805
  },
@@ -340,7 +812,7 @@ function checkViolations(call, analysis, ctx) {
340
812
  resolution: {
341
813
  action: 'parameterize',
342
814
  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.`,
343
- symbols: [call.method],
815
+ symbols: [call.method ?? ''],
344
816
  files: [filePath],
345
817
  lines: [call.line],
346
818
  },
@@ -406,13 +878,27 @@ function isFunctionCall(node, adapter) {
406
878
  return node.type === 'call_expression' ||
407
879
  node.type === 'new_expression';
408
880
  }
409
- function isTemplateLiteral(node, adapter) {
410
- return node.type === 'template_string';
411
- }
881
+ /**
882
+ * The node types that name a variable assignment whose RHS could be a static
883
+ * SQL literal. The previous check compared against `variable_declaration` (TS
884
+ * `var` only) and `binary_expression` (arithmetic/comparison — never an
885
+ * assignment), so `const`/`let`/`:=` declarations and every reassignment were
886
+ * dead. The type is the whole signal; `extractStaticSql` then reads the RHS.
887
+ */
888
+ const VARIABLE_ASSIGNMENT_TYPES = new Set([
889
+ // TS/JS
890
+ 'variable_declaration', // var q = …
891
+ 'lexical_declaration', // const/let q = …
892
+ 'variable_declarator', // the declarator inside either
893
+ 'assignment_expression', // q = …
894
+ // Go
895
+ 'short_var_declaration', // q := …
896
+ 'var_declaration', // var q = …
897
+ 'var_spec', // the spec inside var_declaration
898
+ 'assignment_statement', // q = …
899
+ ]);
412
900
  function isVariableAssignment(node, adapter) {
413
- // Only get the actual variable declaration, not the statement
414
- return node.type === 'variable_declaration' ||
415
- node.type === 'binary_expression' && (node.children?.some(child => adapter.getNodeText(child, '').includes('=')) ?? false);
901
+ return VARIABLE_ASSIGNMENT_TYPES.has(adapter.getNodeType(node));
416
902
  }
417
903
  function containsSQLKeywords(text) {
418
904
  const upperText = text.toUpperCase();
@@ -475,54 +961,6 @@ function stripComments(text) {
475
961
  }
476
962
  return out;
477
963
  }
478
- /**
479
- * Spec 22 R4.2: Requires ≥2 SQL keywords for variable-assignment detection.
480
- *
481
- * Single-keyword substring matches (e.g. "FROM" inside "Array.from") produce
482
- * ~120 false positives on the recall corpus. Genuine SQL in variable
483
- * assignments (string literals, ORM chains) almost always has ≥2 keywords
484
- * (SELECT+FROM, INSERT+INTO, DELETE+FROM, etc.).
485
- *
486
- * This is only used for the variable-assignment fallback path — template
487
- * literals and function calls use separate, context-aware gating.
488
- */
489
- function containsSQLStructure(text) {
490
- const upperText = text.toUpperCase();
491
- const found = SQL_KEYWORDS.filter(keyword => upperText.includes(keyword));
492
- return found.length >= 2;
493
- }
494
- /**
495
- * Spec 17 R2 provenance gate: a template literal is a SQL candidate
496
- * because of where it sits (inside a DB-provenanced call's arguments),
497
- * NOT because its body contains SQL-shaped substrings.
498
- *
499
- * Content scanning with substring matching is removed — template bodies
500
- * containing natural-language words like "from" or "select" are no longer
501
- * misclassified. The cost: template literals assigned to variables whose
502
- * values eventually flow to DB calls are not detected (requires dataflow
503
- * analysis, which is outside the product's stated scope per Spec 15 R3).
504
- */
505
- function isTemplateInDBProvenancedCall(node, adapter, sourceCode, provenanceContext) {
506
- const parent = adapter.getParent(node);
507
- if (!parent)
508
- return false;
509
- const parentType = adapter.getNodeType(parent);
510
- // Only the arguments-of-DB-call path survives the Spec 17 R2 cut.
511
- // Variable-assignment and statement-level conservative passes are
512
- // removed — they were the primary source of false positives.
513
- if (parentType === 'arguments') {
514
- const callExpr = adapter.getParent(parent);
515
- if (!callExpr || adapter.getNodeType(callExpr) !== 'call_expression')
516
- return false;
517
- if (provenanceContext) {
518
- return isDBProvenanced(callExpr, { adapter, sourceCode, context: provenanceContext, methods: DB_CALL_METHODS });
519
- }
520
- // Without provenance context, can't determine DB association —
521
- // do not speculate.
522
- return false;
523
- }
524
- return false;
525
- }
526
964
  /**
527
965
  * Detect that a node sits inside a D1 .prepare() call — the standard safe
528
966
  * pattern for SQL in Cloudflare Workers.
@@ -561,11 +999,14 @@ function isInPrepareBindChain(node, adapter, sourceCode) {
561
999
  function findCallFromEntry(node, adapter) {
562
1000
  const type = adapter.getNodeType(node);
563
1001
  if (type === 'template_string') {
564
- const args = adapter.getParent(node);
565
- if (!args || adapter.getNodeType(args) !== 'arguments')
566
- return null;
567
- const call = adapter.getParent(args);
568
- return call && adapter.getNodeType(call) === 'call_expression' ? call : null;
1002
+ // A nested template (`${cond ? \` WHERE x = ?\` : ''}`) is a fragment of its
1003
+ // enclosing query, not a query in its own right. Resolve it through the
1004
+ // substitution/outer-template chain to the enclosing call (as
1005
+ // `enclosingCallOf` does) so it inherits that call's parameterization —
1006
+ // `.query(sql, params)` / `.prepare().bind()` — instead of being re-read as
1007
+ // a bare interpolation whose direct parent is `template_substitution` (not
1008
+ // `arguments`) and therefore wrongly reported as raw injection ("in unknown").
1009
+ return enclosingCallOf(node, adapter);
569
1010
  }
570
1011
  if (type === 'call_expression')
571
1012
  return node;
@@ -597,15 +1038,23 @@ function memberPropertyName(node, adapter, sourceCode) {
597
1038
  * "all" property for the eager D1 read method (Spec 52 R1). `Promise` is the
598
1039
  * only collision in the eager set — none of run/first/raw/exec/batch are
599
1040
  * Promise methods.
1041
+ *
1042
+ * @param memberExpr the member-expression node to classify
1043
+ * @param adapter the language adapter used to read the member property
1044
+ * @param sourceCode the file source text for reading node text
1045
+ * @returns true when the member is the global `Promise.all`
600
1046
  */
601
- function isPromiseAllMember(memberExpr, adapter, sourceCode) {
1047
+ export function isPromiseAllMember(memberExpr, adapter, sourceCode) {
602
1048
  if (memberPropertyName(memberExpr, adapter, sourceCode) !== 'all')
603
1049
  return false;
604
1050
  const objectNode = adapter.getChildren(memberExpr).find(c => adapter.getNodeType(c) !== 'property_identifier');
605
1051
  if (!objectNode)
606
1052
  return false;
607
- const text = adapter.getNodeText(objectNode, sourceCode);
608
- return text === 'Promise' || text.endsWith('.Promise');
1053
+ // The object must be the bare `Promise` global identifier. `foo.Promise` is a
1054
+ // different binding, so a `.Promise` text suffix must not read as `Promise.all`.
1055
+ if (adapter.getNodeType(objectNode) !== 'identifier')
1056
+ return false;
1057
+ return adapter.getNodeText(objectNode, sourceCode) === 'Promise';
609
1058
  }
610
1059
  /**
611
1060
  * Resolve a db-call node to its call_expression. A template_string db node (the
@@ -643,8 +1092,16 @@ function dbCallMethodName(node, adapter, sourceCode) {
643
1092
  * DB methods that execute I/O immediately (as opposed to `.prepare()`/`.bind()`,
644
1093
  * which only construct a statement object). Used to decide whether a
645
1094
  * prepare/bind call is chained into an eager execution.
1095
+ *
1096
+ * `.get()` is the single-row read shared by better-sqlite3, `node:sqlite`, and
1097
+ * `bun:sqlite` that D1 does not expose (D1's closest is `.first()`). It was
1098
+ * absent, so `db.prepare(…).get(…)` in a loop was misread as statement
1099
+ * construction and skipped — the exact false negative the corpus pins at
1100
+ * `n-plus-one.ts:51`/`:62`. The wider driver-method gap (`.iterate()`, bun's
1101
+ * `.values()`) is documented in specs/rule-evidence-corpus/REPORT.md; it is
1102
+ * deliberately left out until it is measured, not silently dropped.
646
1103
  */
647
- const EAGER_DB_METHODS = new Set(['run', 'all', 'first', 'raw', 'exec', 'batch', 'query']);
1104
+ const EAGER_DB_METHODS = new Set(['run', 'all', 'first', 'raw', 'exec', 'batch', 'query', 'get']);
648
1105
  /**
649
1106
  * True when `node` (a `.prepare()`/`.bind()` call) is chained into an eager
650
1107
  * method — e.g. `db.prepare(sql).bind(x).run()`. The `.run()` at the end of the
@@ -681,8 +1138,8 @@ function hasEagerMethodInCallChain(node, adapter, sourceCode) {
681
1138
  /**
682
1139
  * True when a db-call node is statement construction only — a `.prepare()` /
683
1140
  * `.bind()` that is not chained into an eager method (`.run()` / `.all()` /
684
- * `.first()` / `.raw()` / `.exec()` / `.batch()`). Such a call performs no I/O,
685
- * so it is not a query-in-loop on its own (Spec 52 R1).
1141
+ * `.first()` / `.raw()` / `.exec()` / `.batch()` / `.get()`). Such a call
1142
+ * performs no I/O, so it is not a query-in-loop on its own (Spec 52 R1).
686
1143
  */
687
1144
  function isStatementConstructionOnly(node, adapter, sourceCode) {
688
1145
  const method = dbCallMethodName(node, adapter, sourceCode);
@@ -703,7 +1160,7 @@ function isSqlStringConstruction(node, adapter, sourceCode) {
703
1160
  const method = dbCallMethodName(node, adapter, sourceCode);
704
1161
  if (!method || EAGER_DB_METHODS.has(method))
705
1162
  return false;
706
- if (!ORM_METHODS.has(method))
1163
+ if (!isOrmMethod(method))
707
1164
  return false;
708
1165
  const call = resolveDbCallNode(node, adapter);
709
1166
  if (!call)
@@ -795,15 +1252,25 @@ function isInExecChain(node, adapter, sourceCode) {
795
1252
  return hasSpreadArgument(execCall, adapter);
796
1253
  }
797
1254
  /**
798
- * Detect D1's convenience SQL methods — .all(), .first(), .run() —
799
- * called with bind parameters as a second argument.
1255
+ * Detect eager execution methods called with bind parameters as a second
1256
+ * argument — D1's `.all()`/`.first()`/`.run()`, plus the parameterized
1257
+ * `.query()`/`.execute()` form shared by node-postgres/mysql2 and the tool's
1258
+ * own `IndexHandle`.
1259
+ *
1260
+ * `db.run(query, ...params)` is shorthand for
1261
+ * `db.prepare(query).bind(...params).run()`. If there's a second argument
1262
+ * (the bind params), the call is fully parameterized and safe — the `${}`
1263
+ * interpolations in the SQL are structural (placeholder fragments, identifiers,
1264
+ * fixed value-lists), never bound values.
800
1265
  *
801
- * `db.all(query, ...params)` is shorthand for
802
- * `db.prepare(query).bind(...params).all()`. If there's a second argument
803
- * (the bind params), the call is fully parameterized and safe.
1266
+ * `IndexHandle.query(sql, params)` (the tool's own parameterized index-query
1267
+ * abstraction, `src/types.ts`) is this exact contract: the interpolations carry
1268
+ * `?`-placeholder clauses (`${fp.clause}`, `${inList(types)}`) and `params`
1269
+ * binds the values out-of-band. A single-arg `.query(\`…${input}\`)` — the raw
1270
+ * injection shape — stays flagged because it carries no bind argument.
804
1271
  */
805
1272
  function isD1ConvenienceCall(node, adapter, sourceCode) {
806
- const D1_CONVENIENCE = new Set(['all', 'first', 'run']);
1273
+ const D1_CONVENIENCE = new Set(['all', 'first', 'run', 'query', 'execute']);
807
1274
  const call = findCallFromEntry(node, adapter);
808
1275
  if (!call)
809
1276
  return false;
@@ -866,8 +1333,13 @@ function isWrapperFunctionWithBindParams(node, adapter, sourceCode, wrapperNames
866
1333
  * single-expression form `db.prepare(sql).bind(x).all()`. This method
867
1334
  * catches the common idiom where the prepared statement is stored in a local
868
1335
  * before being bound.
1336
+ *
1337
+ * @param prepareCall the `db.prepare(...)` call-expression node
1338
+ * @param adapter the language adapter used to read the parent assignment
1339
+ * @param sourceCode the file source text for reading node text
1340
+ * @returns true when the prepare result is stored then `.bind()`ed in scope
869
1341
  */
870
- function isPrepareAssignedToVariable(prepareCall, adapter, sourceCode) {
1342
+ export function isPrepareAssignedToVariable(prepareCall, adapter, sourceCode) {
871
1343
  // The parent of the prepare call_expression reveals how the result is used.
872
1344
  const parent = adapter.getParent(prepareCall);
873
1345
  if (!parent)
@@ -893,14 +1365,30 @@ function isPrepareAssignedToVariable(prepareCall, adapter, sourceCode) {
893
1365
  }
894
1366
  if (!varName)
895
1367
  return false;
896
- // Scan the enclosing function scope for `.bind()` on this variable.
1368
+ // Scan the enclosing function scope for a `<varName>.bind(…)` member call.
1369
+ // Walk the subtree structurally: a `.bind(` inside a comment or string is text,
1370
+ // not a member expression, so it is not seen (the old regex over the function
1371
+ // text matched `// stmt.bind(x)` comments).
897
1372
  const fnNode = findEnclosingFunctionNode(prepareCall, adapter);
898
1373
  if (!fnNode)
899
1374
  return false;
900
- const fnText = adapter.getNodeText(fnNode, sourceCode);
901
- const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
902
- const bindPattern = new RegExp(String.raw `\b${escaped}\.bind\s*\(`, 'u');
903
- return bindPattern.test(fnText);
1375
+ const stack = [fnNode];
1376
+ while (stack.length) {
1377
+ const n = stack.pop();
1378
+ const t = adapter.getNodeType(n);
1379
+ if (t === 'member_expression' || t === 'selector_expression') {
1380
+ if (extractMemberExpressionProperty(n, adapter, sourceCode) === 'bind') {
1381
+ const receiver = memberObjectNode(n, adapter);
1382
+ if (receiver && adapter.getNodeType(receiver) === 'identifier'
1383
+ && adapter.getNodeText(receiver, sourceCode) === varName) {
1384
+ return true;
1385
+ }
1386
+ }
1387
+ }
1388
+ for (const c of adapter.getChildren(n) ?? [])
1389
+ stack.push(c);
1390
+ }
1391
+ return false;
904
1392
  }
905
1393
  /**
906
1394
  * Walk up the AST to find the enclosing function node (function_declaration,
@@ -968,7 +1456,17 @@ function isOrmPattern(text) {
968
1456
  /\.createMany\s*\(/,
969
1457
  /\.aggregate\s*\(/,
970
1458
  /\.count\s*\(/,
971
- /\.distinct\s*\(/
1459
+ /\.distinct\s*\(/,
1460
+ // Kysely builder verbs (camelCase SQL — not matched by the SQL-keyword path).
1461
+ /\.selectFrom\s*\(/,
1462
+ /\.selectAll\s*\(/,
1463
+ /\.insertInto\s*\(/,
1464
+ /\.updateTable\s*\(/,
1465
+ /\.deleteFrom\s*\(/,
1466
+ /\.executeTakeFirst\s*\(/,
1467
+ /\.executeTakeFirstOrThrow\s*\(/,
1468
+ /\.onConflict\s*\(/,
1469
+ /\.returning\s*\(/
972
1470
  ];
973
1471
  return ormPatterns.some(pattern => pattern.test(scrubNonTableCalls(text)));
974
1472
  }
@@ -982,152 +1480,158 @@ function collectPatternTables(text, patterns, tables) {
982
1480
  }
983
1481
  });
984
1482
  }
1483
+ /** The immediate object (receiver) child of a member/selector expression —
1484
+ * the child that is neither `.` nor the property identifier. */
1485
+ function memberObjectNode(node, adapter) {
1486
+ const children = adapter.getChildren(node);
1487
+ return children.find((c) => {
1488
+ const ct = adapter.getNodeType(c);
1489
+ return ct !== '.' && ct !== 'property_identifier' && ct !== 'field_identifier';
1490
+ }) ?? null;
1491
+ }
1492
+ /** The first real (non-punctuation) argument node of an `arguments` node. */
1493
+ function firstArgumentNode(argsNode, adapter) {
1494
+ for (const c of adapter.getChildren(argsNode)) {
1495
+ const ct = adapter.getNodeType(c);
1496
+ if (ct === '(' || ct === ')' || ct === ',')
1497
+ continue;
1498
+ return c;
1499
+ }
1500
+ return null;
1501
+ }
1502
+ /** The unquoted body of a string/template literal node, or null. */
1503
+ function stringLiteralValue(node, adapter, sourceCode) {
1504
+ const t = adapter.getNodeType(node);
1505
+ if (t === 'template_string') {
1506
+ const children = adapter.getChildren(node) ?? [];
1507
+ if (children.some((c) => adapter.getNodeType(c) === 'template_substitution'))
1508
+ return null;
1509
+ }
1510
+ if (t === 'string' || t === 'template_string' ||
1511
+ t === 'interpreted_string_literal' || t === 'raw_string_literal') {
1512
+ const raw = adapter.getNodeText(node, sourceCode);
1513
+ if (raw == null)
1514
+ return null;
1515
+ return stripSqlQuotes(raw, { raw: t === 'raw_string_literal' });
1516
+ }
1517
+ return null;
1518
+ }
1519
+ /** The table name an ORM table-taking verb's first argument carries: a bare
1520
+ * identifier's name or an unquoted string literal. Null for anything else (a
1521
+ * dynamic expression, a template with substitutions, a number, …). */
1522
+ function ormTableNameFromArgument(node, adapter, sourceCode) {
1523
+ if (adapter.getNodeType(node) === 'identifier') {
1524
+ const text = adapter.getNodeText(node, sourceCode);
1525
+ return text ? text : null;
1526
+ }
1527
+ const literal = stringLiteralValue(node, adapter, sourceCode);
1528
+ return literal && literal.length > 0 ? literal : null;
1529
+ }
1530
+ /** Kysely builder verbs whose first argument is the table name. */
1531
+ const ORM_TABLE_VERBS = new Set(['selectFrom', 'deleteFrom', 'insertInto', 'updateTable']);
1532
+ /** ORM join verbs whose first argument is the joined table. */
1533
+ const ORM_JOIN_VERBS = new Set(['join', 'leftJoin', 'rightJoin', 'innerJoin']);
985
1534
  /**
986
- * Extract the set of table names referenced in a SQL/ORM fragment, via the
987
- * configured `tablePatterns` plus the `.from(...)` / `db.<table>.<method>()`
988
- * ORM shapes. SQL keywords and aggregates captured in passing are filtered out.
1535
+ * Walk the callee chain of an ORM candidate, reading table references from the
1536
+ * *structure* (member names + argument nodes) rather than the source text. A
1537
+ * `.from(…)` / verb / `db.<table>.<method>()` shape inside a string argument is
1538
+ * not a chain member, so it is never read as a table.
1539
+ *
1540
+ * Three shapes are read:
1541
+ * - `.from(table)` / `.from('table')` (except JS `Array.from`/`Buffer.from`/
1542
+ * `String.from`/`*Array.from` construction),
1543
+ * - a Kysely/join verb carrying the table as its first argument,
1544
+ * - Drizzle's `db.<table>.<method>()` table-access idiom.
989
1545
  */
990
- export function extractTables(text, config) {
991
- // JS `.from(...)` construction (Array.from / Buffer.from / Uint8Array.from)
992
- // and Drizzle `sql.join(...)` are not SQL table references; blank them out
993
- // first so their arguments aren't read as tables.
994
- const scrubbed = scrubNonTableCalls(text);
995
- const tables = new Set();
996
- // Check ORM and SQL patterns
997
- collectPatternTables(scrubbed, config.tablePatterns?.orm, tables);
998
- collectPatternTables(scrubbed, config.tablePatterns?.sql, tables);
999
- // Additional check for common ORM patterns that might be missed
1000
- // Handle patterns like db.select().from(users) where 'users' is a variable
1001
- const ormVariablePattern = /\.from\s*\(\s*([\p{L}_][\p{L}\p{N}_]*)\s*\)/gu;
1002
- const ormMatches = scrubbed.matchAll(ormVariablePattern);
1003
- for (const match of ormMatches) {
1004
- if (match[1] && !match[1].includes('"') && !match[1].includes("'")) {
1005
- tables.add(match[1]);
1546
+ function collectOrmTableReferences(node, adapter, sourceCode, tables) {
1547
+ const t = adapter.getNodeType(node);
1548
+ if (t === 'call_expression') {
1549
+ const callee = getCallExpressionCallee(node, adapter);
1550
+ if (callee) {
1551
+ const calleeType = adapter.getNodeType(callee);
1552
+ if (calleeType === 'member_expression' || calleeType === 'selector_expression') {
1553
+ const method = extractMemberExpressionProperty(callee, adapter, sourceCode);
1554
+ const receiverNode = memberObjectNode(callee, adapter);
1555
+ const receiverName = receiverNode && adapter.getNodeType(receiverNode) === 'identifier'
1556
+ ? adapter.getNodeText(receiverNode, sourceCode)
1557
+ : null;
1558
+ const args = adapter.getChildren(node).find((c) => adapter.getNodeType(c) === 'arguments');
1559
+ const firstArg = args ? firstArgumentNode(args, adapter) : null;
1560
+ if (method && firstArg) {
1561
+ const isFrom = method === 'from'
1562
+ && !(receiverName && (receiverName === 'Buffer' || receiverName === 'String'
1563
+ || receiverName.endsWith('Array')));
1564
+ const isVerb = ORM_TABLE_VERBS.has(method);
1565
+ const isJoin = ORM_JOIN_VERBS.has(method) && receiverName !== 'sql';
1566
+ if (isFrom || isVerb || isJoin) {
1567
+ const name = ormTableNameFromArgument(firstArg, adapter, sourceCode);
1568
+ if (name)
1569
+ tables.add(name);
1570
+ }
1571
+ }
1572
+ }
1573
+ collectOrmTableReferences(callee, adapter, sourceCode, tables);
1006
1574
  }
1007
1575
  }
1008
- // Handle patterns like db.users.find() or db.orders.findOne()
1009
- const dbTablePattern = /db\.([\p{L}_][\p{L}\p{N}_]*)\.\p{L}[\p{L}\p{N}_]*\s*\(/gu;
1010
- const dbMatches = scrubbed.matchAll(dbTablePattern);
1011
- for (const match of dbMatches) {
1012
- if (match[1]) {
1013
- tables.add(match[1]);
1576
+ else if (t === 'member_expression' || t === 'selector_expression') {
1577
+ // db.<table>.<method>() — Drizzle's table-access idiom. The receiver is the
1578
+ // member `db.<table>`, whose property names the table; the root must be the
1579
+ // bare `db` identifier (not `appDb`/`foo`), mirroring the old `db\.…` regex.
1580
+ const object = memberObjectNode(node, adapter);
1581
+ if (object && (adapter.getNodeType(object) === 'member_expression'
1582
+ || adapter.getNodeType(object) === 'selector_expression')) {
1583
+ const innerProp = extractMemberExpressionProperty(object, adapter, sourceCode);
1584
+ const innerObject = memberObjectNode(object, adapter);
1585
+ if (innerProp && innerObject && adapter.getNodeType(innerObject) === 'identifier'
1586
+ && adapter.getNodeText(innerObject, sourceCode) === 'db') {
1587
+ tables.add(innerProp);
1588
+ }
1014
1589
  }
1590
+ if (object)
1591
+ collectOrmTableReferences(object, adapter, sourceCode, tables);
1015
1592
  }
1016
- // Drop SQL keywords/aggregates captured as tables — `FROM MIN(...)` in
1017
- // `EXTRACT(YEAR FROM MIN(...))` yields `MIN`, and `FOR UPDATE SKIP LOCKED`
1018
- // yields `SKIP`; neither is a table.
1019
- return Array.from(tables).filter(t => !isSqlKeyword(t));
1020
- }
1021
- function hasOrganizationFilter(text, config) {
1022
- const patterns = config.organizationPatterns ?? [];
1023
- // No patterns → hardcoded common fallback set.
1024
- const candidates = patterns.length
1025
- ? patterns
1026
- : ['organizationid', 'organization_id', 'orgid', 'org_id',
1027
- 'tenantid', 'tenant_id', 'companyid', 'company_id'];
1028
- // An org-scoping column is a *filter* only when it is used as a predicate
1029
- // operand — the left-hand side of a comparison/IN/IS/LIKE (`org_id = ?`,
1030
- // `tenant_id IN (...)`), the key side of a filter object (`where({ org_id })`),
1031
- // or the column argument of a positional where (`where('org_id', x)`).
1032
- // A column that merely appears in the SELECT list (`SELECT org_id FROM …`) or
1033
- // an INSERT column list is NOT a filter. This replaces the old substring
1034
- // proxy that treated any occurrence of the column name — a SELECT column, a
1035
- // comment, a property name — as evidence of tenant isolation.
1036
- const alt = candidates.map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
1037
- // SQL comparison operand / object-literal key.
1038
- const comparisonRe = new RegExp(`\\b${alt}\\b\\s*(?:=|!=|<>|<=|>=|<|>|\\bIS\\b|\\bIN\\b|\\bLIKE\\b|:)`, 'i');
1039
- if (comparisonRe.test(text))
1040
- return true;
1041
- // Positional ORM where: `.where('org_id', x)` / `.andWhere("org_id", x)`.
1042
- const positionalRe = new RegExp(`\\b(?:where|andWhere|orWhere|whereEq|whereNot|having|on)\\s*\\(\\s*['"\`]\\s*${alt}\\s*['"\`]`, 'i');
1043
- return positionalRe.test(text);
1044
- }
1045
- /**
1046
- * True when a query applies a *row-limiting* filter: a WHERE carrying a real
1047
- * predicate, a HAVING, or a LIMIT. The `unfiltered-query` rule flags a write
1048
- * (DELETE/UPDATE) that lacks such a clause — a mass mutation that touches every
1049
- * row. A `JOIN ... ON` predicate scopes *how* rows match, not *which* rows come
1050
- * back, so it is not a filter; and a tautological `WHERE 1=1` (the placeholder
1051
- * prepended so callers can append `AND x = ?`) limits nothing, so it is not a
1052
- * filter either. Evaluated on comment-stripped text so prose in `//` or `/* *`/
1053
- * comments cannot fabricate a filter.
1054
- */
1055
- function hasQueryFilter(text) {
1056
- const upper = text.toUpperCase();
1057
- return (/\bWHERE\b/.test(upper) && !whereClauseIsTautology(text))
1058
- || /\bHAVING\b/.test(upper)
1059
- || /\bLIMIT\b/.test(upper);
1060
- }
1061
- /**
1062
- * True when the WHERE clause is a bare tautology — `WHERE 1=1` (or `1 = 1`,
1063
- * `TRUE`, or the same repeated under `AND`) — that limits nothing. A WHERE
1064
- * body carrying any real predicate (`WHERE id = ?`, or `WHERE 1=1 AND
1065
- * active = ?`) is not a tautology.
1066
- */
1067
- function whereClauseIsTautology(text) {
1068
- const upper = text.toUpperCase();
1069
- const whereMatch = /\bWHERE\b/.exec(upper);
1070
- if (!whereMatch)
1071
- return false;
1072
- const afterWhere = upper.slice(whereMatch.index + 'WHERE'.length);
1073
- // Body up to the next row-limiting clause keyword.
1074
- const body = afterWhere.split(/\b(?:GROUP\s+BY|ORDER\s+BY|HAVING|LIMIT|OFFSET|UNION)\b/)[0];
1075
- // Drop the SQL-string terminator + call-argument closer (`"), "); …) so the
1076
- // predicate is judged on the clause text alone.
1077
- const stripped = body.replace(/["'`)\s;]+$/g, '').replace(/^\s+/, '').replace(/\s+$/, '');
1078
- // A dangling AND/OR left by a dynamic builder still filters nothing.
1079
- const clean = stripped.replace(/^(?:AND|OR)\s+/i, '').replace(/\s+(?:AND|OR)$/i, '');
1080
- const alwaysTrue = '(?:1\\s*=\\s*1|TRUE)';
1081
- return new RegExp(`^${alwaysTrue}(?:\\s+AND\\s+${alwaysTrue})*$`, 'i').test(clean);
1082
- }
1083
- /**
1084
- * True when a SQL statement carries a write verb (INSERT/DELETE/UPDATE/REPLACE).
1085
- * Spec 52 R2: `REPLACE INTO` is an upsert write — matched as a two-word clause
1086
- * (not a bare `REPLACE` word) so the `REPLACE()` string function is not misread
1087
- * as a write.
1088
- */
1089
- export function hasWriteVerb(text) {
1090
- const upper = text.toUpperCase();
1091
- return /\bINSERT\b/.test(upper) || /\bDELETE\b/.test(upper) || /\bUPDATE\b/.test(upper)
1092
- || /\bREPLACE\s+INTO\b/.test(upper);
1093
- }
1094
- /**
1095
- * True when a statement mutates or deletes existing rows — `DELETE` or `UPDATE`.
1096
- * These are the verbs whose mass effect is a foot-gun when unconstrained;
1097
- * `INSERT` / `REPLACE INTO` always target specific rows and are not "unfiltered"
1098
- * in the dangerous sense.
1099
- */
1100
- function hasMassWriteVerb(text) {
1101
- const upper = text.toUpperCase();
1102
- return /\bDELETE\b/.test(upper) || /\bUPDATE\b/.test(upper);
1103
- }
1104
- /**
1105
- * True when a statement is an upsert — one of the four write spellings Spec 52
1106
- * R2 taught the write classifier, plus MySQL's `ON DUPLICATE KEY UPDATE`. An
1107
- * upsert is keyed by construction: its conflict target / unique key is what
1108
- * scopes the row, and an `INSERT` has no `WHERE` by definition. So an upsert is
1109
- * never an "unfiltered" mass write, even though `INSERT … ON CONFLICT … DO UPDATE`
1110
- * carries the word `UPDATE` (in `DO UPDATE`) with no WHERE clause.
1111
- */
1112
- function isUpsertForm(text) {
1113
- const upper = text.toUpperCase();
1114
- return /\bINSERT\s+OR\s+(?:IGNORE|REPLACE)\b/.test(upper)
1115
- || /\bREPLACE\s+INTO\b/.test(upper)
1116
- || /\bON\s+CONFLICT\b/.test(upper)
1117
- || /\bON\s+DUPLICATE\s+KEY\b/.test(upper);
1593
+ else if (t === 'subscript_expression') {
1594
+ const object = memberObjectNode(node, adapter);
1595
+ if (object)
1596
+ collectOrmTableReferences(object, adapter, sourceCode, tables);
1597
+ }
1598
+ }
1599
+ /**
1600
+ * Extract the set of table names referenced by the ORM chain shapes — read
1601
+ * structurally from the callee chain ({@link collectOrmTableReferences}) plus
1602
+ * the user-configured `orm` table patterns. This is the host-language shape
1603
+ * detector only; SQL relations are derived structurally by sqlAst's
1604
+ * `extractTableNames`, not here (Spec 70 R2 site #1). The default `orm` patterns
1605
+ * carry only the non-structural `table: 'users'` / `table = "users"` shape; the
1606
+ * `.from(x)` / join / Kysely-verb shapes that used to live there as regexes are
1607
+ * now read structurally, so a `.from(…)` inside a string argument is not a table.
1608
+ * @param node The ORM/query-builder candidate node.
1609
+ * @param adapter The language adapter (drives node traversal).
1610
+ * @param sourceCode The file source text.
1611
+ * @param config The data-access analyzer config holding table patterns.
1612
+ * @returns The set of table names referenced by ORM shapes.
1613
+ */
1614
+ export function extractOrmTables(node, adapter, sourceCode, config) {
1615
+ const tables = new Set();
1616
+ collectOrmTableReferences(node, adapter, sourceCode, tables);
1617
+ // User-configured `orm` patterns are arbitrary text shapes with no node to
1618
+ // read (e.g. `table: 'users'`), so they still run over the source text.
1619
+ const nodeText = stripComments(adapter.getNodeText(node, sourceCode));
1620
+ const scrubbed = scrubNonTableCalls(nodeText);
1621
+ collectPatternTables(scrubbed, config.tablePatterns?.orm, tables);
1622
+ return Array.from(tables);
1118
1623
  }
1119
1624
  /**
1120
1625
  * True when a call should be surfaced by the `unfiltered-query` rule (Spec 55
1121
- * R5, Spec 56 R1): a write statement (`DELETE` / `UPDATE`) with no row-limiting
1122
- * clause (WHERE carrying a real predicate, HAVING, or LIMIT). `DELETE FROM t` or
1123
- * `UPDATE t SET …` with no filter mutates/deletes every row — the classic SQL
1124
- * foot-gun. Unfiltered *reads* (`SELECT` without WHERE) are often intentional
1125
- * full-set loads and are therefore out of scope, and an upsert (any of the four
1126
- * spellings) is keyed by construction, so it never fires.
1626
+ * R5, Spec 56 R1): a mass-write statement (`UPDATE … SET`) with no row-limiting
1627
+ * clause. The write/upsert facts are AST-derived (Spec 70 R2 sites #2/#3): a
1628
+ * Kysely `updateTable` builder verb is folded into `isWrite`/`isMassWrite` by
1629
+ * the producer (host-language shape, not SQL). `DELETE FROM t` is not a mass
1630
+ * write (disposition (a)); an upsert is keyed by construction, so it never fires.
1127
1631
  */
1128
1632
  function isUnfilteredWrite(call) {
1129
- return !isUpsertForm(call.queryText)
1130
- && hasMassWriteVerb(call.queryText)
1633
+ return !call.isUpsert
1634
+ && call.isMassWrite
1131
1635
  && !call.hasFilter;
1132
1636
  }
1133
1637
  /**
@@ -1136,14 +1640,12 @@ function isUnfilteredWrite(call) {
1136
1640
  * table that carries declared tenancy. A filterless full-table read of a
1137
1641
  * tenant table is the same tenant-leak surface the write rule guards — it
1138
1642
  * sweeps every tenant's rows. Non-tenant tables (lookups, config) stay out of
1139
- * scope, which is precisely what kept the Spec 55 narrowing's false positives
1140
- * (non-tenant full-set loads) from returning. Reuses `requiresOrgFilter` for
1141
- * the tenancy determination so the write case and this read case cannot drift
1142
- * to two different tenancy definitions.
1643
+ * scope. Reuses `requiresOrgFilter` for the tenancy determination so the write
1644
+ * case and this read case cannot drift to two different tenancy definitions.
1143
1645
  */
1144
1646
  function isUnfilteredRead(call, config) {
1145
1647
  return !call.hasFilter
1146
- && !hasWriteVerb(call.queryText)
1648
+ && !call.isWrite
1147
1649
  && requiresOrgFilter(call.tables, config);
1148
1650
  }
1149
1651
  /**
@@ -1186,15 +1688,28 @@ function effectiveWrapperNames(scan) {
1186
1688
  * config-driven sanitizer allowlist, the adapter's cross-function safety
1187
1689
  * analysis, or a static-constant resolution for bare identifiers. Otherwise
1188
1690
  * the part counts as unresolved (a candidate injection).
1691
+ *
1692
+ * @param part the dynamic interpolation part to classify
1693
+ * @param ast the file's parsed AST
1694
+ * @param scan the data-access scan context (adapter, source, config)
1695
+ * @returns true when the part is provably safe, false when unresolved
1189
1696
  */
1190
- function isSafeDynamicPart(part, ast, scan) {
1697
+ export function isSafeDynamicPart(part, ast, scan) {
1191
1698
  const { adapter, sourceCode, config } = scan;
1192
1699
  // Config-driven sanitizer allowlist (escapeSql(x), …) applies to
1193
1700
  // non-identifier expressions — an interpolation wrapped in a known
1194
1701
  // sanitizer isn't raw (same pattern as dbWrapperNames for provenance).
1702
+ // The sanitizer name is read from the part's call node, not from a text
1703
+ // prefix: `mySanitizer (x)` (two spaces) still names `mySanitizer`, but
1704
+ // `startsWith(name + '(')` would not see it through the extra space.
1195
1705
  if (!part.isIdentifier) {
1196
- const normalized = part.text.trim();
1197
- const sanitized = (config.sanitizerNames ?? []).some(name => normalized.startsWith(name + '(') || normalized.startsWith(name + ' ('));
1706
+ const callee = part.node && adapter.getNodeType(part.node) === 'call_expression'
1707
+ ? getCallExpressionCallee(part.node, adapter)
1708
+ : null;
1709
+ const calleeName = callee && adapter.getNodeType(callee) === 'identifier'
1710
+ ? adapter.getNodeText(callee, sourceCode)
1711
+ : null;
1712
+ const sanitized = calleeName != null && (config.sanitizerNames ?? []).includes(calleeName);
1198
1713
  if (sanitized)
1199
1714
  return true;
1200
1715
  }
@@ -1222,6 +1737,54 @@ function isSafeDynamicPart(part, ast, scan) {
1222
1737
  // prove it safe, so treat it as unresolved.
1223
1738
  return false;
1224
1739
  }
1740
+ /**
1741
+ * The tagged-template call node when `node` is (or wraps) a drizzle `sql`/`db`
1742
+ * tagged template; null otherwise. Handles both candidate shapes: a bare
1743
+ * `sql\`…\`` (the candidate node IS the tag) and `db.execute(sql\`…\`)` (the
1744
+ * tag is the enclosing call's argument).
1745
+ */
1746
+ function findTaggedTemplateCall(node, adapter, sourceCode, tagNames) {
1747
+ if (isTaggedTemplateSqlCall(node, adapter, sourceCode, tagNames))
1748
+ return node;
1749
+ const args = adapter.getChildren(node).find((c) => adapter.getNodeType(c) === 'arguments');
1750
+ if (!args)
1751
+ return null;
1752
+ for (const arg of adapter.getChildren(args)) {
1753
+ const t = adapter.getNodeType(arg);
1754
+ if (t === '(' || t === ')' || t === ',')
1755
+ continue;
1756
+ if (isTaggedTemplateSqlCall(arg, adapter, sourceCode, tagNames))
1757
+ return arg;
1758
+ }
1759
+ return null;
1760
+ }
1761
+ /**
1762
+ * True when `node` is (or wraps) a drizzle `sql`/`db` tagged template whose
1763
+ * `${…}` interpolations are all bare identifiers — the parameterized-by-
1764
+ * construction form. Drizzle's `sql` tag turns a clean `${id}` into a `?`
1765
+ * placeholder, so a bare-identifier interpolation is not an injection vector.
1766
+ *
1767
+ * A non-identifier interpolation — string concatenation (`${'%' + x + '%'}`), a
1768
+ * call (`${fn(x)}`), a member access (`${session.id}`) — is raw assembly, NOT a
1769
+ * bound parameter, so it is deliberately excluded and falls through to the
1770
+ * dynamic-string construction path (§69 Fix 2).
1771
+ */
1772
+ function isParameterizedTaggedTemplate(node, adapter, sourceCode, tagNames) {
1773
+ const tagCall = findTaggedTemplateCall(node, adapter, sourceCode, tagNames);
1774
+ if (!tagCall)
1775
+ return false;
1776
+ const template = adapter.getChildren(tagCall).find((c) => isTemplateLiteral(c, adapter));
1777
+ if (!template)
1778
+ return false;
1779
+ for (const child of adapter.getChildren(template)) {
1780
+ if (adapter.getNodeType(child) !== 'template_substitution')
1781
+ continue;
1782
+ const inner = adapter.getChildren(child).find((c) => adapter.getNodeType(c) !== 'template_substitution');
1783
+ if (!inner || adapter.getNodeType(inner) !== 'identifier')
1784
+ return false;
1785
+ }
1786
+ return true;
1787
+ }
1225
1788
  function checkQuerySecurity(node, text, ast, scan) {
1226
1789
  const { adapter, sourceCode, config } = scan;
1227
1790
  // Parameterized chains (.prepare().bind(), .exec() spread, D1 convenience,
@@ -1229,6 +1792,12 @@ function checkQuerySecurity(node, text, ast, scan) {
1229
1792
  if (isParameterizedByChain(node, adapter, sourceCode, config, effectiveWrapperNames(scan))) {
1230
1793
  return { parameterized: true, injectionRisk: false, escaped: false };
1231
1794
  }
1795
+ // A drizzle `sql`/`db` tagged template parameterizes bare `${id}` interpolations
1796
+ // into `?` placeholders by construction — the "right reason" a clean tagged
1797
+ // template is quiet (§69 Fix 2), not the broken recursion that used to hide it.
1798
+ if (isParameterizedTaggedTemplate(node, adapter, sourceCode, config.sqlTagNames ?? SQL_TAG_NAMES)) {
1799
+ return { parameterized: true, injectionRisk: false, escaped: false };
1800
+ }
1232
1801
  if ((config.securityPatterns?.parameterizedQueries || []).some(p => text.includes(p))) {
1233
1802
  return { parameterized: true, injectionRisk: false, escaped: false };
1234
1803
  }
@@ -1256,39 +1825,39 @@ function checkQuerySecurity(node, text, ast, scan) {
1256
1825
  };
1257
1826
  }
1258
1827
  function extractCallExpressionMethod(callExpr, adapter, sourceCode) {
1259
- // Extract the method name from the callee of a call expression.
1260
- // For `sql.exec(...)` the callee is a member_expression whose
1261
- // property_identifier is "exec". Walking the AST avoids picking up SQL
1262
- // keywords (COUNT, JOIN, WHERE, ...) that appear inside template literals
1263
- // in the call arguments, which a regex scan of the full call-expression
1264
- // text would incorrectly match.
1265
- // Use the shared callee extractor: it recurses through `await_expression` and
1266
- // skips `type_arguments`, so `await d1<{...}>(...)` resolves to `d1` rather
1267
- // than falling through to the "unknown" regex fallback.
1828
+ // Walk the AST to the callee and read the method name as a node. The name is
1829
+ // `property_identifier` on a `member_expression` (or `field_identifier` on a Go
1830
+ // `selector_expression`); a bare `identifier` callee is a named function call.
1831
+ // There is no shape where scanning the call text for `name(` is the right
1832
+ // answer — an unrecognised callee shape is "no method name" (null), never a
1833
+ // guessed name. `getCallExpressionCallee` recurses through `await_expression`
1834
+ // and stops before `arguments`, so a keyword inside a template-literal argument
1835
+ // is never read as the name.
1268
1836
  const callee = getCallExpressionCallee(callExpr, adapter);
1269
- if (callee) {
1270
- const calleeType = adapter.getNodeType(callee);
1271
- if (calleeType === 'member_expression') {
1272
- const mc = adapter.getChildren(callee);
1273
- const prop = mc.find(c => adapter.getNodeType(c) === 'property_identifier');
1274
- if (prop)
1275
- return adapter.getNodeText(prop, sourceCode);
1276
- }
1277
- else {
1278
- // Bare identifier call (e.g. `exec(...)`)
1279
- return adapter.getNodeText(callee, sourceCode);
1280
- }
1837
+ if (!callee)
1838
+ return null;
1839
+ const calleeType = adapter.getNodeType(callee);
1840
+ if (calleeType === 'member_expression') {
1841
+ const prop = adapter.getChildren(callee).find(c => adapter.getNodeType(c) === 'property_identifier');
1842
+ return prop ? adapter.getNodeText(prop, sourceCode) : null;
1281
1843
  }
1282
- // Fallback: regex on just the callee portion of the text
1283
- const callText = adapter.getNodeText(callExpr, sourceCode);
1284
- const m = callText.match(/\.(\w+)\s*[<(]/);
1285
- return m ? m[1] : 'unknown';
1844
+ if (calleeType === 'selector_expression') {
1845
+ const field = adapter.getChildren(callee).find(c => adapter.getNodeType(c) === 'field_identifier');
1846
+ return field ? adapter.getNodeText(field, sourceCode) : null;
1847
+ }
1848
+ if (calleeType === 'identifier') {
1849
+ return adapter.getNodeText(callee, sourceCode);
1850
+ }
1851
+ if (calleeType === 'call_expression') {
1852
+ return extractCallExpressionMethod(callee, adapter, sourceCode);
1853
+ }
1854
+ return null;
1286
1855
  }
1287
1856
  function extractMethodName(node, adapter, sourceCode) {
1288
1857
  const nodeType = adapter.getNodeType(node);
1289
1858
  // For template literals inside a DB-provenanced call, extract the method
1290
1859
  // name from the enclosing call expression instead of the template body.
1291
- if (nodeType === 'template_string' || nodeType === 'template_literal') {
1860
+ if (nodeType === 'template_string') {
1292
1861
  const parent = adapter.getParent(node);
1293
1862
  if (parent && adapter.getNodeType(parent) === 'arguments') {
1294
1863
  const callExpr = adapter.getParent(parent);
@@ -1296,17 +1865,15 @@ function extractMethodName(node, adapter, sourceCode) {
1296
1865
  return extractCallExpressionMethod(callExpr, adapter, sourceCode);
1297
1866
  }
1298
1867
  }
1868
+ return null;
1299
1869
  }
1300
- // For call expressions themselves, extract from the callee child. The
1301
- // full node text includes the arguments (which may contain template
1302
- // literals with SQL keywords), so a naive regex scan of the whole text
1303
- // can match COUNT, JOIN, WHERE, etc. instead of the real method name.
1304
1870
  if (nodeType === 'call_expression') {
1305
1871
  return extractCallExpressionMethod(node, adapter, sourceCode);
1306
1872
  }
1307
- const text = adapter.getNodeText(node, sourceCode);
1308
- const match = text.match(/([\p{L}\p{N}_]+)\s*\(/u);
1309
- return match ? match[1] : 'unknown';
1873
+ // Any other node shape has no callee to walk to: there is no method name. The
1874
+ // site abstains (null) rather than scanning the node's text for a `name(` that
1875
+ // would turn "I don't recognise this shape" into a confident wrong name.
1876
+ return null;
1310
1877
  }
1311
1878
  /**
1312
1879
  * Spec 21 R6.2 (reworked for Spec 44 — rule authenticity, and Spec 62 Amendment
@@ -1394,14 +1961,32 @@ const MESSAGE_LIFECYCLE_METHODS = new Set(['ack', 'nack', 'acknowledge', 'delete
1394
1961
  * Each finding carries the query call location (never line 1).
1395
1962
  */
1396
1963
  function checkLoopQueries(ast, scan) {
1397
- const { adapter, sourceCode, provenanceContext } = scan;
1398
- const violations = [];
1964
+ return collectLoopQueryCandidates(ast, scan).map((c) => {
1965
+ const depthMsg = c.depth > 1 ? ` (nested ${c.depth} levels deep)` : '';
1966
+ return makeViolation(c.file, { line: c.line, column: c.column }, `Database query inside loop${depthMsg} ` +
1967
+ `(loop at line ${c.loopLine}). ` +
1968
+ `This may cause N+1 performance issues. Consider batching queries or using a join.`, { severity: 'severe', rule: 'loop-query', symbol: c.symbol });
1969
+ });
1970
+ }
1971
+ /**
1972
+ * R4.1: the candidate-collection half of `checkLoopQueries`, factored out so the
1973
+ * `loop-queries` producer projects the same loop→query candidates without
1974
+ * producing violations. Dedup (one candidate per loop, anchored at the first
1975
+ * query) and the LLM/queue suppression live here — they need the AST.
1976
+ */
1977
+ function collectLoopQueryCandidates(ast, scan) {
1978
+ const { adapter, sourceCode } = scan;
1979
+ const candidates = [];
1399
1980
  // Spec 21: provenance-gated detection of database calls.
1400
1981
  const dbNodes = adapter.findNodes(ast, {
1401
- custom: (node) => isDbCallNode(node, adapter, sourceCode, provenanceContext),
1982
+ custom: (node) => isDbCallNode(node, scan),
1402
1983
  });
1403
1984
  const reported = new Set();
1404
1985
  const loopOrdinals = new Map();
1986
+ // §13.1: functions/methods this file invokes inside a `db.transaction(fn)`
1987
+ // callback have their writes already batched by the caller's transaction (see
1988
+ // the helper) — provided their statements were prepared outside the loop.
1989
+ const transactionWrappedFunctions = collectTransactionWrappedFunctions(ast, adapter, sourceCode);
1405
1990
  for (const node of dbNodes) {
1406
1991
  // A DB call and its template-literal SQL argument both satisfy isDbCallNode —
1407
1992
  // the call via provenance, the literal via the template-literal branch — so one
@@ -1424,9 +2009,63 @@ function checkLoopQueries(ast, scan) {
1424
2009
  // fragment, not a query result (knex `sql.push(...this.aggregate(stmt))`).
1425
2010
  if (isSqlStringConstruction(node, adapter, sourceCode))
1426
2011
  continue;
1427
- const loopInfo = findEnclosingLoop(node, adapter);
2012
+ const loopInfo = findEnclosingLoop(node, adapter, sourceCode);
1428
2013
  if (!loopInfo)
1429
2014
  continue;
2015
+ // §13.3 (for-of-iterable discriminator): a DB call in the iterable/header of
2016
+ // a `for…of` loop — `for (const c of loadCatalog())` where `loadCatalog`
2017
+ // memoizes a single `indexHandle.query` — is evaluated once to produce the
2018
+ // iterated array, then the body iterates it in memory. That is not
2019
+ // per-iteration I/O, so it is not an N+1 — the same subject-vs-callback
2020
+ // distinction `isIteratorCallback` draws for `db.all().map(…)`. (A DB call in
2021
+ // the *body* still fires; only the header position is exempted.)
2022
+ if (isForOfIterableDbCall(node, loopInfo.loopNode, adapter)) {
2023
+ continue;
2024
+ }
2025
+ // §13.1 (hoisted-reuse discriminator): a prepared statement executed inside a
2026
+ // loop is not an N+1 when the statement is prepared *outside* the loop — the
2027
+ // loop re-runs one compiled statement with bound parameters, which is exactly
2028
+ // the "batch the queries" remediation the finding would prescribe, so flagging
2029
+ // it is a false positive. Two shapes of "prepared outside the loop" are
2030
+ // recognized, both keyed on the *prepare* (a relation between two located
2031
+ // facts), never on a property of the call shape:
2032
+ // (1) transaction-batched — the loop is enclosed by a single
2033
+ // `db.transaction(fn)` callback (lexically, or via a helper invoked
2034
+ // inside it), so every write defers to one commit;
2035
+ // (2) hoisted re-run without a transaction — the loop re-runs a statement
2036
+ // object prepared outside it (`const stmt = db.prepare(…);
2037
+ // for (…) stmt.run(x)` or `stmt.bind(x).run()`). The in-loop call must
2038
+ // be a member call whose base identifier is bound to a `.prepare()`
2039
+ // result declared *outside* the loop (see `isHoistedStatementReRun`).
2040
+ // Both require the loop body to NOT `.prepare(...)` per iteration — a loop that
2041
+ // still calls `.prepare(...)` re-prepares every pass and remains a genuine N+1,
2042
+ // so it keeps firing. Crucially, shape (2) does NOT key on "no SQL string
2043
+ // argument": an ORM builder chain (`db.select().from(…).where(…)`,
2044
+ // `prisma.user.findUnique({ where: … })`) and a connection call whose SQL is a
2045
+ // hoisted variable both carry no SQL literal, yet the builder is a genuine
2046
+ // per-iteration N+1 and the receiver is not prepare-bound, so neither is a
2047
+ // statement re-run — they keep firing. (A per-iteration
2048
+ // `db.transaction(() => …)` *inside* the loop also still fires — that does not
2049
+ // wrap the loop, and each iteration commits separately.)
2050
+ const enclosingFnName = findEnclosingFunctionIdentity(loopInfo.loopNode, adapter, ast.filePath).name;
2051
+ const transactionEnclosesLoop = isInsideDbTransaction(loopInfo.loopNode, adapter, sourceCode) ||
2052
+ (enclosingFnName !== null && transactionWrappedFunctions.has(enclosingFnName));
2053
+ const preparesInLoop = loopBodyContainsPrepare(loopInfo.loopNode, adapter, sourceCode);
2054
+ const hoistedReuse = !preparesInLoop &&
2055
+ (transactionEnclosesLoop ||
2056
+ isHoistedStatementReRun(node, loopInfo.loopNode, adapter, sourceCode));
2057
+ if (hoistedReuse) {
2058
+ continue;
2059
+ }
2060
+ // §13.2 (per-batch binding discriminator): a DB call whose bound parameters
2061
+ // are a *spread of a collection-derived expression* (a `.slice()`/`.map()`/
2062
+ // chunk of the iterated set) executes one statement per batch, not per row —
2063
+ // a chunked `id IN (…)` write is not an N+1 at any chunk size. Only a scalar
2064
+ // or property read off a single loop element (`run(row.id)`) is per-row. See
2065
+ // `bindsBatchArgument`.
2066
+ if (bindsBatchArgument(node, loopInfo.loopNode, adapter, sourceCode)) {
2067
+ continue;
2068
+ }
1430
2069
  // R4.1 (Spec 46): LLM-pipeline discriminator. A loop whose body invokes an
1431
2070
  // LLM/agent (embedding, model completion, corpus extraction) is an intentional
1432
2071
  // *sequential pipeline* — its per-item DB calls are persistence steps gated by
@@ -1451,13 +2090,25 @@ function checkLoopQueries(ast, scan) {
1451
2090
  continue;
1452
2091
  reported.add(dedupKey);
1453
2092
  const sym = nextSymbol(enclosingIdentity(node, adapter, ast.filePath), 'loop-query', loopOrdinals);
1454
- // R4.2: Nested-loop attribution.
1455
- const depthMsg = loopInfo.depth > 1 ? ` (nested ${loopInfo.depth} levels deep)` : '';
1456
- violations.push(makeViolation(ast.filePath, node.location.start, `Database query inside loop${depthMsg} ` +
1457
- `(loop at line ${loopInfo.loopNode.location.start.line}). ` +
1458
- `This may cause N+1 performance issues. Consider batching queries or using a join.`, { severity: 'severe', rule: 'loop-query', symbol: sym }));
2093
+ // Anchor to the resolved callee, not the raw node. For `await db.prepare(…)
2094
+ // .bind(…).all<T>(…)` tree-sitter emits an OUTER call_expression at the `await`
2095
+ // keyword (its function is an await_expression wrapping the chain) alongside the
2096
+ // inner call at `db`. Parent-before-child traversal hands the outer node to this
2097
+ // loop first, so the caret would land on `await` instead of the query. Resolving
2098
+ // the callee (which already recurses through await_expression) re-anchors to the
2099
+ // `db` receiver; for every non-await shape the callee starts at the same column
2100
+ // as the node, so nothing else moves.
2101
+ const anchor = getCallExpressionCallee(node, adapter) ?? node;
2102
+ candidates.push({
2103
+ file: ast.filePath,
2104
+ line: anchor.location.start.line,
2105
+ column: anchor.location.start.column,
2106
+ symbol: sym,
2107
+ loopLine: loopInfo.loopNode.location.start.line,
2108
+ depth: loopInfo.depth,
2109
+ });
1459
2110
  }
1460
- return violations;
2111
+ return candidates;
1461
2112
  }
1462
2113
  /**
1463
2114
  * R4.1 (Spec 46): True when the loop's subtree contains an LLM/agent invocation.
@@ -1498,36 +2149,587 @@ function loopBodyContainsMessageLifecycleCall(loopNode, adapter, sourceCode) {
1498
2149
  });
1499
2150
  return found;
1500
2151
  }
1501
- /** Depth-first walk over an ASTNode subtree (children only, no parent links). */
1502
- function walkSubtree(node, adapter, visitor) {
1503
- visitor(node);
1504
- for (const child of adapter.getChildren(node)) {
1505
- walkSubtree(child, adapter, visitor);
2152
+ /**
2153
+ * §13.1 (prepare-outside-loop guard): true when the loop's subtree issues a
2154
+ * `.prepare(...)` call — i.e. the statement is (re)built on every iteration, not
2155
+ * prepared once outside the loop. This is the second half of the
2156
+ * transaction-batched discriminator: a loop wrapped in a transaction is only a
2157
+ * non-N+1 when it reuses a statement prepared outside; a per-iteration
2158
+ * `db.prepare(sql).run(…)` (or `const stmt = db.prepare(…); stmt.run(…)`) still
2159
+ * re-prepares every pass and remains a genuine N+1 under a transaction. Walking
2160
+ * the whole loop subtree (header + body) is safe: a `.prepare` is only ever in
2161
+ * the body, never the header.
2162
+ */
2163
+ function loopBodyContainsPrepare(loopNode, adapter, sourceCode) {
2164
+ let found = false;
2165
+ walkSubtree(loopNode, adapter, (node) => {
2166
+ if (found || adapter.getNodeType(node) !== 'call_expression')
2167
+ return;
2168
+ if (dbCallMethodName(node, adapter, sourceCode) === 'prepare')
2169
+ found = true;
2170
+ });
2171
+ return found;
2172
+ }
2173
+ /**
2174
+ * §13.1 (hoisted-reuse discriminator, non-transaction shape): true when the
2175
+ * in-loop DB call re-runs a statement object prepared *outside* the loop —
2176
+ * `const stmt = db.prepare(…); for (…) stmt.run(x)` or `stmt.bind(x).run()`.
2177
+ *
2178
+ * The signal is the *prepare* — a relation between two located facts — not a
2179
+ * property of the call shape. The in-loop eager call's member chain must be
2180
+ * rooted at an identifier (`stmt`) that is declared, in an enclosing scope
2181
+ * *outside* the loop, with a `.prepare()` initializer. An ORM builder chain
2182
+ * (`db.select().from(…).where(…)`, `prisma.user.findUnique({ where: … })`),
2183
+ * a direct connection call (`db.exec("…")`), a bare DB-provenanced helper
2184
+ * (`resolveHero(db, slug)`), and a Map/Array method that provenance over-matched
2185
+ * (`counts.get(x)`, `adjudication.find(x)`) are all rooted at an identifier that
2186
+ * is NOT prepare-bound, so none of them reads as a statement re-run and each
2187
+ * keeps firing.
2188
+ */
2189
+ function isHoistedStatementReRun(node, loopNode, adapter, sourceCode) {
2190
+ const base = memberChainBaseIdentifier(node, adapter, sourceCode);
2191
+ if (base && identifierBoundToPrepareOutsideLoop(base, loopNode, adapter, sourceCode)) {
2192
+ return true;
1506
2193
  }
2194
+ return isBarePrepareBoundHelperCall(node, loopNode, adapter, sourceCode);
1507
2195
  }
1508
2196
  /**
1509
- * Classify a call_expression as an LLM/agent invocation via three unambiguous
1510
- * signals: a provider/SDK namespace in the callee text, an LLM action verb in the
1511
- * callee text, or a model-client identifier passed as an argument / used as the
1512
- * member-callee object.
2197
+ * §13.1 (hoisted-reuse discriminator, bare-wrapper shape): true when the in-loop
2198
+ * DB call is a bare local-helper invocation (`deleteFileEntries(clears, path)`)
2199
+ * whose argument is an identifier bound, outside the loop, to a prepare result —
2200
+ * `clears = prepareStyleClearStatements(rawDb)`, then the loop body forwards
2201
+ * `clears` into a helper that runs `clears.decl.run(path)` on every pass. The
2202
+ * helper re-runs pre-prepared statements; the prepare is the signal, located via
2203
+ * the *argument's* binding rather than the call's own member chain (a bare
2204
+ * identifier callee has none). Member-shape re-runs are handled above; this is
2205
+ * the bare-call complement.
1513
2206
  */
1514
- function isLlmCallNode(node, adapter, sourceCode) {
1515
- const children = adapter.getChildren(node);
1516
- const callee = children.find(c => adapter.getNodeType(c) !== 'arguments');
1517
- if (callee) {
1518
- const calleeText = adapter.getNodeText(callee, sourceCode);
1519
- if (LLM_PROVIDER_RE.test(calleeText) || LLM_ACTION_RE.test(calleeText))
2207
+ function isBarePrepareBoundHelperCall(node, loopNode, adapter, sourceCode) {
2208
+ const call = resolveDbCallNode(node, adapter);
2209
+ if (!call)
2210
+ return false;
2211
+ const callee = getCallExpressionCallee(call, adapter);
2212
+ if (!callee || adapter.getNodeType(callee) !== 'identifier')
2213
+ return false;
2214
+ const argsNode = adapter.getChildren(call).find((c) => adapter.getNodeType(c) === 'arguments');
2215
+ if (!argsNode)
2216
+ return false;
2217
+ for (const arg of adapter.getChildren(argsNode)) {
2218
+ if (adapter.getNodeType(arg) !== 'identifier')
2219
+ continue;
2220
+ if (identifierBoundToPrepareOutsideLoop(adapter.getNodeText(arg, sourceCode), loopNode, adapter, sourceCode)) {
1520
2221
  return true;
1521
- // Member callee whose object is an LLM client: `model.chat(…)`, `llm.invoke(…)`.
1522
- if (adapter.getNodeType(callee) === 'member_expression') {
1523
- const obj = adapter.getChildren(callee).find(c => adapter.getNodeType(c) === 'identifier');
1524
- if (obj && LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(obj, sourceCode)))
1525
- return true;
1526
2222
  }
1527
2223
  }
1528
- // Argument signal: an LLM client identifier passed into the call, e.g.
1529
- // `extractBuildsFromCorpus(db, model, …)`.
1530
- const args = children.find(c => adapter.getNodeType(c) === 'arguments');
2224
+ return false;
2225
+ }
2226
+ /**
2227
+ * The base identifier a member-call chain is rooted at — `stmt` for `stmt.run(x)`
2228
+ * and for `stmt.bind(x).run()` (the `.bind()` call's receiver), `db` for
2229
+ * `db.select().from(…).where(…)`, `prisma` for `prisma.user.findUnique(…)`.
2230
+ * Returns null when the chain is not a member call (a bare `helper(db, …)`) or
2231
+ * has no identifier root (an indexed access `x[i]()`).
2232
+ */
2233
+ function memberChainBaseIdentifier(node, adapter, sourceCode) {
2234
+ const eagerCall = findEagerExecutionCall(node, adapter, sourceCode);
2235
+ if (!eagerCall)
2236
+ return null;
2237
+ const memberExpr = findMemberCallee(eagerCall, adapter);
2238
+ if (!memberExpr)
2239
+ return null;
2240
+ return baseIdentifierOfMemberExpr(memberExpr, adapter, sourceCode);
2241
+ }
2242
+ /**
2243
+ * Recurse down a member_expression's object side to the base identifier: the
2244
+ * object of `a.b.c` is `a.b`, whose object is `a`; the object of `stmt.bind(x)`
2245
+ * is the call, whose own member callee is `stmt.bind`, whose object is `stmt`.
2246
+ */
2247
+ function baseIdentifierOfMemberExpr(memberExpr, adapter, sourceCode) {
2248
+ const object = adapter.getChildren(memberExpr).find((c) => {
2249
+ const t = adapter.getNodeType(c);
2250
+ return t !== 'property_identifier' && t !== 'field_identifier' && t !== '.';
2251
+ });
2252
+ if (!object)
2253
+ return null;
2254
+ const type = adapter.getNodeType(object);
2255
+ if (type === 'identifier')
2256
+ return adapter.getNodeText(object, sourceCode);
2257
+ if (type === 'member_expression')
2258
+ return baseIdentifierOfMemberExpr(object, adapter, sourceCode);
2259
+ if (type === 'call_expression') {
2260
+ const inner = findMemberCallee(object, adapter);
2261
+ return inner ? baseIdentifierOfMemberExpr(inner, adapter, sourceCode) : null;
2262
+ }
2263
+ return null;
2264
+ }
2265
+ /**
2266
+ * True when `name` is declared, in the enclosing function (or program), with a
2267
+ * `.prepare()` initializer, at a position *outside* the loop. Walking the whole
2268
+ * enclosing function is safe for this signal: the caller has already established
2269
+ * `preparesInLoop` is false, so the only `.prepare()` sites are outside the loop,
2270
+ * and requiring the declarator to sit before the loop excludes any same-named
2271
+ * declaration in a sibling scope after it. (A same-named variable in a *nested*
2272
+ * function would be a false match; that shadowing shape is not present in the
2273
+ * measured corpora and is recorded here rather than silently dropped.)
2274
+ */
2275
+ function identifierBoundToPrepareOutsideLoop(name, loopNode, adapter, sourceCode) {
2276
+ const scope = findEnclosingFunctionNode(loopNode, adapter) ?? loopNode;
2277
+ const loopStart = loopNode.range[0];
2278
+ let found = false;
2279
+ walkSubtree(scope, adapter, (n) => {
2280
+ if (found)
2281
+ return;
2282
+ if (n === loopNode)
2283
+ return;
2284
+ if (adapter.getNodeType(n) !== 'variable_declarator')
2285
+ return;
2286
+ if (n.range[0] >= loopStart)
2287
+ return;
2288
+ const { name: declaredName, value } = splitDeclarator(n, adapter, sourceCode);
2289
+ if (declaredName !== name)
2290
+ return;
2291
+ if (value && subtreeContainsPrepare(value, adapter, sourceCode))
2292
+ found = true;
2293
+ });
2294
+ if (found)
2295
+ return true;
2296
+ // A parameter typed as a prepared-statement bundle (`stmts: StyleStatements`,
2297
+ // `stmt: SqliteStatement`) is prepare-bound by construction — its members are
2298
+ // the `.prepare()` results the caller hoisted before the loop. Re-running
2299
+ // `stmts.decl.run(x)` is therefore the same hoisted-reuse signal as a local
2300
+ // `const stmt = db.prepare(…)` re-run. A handle parameter (`SqliteDatabase`,
2301
+ // `IndexHandle`, `any`) does not name a Statement type and keeps firing.
2302
+ return scope !== loopNode && parameterBoundToStatementType(name, scope, adapter, sourceCode);
2303
+ }
2304
+ /**
2305
+ * True when `name` is a parameter of `fnNode` whose type annotation names a
2306
+ * prepared-statement type. The type is the signal (mirroring how a local
2307
+ * declarator's `.prepare()` initializer is), never the parameter's name: a
2308
+ * `*Statement`/`*Statements` annotation means the value is a compiled statement
2309
+ * (or a bundle of them), so a loop that re-runs it re-runs pre-prepared SQL.
2310
+ *
2311
+ * @param name the parameter name to match
2312
+ * @param fnNode the function node whose parameters are inspected
2313
+ * @param adapter the language adapter used to read parameter nodes
2314
+ * @param sourceCode the file source text for reading node text
2315
+ * @returns true when the named parameter is annotated with a statement type
2316
+ */
2317
+ export function parameterBoundToStatementType(name, fnNode, adapter, sourceCode) {
2318
+ const params = adapter.getChildren(fnNode).find((c) => adapter.getNodeType(c) === 'formal_parameters');
2319
+ if (!params)
2320
+ return false;
2321
+ for (const p of adapter.getChildren(params)) {
2322
+ const t = adapter.getNodeType(p);
2323
+ if (t !== 'required_parameter' && t !== 'optional_parameter')
2324
+ continue;
2325
+ // The parameter name is the `identifier` child; the type is the
2326
+ // `type_annotation` child's text, read structurally (not by regex over the
2327
+ // whole parameter text, which split `x: StatementFactory` into a name/type
2328
+ // pair but then matched any `Statement…` prefix).
2329
+ const nameNode = adapter.getChildren(p).find((c) => adapter.getNodeType(c) === 'identifier');
2330
+ if (!nameNode)
2331
+ continue;
2332
+ if (adapter.getNodeText(nameNode, sourceCode) !== name)
2333
+ continue;
2334
+ const typeAnn = adapter.getChildren(p).find((c) => adapter.getNodeType(c) === 'type_annotation');
2335
+ if (!typeAnn)
2336
+ continue;
2337
+ const typeText = adapter.getNodeText(typeAnn, sourceCode)?.trim() ?? '';
2338
+ // A statement type *ends* in `Statement`/`Statements` (`SqliteStatement`,
2339
+ // `StyleStatements`). `StatementFactory` embeds "Statement" but is a factory,
2340
+ // not a statement type, so it does not qualify.
2341
+ if (/Statement(s)?$/.test(typeText))
2342
+ return true;
2343
+ }
2344
+ return false;
2345
+ }
2346
+ /** True when a subtree contains a `.prepare()` call (an initializer that is or
2347
+ * chains into `db.prepare(…)`, e.g. `db.prepare(sql).bind(…)`) or a `prepare*`
2348
+ * factory call (`prepareStyleClearStatements(rawDb)`) — a function whose whole
2349
+ * job is to return pre-prepared statement objects, so its name carries the same
2350
+ * "statement prepared here" signal the `.prepare()` member call does. */
2351
+ function subtreeContainsPrepare(node, adapter, sourceCode) {
2352
+ let found = false;
2353
+ walkSubtree(node, adapter, (n) => {
2354
+ if (found || adapter.getNodeType(n) !== 'call_expression')
2355
+ return;
2356
+ if (dbCallMethodName(n, adapter, sourceCode) === 'prepare') {
2357
+ found = true;
2358
+ return;
2359
+ }
2360
+ const callee = getCallExpressionCallee(n, adapter);
2361
+ if (callee && adapter.getNodeType(callee) === 'identifier') {
2362
+ const name = adapter.getNodeText(callee, sourceCode);
2363
+ if (name && /^prepare/i.test(name))
2364
+ found = true;
2365
+ }
2366
+ });
2367
+ return found;
2368
+ }
2369
+ /**
2370
+ * §13.1: True when `node` sits inside a `db.transaction(fn)` callback. Walks the
2371
+ * parent chain looking for a `call_expression` whose callee is a
2372
+ * `member_expression` with a `transaction` property. The loop is the input (not
2373
+ * the DB call) so a transaction that wraps only a single per-iteration call —
2374
+ * `for (…) { db.transaction(() => run()) }` — does not read as "batched": there
2375
+ * the loop itself is outside the transaction and every iteration commits
2376
+ * separately, so it must still fire.
2377
+ */
2378
+ function isInsideDbTransaction(node, adapter, sourceCode) {
2379
+ let current = node;
2380
+ while (current) {
2381
+ const parent = adapter.getParent(current);
2382
+ if (!parent)
2383
+ break;
2384
+ if (adapter.getNodeType(parent) === 'call_expression') {
2385
+ const callee = adapter.getChildren(parent).find((c) => adapter.getNodeType(c) === 'member_expression');
2386
+ if (callee && memberPropertyName(callee, adapter, sourceCode) === 'transaction') {
2387
+ return true;
2388
+ }
2389
+ }
2390
+ current = parent;
2391
+ }
2392
+ return false;
2393
+ }
2394
+ /**
2395
+ * §13.1 (transaction-wrapped-helper discriminator): names of the functions this
2396
+ * file invokes *inside* a `db.transaction(fn)` callback — either as a
2397
+ * `this.method(...)` call (`this.db.transaction(() => this.syncFileIndexRow(...))`)
2398
+ * or as a free function reference/call (`rawDb.transaction(insertAll)`, or
2399
+ * `rawDb.transaction(() => { … insertOne(…) })`). A private helper written to run
2400
+ * inside the caller's transaction has its per-iteration writes already batched
2401
+ * into the caller's single commit, so its internal loops are not a batchable N+1
2402
+ * *provided their statements are prepared outside the loop* (see the
2403
+ * no-`.prepare()`-in-loop guard at the call site). This mirrors the textual
2404
+ * {@link isInsideDbTransaction} check but bridges the call boundary: the loop
2405
+ * lives in the helper's body while the transaction wraps the *call*, which the
2406
+ * purely lexical check cannot see. A per-iteration `db.transaction(() => …)`
2407
+ * *inside* a loop is unaffected — the helper is only recognized when it is the
2408
+ * thing being invoked within a transaction callback.
2409
+ */
2410
+ function collectTransactionWrappedFunctions(ast, adapter, sourceCode) {
2411
+ const wrapped = new Set();
2412
+ for (const callNode of adapter.findNodes(ast, {
2413
+ custom: (n) => adapter.getNodeType(n) === 'call_expression',
2414
+ })) {
2415
+ const callee = adapter.getChildren(callNode).find((c) => adapter.getNodeType(c) === 'member_expression');
2416
+ if (!callee || memberPropertyName(callee, adapter, sourceCode) !== 'transaction')
2417
+ continue;
2418
+ // (1) A bare function reference handed straight to `transaction` — the
2419
+ // reducer shape `rawDb.transaction(insertAll)()`. The callback argument is
2420
+ // the helper's name, not a call inside it.
2421
+ const args = adapter.getChildren(callNode).find((c) => adapter.getNodeType(c) === 'arguments');
2422
+ if (args) {
2423
+ for (const arg of adapter.getChildren(args)) {
2424
+ if (adapter.getNodeType(arg) !== 'identifier')
2425
+ continue;
2426
+ const name = adapter.getNodeText(arg, sourceCode);
2427
+ if (name)
2428
+ wrapped.add(name);
2429
+ }
2430
+ }
2431
+ // (2) Calls inside the callback: `this.method(...)` and free-function calls
2432
+ // (`insertOne(...)`).
2433
+ walkSubtree(callNode, adapter, (n) => {
2434
+ if (n === callNode)
2435
+ return;
2436
+ if (adapter.getNodeType(n) !== 'call_expression')
2437
+ return;
2438
+ const innerCallee = getCallExpressionCallee(n, adapter);
2439
+ if (!innerCallee)
2440
+ return;
2441
+ if (adapter.getNodeType(innerCallee) === 'identifier') {
2442
+ const name = adapter.getNodeText(innerCallee, sourceCode);
2443
+ if (name)
2444
+ wrapped.add(name);
2445
+ return;
2446
+ }
2447
+ if (adapter.getNodeType(innerCallee) !== 'member_expression')
2448
+ return;
2449
+ const obj = adapter.getChildren(innerCallee).find((c) => adapter.getNodeType(c) !== 'property_identifier');
2450
+ const objText = obj ? adapter.getNodeText(obj, sourceCode) : '';
2451
+ if (objText !== 'this')
2452
+ return;
2453
+ const prop = memberPropertyName(innerCallee, adapter, sourceCode);
2454
+ if (prop)
2455
+ wrapped.add(prop);
2456
+ });
2457
+ }
2458
+ return wrapped;
2459
+ }
2460
+ /** Depth-first walk over an ASTNode subtree (children only, no parent links). */
2461
+ function walkSubtree(node, adapter, visitor) {
2462
+ visitor(node);
2463
+ for (const child of adapter.getChildren(node)) {
2464
+ walkSubtree(child, adapter, visitor);
2465
+ }
2466
+ }
2467
+ /**
2468
+ * Array methods that transform one collection into another collection (a batch):
2469
+ * the elements of a `.slice()`/`.splice()`/`.map()`/`.filter()`/`.concat()`/
2470
+ * `.flatMap()` result are a *sub*-collection, not a single row.
2471
+ */
2472
+ const COLLECTION_TRANSFORM_METHODS = new Set(['slice', 'splice', 'map', 'filter', 'concat', 'flatMap']);
2473
+ /**
2474
+ * Free-function callee names that chunk a collection into batches — `chunk(ids,
2475
+ * n)`, `chunkArray(ids, n)`, `partition(rows, p)`. Recognized by name (not by a
2476
+ * slice call) because a chunking helper is a user-written function whose internal
2477
+ * shape the analyzer cannot see; the name is the only stable signal.
2478
+ */
2479
+ const CHUNK_HELPER_RE = /^(chunk|chunks|chunkArray|chunkBy|partition|paginate|paged)$/i;
2480
+ /** Split a `variable_declarator` into its name text and initializer (value) node. */
2481
+ function splitDeclarator(node, adapter, sourceCode) {
2482
+ let name = null;
2483
+ let value = null;
2484
+ let pastEquals = false;
2485
+ for (const child of adapter.getChildren(node)) {
2486
+ const t = adapter.getNodeType(child);
2487
+ if (t === '=' || t === 'equals') {
2488
+ pastEquals = true;
2489
+ continue;
2490
+ }
2491
+ if (t === ':' || t === 'type_annotation')
2492
+ continue;
2493
+ if (!pastEquals && !name && (t === 'identifier' || t === 'object_pattern' || t === 'array_pattern')) {
2494
+ name = adapter.getNodeText(child, sourceCode);
2495
+ continue;
2496
+ }
2497
+ if ((pastEquals || name) && !value)
2498
+ value = child;
2499
+ }
2500
+ return { name, value };
2501
+ }
2502
+ /**
2503
+ * §13.2: resolve a bare identifier to the initializer of the `const`/`let` that
2504
+ * declares it inside the loop subtree (`const chunk = ids.slice(…)` → the slice
2505
+ * call). Scoped to the loop so a same-named variable elsewhere in the function is
2506
+ * not mistaken for the loop's chunk. Returns null when the identifier is the
2507
+ * loop's own element (declared in a `for…of` header, not a `variable_declarator`).
2508
+ */
2509
+ function resolveLoopLocalBinding(identifierNode, loopNode, adapter, sourceCode) {
2510
+ const target = adapter.getNodeText(identifierNode, sourceCode);
2511
+ if (!target)
2512
+ return null;
2513
+ let result = null;
2514
+ walkSubtree(loopNode, adapter, (n) => {
2515
+ if (result)
2516
+ return;
2517
+ if (adapter.getNodeType(n) !== 'variable_declarator')
2518
+ return;
2519
+ const { name, value } = splitDeclarator(n, adapter, sourceCode);
2520
+ if (name === target)
2521
+ result = value;
2522
+ });
2523
+ return result;
2524
+ }
2525
+ /**
2526
+ * §13.2: true when `expr` denotes a collection-derived array — a batch of rows —
2527
+ * rather than a single row's scalar. Three shapes qualify:
2528
+ * (a) a call to an array-transform method (`ids.slice(…)`, `rows.map(…)`);
2529
+ * (b) a call to a chunking helper (`chunkArray(ids, n)`);
2530
+ * (c) a local `const chunk = <one of the above>` bound inside the loop.
2531
+ */
2532
+ function isCollectionDerivedExpression(expr, loopNode, adapter, sourceCode) {
2533
+ const type = adapter.getNodeType(expr);
2534
+ if (type === 'call_expression') {
2535
+ const memberExpr = findMemberCallee(expr, adapter);
2536
+ if (memberExpr) {
2537
+ const method = memberPropertyName(memberExpr, adapter, sourceCode);
2538
+ if (method && COLLECTION_TRANSFORM_METHODS.has(method))
2539
+ return true;
2540
+ }
2541
+ const callee = getCallExpressionCallee(expr, adapter);
2542
+ if (callee && adapter.getNodeType(callee) === 'identifier') {
2543
+ const name = adapter.getNodeText(callee, sourceCode);
2544
+ if (name && CHUNK_HELPER_RE.test(name))
2545
+ return true;
2546
+ }
2547
+ return false;
2548
+ }
2549
+ if (type === 'identifier') {
2550
+ const value = resolveLoopLocalBinding(expr, loopNode, adapter, sourceCode);
2551
+ return value !== null && isCollectionDerivedExpression(value, loopNode, adapter, sourceCode);
2552
+ }
2553
+ return false;
2554
+ }
2555
+ /**
2556
+ * §13.3: true when a DB call sits in the iterable/header of a `for…of` loop
2557
+ * rather than its body. `for (const c of loadCatalog())` runs `loadCatalog` once
2558
+ * (it memoizes the query) and iterates the returned array in memory — the query
2559
+ * is not per-iteration. Only the header position qualifies; a DB call in the
2560
+ * `statement_block` body still executes once per pass and keeps firing.
2561
+ */
2562
+ function isForOfIterableDbCall(node, loopNode, adapter) {
2563
+ if (adapter.getNodeType(loopNode) !== 'for_in_statement')
2564
+ return false;
2565
+ let body = null;
2566
+ for (const child of adapter.getChildren(loopNode)) {
2567
+ if (adapter.getNodeType(child) === 'statement_block') {
2568
+ body = child;
2569
+ break;
2570
+ }
2571
+ }
2572
+ if (!body)
2573
+ return false;
2574
+ return !isDescendantOf(node, body, adapter);
2575
+ }
2576
+ /** True when `node` is `ancestor` or a descendant of it (parent-chain walk). */
2577
+ function isDescendantOf(node, ancestor, adapter) {
2578
+ let cur = node;
2579
+ while (cur) {
2580
+ if (cur === ancestor)
2581
+ return true;
2582
+ cur = adapter.getParent(cur);
2583
+ }
2584
+ return false;
2585
+ }
2586
+ /**
2587
+ * §13.2: the element and iterated-collection shape of a `for…of` loop. Needed to
2588
+ * distinguish `for (const chunk of chunkArray(ids)) { run(…chunk) }` — spreading
2589
+ * the loop element is a *batch* here because the collection itself is chunked —
2590
+ * from `for (const row of rows) { run(…row) }` — spreading the element is a
2591
+ * per-row write because the collection is a plain array.
2592
+ */
2593
+ function forOfIteration(loopNode, adapter, sourceCode) {
2594
+ const elementNames = new Set();
2595
+ let iteratesChunkedCollection = false;
2596
+ if (adapter.getNodeType(loopNode) !== 'for_in_statement') {
2597
+ return { elementNames, iteratesChunkedCollection };
2598
+ }
2599
+ let element = null;
2600
+ let collection = null;
2601
+ for (const child of adapter.getChildren(loopNode)) {
2602
+ const t = adapter.getNodeType(child);
2603
+ if (t === 'statement_block')
2604
+ break; // the body — nothing after it matters
2605
+ if (!element && (t === 'identifier' || t === 'object_pattern' || t === 'array_pattern')) {
2606
+ element = child;
2607
+ }
2608
+ else if (element && !collection) {
2609
+ collection = child;
2610
+ }
2611
+ }
2612
+ if (element) {
2613
+ const name = adapter.getNodeText(element, sourceCode);
2614
+ if (name)
2615
+ elementNames.add(name);
2616
+ }
2617
+ if (collection) {
2618
+ iteratesChunkedCollection = isCollectionDerivedExpression(collection, loopNode, adapter, sourceCode);
2619
+ }
2620
+ return { elementNames, iteratesChunkedCollection };
2621
+ }
2622
+ /**
2623
+ * §13.2 (per-batch binding discriminator): true when the DB call executes I/O
2624
+ * that binds a *batch* — a spread of a collection-derived expression — rather
2625
+ * than per-row scalars, so it is not an N+1. The eager call (`stmt.run(…)`, or
2626
+ * the `.run(…)` chained off `db.prepare(…)`) is where parameters are bound; its
2627
+ * arguments decide the row-vs-batch question.
2628
+ */
2629
+ function bindsBatchArgument(node, loopNode, adapter, sourceCode) {
2630
+ const eagerCall = findEagerExecutionCall(node, adapter, sourceCode);
2631
+ if (!eagerCall)
2632
+ return false;
2633
+ const argsNode = adapter.getChildren(eagerCall).find((c) => adapter.getNodeType(c) === 'arguments');
2634
+ if (!argsNode)
2635
+ return false;
2636
+ const { elementNames, iteratesChunkedCollection } = forOfIteration(loopNode, adapter, sourceCode);
2637
+ for (const arg of adapter.getChildren(argsNode)) {
2638
+ if (adapter.getNodeType(arg) === 'spread_element') {
2639
+ const operand = adapter.getChildren(arg).find((c) => adapter.getNodeType(c) !== '...');
2640
+ if (!operand)
2641
+ continue;
2642
+ // Spreading the loop's own element is per-row (`…row`) unless the element
2643
+ // is itself a chunk of a chunked collection (`for (const chunk of chunkArray(ids))`).
2644
+ if (adapter.getNodeType(operand) === 'identifier' &&
2645
+ elementNames.has(adapter.getNodeText(operand, sourceCode))) {
2646
+ if (iteratesChunkedCollection)
2647
+ return true;
2648
+ continue;
2649
+ }
2650
+ if (isCollectionDerivedExpression(operand, loopNode, adapter, sourceCode))
2651
+ return true;
2652
+ continue;
2653
+ }
2654
+ // A bare collection-derived argument (no spread) is also a batch:
2655
+ // `run(rows.map(…))`, or a chunk bound inside the loop (`const chunk =
2656
+ // rows.slice(…)` then `run(chunk)` — the identifier resolves to the slice).
2657
+ // A bare loop-element identifier (`for (const row of rows) run(row)`) is NOT
2658
+ // collection-derived: `resolveLoopLocalBinding` only sees `variable_declarator`
2659
+ // bindings, and a `for…of` element is declared in the header, so `row` stays
2660
+ // unresolved and the call keeps firing as per-row.
2661
+ if ((adapter.getNodeType(arg) === 'call_expression' ||
2662
+ adapter.getNodeType(arg) === 'identifier') &&
2663
+ isCollectionDerivedExpression(arg, loopNode, adapter, sourceCode)) {
2664
+ return true;
2665
+ }
2666
+ }
2667
+ return false;
2668
+ }
2669
+ /**
2670
+ * §13.2: the eager I/O call that actually binds parameters for a DB call — the
2671
+ * call itself when it is eager (`stmt.run(…)`, `db.query(…)`), or the eager call
2672
+ * chained off a statement-construction call (`db.prepare(sql).run(…)`). Returns
2673
+ * null when the call never executes I/O (already filtered upstream).
2674
+ */
2675
+ function findEagerExecutionCall(node, adapter, sourceCode) {
2676
+ const call = resolveDbCallNode(node, adapter);
2677
+ if (!call)
2678
+ return null;
2679
+ const direct = dbCallMethodName(call, adapter, sourceCode);
2680
+ if (direct && EAGER_DB_METHODS.has(direct)) {
2681
+ const memberExpr = findMemberCallee(call, adapter);
2682
+ if (!(memberExpr && isPromiseAllMember(memberExpr, adapter, sourceCode)))
2683
+ return call;
2684
+ }
2685
+ let cur = adapter.getParent(call);
2686
+ while (cur) {
2687
+ const type = adapter.getNodeType(cur);
2688
+ if (type === 'call_expression') {
2689
+ const memberExpr = findMemberCallee(cur, adapter);
2690
+ const name = memberExpr ? memberPropertyName(memberExpr, adapter, sourceCode) : null;
2691
+ if (name && EAGER_DB_METHODS.has(name) && !isPromiseAllMember(memberExpr, adapter, sourceCode)) {
2692
+ return cur;
2693
+ }
2694
+ }
2695
+ if (type === 'expression_statement' ||
2696
+ type === 'variable_declarator' ||
2697
+ type === 'return_statement' ||
2698
+ type === 'lexical_declaration' ||
2699
+ type === 'for_statement' ||
2700
+ type === 'for_in_statement' ||
2701
+ type === 'while_statement' ||
2702
+ type === 'statement_block' ||
2703
+ type === 'block') {
2704
+ break;
2705
+ }
2706
+ cur = adapter.getParent(cur);
2707
+ }
2708
+ return null;
2709
+ }
2710
+ /**
2711
+ * Classify a call_expression as an LLM/agent invocation via three unambiguous
2712
+ * signals: a provider/SDK namespace in the callee text, an LLM action verb in the
2713
+ * callee text, or a model-client identifier passed as an argument / used as the
2714
+ * member-callee object.
2715
+ */
2716
+ function isLlmCallNode(node, adapter, sourceCode) {
2717
+ const children = adapter.getChildren(node);
2718
+ const callee = children.find(c => adapter.getNodeType(c) !== 'arguments');
2719
+ if (callee) {
2720
+ const calleeText = adapter.getNodeText(callee, sourceCode);
2721
+ if (LLM_PROVIDER_RE.test(calleeText) || LLM_ACTION_RE.test(calleeText))
2722
+ return true;
2723
+ // Member callee whose object is an LLM client: `model.chat(…)`, `llm.invoke(…)`.
2724
+ if (adapter.getNodeType(callee) === 'member_expression') {
2725
+ const obj = adapter.getChildren(callee).find(c => adapter.getNodeType(c) === 'identifier');
2726
+ if (obj && LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(obj, sourceCode)))
2727
+ return true;
2728
+ }
2729
+ }
2730
+ // Argument signal: an LLM client identifier passed into the call, e.g.
2731
+ // `extractBuildsFromCorpus(db, model, …)`.
2732
+ const args = children.find(c => adapter.getNodeType(c) === 'arguments');
1531
2733
  if (args) {
1532
2734
  for (const arg of adapter.getChildren(args)) {
1533
2735
  if (adapter.getNodeType(arg) === 'identifier' &&
@@ -1544,48 +2746,31 @@ function isLlmCallNode(node, adapter, sourceCode) {
1544
2746
  }
1545
2747
  return false;
1546
2748
  }
1547
- /** True when `node` is a function call whose callee is DB-provenanced. */
1548
- function isDBProvenancedFunctionCall(node, adapter, sourceCode, provenanceContext) {
1549
- return isFunctionCall(node, adapter) &&
1550
- isDBProvenanced(node, { adapter, sourceCode, context: provenanceContext, methods: DB_CALL_METHODS });
1551
- }
1552
2749
  /**
1553
2750
  * R4.1: Determine if a node is a database call expression.
1554
- * Spec 21: When provenance context is available, uses provenance-based detection
1555
- * (conjunctive guard — never name alone). In names mode or without context,
1556
- * falls back to the legacy dbPatterns text match.
2751
+ * Spec 21: provenance-based detection (conjunctive guard — never name alone).
2752
+ *
2753
+ * Spec 70 R3/R4: handle-ness is decided once, by `identifyHandle` (via
2754
+ * `handleVerdictForCall`); only a proven `handle` — a parsed SQL argument (R3)
2755
+ * or a package in the manifest (R4) — is a DB node for the loop walk. `unproven`
2756
+ * is `cannot-fire` (visible, never a finding), so a `map.delete()` / `set.join()`
2757
+ * on an unresolved receiver does not read as a DB call inside a loop.
2758
+ *
2759
+ * The legacy `dbPatterns` substring fallback (a call whose *text* contained
2760
+ * `select`/`insert`/`delete`/…) is deleted, not fixed: `buildDataAccessScan`
2761
+ * always provides a provenance context (`mode` defaults to `'hybrid'`), so the
2762
+ * no-context / `names`-mode arm was unreachable in a default run. Without
2763
+ * provenance there is no honest handle verdict, so the node is not a DB call.
2764
+ *
2765
+ * @param node the candidate call node to classify
2766
+ * @param scan the data-access scan context (adapter, source, provenance)
2767
+ * @returns true when the node's handle verdict is `handle`
1557
2768
  */
1558
- function isDbCallNode(node, adapter, sourceCode, provenanceContext) {
1559
- // Spec 21: Provenance-first detection when context is available
2769
+ export function isDbCallNode(node, scan) {
2770
+ const { provenanceContext } = scan;
1560
2771
  if (provenanceContext && provenanceContext.mode !== 'names') {
1561
- // In provenance or hybrid mode, use provenance check
1562
- if (isDBProvenancedFunctionCall(node, adapter, sourceCode, provenanceContext)) {
1563
- return true;
1564
- }
1565
- // Spec 17 R2: Template literal is a DB node only when it sits
1566
- // inside a DB-provenanced call's arguments — no content scan.
1567
- if (isTemplateLiteral(node, adapter)) {
1568
- const parent = adapter.getParent(node);
1569
- if (parent && adapter.getNodeType(parent) === 'arguments') {
1570
- const callExpr = adapter.getParent(parent);
1571
- if (callExpr && adapter.getNodeType(callExpr) === 'call_expression') {
1572
- return isDBProvenancedFunctionCall(callExpr, adapter, sourceCode, provenanceContext);
1573
- }
1574
- }
1575
- return false;
1576
- }
1577
- return false;
1578
- }
1579
- // Legacy fallback: name-based matching for names mode / no context.
1580
- // Template-literal content scanning is removed (Spec 17 R2) — without
1581
- // provenance context, we can't determine if a template literal is SQL;
1582
- // function-call-based SQL detection still works via dbPatterns match.
1583
- const nodeText = adapter.getNodeText(node, sourceCode);
1584
- if (isFunctionCall(node, adapter)) {
1585
- const dbPatterns = ['select', 'insert', 'update', 'delete', 'from', 'where', 'execute', 'query', 'find', 'aggregate', 'count', 'distinct'];
1586
- if (dbPatterns.some(pattern => nodeText.toLowerCase().includes(pattern))) {
1587
- return true;
1588
- }
2772
+ const verdict = handleVerdictForCall(node, scan);
2773
+ return verdict !== null && verdict.kind === 'handle';
1589
2774
  }
1590
2775
  return false;
1591
2776
  }
@@ -1597,7 +2782,7 @@ function isDbCallNode(node, adapter, sourceCode, provenanceContext) {
1597
2782
  * - for / while / do loops via adapter.isLoop()
1598
2783
  * - .forEach / .map / .filter callbacks via AST pattern matching
1599
2784
  */
1600
- function findEnclosingLoop(node, adapter) {
2785
+ function findEnclosingLoop(node, adapter, sourceCode) {
1601
2786
  let current = node;
1602
2787
  let depth = 0;
1603
2788
  const foundLoops = [];
@@ -1610,7 +2795,7 @@ function findEnclosingLoop(node, adapter) {
1610
2795
  foundLoops.push(parent);
1611
2796
  }
1612
2797
  // Check for iterator callbacks (.forEach, .map, .filter, etc.)
1613
- if (isIteratorCallback(parent, adapter)) {
2798
+ if (isIteratorCallback(parent, current, adapter, sourceCode)) {
1614
2799
  foundLoops.push(parent);
1615
2800
  }
1616
2801
  current = parent;
@@ -1628,47 +2813,46 @@ function findEnclosingLoop(node, adapter) {
1628
2813
  * R4.1: Check if a node is a call_expression invoking an iterator method
1629
2814
  * (.forEach, .map, .filter, .reduce, .some, .every) — these create
1630
2815
  * implicit loops where a DB query inside the callback is an N+1 risk.
2816
+ *
2817
+ * `child` is the node one level below `node` on the walk-up path from the query.
2818
+ * An iterator call only encloses the query when the query sits *inside the
2819
+ * callback argument* (an arrow/function expression), not when the query is the
2820
+ * *subject* the method is called on. `this.db.prepare(sql).all().map(r => …)`
2821
+ * runs the query once and maps the rows in memory — a single query, not an N+1
2822
+ * — yet without this check the `.map` call would read as an enclosing loop
2823
+ * because the query is a descendant of the call expression. Only a query inside
2824
+ * the callback body executes once per iteration.
1631
2825
  */
1632
- function isIteratorCallback(node, adapter) {
2826
+ function isIteratorCallback(node, child, adapter, sourceCode) {
1633
2827
  // Must be a call_expression
1634
- if (node.type !== 'call_expression')
2828
+ if (adapter.getNodeType(node) !== 'call_expression')
1635
2829
  return false;
1636
2830
  // Callee must be a member_expression whose property matches iterator method names
1637
2831
  const children = adapter.getChildren(node);
1638
- const callee = children.find(c => c.type === 'member_expression');
2832
+ const callee = children.find(c => adapter.getNodeType(c) === 'member_expression');
1639
2833
  if (!callee)
1640
2834
  return false;
1641
2835
  const calleeChildren = adapter.getChildren(callee);
1642
- const propertyNode = calleeChildren.find(c => c.type === 'property_identifier' || c.type === 'string');
2836
+ const propertyNode = calleeChildren.find(c => adapter.getNodeType(c) === 'property_identifier' || adapter.getNodeType(c) === 'string');
1643
2837
  if (!propertyNode)
1644
2838
  return false;
1645
- const methodName = adapter.getNodeType(propertyNode) === 'property_identifier'
1646
- ? propertyNode.text ?? adapter.getNodeText(propertyNode, '')
1647
- : '';
1648
- // Normalize: the method name might come from the node type or need text extraction
1649
2839
  const iteratorMethods = ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find', 'findIndex', 'flatMap'];
1650
- // Try multiple ways to get the method name
1651
- const propText = methodName || getPropertyName(propertyNode, adapter);
1652
- return iteratorMethods.includes(propText);
1653
- }
1654
- /**
1655
- * Extract the property name from a property_identifier node.
1656
- */
1657
- function getPropertyName(node, adapter) {
1658
- // Try named children
1659
- if (node.name)
1660
- return node.name;
1661
- if (node.text)
1662
- return node.text;
1663
- // Try to get it from children
1664
- const children = adapter.getChildren(node);
1665
- for (const child of children) {
1666
- if (child.name)
1667
- return child.name;
1668
- if (child.text)
1669
- return child.text;
1670
- }
1671
- return '';
2840
+ // The property text must come from the source slice (web-tree-sitter nodes
2841
+ // carry no `.text`/`.name` payload; `getNodeText` with the real source is the
2842
+ // established extraction — cf. `memberPropertyName`). Passing an empty string
2843
+ // here returned '' for every node, so iterator callbacks were never matched.
2844
+ const propText = adapter.getNodeText(propertyNode, sourceCode);
2845
+ if (!iteratorMethods.includes(propText))
2846
+ return false;
2847
+ // The direct child on the path from the query must be the call's `arguments`
2848
+ // list (the callback), not the callee/subject the method is invoked on. The
2849
+ // child is the immediate descendant of the call_expression that the query sits
2850
+ // under: `arguments` when the query is inside the callback, the `member_expression`
2851
+ // callee when the query is the *subject* (`db.prepare(sql).all().map(r => …)`
2852
+ // runs once and maps rows in memory). Checking the child's node type directly —
2853
+ // rather than the callback function's — is what survives the `arguments` node
2854
+ // tree-sitter inserts between the call and its arrow/function argument.
2855
+ return adapter.getNodeType(child) === 'arguments';
1672
2856
  }
1673
2857
  /**
1674
2858
  * The enclosing-function identity used for stable fingerprint symbols. The
@@ -1676,74 +2860,508 @@ function getPropertyName(node, adapter) {
1676
2860
  * definition lives in codeAnalysis.ts (Spec 61 Amendment A).
1677
2861
  */
1678
2862
  const enclosingIdentity = (node, adapter, filePath) => functionIdentityLabel(findEnclosingFunctionIdentity(node, adapter, filePath));
2863
+ /** Build the shared data-access scan context (config + provenance + imports). */
1679
2864
  /**
1680
- * Universal data access analyzer.
1681
- */
1682
- export class UniversalDataAccessAnalyzer extends UniversalAnalyzer {
1683
- name = 'data-access';
1684
- description = 'Analyzes database access patterns and data layer interactions';
1685
- category = 'security';
1686
- async analyzeAST(ast, adapter, config, sourceCode) {
1687
- return (await this.analyzeWithFacts(ast, adapter, config, sourceCode)).violations;
1688
- }
1689
- /**
1690
- * Spec 62 Amendment B — run the full data-access scan and return BOTH the
1691
- * violations and the extracted query facts (`DatabaseCall[]`). The pipeline's
1692
- * data-access visitor emits the calls as facts for the Stage-4
1693
- * `missing-org-filter` derived reducer to join against the declared + DDL
1694
- * tenant tiers; the remaining rules (sql-injection, loop-query,
1695
- * unfiltered-query, hardcoded-connection, complex-query) fire here at Stage 2
1696
- * exactly as before. Emitting the calls as a by-product of this one scan
1697
- * avoids a second traversal or re-parse.
1698
- */
1699
- async analyzeWithFacts(ast, adapter, config, sourceCode) {
1700
- const violations = [];
1701
- const finalConfig = { ...DEFAULT_DATA_ACCESS_CONFIG, ...config };
1702
- // Spec 21 R1: provenance-primary detection (names owned by THIS analyzer).
1703
- const detectionMode = finalConfig.detection?.mode ?? 'hybrid';
1704
- const p0 = performance.now();
1705
- const provenanceContext = buildProvenanceContext(ast, adapter, sourceCode, {
1706
- mode: detectionMode,
1707
- dbReceiverNames: finalConfig.dbReceiverNames,
1708
- dbBindingNames: finalConfig.dbBindingNames,
1709
- dbCallMethods: finalConfig.dbCallMethods,
1710
- dbWrapperNames: finalConfig.dbWrapperNames,
1711
- });
1712
- const timingAcc = config._provenanceTiming;
1713
- if (timingAcc)
1714
- timingAcc.totalMs += performance.now() - p0;
1715
- // Spec 34: bundle per-file context to stay under the 4-parameter gate.
1716
- const scan = {
1717
- adapter,
1718
- sourceCode,
1719
- dbImports: mapDatabaseImports(adapter.extractImports(ast), finalConfig),
1720
- config: finalConfig,
1721
- provenanceContext,
1722
- };
1723
- // Spec 55 R3 — test/spec files are excluded from the query-shape rules
1724
- // (loop-query, unfiltered-query). Security and org-filter rules still fire:
1725
- // a test with a hardcoded connection string or an injected query is as real
1726
- // a signal as in production code. `skipTestFiles: false` overrides (oracle
1727
- // fixtures assert positive loop-query detections on files under __tests__).
1728
- const skipTestRules = finalConfig.skipTestFiles !== false && isTestOrSpecPath(ast.filePath);
1729
- // Analyze each database call, tracking symbol ordinals for stable fingerprints.
1730
- const calls = extractDatabaseCalls(ast, scan);
1731
- const symbolOrdinals = new Map();
1732
- for (const call of calls) {
1733
- const analysis = analyzeQuery(call, finalConfig);
1734
- violations.push(...checkViolations(call, analysis, {
1735
- filePath: ast.filePath,
1736
- config: finalConfig,
1737
- symbolOrdinals,
1738
- skipTestRules,
1739
- }));
1740
- }
1741
- // R4.1: loop-query (N+1) detection + general patterns.
1742
- if (!skipTestRules) {
1743
- violations.push(...checkLoopQueries(ast, scan));
2865
+ * Build the TypeScript binding environment `identifyHandle` reads for declaration
2866
+ * resolution, keyed to the file's own imports/declarations plus the within-file
2867
+ * provenance set. Absent (undefined) for Go, which resolves cross-file and has no
2868
+ * `RootResolutionEnv`.
2869
+ */
2870
+ function buildHandleEnv(ast, adapter, sourceCode, provenanceContext) {
2871
+ if (adapter.name === 'go')
2872
+ return undefined;
2873
+ return {
2874
+ provenance: provenanceContext.dbProvenanced,
2875
+ bindings: buildBindingEnv(ast, adapter, sourceCode),
2876
+ interfaceFields: extractInterfaceFields(ast, adapter, sourceCode),
2877
+ adapter,
2878
+ sourceCode,
2879
+ };
2880
+ }
2881
+ /** Build the Go binding environment `identifyHandle` reads for Go declaration
2882
+ * resolution. The provenance seed (cross-file `db *sql.DB` resolution) is what
2883
+ * proves `handle`; bindings + imports let `classifyGoRootIdentifier` dispose a
2884
+ * non-seeded receiver (`unproven` for an unrecognized package, `not-handle` for
2885
+ * a non-DB binding). Absent (undefined) for non-Go formats. */
2886
+ function buildGoEnv(ast, adapter, sourceCode, provenanceContext) {
2887
+ if (adapter.name !== 'go')
2888
+ return undefined;
2889
+ return {
2890
+ provenance: provenanceContext.dbProvenanced,
2891
+ bindings: buildGoBindingEnv(ast, adapter, sourceCode),
2892
+ imports: buildGoImportMap(ast, adapter),
2893
+ adapter,
2894
+ sourceCode,
2895
+ };
2896
+ }
2897
+ function buildDataAccessScan(ast, adapter, sourceCode, config) {
2898
+ const finalConfig = { ...DEFAULT_DATA_ACCESS_CONFIG, ...config };
2899
+ const detectionMode = finalConfig.detection?.mode ?? 'hybrid';
2900
+ const seedProvenance = config?._receiverProvenance;
2901
+ const provenanceContext = buildProvenanceContext(ast, adapter, sourceCode, {
2902
+ mode: detectionMode,
2903
+ dbBindingNames: finalConfig.dbBindingNames,
2904
+ dbWrapperNames: finalConfig.dbWrapperNames,
2905
+ seedProvenance,
2906
+ // Spec 70 criterion 8 — thread the corpus's dialect so R3 proves a receiver
2907
+ // whose (now-deleted) type annotation was its only signal, and so wrapper
2908
+ // detection (detectDbWrappers) sees the proven receiver in helper bodies.
2909
+ sqlDialect: finalConfig.dialect ?? null,
2910
+ });
2911
+ return {
2912
+ adapter,
2913
+ sourceCode,
2914
+ config: finalConfig,
2915
+ ast,
2916
+ provenanceContext,
2917
+ handleEnv: buildHandleEnv(ast, adapter, sourceCode, provenanceContext),
2918
+ goEnv: buildGoEnv(ast, adapter, sourceCode, provenanceContext),
2919
+ };
2920
+ }
2921
+ /**
2922
+ * Spec 68 §3.2 — the `data-access-calls` FileProcessor extraction, factored out
2923
+ * of `analyzeWithFacts` so the phase-model producer can obtain the per-file
2924
+ * resolved calls synchronously (the analyzer method is `async` but does no real
2925
+ * awaits — its body is entirely synchronous). Returns the `DatabaseCall[]` the
2926
+ * missing-org-filter / unfiltered-query / sql-injection-risk rules read, with no
2927
+ * violation production and no side effects. Config is the §10 tuning surface;
2928
+ * omitted here, the extraction runs on {@link DEFAULT_DATA_ACCESS_CONFIG}.
2929
+ * @param ast The parsed file AST.
2930
+ * @param adapter The language adapter for the file's syntax.
2931
+ * @param sourceCode The raw source text.
2932
+ * @param config The data-access analyzer config (defaults when omitted).
2933
+ * @returns The database calls extracted from the file.
2934
+ */
2935
+ export function extractDataAccessCalls(ast, adapter, sourceCode, config) {
2936
+ return extractDatabaseCalls(ast, buildDataAccessScan(ast, adapter, sourceCode, config));
2937
+ }
2938
+ /**
2939
+ * Extract the loop-query candidates (loops whose body issues a DB call) as the
2940
+ * serializable `LoopQueryCandidate[]` the `loop-queries` producer projects. The
2941
+ * candidate set is exactly what `checkLoopQueries` reduces to findings, computed
2942
+ * on the same provenance context — so detection parity holds by construction,
2943
+ * not by re-implementation. Config is the §10 tuning surface; omitted here, the
2944
+ * extraction runs on {@link DEFAULT_DATA_ACCESS_CONFIG}.
2945
+ * @param ast The parsed file AST.
2946
+ * @param adapter The language adapter for the file's syntax.
2947
+ * @param sourceCode The raw source text.
2948
+ * @param config The data-access analyzer config (defaults when omitted).
2949
+ * @returns The loop-query candidates extracted from the file.
2950
+ */
2951
+ export function extractLoopQueries(ast, adapter, sourceCode, config) {
2952
+ return collectLoopQueryCandidates(ast, buildDataAccessScan(ast, adapter, sourceCode, config));
2953
+ }
2954
+ // ── Spec 70 Item 4 (step 3) — raw candidate extractors ───────────────────────
2955
+ //
2956
+ // The two remaining receiver consumers (`data-access-calls`, `loop-queries`) are
2957
+ // converted to corpus producers: while the AST lives, these extractors project the
2958
+ // *provenance-free* half of `buildDatabaseCall` / `collectLoopQueryCandidates` into
2959
+ // serializable candidates, and the corpus producers re-fold the provenance-dependent
2960
+ // half (the `identifyHandle` verdict → admission + injection-risk gate, the site
2961
+ // dialect → the SQL parse, and the loop dedup) over the re-derived `dbProvenanced`.
2962
+ // They run on an EMPTY-provenance scan so `checkQuerySecurity`'s wrapper arm sees
2963
+ // only `config.dbWrapperNames` (the learned wrappers are corrected corpus-side).
2964
+ /** Build a scan with an empty provenance context — the raw producer's discovery and
2965
+ * static-security arms must not read the cross-file seed (which only the corpus
2966
+ * producer sees), so every provenance-dependent signal is captured as an identity
2967
+ * and re-folded later. `handleEnv`/`goEnv` still build their provenance-free
2968
+ * bindings/imports, which the structural discovery predicates read. */
2969
+ function buildCandidateScan(ast, adapter, sourceCode, config) {
2970
+ const finalConfig = { ...DEFAULT_DATA_ACCESS_CONFIG, ...config };
2971
+ const provenanceContext = {
2972
+ dbProvenanced: new Map(),
2973
+ validatorProvenanced: new Map(),
2974
+ mode: finalConfig.detection?.mode ?? 'hybrid',
2975
+ dbActivity: new Set(),
2976
+ };
2977
+ return {
2978
+ adapter,
2979
+ sourceCode,
2980
+ config: finalConfig,
2981
+ ast,
2982
+ provenanceContext,
2983
+ handleEnv: buildHandleEnv(ast, adapter, sourceCode, provenanceContext),
2984
+ goEnv: buildGoEnv(ast, adapter, sourceCode, provenanceContext),
2985
+ };
2986
+ }
2987
+ // ── Structural discovery (empty-provenance supersets) ────────────────────────
2988
+ /** True when `node` is a bare-identifier call whose callee name has any binding —
2989
+ * the raw-side superset of `handleVerdictForCall`'s identifier arm. A provenanced
2990
+ * wrapper, a type-annotated handle, or a propagated local are all *bound*, so a
2991
+ * name with no binding is provably not a handle candidate and is dropped here. */
2992
+ function isBoundIdentifierCall(node, scan) {
2993
+ const { adapter, sourceCode, handleEnv } = scan;
2994
+ if (!handleEnv)
2995
+ return false;
2996
+ if (adapter.getNodeType(node) !== 'call_expression')
2997
+ return false;
2998
+ const callee = getCallExpressionCallee(node, adapter);
2999
+ if (!callee || callee.type !== 'identifier')
3000
+ return false;
3001
+ const name = adapter.getNodeText(callee, sourceCode);
3002
+ return !!name && handleEnv.bindings.has(name);
3003
+ }
3004
+ /** True when `node` is a member/selector call whose receiver root is DB-shaped
3005
+ * (handle or unproven) or that carries a static SQL argument — the structural
3006
+ * superset of `handleVerdictForCall`'s member arm (and Go's selector arm, which
3007
+ * is the same shape). The package discriminant — not the method name — admits a
3008
+ * receiver (`join` is `Array.prototype.join` and also `SQL JOIN`); the empty
3009
+ * provenance of the candidate scan makes more roots `unproven`, so this stays a
3010
+ * strict superset of the corpus-side re-fold. */
3011
+ function isMemberShapeCall(node, scan) {
3012
+ const { adapter, sourceCode, handleEnv, goEnv } = scan;
3013
+ if (adapter.getNodeType(node) !== 'call_expression')
3014
+ return false;
3015
+ const callee = getCallExpressionCallee(node, adapter);
3016
+ if (!callee || (callee.type !== 'member_expression' && callee.type !== 'selector_expression'))
3017
+ return false;
3018
+ const root = resolveReceiverRoot(callee, adapter, sourceCode);
3019
+ if (root === null)
3020
+ return false;
3021
+ const sqlArg = adapter.name === 'go'
3022
+ ? extractGoStaticSql(node, adapter, sourceCode, goEnv)
3023
+ : extractStaticSql(node, adapter, sourceCode);
3024
+ if (sqlArg === null) {
3025
+ return adapter.name === 'go'
3026
+ ? !!goEnv && classifyGoRootIdentifier(root, goEnv) !== 'not-handle'
3027
+ : !!handleEnv && isDbShapedRoot(root, handleEnv, { thisField: receiverIsThisRooted(callee, adapter) });
3028
+ }
3029
+ return true;
3030
+ }
3031
+ /** True when `node` is a template literal whose enclosing call is a bound-identifier
3032
+ * or member-shape call — the raw-side superset of `handleVerdictForCall`'s template
3033
+ * branch (`callNode = enclosingCallOf(template)`). */
3034
+ function isTemplateOfDbCall(node, scan) {
3035
+ const { adapter, sourceCode, config } = scan;
3036
+ if (!isTemplateLiteral(node, adapter))
3037
+ return false;
3038
+ const callNode = enclosingCallOf(node, adapter);
3039
+ if (!callNode)
3040
+ return false;
3041
+ // A tagged template (`this.sql`…``, `sql`…``) carries its template as its own
3042
+ // body, not as a template *argument* to a DB call. The tag is already a
3043
+ // candidate via `isTaggedTemplateSqlCall`; re-admitting the body here would
3044
+ // double-discover the site and let the body (method `unknown`) shadow the tag
3045
+ // (method `sql`) in line dedup.
3046
+ const tagNames = config.sqlTagNames ?? SQL_TAG_NAMES;
3047
+ if (isTaggedTemplateSqlCall(callNode, adapter, sourceCode, tagNames))
3048
+ return false;
3049
+ return isBoundIdentifierCall(callNode, scan) || isMemberShapeCall(callNode, scan);
3050
+ }
3051
+ /** The broadened data-access discovery — a strict superset of `isDbCallCandidate`
3052
+ * over the same pre-order. The three shape arms (query-builder, tagged template,
3053
+ * variable assignment) are provenance-free; the identifier/member/template arms
3054
+ * cover every `handleVerdictForCall` shape with the empty seed. */
3055
+ function isBroadenedDataAccessCandidate(node, scan, tagNames) {
3056
+ const { adapter, sourceCode } = scan;
3057
+ if (isQueryBuilderShape(node, adapter, sourceCode))
3058
+ return true;
3059
+ if (isTaggedTemplateSqlCall(node, adapter, sourceCode, tagNames))
3060
+ return true;
3061
+ if (isVariableAssignment(node, adapter) && extractStaticSql(node, adapter, sourceCode) !== null)
3062
+ return true;
3063
+ if (isBoundIdentifierCall(node, scan))
3064
+ return true;
3065
+ if (isMemberShapeCall(node, scan))
3066
+ return true;
3067
+ if (isTemplateOfDbCall(node, scan))
3068
+ return true;
3069
+ return false;
3070
+ }
3071
+ /** The broadened loop-query discovery — a strict superset of `isDbCallNode` (which
3072
+ * is `handleVerdictForCall(node).kind === 'handle'`). The template-literal branch is
3073
+ * skipped later (as in `collectLoopQueryCandidates`), so only the two structural
3074
+ * call arms remain. */
3075
+ function isBroadenedLoopCandidate(node, scan) {
3076
+ return isBoundIdentifierCall(node, scan) || isMemberShapeCall(node, scan);
3077
+ }
3078
+ const NULL_HANDLE_IDENTITY = {
3079
+ calleeType: null, name: null, root: null, receiver: null, method: null,
3080
+ thisField: false, thisHeritage: null, sqlArg: null, siteReceiver: null, scope: 0,
3081
+ };
3082
+ /** Project `handleVerdictForCall`'s structural gates (without `identifyHandle`) into
3083
+ * the identity the corpus producer re-folds. Mirrors the TS identifier/member arms
3084
+ * and the Go selector arm exactly — same method/root gates, same template → enclosing
3085
+ * call resolution, same `extractStaticSql(callNode)` argument. */
3086
+ function handleCallSiteIdentity(node, scan) {
3087
+ const { adapter, sourceCode } = scan;
3088
+ if (adapter.name === 'go') {
3089
+ // Go selector arm (goHandleVerdictForCall) — node is always a call_expression.
3090
+ if (!isFunctionCall(node, adapter))
3091
+ return NULL_HANDLE_IDENTITY;
3092
+ const callee = getCallExpressionCallee(node, adapter);
3093
+ if (!callee || callee.type !== 'selector_expression')
3094
+ return NULL_HANDLE_IDENTITY;
3095
+ const method = extractMemberExpressionProperty(callee, adapter, sourceCode);
3096
+ if (!method)
3097
+ return NULL_HANDLE_IDENTITY;
3098
+ const root = resolveReceiverRoot(callee, adapter, sourceCode);
3099
+ if (root === null)
3100
+ return NULL_HANDLE_IDENTITY;
3101
+ const sqlArg = extractGoStaticSql(node, adapter, sourceCode, scan.goEnv);
3102
+ if (sqlArg === null && (!scan.goEnv || classifyGoRootIdentifier(root, scan.goEnv) === 'not-handle')) {
3103
+ return NULL_HANDLE_IDENTITY;
1744
3104
  }
1745
- violations.push(...checkGeneralPatterns(ast, adapter, sourceCode, finalConfig));
1746
- return { violations, calls };
3105
+ const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode) ?? root;
3106
+ return {
3107
+ calleeType: 'member', name: null, root, receiver, method, thisField: false,
3108
+ thisHeritage: null,
3109
+ sqlArg,
3110
+ siteReceiver: getMemberExpressionReceiver(callee, adapter, sourceCode),
3111
+ scope: 0,
3112
+ };
3113
+ }
3114
+ const callNode = isTemplateLiteral(node, adapter) ? enclosingCallOf(node, adapter) : node;
3115
+ if (!callNode || !isFunctionCall(callNode, adapter))
3116
+ return NULL_HANDLE_IDENTITY;
3117
+ if (callNode.type === 'new_expression')
3118
+ return NULL_HANDLE_IDENTITY;
3119
+ const callee = getCallExpressionCallee(callNode, adapter);
3120
+ if (!callee)
3121
+ return NULL_HANDLE_IDENTITY;
3122
+ if (callee.type === 'identifier') {
3123
+ const name = adapter.getNodeText(callee, sourceCode);
3124
+ if (!name)
3125
+ return NULL_HANDLE_IDENTITY;
3126
+ return {
3127
+ calleeType: 'identifier', name, root: name, receiver: name, method: name,
3128
+ thisField: false,
3129
+ thisHeritage: null,
3130
+ sqlArg: extractStaticSql(callNode, adapter, sourceCode),
3131
+ siteReceiver: null,
3132
+ scope: findEnclosingFunctionNode(callNode, adapter)?.range[0] ?? 0,
3133
+ };
3134
+ }
3135
+ if (callee.type !== 'member_expression' && callee.type !== 'selector_expression') {
3136
+ return NULL_HANDLE_IDENTITY;
3137
+ }
3138
+ const method = extractMemberExpressionProperty(callee, adapter, sourceCode);
3139
+ if (!method)
3140
+ return NULL_HANDLE_IDENTITY;
3141
+ const root = resolveReceiverRoot(callee, adapter, sourceCode);
3142
+ if (root === null)
3143
+ return NULL_HANDLE_IDENTITY;
3144
+ const sqlArg = extractStaticSql(callNode, adapter, sourceCode);
3145
+ const thisRooted = receiverIsThisRooted(callee, adapter);
3146
+ if (sqlArg === null && (!scan.handleEnv || !isDbShapedRoot(root, scan.handleEnv, { thisField: thisRooted }))) {
3147
+ return NULL_HANDLE_IDENTITY;
3148
+ }
3149
+ const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode) ?? root;
3150
+ // Spec 70 Q3 — a `this.<root>` receiver resolves to the enclosing class's
3151
+ // base-class field type (`extends WorkflowEntrypoint<Env>` → `this.env` is
3152
+ // `Env`), re-folded through the member path by the same heritage seam as the
3153
+ // build side. The raw `extends` text is carried, resolved at fold time; null
3154
+ // when not `this`-rooted or the class has no base class.
3155
+ const thisHeritage = thisRooted
3156
+ ? findEnclosingClassHeritage(scan.ast, adapter, callNode, sourceCode)
3157
+ : null;
3158
+ return {
3159
+ calleeType: 'member', name: null, root, receiver, method,
3160
+ thisField: thisRooted,
3161
+ thisHeritage,
3162
+ sqlArg,
3163
+ siteReceiver: getMemberExpressionReceiver(callee, adapter, sourceCode),
3164
+ scope: findEnclosingFunctionNode(callNode, adapter)?.range[0] ?? 0,
3165
+ };
3166
+ }
3167
+ function siteDialectIdentity(node, scan) {
3168
+ const { adapter, sourceCode } = scan;
3169
+ const callee = getCallExpressionCallee(node, adapter);
3170
+ if (!callee)
3171
+ return { calleeType: null, name: null, receiver: null };
3172
+ if (callee.type === 'identifier') {
3173
+ return { calleeType: 'identifier', name: adapter.getNodeText(callee, sourceCode) || null, receiver: null };
3174
+ }
3175
+ if (callee.type === 'member_expression' || callee.type === 'selector_expression') {
3176
+ return { calleeType: 'member', name: null, receiver: getMemberExpressionReceiver(callee, adapter, sourceCode) };
3177
+ }
3178
+ return { calleeType: null, name: null, receiver: null };
3179
+ }
3180
+ /** The bare-identifier callee name of a template's enclosing call, only when the
3181
+ * call carries ≥2 real args — the `isWrapperFunctionWithBindParams` shape's identity
3182
+ * (arm 4). Captured raw-side so the corpus producer can correct the static-security
3183
+ * result once it learns the name as a DB wrapper (`reason: 'wrapper'`). */
3184
+ function wrapperCalleeNameForTemplate(node, adapter, sourceCode) {
3185
+ if (adapter.getNodeType(node) !== 'template_string')
3186
+ return null;
3187
+ const args = adapter.getParent(node);
3188
+ if (!args || adapter.getNodeType(args) !== 'arguments')
3189
+ return null;
3190
+ const call = adapter.getParent(args);
3191
+ if (!call || adapter.getNodeType(call) !== 'call_expression')
3192
+ return null;
3193
+ const callee = getCallExpressionCallee(call, adapter);
3194
+ if (!callee || adapter.getNodeType(callee) !== 'identifier')
3195
+ return null;
3196
+ const realArgs = adapter.getChildren(args).filter((c) => !['(', ')', ','].includes(adapter.getNodeType(c)));
3197
+ if (realArgs.length < 2)
3198
+ return null;
3199
+ return adapter.getNodeText(callee, sourceCode) || null;
3200
+ }
3201
+ // ── The `data-access-calls-candidates` extractor ─────────────────────────────
3202
+ /** Project one broadened candidate into the raw, provenance-free `DataAccessCallCandidate`
3203
+ * the corpus `data-access-calls` producer re-folds. The static-security arms run here
3204
+ * on the empty-provenance scan; only `arm4CalleeName` is corrected corpus-side. */
3205
+ function buildDataAccessCallCandidate(node, ast, scan) {
3206
+ const { adapter, sourceCode, config } = scan;
3207
+ const nodeText = stripComments(adapter.getNodeText(node, sourceCode));
3208
+ if (!nodeText || nodeText.trim().length < 10)
3209
+ return null;
3210
+ const sqlArg = adapter.name === 'go'
3211
+ ? extractGoStaticSql(node, adapter, sourceCode, scan.goEnv)
3212
+ : extractStaticSql(node, adapter, sourceCode);
3213
+ const isOrmCall = isOrmPattern(nodeText);
3214
+ const tagNames = config.sqlTagNames ?? SQL_TAG_NAMES;
3215
+ const isTaggedSqlCall = isTaggedTemplateSqlCall(node, adapter, sourceCode, tagNames);
3216
+ const isQueryBuilder = isQueryBuilderShape(node, adapter, sourceCode);
3217
+ const isVariableAssignmentSql = isVariableAssignment(node, adapter) && sqlArg !== null;
3218
+ const isTemplate = isTemplateLiteral(node, adapter);
3219
+ const handle = handleCallSiteIdentity(node, scan);
3220
+ const site = siteDialectIdentity(node, scan);
3221
+ const security = checkQuerySecurity(node, nodeText, ast, scan);
3222
+ return {
3223
+ file: ast.filePath,
3224
+ line: node.location.start.line,
3225
+ column: node.location.start.column,
3226
+ format: adapter.name === 'go' ? 'go' : 'typescript',
3227
+ nodeText,
3228
+ method: extractMethodName(node, adapter, sourceCode),
3229
+ sqlArg,
3230
+ isOrmCall,
3231
+ isTaggedSqlCall,
3232
+ isQueryBuilderShape: isQueryBuilder,
3233
+ isVariableAssignmentSql,
3234
+ isTemplateLiteral: isTemplate,
3235
+ handleCalleeType: handle.calleeType,
3236
+ handleName: handle.name,
3237
+ handleRoot: handle.root,
3238
+ handleReceiver: handle.receiver,
3239
+ handleMethod: handle.method,
3240
+ handleThisField: handle.thisField,
3241
+ handleThisHeritage: handle.thisHeritage,
3242
+ handleSqlArg: handle.sqlArg,
3243
+ handleSiteReceiver: handle.siteReceiver,
3244
+ handleRootScope: handle.scope,
3245
+ skipCallForTemplateArg: shouldSkipCallForTemplateArg(node, adapter),
3246
+ siteCalleeType: site.calleeType,
3247
+ siteName: site.name,
3248
+ siteReceiver: site.receiver,
3249
+ hasOrganizationFilter: hasOrganizationFilter(nodeText, config),
3250
+ enclosingFunction: enclosingIdentity(node, adapter, ast.filePath),
3251
+ resolvedWhere: resolveWhereBinding(node, sourceCode, adapter),
3252
+ ormTables: isOrmCall ? extractOrmTables(node, adapter, sourceCode, config) : [],
3253
+ builderVerb: builderWriteVerb(node, adapter, sourceCode),
3254
+ ormHasFilter: hasOrmFilterShape(node, adapter, sourceCode),
3255
+ staticParameterized: security.parameterized,
3256
+ staticInjectionRisk: security.injectionRisk,
3257
+ staticEscaped: security.escaped,
3258
+ arm4CalleeName: wrapperCalleeNameForTemplate(node, adapter, sourceCode),
3259
+ };
3260
+ }
3261
+ /**
3262
+ * Extract the raw, provenance-free `data-access-calls-candidates` for one file —
3263
+ * the extract half of the corpus `data-access-calls` reduction (Spec 70 Item 4,
3264
+ * step 3). No dedup happens here: the broadened discovery can perturb line-dedup
3265
+ * (a bound bare call and a member call on the same line), so the corpus producer
3266
+ * dedups after re-folding admission. Config is the §10 tuning surface; omitted
3267
+ * here, extraction runs on {@link DEFAULT_DATA_ACCESS_CONFIG}.
3268
+ * @param ast - The file's parsed AST.
3269
+ * @param adapter - The language adapter (drives node discovery + text reads).
3270
+ * @param sourceCode - The file's source text.
3271
+ * @param config - Optional data-access config; defaults to the shared config.
3272
+ * @returns The raw, provenance-free `data-access-calls-candidates`.
3273
+ */
3274
+ export function extractDataAccessCallCandidates(ast, adapter, sourceCode, config) {
3275
+ const scan = buildCandidateScan(ast, adapter, sourceCode, config);
3276
+ const tagNames = scan.config.sqlTagNames ?? SQL_TAG_NAMES;
3277
+ const nodes = adapter.findNodes(ast, {
3278
+ custom: (node) => isBroadenedDataAccessCandidate(node, scan, tagNames),
3279
+ });
3280
+ const out = [];
3281
+ for (const node of nodes) {
3282
+ const cand = buildDataAccessCallCandidate(node, ast, scan);
3283
+ if (cand)
3284
+ out.push(cand);
3285
+ }
3286
+ return out;
3287
+ }
3288
+ // ── The `loop-query-candidates` extractor ────────────────────────────────────
3289
+ /**
3290
+ * Extract the raw, provenance-free `loop-query-candidates` for one file — the
3291
+ * extract half of the corpus `loop-queries` reduction. Mirrors `collectLoopQueryCandidates`
3292
+ * with the broadened discovery and without the three provenance-dependent steps
3293
+ * (the strict-handle filter, the per-loop dedup, the stable symbol), which the
3294
+ * corpus producer re-folds once `dbProvenanced` is re-derived. The provenance-free
3295
+ * discriminators (statement-construction, sql-string-construction, for-of-iterable,
3296
+ * hoisted-reuse, batch-argument, LLM/queue suppression) already ran here.
3297
+ * @param ast - The file's parsed AST.
3298
+ * @param adapter - The language adapter (drives node discovery + text reads).
3299
+ * @param sourceCode - The file's source text.
3300
+ * @param config - Optional data-access config; defaults to the shared config.
3301
+ * @returns The raw, provenance-free `loop-query-candidates`.
3302
+ */
3303
+ export function extractLoopQueryRawCandidates(ast, adapter, sourceCode, config) {
3304
+ const scan = buildCandidateScan(ast, adapter, sourceCode, config);
3305
+ const candidates = [];
3306
+ const dbNodes = adapter.findNodes(ast, {
3307
+ custom: (node) => isBroadenedLoopCandidate(node, scan),
3308
+ });
3309
+ const transactionWrappedFunctions = collectTransactionWrappedFunctions(ast, adapter, sourceCode);
3310
+ for (const node of dbNodes) {
3311
+ // A DB call and its template-literal SQL argument both satisfy the structural
3312
+ // discovery — collapse to the call node, exactly as `collectLoopQueryCandidates`
3313
+ // does (isDbCallNode only admits a literal whose enclosing call is provenanced,
3314
+ // so skipping the literal loses nothing).
3315
+ if (isTemplateLiteral(node, adapter))
3316
+ continue;
3317
+ const nodeText = adapter.getNodeText(node, sourceCode);
3318
+ if (!nodeText || nodeText.trim().length < 10)
3319
+ continue;
3320
+ if (isStatementConstructionOnly(node, adapter, sourceCode))
3321
+ continue;
3322
+ if (isSqlStringConstruction(node, adapter, sourceCode))
3323
+ continue;
3324
+ const loopInfo = findEnclosingLoop(node, adapter, sourceCode);
3325
+ if (!loopInfo)
3326
+ continue;
3327
+ if (isForOfIterableDbCall(node, loopInfo.loopNode, adapter))
3328
+ continue;
3329
+ const enclosingFnName = findEnclosingFunctionIdentity(loopInfo.loopNode, adapter, ast.filePath).name;
3330
+ const transactionEnclosesLoop = isInsideDbTransaction(loopInfo.loopNode, adapter, sourceCode) ||
3331
+ (enclosingFnName !== null && transactionWrappedFunctions.has(enclosingFnName));
3332
+ const preparesInLoop = loopBodyContainsPrepare(loopInfo.loopNode, adapter, sourceCode);
3333
+ const hoistedReuse = !preparesInLoop &&
3334
+ (transactionEnclosesLoop || isHoistedStatementReRun(node, loopInfo.loopNode, adapter, sourceCode));
3335
+ if (hoistedReuse)
3336
+ continue;
3337
+ if (bindsBatchArgument(node, loopInfo.loopNode, adapter, sourceCode))
3338
+ continue;
3339
+ if (loopBodyContainsLlmCall(loopInfo.loopNode, adapter, sourceCode))
3340
+ continue;
3341
+ if (loopBodyContainsMessageLifecycleCall(loopInfo.loopNode, adapter, sourceCode))
3342
+ continue;
3343
+ const handle = handleCallSiteIdentity(node, scan);
3344
+ const anchor = getCallExpressionCallee(node, adapter) ?? node;
3345
+ candidates.push({
3346
+ file: ast.filePath,
3347
+ line: anchor.location.start.line,
3348
+ column: anchor.location.start.column,
3349
+ enclosingFunction: enclosingIdentity(node, adapter, ast.filePath),
3350
+ loopStartOffset: loopInfo.loopNode.range[0],
3351
+ loopLine: loopInfo.loopNode.location.start.line,
3352
+ depth: loopInfo.depth,
3353
+ handleCalleeType: handle.calleeType,
3354
+ handleName: handle.name,
3355
+ handleRoot: handle.root,
3356
+ handleReceiver: handle.receiver,
3357
+ handleMethod: handle.method,
3358
+ handleThisField: handle.thisField,
3359
+ handleThisHeritage: handle.thisHeritage,
3360
+ handleSiteReceiver: handle.siteReceiver,
3361
+ sqlArg: handle.sqlArg,
3362
+ handleRootScope: handle.scope,
3363
+ });
1747
3364
  }
3365
+ return candidates;
1748
3366
  }
1749
3367
  //# sourceMappingURL=UniversalDataAccessAnalyzer.js.map