code-auditor-mcp 4.0.0 → 4.0.2

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