code-auditor-mcp 3.4.11 → 3.4.13

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 (254) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +119 -1
  3. package/dist/analyzers/__verify_self_probe.d.ts +2 -0
  4. package/dist/analyzers/__verify_self_probe.d.ts.map +1 -0
  5. package/dist/analyzers/__verify_self_probe.js +5 -0
  6. package/dist/analyzers/__verify_self_probe.js.map +1 -0
  7. package/dist/analyzers/analyzerUtils.d.ts +34 -1
  8. package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
  9. package/dist/analyzers/analyzerUtils.js +61 -22
  10. package/dist/analyzers/analyzerUtils.js.map +1 -1
  11. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -23
  12. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  13. package/dist/analyzers/cross-language/APIContractAnalyzer.js +198 -186
  14. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  15. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +78 -55
  16. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  17. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +244 -243
  18. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  19. package/dist/analyzers/cross-language/SchemaValidator.d.ts +27 -37
  20. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  21. package/dist/analyzers/cross-language/SchemaValidator.js +314 -275
  22. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  23. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +17 -46
  24. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  25. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +620 -523
  26. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  27. package/dist/analyzers/documentationAnalyzer.d.ts +7 -2
  28. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  29. package/dist/analyzers/documentationAnalyzer.js +259 -198
  30. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  31. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  32. package/dist/analyzers/invariantsAnalyzer.js +59 -46
  33. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  34. package/dist/analyzers/orm/adapterRegistry.d.ts +10 -0
  35. package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
  36. package/dist/analyzers/orm/adapterRegistry.js +10 -0
  37. package/dist/analyzers/orm/adapterRegistry.js.map +1 -1
  38. package/dist/analyzers/orm/drizzleAdapter.d.ts +22 -0
  39. package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
  40. package/dist/analyzers/orm/drizzleAdapter.js +42 -44
  41. package/dist/analyzers/orm/drizzleAdapter.js.map +1 -1
  42. package/dist/analyzers/orm/prismaAdapter.d.ts +30 -0
  43. package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
  44. package/dist/analyzers/orm/prismaAdapter.js +30 -0
  45. package/dist/analyzers/orm/prismaAdapter.js.map +1 -1
  46. package/dist/analyzers/provenance.d.ts +72 -2
  47. package/dist/analyzers/provenance.d.ts.map +1 -1
  48. package/dist/analyzers/provenance.js +462 -338
  49. package/dist/analyzers/provenance.js.map +1 -1
  50. package/dist/analyzers/reactAnalyzer.d.ts +11 -0
  51. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  52. package/dist/analyzers/reactAnalyzer.js +188 -125
  53. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  54. package/dist/analyzers/ruleRegistry.d.ts +118 -1
  55. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  56. package/dist/analyzers/ruleRegistry.js +1717 -104
  57. package/dist/analyzers/ruleRegistry.js.map +1 -1
  58. package/dist/analyzers/ruleTiming.d.ts +54 -0
  59. package/dist/analyzers/ruleTiming.d.ts.map +1 -0
  60. package/dist/analyzers/ruleTiming.js +71 -0
  61. package/dist/analyzers/ruleTiming.js.map +1 -0
  62. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +13 -0
  63. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  64. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +362 -283
  65. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  66. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +26 -68
  67. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  68. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +430 -391
  69. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  70. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +24 -186
  71. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  72. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1081 -1061
  73. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  74. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -49
  75. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  76. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +414 -340
  77. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  78. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +48 -5
  79. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  80. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +262 -134
  81. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  82. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +23 -212
  83. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  84. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1518
  85. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  86. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +78 -79
  87. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  88. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +613 -472
  89. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  90. package/dist/analyzers/universal/schema/codeAnalysis.d.ts +307 -0
  91. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -0
  92. package/dist/analyzers/universal/schema/codeAnalysis.js +965 -0
  93. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -0
  94. package/dist/analyzers/universal/schema/config.d.ts +31 -0
  95. package/dist/analyzers/universal/schema/config.d.ts.map +1 -0
  96. package/dist/analyzers/universal/schema/config.js +57 -0
  97. package/dist/analyzers/universal/schema/config.js.map +1 -0
  98. package/dist/analyzers/universal/schema/discovery.d.ts +167 -0
  99. package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -0
  100. package/dist/analyzers/universal/schema/discovery.js +616 -0
  101. package/dist/analyzers/universal/schema/discovery.js.map +1 -0
  102. package/dist/analyzers/universal/schema/jsonSchema.d.ts +27 -0
  103. package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -0
  104. package/dist/analyzers/universal/schema/jsonSchema.js +368 -0
  105. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -0
  106. package/dist/analyzers/universal/schema/migrations.d.ts +90 -0
  107. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -0
  108. package/dist/analyzers/universal/schema/migrations.js +238 -0
  109. package/dist/analyzers/universal/schema/migrations.js.map +1 -0
  110. package/dist/analyzers/universal/schema/types.d.ts +131 -0
  111. package/dist/analyzers/universal/schema/types.d.ts.map +1 -0
  112. package/dist/analyzers/universal/schema/types.js +10 -0
  113. package/dist/analyzers/universal/schema/types.js.map +1 -0
  114. package/dist/analyzers/universal/schema/violations.d.ts +44 -0
  115. package/dist/analyzers/universal/schema/violations.d.ts.map +1 -0
  116. package/dist/analyzers/universal/schema/violations.js +54 -0
  117. package/dist/analyzers/universal/schema/violations.js.map +1 -0
  118. package/dist/auditRunner.d.ts.map +1 -1
  119. package/dist/auditRunner.js +101 -39
  120. package/dist/auditRunner.js.map +1 -1
  121. package/dist/cli.js +348 -40
  122. package/dist/cli.js.map +1 -1
  123. package/dist/config/configLoader.d.ts.map +1 -1
  124. package/dist/config/configLoader.js +9 -7
  125. package/dist/config/configLoader.js.map +1 -1
  126. package/dist/config/defaults.d.ts +12 -3
  127. package/dist/config/defaults.d.ts.map +1 -1
  128. package/dist/config/defaults.js +15 -5
  129. package/dist/config/defaults.js.map +1 -1
  130. package/dist/config/effectiveConfig.d.ts +75 -0
  131. package/dist/config/effectiveConfig.d.ts.map +1 -0
  132. package/dist/config/effectiveConfig.js +213 -0
  133. package/dist/config/effectiveConfig.js.map +1 -0
  134. package/dist/config/pathProfiles.d.ts +7 -4
  135. package/dist/config/pathProfiles.d.ts.map +1 -1
  136. package/dist/config/pathProfiles.js +8 -10
  137. package/dist/config/pathProfiles.js.map +1 -1
  138. package/dist/config/thresholdRationales.d.ts +45 -0
  139. package/dist/config/thresholdRationales.d.ts.map +1 -0
  140. package/dist/config/thresholdRationales.js +67 -0
  141. package/dist/config/thresholdRationales.js.map +1 -0
  142. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  143. package/dist/conventions/conventionMiner.js +6 -2
  144. package/dist/conventions/conventionMiner.js.map +1 -1
  145. package/dist/enforcement/diffGate.d.ts +55 -0
  146. package/dist/enforcement/diffGate.d.ts.map +1 -0
  147. package/dist/enforcement/diffGate.js +136 -0
  148. package/dist/enforcement/diffGate.js.map +1 -0
  149. package/dist/enforcement/gate.d.ts +36 -0
  150. package/dist/enforcement/gate.d.ts.map +1 -0
  151. package/dist/enforcement/gate.js +72 -0
  152. package/dist/enforcement/gate.js.map +1 -0
  153. package/dist/enforcement/suppressions.d.ts +41 -0
  154. package/dist/enforcement/suppressions.d.ts.map +1 -0
  155. package/dist/enforcement/suppressions.js +170 -0
  156. package/dist/enforcement/suppressions.js.map +1 -0
  157. package/dist/fingerprint.d.ts.map +1 -1
  158. package/dist/fingerprint.js +4 -1
  159. package/dist/fingerprint.js.map +1 -1
  160. package/dist/grammars/tree-sitter-scss.wasm +0 -0
  161. package/dist/invariants/ruleEngine.d.ts +3 -4
  162. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  163. package/dist/invariants/ruleEngine.js +2 -2
  164. package/dist/invariants/ruleEngine.js.map +1 -1
  165. package/dist/languages/LanguageOrchestrator.d.ts +14 -0
  166. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  167. package/dist/languages/LanguageOrchestrator.js +93 -55
  168. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  169. package/dist/languages/LanguageRegistry.d.ts +8 -0
  170. package/dist/languages/LanguageRegistry.d.ts.map +1 -1
  171. package/dist/languages/LanguageRegistry.js +8 -0
  172. package/dist/languages/LanguageRegistry.js.map +1 -1
  173. package/dist/languages/RuntimeManager.d.ts +82 -60
  174. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  175. package/dist/languages/RuntimeManager.js +404 -349
  176. package/dist/languages/RuntimeManager.js.map +1 -1
  177. package/dist/languages/UniversalAnalyzer.d.ts +51 -6
  178. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  179. package/dist/languages/UniversalAnalyzer.js +127 -88
  180. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  181. package/dist/languages/adapterBridge.d.ts +36 -0
  182. package/dist/languages/adapterBridge.d.ts.map +1 -1
  183. package/dist/languages/adapterBridge.js +89 -35
  184. package/dist/languages/adapterBridge.js.map +1 -1
  185. package/dist/languages/go/GoAdapter.d.ts +66 -23
  186. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  187. package/dist/languages/go/GoAdapter.js +553 -474
  188. package/dist/languages/go/GoAdapter.js.map +1 -1
  189. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +38 -9
  190. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
  191. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +111 -74
  192. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
  193. package/dist/languages/tree-sitter/converter.d.ts +18 -0
  194. package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
  195. package/dist/languages/tree-sitter/converter.js +18 -0
  196. package/dist/languages/tree-sitter/converter.js.map +1 -1
  197. package/dist/languages/tree-sitter/parser.d.ts +39 -2
  198. package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
  199. package/dist/languages/tree-sitter/parser.js +95 -12
  200. package/dist/languages/tree-sitter/parser.js.map +1 -1
  201. package/dist/languages/types.d.ts +67 -2
  202. package/dist/languages/types.d.ts.map +1 -1
  203. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +253 -62
  204. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  205. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1461 -791
  206. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  207. package/dist/mcp-tools/whitelistTools.d.ts +1 -1
  208. package/dist/mcp-tools-shared.d.ts.map +1 -1
  209. package/dist/mcp-tools-shared.js +1 -0
  210. package/dist/mcp-tools-shared.js.map +1 -1
  211. package/dist/mcpAuditJobs.d.ts.map +1 -1
  212. package/dist/mcpAuditJobs.js +1 -0
  213. package/dist/mcpAuditJobs.js.map +1 -1
  214. package/dist/pipeline.d.ts +30 -1
  215. package/dist/pipeline.d.ts.map +1 -1
  216. package/dist/pipeline.js +527 -203
  217. package/dist/pipeline.js.map +1 -1
  218. package/dist/pipelineAdapters.d.ts +5 -12
  219. package/dist/pipelineAdapters.d.ts.map +1 -1
  220. package/dist/pipelineAdapters.js +175 -100
  221. package/dist/pipelineAdapters.js.map +1 -1
  222. package/dist/presets/presets.d.ts +54 -0
  223. package/dist/presets/presets.d.ts.map +1 -0
  224. package/dist/presets/presets.js +181 -0
  225. package/dist/presets/presets.js.map +1 -0
  226. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  227. package/dist/reporting/sarifReportGenerator.js +0 -2
  228. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  229. package/dist/ruleAliases.d.ts +67 -0
  230. package/dist/ruleAliases.d.ts.map +1 -0
  231. package/dist/ruleAliases.js +79 -0
  232. package/dist/ruleAliases.js.map +1 -0
  233. package/dist/styles/cssAstExtractor.d.ts +20 -5
  234. package/dist/styles/cssAstExtractor.d.ts.map +1 -1
  235. package/dist/styles/cssAstExtractor.js +247 -9
  236. package/dist/styles/cssAstExtractor.js.map +1 -1
  237. package/dist/styles/styleIndexer.js +13 -4
  238. package/dist/styles/styleIndexer.js.map +1 -1
  239. package/dist/styles/tailwindConfigLoader.d.ts +8 -6
  240. package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
  241. package/dist/styles/tailwindConfigLoader.js +61 -15
  242. package/dist/styles/tailwindConfigLoader.js.map +1 -1
  243. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  244. package/dist/styles/tailwindUtilityExpander.js +13 -1
  245. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  246. package/dist/types.d.ts +201 -1
  247. package/dist/types.d.ts.map +1 -1
  248. package/dist/types.js +8 -0
  249. package/dist/types.js.map +1 -1
  250. package/package.json +7 -4
  251. package/plugin/.claude-plugin/plugin.json +1 -1
  252. package/plugin/hooks/hooks.json +1 -1
  253. package/plugin/scripts/hook-audit.sh +25 -7
  254. package/plugin/skills/code-auditor/SKILL.md +5 -5
@@ -65,7 +65,26 @@ export const DB_TYPES = new Set([
65
65
  'SqliteDatabase',
66
66
  'BetterSQLite3Database',
67
67
  ]);
68
- /** DB call methods — the fixed API surface (language-invariant) */
68
+ /**
69
+ * DB call methods — the fixed API surface (language-invariant).
70
+ *
71
+ * Spec 33 Item 11 FP category 5: the bare-identifier hybrid fallback in
72
+ * `isDBProvenanced` treated any `get(...)` / `each(...)` / `values(...)` call
73
+ * as a DB query, flagging lodash-style object accessors (e.g.
74
+ * `@directus/utils`'s `get(item, ...)`) as sql-injection. Those three names
75
+ * are also common non-DB methods (lodash `get`, jQuery/iterator
76
+ * `each`, Map/WebSocket `.values()`), so they are removed from the fallback —
77
+ * mirroring the `get`/`each` trim in CHANGELOG 3.4.9 (DB_CALL_METHOD_NAMES).
78
+ *
79
+ * `query` is deliberately RETAINED: it is a genuine query-execution method on
80
+ * mysql2, pg, node-postgres, D1 and Planetscale (`.query(...)`), and the
81
+ * spec-19 data-access fixtures exercise it as a canonical DB entry point.
82
+ * Removing it would turn real SQL-injection positives into false negatives.
83
+ *
84
+ * `raw` is deliberately retained: it is a genuine raw-execution method on
85
+ * D1 prepared statements, Knex, and Kysely, and the Item-6 taint-tracking
86
+ * fixtures exercise it as the canonical raw-SQL entry point.
87
+ */
69
88
  export const DB_CALL_METHODS = new Set([
70
89
  'exec',
71
90
  'prepare',
@@ -74,10 +93,7 @@ export const DB_CALL_METHODS = new Set([
74
93
  'all',
75
94
  'first',
76
95
  'query',
77
- 'get',
78
- 'each',
79
96
  'raw',
80
- 'values',
81
97
  ]);
82
98
  /** ORM method patterns — fixed API surface for ORM recognition (Spec 21 R1) */
83
99
  export const ORM_METHODS = new Set([
@@ -128,6 +144,9 @@ function matchesValidatorPackage(specifier) {
128
144
  *
129
145
  * An import like `import Database from 'better-sqlite3'` produces
130
146
  * `Database` as DB-provenanced with reason "package".
147
+ * @param adapter
148
+ * @param ast
149
+ * @returns
131
150
  */
132
151
  export function extractDBProvenancedImports(ast, adapter) {
133
152
  const seedMap = new Map();
@@ -159,6 +178,9 @@ export function extractDBProvenancedImports(ast, adapter) {
159
178
  /**
160
179
  * Extract all validator-provenanced identifiers from a file's imports.
161
180
  * Same pattern as extractDBProvenancedImports but for validator packages.
181
+ * @param adapter
182
+ * @param ast
183
+ * @returns
162
184
  */
163
185
  export function extractValidatorProvenancedImports(ast, adapter) {
164
186
  const seedMap = new Map();
@@ -189,6 +211,133 @@ export function extractValidatorProvenancedImports(ast, adapter) {
189
211
  // ═══════════════════════════════════════════════════════════════════════════
190
212
  // Provenance propagation (R1 core)
191
213
  // ═══════════════════════════════════════════════════════════════════════════
214
+ /**
215
+ * Propagate provenance through a variable declaration (rules 1-3, 8).
216
+ * Returns true if any new identifier was added.
217
+ */
218
+ function propagateVariableDeclaration(node, ctx) {
219
+ const { nameNode, valueNode, typeAnnotationNode } = splitVariableDeclarator(node, ctx.adapter);
220
+ if (!nameNode)
221
+ return false;
222
+ let mutated = false;
223
+ // Rule 8: type annotation — let x: D1Database
224
+ if (typeAnnotationNode) {
225
+ mutated = propagateFromTypeAnnotation(nameNode, typeAnnotationNode, ctx);
226
+ }
227
+ if (valueNode) {
228
+ mutated = propagateFromValue(nameNode, valueNode, ctx) || mutated;
229
+ }
230
+ return mutated;
231
+ }
232
+ /** Rule 8: type annotation — `let x: D1Database`. */
233
+ function propagateFromTypeAnnotation(nameNode, typeAnnotationNode, ctx) {
234
+ if (nameNode.type !== 'identifier')
235
+ return false;
236
+ const typeText = ctx.adapter.getNodeText(typeAnnotationNode, ctx.sourceCode).trim();
237
+ if (!DB_TYPES.has(typeText))
238
+ return false;
239
+ const name = ctx.adapter.getNodeText(nameNode, ctx.sourceCode);
240
+ if (ctx.provenanceMap.has(name))
241
+ return false;
242
+ ctx.provenanceMap.set(name, {
243
+ identifier: name,
244
+ reason: 'type',
245
+ source: `type annotation ${typeText}`,
246
+ chain: [],
247
+ });
248
+ return true;
249
+ }
250
+ /** Propagate provenance from a declarator's value expression into its names. */
251
+ function propagateFromValue(nameNode, valueNode, ctx) {
252
+ const propagated = tryPropagateFromExpression(valueNode, ctx.adapter, ctx.sourceCode, ctx.provenanceMap);
253
+ if (!propagated)
254
+ return false;
255
+ let mutated = false;
256
+ for (const varName of extractPatternNames(nameNode, ctx.adapter, ctx.sourceCode)) {
257
+ if (!ctx.provenanceMap.has(varName)) {
258
+ ctx.provenanceMap.set(varName, {
259
+ identifier: varName,
260
+ reason: 'propagation',
261
+ source: propagated.source,
262
+ chain: [...propagated.chain, propagated.identifier],
263
+ });
264
+ mutated = true;
265
+ }
266
+ }
267
+ return mutated;
268
+ }
269
+ /**
270
+ * Propagate provenance through a default parameter (rule 6).
271
+ * Returns true if a new identifier was added.
272
+ */
273
+ function propagateDefaultParameter(node, adapter, sourceCode, provenanceMap) {
274
+ const children = adapter.getChildren(node);
275
+ // assignment_pattern has [left, right]
276
+ if (children.length < 2)
277
+ return false;
278
+ const leftNode = children[0];
279
+ const rightNode = children[1];
280
+ if (leftNode.type !== 'identifier')
281
+ return false;
282
+ const paramName = adapter.getNodeText(leftNode, sourceCode);
283
+ const propagated = tryPropagateFromExpression(rightNode, adapter, sourceCode, provenanceMap);
284
+ if (propagated && !provenanceMap.has(paramName)) {
285
+ provenanceMap.set(paramName, {
286
+ identifier: paramName,
287
+ reason: 'propagation',
288
+ source: `default parameter = ${propagated.source}`,
289
+ chain: [...propagated.chain, propagated.identifier],
290
+ });
291
+ return true;
292
+ }
293
+ return false;
294
+ }
295
+ /**
296
+ * Propagate provenance through a class field initialization (rule 7).
297
+ * Returns true if a new identifier was added.
298
+ */
299
+ function propagateClassField(node, adapter, sourceCode, provenanceMap) {
300
+ const children = adapter.getChildren(node);
301
+ // Typically [name, value] or [decorators..., name, value]
302
+ const nameChild = children.find((c) => c.type === 'property_identifier');
303
+ const valueChild = children.find((c) => c.type !== 'property_identifier' &&
304
+ c.type !== 'decorator' &&
305
+ c.type !== 'private' &&
306
+ c.type !== 'public' &&
307
+ c.type !== 'protected' &&
308
+ c.type !== 'static' &&
309
+ c.type !== 'readonly' &&
310
+ c.type !== 'abstract');
311
+ if (nameChild && valueChild) {
312
+ const fieldName = adapter.getNodeText(nameChild, sourceCode);
313
+ const propagated = tryPropagateFromExpression(valueChild, adapter, sourceCode, provenanceMap);
314
+ if (propagated && !provenanceMap.has(fieldName)) {
315
+ provenanceMap.set(fieldName, {
316
+ identifier: fieldName,
317
+ reason: 'propagation',
318
+ source: `class field initialized from ${propagated.source}`,
319
+ chain: [...propagated.chain, propagated.identifier],
320
+ });
321
+ return true;
322
+ }
323
+ }
324
+ return false;
325
+ }
326
+ function applyPropagationRule(node, parent, ctx) {
327
+ const { adapter, sourceCode, provenanceMap } = ctx;
328
+ if (node.type === 'variable_declarator') {
329
+ return propagateVariableDeclaration(node, ctx);
330
+ }
331
+ if (node.type === 'assignment_pattern' &&
332
+ parent?.type === 'formal_parameters') {
333
+ return propagateDefaultParameter(node, adapter, sourceCode, provenanceMap);
334
+ }
335
+ if (node.type === 'public_field_definition' ||
336
+ node.type === 'field_definition') {
337
+ return propagateClassField(node, adapter, sourceCode, provenanceMap);
338
+ }
339
+ return false;
340
+ }
192
341
  /**
193
342
  * Propagate provenance through assignments, destructuring, parameters,
194
343
  * class fields, and type annotations within a single file.
@@ -202,10 +351,16 @@ export function extractValidatorProvenancedImports(ast, adapter) {
202
351
  * 6. default parameter with DB value
203
352
  * 7. class field initialized with DB value
204
353
  * 8. type annotation with known DB type
354
+ * @param adapter
355
+ * @param ast
356
+ * @param seedMap
357
+ * @param sourceCode
358
+ * @returns
205
359
  */
206
360
  export function propagateProvenance(ast, adapter, sourceCode, seedMap) {
207
361
  // Work on a copy so we can add newly-provenanced identifiers during the walk
208
362
  const provenanceMap = new Map(seedMap);
363
+ const ctx = { adapter, sourceCode, provenanceMap };
209
364
  // Keep iterating until no new identifiers are discovered (handles chains)
210
365
  let changed = true;
211
366
  let iterations = 0;
@@ -214,100 +369,8 @@ export function propagateProvenance(ast, adapter, sourceCode, seedMap) {
214
369
  changed = false;
215
370
  iterations++;
216
371
  walkAST(ast.root, (node, parent) => {
217
- // ── Rule 1 & 2 & 3 & 8: variable declarations ──
218
- if (node.type === 'variable_declarator') {
219
- const { nameNode, valueNode, typeAnnotationNode } = splitVariableDeclarator(node, adapter);
220
- if (!nameNode)
221
- return;
222
- // Rule 8: type annotation — let x: D1Database
223
- if (typeAnnotationNode && nameNode.type === 'identifier') {
224
- const typeText = adapter.getNodeText(typeAnnotationNode, sourceCode).trim();
225
- if (DB_TYPES.has(typeText)) {
226
- const name = adapter.getNodeText(nameNode, sourceCode);
227
- if (!provenanceMap.has(name)) {
228
- provenanceMap.set(name, {
229
- identifier: name,
230
- reason: 'type',
231
- source: `type annotation ${typeText}`,
232
- chain: [],
233
- });
234
- changed = true;
235
- }
236
- // Type-provenanced names count as DB-provenanced for further propagation
237
- }
238
- }
239
- if (valueNode) {
240
- const propagated = tryPropagateFromExpression(valueNode, adapter, sourceCode, provenanceMap);
241
- if (propagated) {
242
- // Extract the variable name(s) from the name node
243
- const varNames = extractPatternNames(nameNode, adapter, sourceCode);
244
- for (const varName of varNames) {
245
- if (!provenanceMap.has(varName)) {
246
- provenanceMap.set(varName, {
247
- identifier: varName,
248
- reason: 'propagation',
249
- source: propagated.source,
250
- chain: [...propagated.chain, propagated.identifier],
251
- });
252
- changed = true;
253
- }
254
- }
255
- }
256
- }
257
- return;
258
- }
259
- // ── Rule 6: default parameters ──
260
- if (node.type === 'assignment_pattern' &&
261
- parent?.type === 'formal_parameters') {
262
- const children = adapter.getChildren(node);
263
- // assignment_pattern has [left, right]
264
- if (children.length >= 2) {
265
- const leftNode = children[0];
266
- const rightNode = children[1];
267
- if (leftNode.type === 'identifier') {
268
- const paramName = adapter.getNodeText(leftNode, sourceCode);
269
- const propagated = tryPropagateFromExpression(rightNode, adapter, sourceCode, provenanceMap);
270
- if (propagated && !provenanceMap.has(paramName)) {
271
- provenanceMap.set(paramName, {
272
- identifier: paramName,
273
- reason: 'propagation',
274
- source: `default parameter = ${propagated.source}`,
275
- chain: [...propagated.chain, propagated.identifier],
276
- });
277
- changed = true;
278
- }
279
- }
280
- }
281
- return;
282
- }
283
- // ── Rule 7: class field initialization ──
284
- if (node.type === 'public_field_definition' ||
285
- node.type === 'field_definition') {
286
- const children = adapter.getChildren(node);
287
- // Typically [name, value] or [decorators..., name, value]
288
- const nameChild = children.find((c) => c.type === 'property_identifier');
289
- const valueChild = children.find((c) => c.type !== 'property_identifier' &&
290
- c.type !== 'decorator' &&
291
- c.type !== 'private' &&
292
- c.type !== 'public' &&
293
- c.type !== 'protected' &&
294
- c.type !== 'static' &&
295
- c.type !== 'readonly' &&
296
- c.type !== 'abstract');
297
- if (nameChild && valueChild) {
298
- const fieldName = adapter.getNodeText(nameChild, sourceCode);
299
- const propagated = tryPropagateFromExpression(valueChild, adapter, sourceCode, provenanceMap);
300
- if (propagated && !provenanceMap.has(fieldName)) {
301
- provenanceMap.set(fieldName, {
302
- identifier: fieldName,
303
- reason: 'propagation',
304
- source: `class field initialized from ${propagated.source}`,
305
- chain: [...propagated.chain, propagated.identifier],
306
- });
307
- changed = true;
308
- }
309
- }
310
- return;
372
+ if (applyPropagationRule(node, parent, ctx)) {
373
+ changed = true;
311
374
  }
312
375
  });
313
376
  }
@@ -337,24 +400,9 @@ function tryPropagateFromExpression(node, adapter, sourceCode, provenanceMap) {
337
400
  // ── Rule 2: drizzle(env.DB) — call where callee is DB-provenanced ──
338
401
  // ── Rule 3: db.prepare(sql) — member expression call on DB receiver ──
339
402
  if (node.type === 'call_expression') {
340
- // Walk children before arguments to get the callee
341
- const calleeNode = getCallExpressionCallee(node, adapter);
342
- if (calleeNode) {
343
- // Case: simple identifier call — drizzle(...)
344
- if (calleeNode.type === 'identifier') {
345
- const name = adapter.getNodeText(calleeNode, sourceCode);
346
- if (name && provenanceMap.has(name)) {
347
- return provenanceMap.get(name);
348
- }
349
- }
350
- // Case: member expression — db.prepare(...)
351
- if (calleeNode.type === 'member_expression') {
352
- const receiver = getMemberExpressionReceiver(calleeNode, adapter, sourceCode);
353
- if (receiver && provenanceMap.has(receiver)) {
354
- return provenanceMap.get(receiver);
355
- }
356
- }
357
- }
403
+ const viaCall = tryCallProvenance(node, adapter, sourceCode, provenanceMap);
404
+ if (viaCall)
405
+ return viaCall;
358
406
  }
359
407
  // ── Simple identifier reference (for destructuring sources) ──
360
408
  if (node.type === 'identifier') {
@@ -372,6 +420,31 @@ function tryPropagateFromExpression(node, adapter, sourceCode, provenanceMap) {
372
420
  }
373
421
  return null;
374
422
  }
423
+ /**
424
+ * Rule 2/3: check a call_expression whose callee is either a DB-provenanced
425
+ * identifier (e.g. `drizzle(...)`) or a member expression on a DB receiver
426
+ * (e.g. `db.prepare(sql)`).
427
+ */
428
+ function tryCallProvenance(node, adapter, sourceCode, provenanceMap) {
429
+ const calleeNode = getCallExpressionCallee(node, adapter);
430
+ if (!calleeNode)
431
+ return null;
432
+ // Case: simple identifier call — drizzle(...)
433
+ if (calleeNode.type === 'identifier') {
434
+ const name = adapter.getNodeText(calleeNode, sourceCode);
435
+ if (name && provenanceMap.has(name)) {
436
+ return provenanceMap.get(name);
437
+ }
438
+ }
439
+ // Case: member expression — db.prepare(...)
440
+ if (calleeNode.type === 'member_expression') {
441
+ const receiver = getMemberExpressionReceiver(calleeNode, adapter, sourceCode);
442
+ if (receiver && provenanceMap.has(receiver)) {
443
+ return provenanceMap.get(receiver);
444
+ }
445
+ }
446
+ return null;
447
+ }
375
448
  /**
376
449
  * Split a variable_declarator into its name, value, and type annotation
377
450
  * child nodes.
@@ -425,59 +498,57 @@ function splitVariableDeclarator(node, adapter) {
425
498
  */
426
499
  function extractPatternNames(node, adapter, sourceCode) {
427
500
  const names = [];
428
- function collect(node) {
429
- if (node.type === 'identifier') {
430
- const name = adapter.getNodeText(node, sourceCode);
431
- if (name)
432
- names.push(name);
433
- }
434
- else if (node.type === 'object_pattern') {
435
- // Each child is a shorthand_property_identifier, pair_pattern, or rest_pattern
436
- for (const child of adapter.getChildren(node)) {
437
- if (child.type === '{' ||
438
- child.type === '}' ||
439
- child.type === ',') {
440
- continue;
441
- }
442
- if (child.type === 'shorthand_property_identifier') {
443
- const name = adapter.getNodeText(child, sourceCode);
444
- if (name)
445
- names.push(name);
446
- }
447
- else if (child.type === 'pair_pattern') {
448
- // pair_pattern children: [property, value]
449
- // Extract from the value side (which might be an identifier or nested pattern)
450
- const pairChildren = adapter.getChildren(child);
451
- if (pairChildren.length >= 2) {
452
- collect(pairChildren[1]);
453
- }
454
- }
455
- else if (child.type === 'rest_pattern') {
456
- collectChildren(child, adapter, sourceCode, names);
457
- }
458
- else {
459
- collect(child);
460
- }
501
+ collectPatternNames(node, adapter, sourceCode, names);
502
+ return names;
503
+ }
504
+ /** Recursively collect identifier names from a destructuring pattern node. */
505
+ function collectPatternNames(node, adapter, sourceCode, names) {
506
+ if (node.type === 'identifier') {
507
+ const name = adapter.getNodeText(node, sourceCode);
508
+ if (name)
509
+ names.push(name);
510
+ return;
511
+ }
512
+ if (node.type === 'object_pattern') {
513
+ for (const child of adapter.getChildren(node)) {
514
+ if (child.type === '{' || child.type === '}' || child.type === ',')
515
+ continue;
516
+ if (child.type === 'shorthand_property_identifier') {
517
+ const name = adapter.getNodeText(child, sourceCode);
518
+ if (name)
519
+ names.push(name);
461
520
  }
462
- }
463
- else if (node.type === 'array_pattern') {
464
- for (const child of adapter.getChildren(node)) {
465
- if (child.type === '[' || child.type === ']' || child.type === ',') {
466
- continue;
521
+ else if (child.type === 'pair_pattern') {
522
+ // pair_pattern children: [property, value] — collect from value side
523
+ const pairChildren = adapter.getChildren(child);
524
+ if (pairChildren.length >= 2) {
525
+ collectPatternNames(pairChildren[1], adapter, sourceCode, names);
467
526
  }
468
- collect(child);
469
527
  }
470
- }
471
- else if (node.type === 'assignment_pattern') {
472
- // Default value in destructuring — collect from the left side
473
- const children = adapter.getChildren(node);
474
- if (children.length >= 1) {
475
- collect(children[0]);
528
+ else if (child.type === 'rest_pattern') {
529
+ collectChildren(child, adapter, sourceCode, names);
530
+ }
531
+ else {
532
+ collectPatternNames(child, adapter, sourceCode, names);
476
533
  }
477
534
  }
535
+ return;
536
+ }
537
+ if (node.type === 'array_pattern') {
538
+ for (const child of adapter.getChildren(node)) {
539
+ if (child.type === '[' || child.type === ']' || child.type === ',')
540
+ continue;
541
+ collectPatternNames(child, adapter, sourceCode, names);
542
+ }
543
+ return;
544
+ }
545
+ // assignment_pattern — default value in destructuring, collect from left side
546
+ if (node.type === 'assignment_pattern') {
547
+ const children = adapter.getChildren(node);
548
+ if (children.length >= 1) {
549
+ collectPatternNames(children[0], adapter, sourceCode, names);
550
+ }
478
551
  }
479
- collect(node);
480
- return names;
481
552
  }
482
553
  function collectChildren(node, adapter, sourceCode, names) {
483
554
  for (const child of adapter.getChildren(node)) {
@@ -510,8 +581,10 @@ function getMemberExpressionReceiver(node, adapter, sourceCode) {
510
581
  firstChild.type !== 'property_identifier' && firstChild.type !== 'field_identifier') {
511
582
  if (firstChild.type === 'member_expression' ||
512
583
  firstChild.type === 'selector_expression') {
513
- current = firstChild;
514
- continue;
584
+ // Compound receiver: env.DB.prepare → receiver is "env.DB", not "env".
585
+ // Return the full text of the inner member expression so fallback
586
+ // entries like dbBindingNames: ['env.DB'] match the provenance check.
587
+ return adapter.getNodeText(firstChild, sourceCode);
515
588
  }
516
589
  if (firstChild.type === 'identifier') {
517
590
  return adapter.getNodeText(firstChild, sourceCode);
@@ -592,6 +665,11 @@ function walkAST(root, visitor) {
592
665
  *
593
666
  * This is the main entry point — call once per file before analysis.
594
667
  * Combines import extraction, propagation, and mode-based fallback.
668
+ * @param adapter
669
+ * @param ast
670
+ * @param options
671
+ * @param sourceCode
672
+ * @returns
595
673
  */
596
674
  export function buildProvenanceContext(ast, adapter, sourceCode, options) {
597
675
  const mode = options.mode;
@@ -608,11 +686,15 @@ export function buildProvenanceContext(ast, adapter, sourceCode, options) {
608
686
  // 3. Fallback: in hybrid mode, add identifiers that match name lists
609
687
  // but weren't caught by provenance (R3)
610
688
  if (mode === 'hybrid') {
611
- dbProvenanced = addNameListFallbacks(dbProvenanced, sourceCode, options.dbReceiverNames ?? [], options.dbBindingNames ?? []);
689
+ dbProvenanced = addNameListFallbacks(dbProvenanced, sourceCode, {
690
+ dbReceiverNames: options.dbReceiverNames ?? [],
691
+ dbBindingNames: options.dbBindingNames ?? [],
692
+ dbWrapperNames: options.dbWrapperNames ?? [],
693
+ });
612
694
  }
613
695
  // 4. In names mode, use ONLY name lists
614
696
  if (mode === 'names') {
615
- dbProvenanced = buildNamesOnlyProvenance(sourceCode, options.dbReceiverNames ?? [], options.dbBindingNames ?? []);
697
+ dbProvenanced = buildNamesOnlyProvenance(sourceCode, options.dbReceiverNames ?? [], options.dbBindingNames ?? [], options.dbWrapperNames ?? []);
616
698
  }
617
699
  return {
618
700
  dbProvenanced,
@@ -620,61 +702,63 @@ export function buildProvenanceContext(ast, adapter, sourceCode, options) {
620
702
  mode,
621
703
  };
622
704
  }
623
- /**
624
- * Add fallback provenance entries for identifiers that match name lists
625
- * but weren't caught by the provenance chain (R3 hybrid mode).
626
- *
627
- * These entries carry `reason: 'fallback'` — visible in config detection
628
- * so users can audit and tighten their chains.
629
- */
630
- function addNameListFallbacks(provenanceMap, sourceCode, dbReceiverNames, dbBindingNames) {
705
+ function addNameListFallbacks(provenanceMap, sourceCode, nameLists) {
706
+ const { dbReceiverNames, dbBindingNames, dbWrapperNames } = nameLists;
631
707
  const result = new Map(provenanceMap);
632
- // Scan for DB receiver names used as identifiers
633
- for (const name of dbReceiverNames) {
708
+ addIdentifierFallbacks(result, sourceCode, dbReceiverNames, 'dbReceiverNames');
709
+ addBindingFallbacks(result, sourceCode, dbBindingNames);
710
+ addIdentifierFallbacks(result, sourceCode, dbWrapperNames, 'dbWrapperNames');
711
+ return result;
712
+ }
713
+ /** Add fallback entries for identifiers that appear in source but lack provenance. */
714
+ function addIdentifierFallbacks(result, sourceCode, names, label) {
715
+ for (const name of names) {
634
716
  if (result.has(name))
635
717
  continue; // already provenanced — provenance wins
636
- // Check if this name appears as an identifier in the source
637
718
  if (identifierAppearsInSource(sourceCode, name)) {
638
719
  result.set(name, {
639
720
  identifier: name,
640
721
  reason: 'fallback',
641
- source: `name list match: dbReceiverNames contains "${name}"`,
722
+ source: `name list match: ${label} contains "${name}"`,
642
723
  chain: [],
643
724
  });
644
725
  }
645
726
  }
646
- // Scan for binding names like env.DB
647
- for (const binding of dbBindingNames) {
648
- if (sourceCode.includes(binding)) {
649
- // Extract the property after the dot, e.g. "DB" from "env.DB"
650
- const dotIdx = binding.lastIndexOf('.');
651
- const shortName = dotIdx >= 0 ? binding.substring(dotIdx + 1) : binding;
652
- // Also add the full binding path
653
- if (!result.has(binding)) {
654
- result.set(binding, {
655
- identifier: binding,
656
- reason: 'fallback',
657
- source: `name list match: dbBindingNames contains "${binding}"`,
658
- chain: [],
659
- });
660
- }
661
- if (shortName !== binding && !result.has(shortName)) {
662
- result.set(shortName, {
663
- identifier: shortName,
664
- reason: 'fallback',
665
- source: `from binding ${binding}`,
666
- chain: [],
667
- });
668
- }
727
+ }
728
+ /** Add fallback entries for binding names like `env.DB` (plus their short forms). */
729
+ function addBindingFallbacks(result, sourceCode, bindings) {
730
+ for (const binding of bindings) {
731
+ if (!sourceCode.includes(binding))
732
+ continue;
733
+ const dotIdx = binding.lastIndexOf('.');
734
+ const shortName = dotIdx >= 0 ? binding.substring(dotIdx + 1) : binding;
735
+ if (!result.has(binding)) {
736
+ result.set(binding, {
737
+ identifier: binding,
738
+ reason: 'fallback',
739
+ source: `name list match: dbBindingNames contains "${binding}"`,
740
+ chain: [],
741
+ });
742
+ }
743
+ if (shortName !== binding && !result.has(shortName)) {
744
+ result.set(shortName, {
745
+ identifier: shortName,
746
+ reason: 'fallback',
747
+ source: `from binding ${binding}`,
748
+ chain: [],
749
+ });
669
750
  }
670
751
  }
671
- return result;
672
752
  }
673
753
  /**
674
754
  * Build a provenance map using ONLY name lists (for names mode).
675
755
  */
676
- function buildNamesOnlyProvenance(sourceCode, dbReceiverNames, dbBindingNames) {
677
- return addNameListFallbacks(new Map(), sourceCode, dbReceiverNames, dbBindingNames);
756
+ function buildNamesOnlyProvenance(sourceCode, dbReceiverNames, dbBindingNames, dbWrapperNames) {
757
+ return addNameListFallbacks(new Map(), sourceCode, {
758
+ dbReceiverNames,
759
+ dbBindingNames,
760
+ dbWrapperNames,
761
+ });
678
762
  }
679
763
  /** Check if an identifier name appears as a standalone identifier in source. */
680
764
  function identifierAppearsInSource(sourceCode, name) {
@@ -685,9 +769,6 @@ function identifierAppearsInSource(sourceCode, name) {
685
769
  function escapeRegex(s) {
686
770
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
687
771
  }
688
- // ═══════════════════════════════════════════════════════════════════════════
689
- // Core detection — is this call DB-provenanced?
690
- // ═══════════════════════════════════════════════════════════════════════════
691
772
  /**
692
773
  * Determine if a call-expression node's callee is DB-provenanced.
693
774
  *
@@ -699,11 +780,15 @@ function escapeRegex(s) {
699
780
  * 2. Member expression call → is the receiver DB-provenanced AND is the
700
781
  * method in the DB call method set?
701
782
  * 3. ORM patterns → receiver is DB-provenanced and method matches ORM API
783
+ *
784
+ * @param node the call-expression node whose callee is under test
785
+ * @param query bundled adapter, source, provenance context, and DB methods
786
+ * @returns true if the call's callee resolves to a DB-provenanced target
702
787
  */
703
- export function isDBProvenanced(node, adapter, sourceCode, context, dbCallMethods) {
788
+ export function isDBProvenanced(node, query) {
704
789
  if (node.type !== 'call_expression')
705
790
  return false;
706
- const methods = dbCallMethods ?? DB_CALL_METHODS;
791
+ const { adapter, sourceCode, context, methods } = query;
707
792
  const calleeNode = getCallExpressionCallee(node, adapter);
708
793
  if (!calleeNode)
709
794
  return false;
@@ -725,7 +810,7 @@ export function isDBProvenanced(node, adapter, sourceCode, context, dbCallMethod
725
810
  }
726
811
  // Case 2: Member expression — e.g. db.prepare(...)
727
812
  if (calleeNode.type === 'member_expression' || calleeNode.type === 'selector_expression') {
728
- return isMemberExpressionDBProvenanced(calleeNode, adapter, sourceCode, context, methods);
813
+ return isMemberExpressionDBProvenanced(calleeNode, query);
729
814
  }
730
815
  return false;
731
816
  }
@@ -736,9 +821,42 @@ export function isDBProvenanced(node, adapter, sourceCode, context, dbCallMethod
736
821
  * checks if it's in the provenance context, and verifies
737
822
  * the method matches the DB call/ORM API.
738
823
  */
739
- function isMemberExpressionDBProvenanced(node, adapter, sourceCode, context, methods) {
740
- // Walk the member expression chain to find the root
741
- let rootReceiver = null;
824
+ function isMemberExpressionDBProvenanced(node, query) {
825
+ const { adapter, sourceCode, context } = query;
826
+ const rootReceiver = findRootReceiver(node, adapter, sourceCode);
827
+ if (!rootReceiver)
828
+ return false;
829
+ // In names mode, check the method name directly
830
+ if (context.mode === 'names') {
831
+ return isDBMethodCall(node, query);
832
+ }
833
+ // Check if the root receiver is DB-provenanced.
834
+ // Compound receivers (e.g. "db.users" or "env.DB") need to match both
835
+ // the full text (for bindings like "env.DB") and each sub-identifier
836
+ // (for receivers like "db.users" where "db" is in dbReceiverNames).
837
+ const matchesProvenance = (r) => {
838
+ if (context.dbProvenanced.has(r))
839
+ return true;
840
+ for (const part of r.split('.')) {
841
+ if (context.dbProvenanced.has(part))
842
+ return true;
843
+ }
844
+ return false;
845
+ };
846
+ if (!matchesProvenance(rootReceiver)) {
847
+ if (rootReceiver !== 'this')
848
+ return false;
849
+ // For `this.xxx`, check if the method chain itself suggests DB usage
850
+ return isDBMethodOnThis(node, query);
851
+ }
852
+ // Check that the method is in the DB call/ORM API
853
+ return isDBMethodCall(node, query);
854
+ }
855
+ /**
856
+ * Walk a member/selector expression chain to its root receiver text.
857
+ * Returns `'this'` for `this.xxx` chains and `null` when no root is found.
858
+ */
859
+ function findRootReceiver(node, adapter, sourceCode) {
742
860
  let current = node;
743
861
  while (current.type === 'member_expression' || current.type === 'selector_expression') {
744
862
  const children = adapter.getChildren(current);
@@ -746,41 +864,28 @@ function isMemberExpressionDBProvenanced(node, adapter, sourceCode, context, met
746
864
  if (!firstChild)
747
865
  break;
748
866
  if (firstChild.type === 'identifier') {
749
- rootReceiver = adapter.getNodeText(firstChild, sourceCode);
750
- break;
867
+ return adapter.getNodeText(firstChild, sourceCode);
751
868
  }
752
869
  if (firstChild.type === 'member_expression' || firstChild.type === 'selector_expression') {
753
- current = firstChild;
754
- continue;
870
+ // Compound receiver: env.DB.prepare → root is "env.DB", not "env".
871
+ // Returning the full text of the inner member expression ensures
872
+ // fallback entries match (e.g. dbBindingNames: ['env.DB']).
873
+ return adapter.getNodeText(firstChild, sourceCode);
755
874
  }
756
875
  // this.db.prepare → root is a chain on `this`, check `this.xxx`
757
876
  if (firstChild.type === 'this' || firstChild.type === 'super') {
758
- rootReceiver = 'this';
759
- break;
877
+ return 'this';
760
878
  }
761
879
  break;
762
880
  }
763
- if (!rootReceiver)
764
- return false;
765
- // In names mode, check the method name directly
766
- if (context.mode === 'names') {
767
- return isDBMethodCall(node, adapter, sourceCode, context, methods);
768
- }
769
- // Check if the root receiver is DB-provenanced
770
- if (!context.dbProvenanced.has(rootReceiver)) {
771
- if (rootReceiver !== 'this')
772
- return false;
773
- // For `this.xxx`, check if the method chain itself suggests DB usage
774
- return isDBMethodOnThis(node, adapter, sourceCode, context, methods);
775
- }
776
- // Check that the method is in the DB call/ORM API
777
- return isDBMethodCall(node, adapter, sourceCode, context, methods);
881
+ return null;
778
882
  }
779
883
  /**
780
884
  * Check if a call through a member expression uses a DB method
781
885
  * (exec, prepare, all, etc.) or an ORM method (find, insert, etc.).
782
886
  */
783
- function isDBMethodCall(node, _adapter, _sourceCode, _context, methods) {
887
+ function isDBMethodCall(node, query) {
888
+ const { adapter, sourceCode, methods } = query;
784
889
  // Walk the member expression chain and check each property
785
890
  let current = node;
786
891
  while (current.type === 'member_expression' ||
@@ -790,7 +895,7 @@ function isDBMethodCall(node, _adapter, _sourceCode, _context, methods) {
790
895
  for (const child of children) {
791
896
  if (child.type === 'property_identifier' ||
792
897
  child.type === 'field_identifier') {
793
- const propName = _adapter.getNodeText(child, _sourceCode);
898
+ const propName = adapter.getNodeText(child, sourceCode);
794
899
  const lower = propName.toLowerCase();
795
900
  if (methods.has(lower) || ORM_METHODS.has(lower)) {
796
901
  return true;
@@ -813,7 +918,8 @@ function isDBMethodCall(node, _adapter, _sourceCode, _context, methods) {
813
918
  * For `this.xxx.method()` calls — check if the method chain suggests
814
919
  * DB access. Used when the receiver is `this` (not directly DB-provenanced).
815
920
  */
816
- function isDBMethodOnThis(node, adapter, sourceCode, _context, methods) {
921
+ function isDBMethodOnThis(node, query) {
922
+ const { adapter, sourceCode, methods } = query;
817
923
  // Walk the chain: this.db.prepare → check if any property matches DB methods
818
924
  let current = node;
819
925
  while (current.type === 'member_expression' ||
@@ -844,6 +950,9 @@ function isDBMethodOnThis(node, adapter, sourceCode, _context, methods) {
844
950
  * Check if an identifier is validator-provenanced (R4 infrastructure).
845
951
  *
846
952
  * This will be consumed by Spec 15's validator-bypass detection.
953
+ * @param context
954
+ * @param identifier
955
+ * @returns
847
956
  */
848
957
  export function isValidatorProvenanced(identifier, context) {
849
958
  return context.validatorProvenanced.has(identifier);
@@ -860,108 +969,124 @@ export function isValidatorProvenanced(identifier, context) {
860
969
  * (b) can be traced back to a provenanced source through assignments.
861
970
  *
862
971
  * Deferred: full implementation in R2 step.
972
+ * @param adapter
973
+ * @param fileAst
974
+ * @param provenancedSet
975
+ * @param sourceCode
976
+ * @returns
863
977
  */
864
978
  export function inferReceivers(provenancedSet, fileAst, adapter, sourceCode) {
865
979
  const inferred = { identifiers: [], evidence: [] };
866
980
  const seen = new Set();
867
981
  // Step 1: Build an assignment graph — name → source text of initializer
868
982
  const assignmentGraph = buildAssignmentGraph(fileAst, adapter, sourceCode);
983
+ const ctx = {
984
+ adapter, sourceCode, provenancedSet, assignmentGraph, seen, inferred,
985
+ };
869
986
  // Step 2: Walk all call expressions whose callee is a member expression
870
- walkAST(fileAst.root, (node) => {
871
- if (node.type !== 'call_expression')
872
- return;
873
- const callee = getCallExpressionCallee(node, adapter);
874
- if (!callee || (callee.type !== 'member_expression' && callee.type !== 'selector_expression'))
875
- return;
876
- // Extract method name (the property being called)
877
- const methodName = extractMemberExpressionProperty(callee, adapter, sourceCode);
878
- if (!methodName || !DB_CALL_METHODS.has(methodName.toLowerCase()))
879
- return;
880
- // Get the receiver identifier
881
- const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode);
882
- if (!receiver)
883
- return;
884
- // Already in the provenanced set → skip (already first-class, not inferred)
885
- if (provenancedSet.has(receiver))
886
- return;
887
- // Already added to inferred
888
- if (seen.has(receiver))
889
- return;
890
- // Step 3: Trace receiver through assignment chain to see if it reaches
891
- // a provenanced source
892
- const traced = traceAssignmentChain(receiver, assignmentGraph, provenancedSet);
893
- if (traced.found) {
894
- seen.add(receiver);
895
- inferred.identifiers.push(receiver);
896
- inferred.evidence.push({
897
- identifier: receiver,
898
- reason: `calls .${methodName}() traced to ${traced.chains[0]} — ${traced.reason}`,
899
- });
900
- }
901
- });
987
+ walkAST(fileAst.root, (node) => inferReceiverFromCall(node, ctx));
902
988
  return inferred;
903
989
  }
990
+ /** Infer a receiver from a single call_expression node, if it qualifies. */
991
+ function inferReceiverFromCall(node, ctx) {
992
+ const { adapter, sourceCode, provenancedSet, assignmentGraph, seen, inferred } = ctx;
993
+ if (node.type !== 'call_expression')
994
+ return;
995
+ const callee = getCallExpressionCallee(node, adapter);
996
+ if (!callee || (callee.type !== 'member_expression' && callee.type !== 'selector_expression'))
997
+ return;
998
+ const methodName = extractMemberExpressionProperty(callee, adapter, sourceCode);
999
+ if (!methodName || !DB_CALL_METHODS.has(methodName.toLowerCase()))
1000
+ return;
1001
+ const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode);
1002
+ if (!receiver)
1003
+ return;
1004
+ // Already provenanced → first-class, not inferred; already added → skip
1005
+ if (provenancedSet.has(receiver) || seen.has(receiver))
1006
+ return;
1007
+ // Step 3: Trace receiver through assignment chain to a provenanced source
1008
+ const traced = traceAssignmentChain(receiver, assignmentGraph, provenancedSet);
1009
+ if (traced.found) {
1010
+ seen.add(receiver);
1011
+ inferred.identifiers.push(receiver);
1012
+ inferred.evidence.push({
1013
+ identifier: receiver,
1014
+ reason: `calls .${methodName}() traced to ${traced.chains[0]} — ${traced.reason}`,
1015
+ });
1016
+ }
1017
+ }
904
1018
  /**
905
1019
  * Build a reverse assignment graph from the AST.
906
1020
  * Maps variable name → the text of its initializer expression.
907
1021
  * Handles: const/let/var declarations, parameter defaults, class fields.
908
1022
  */
909
1023
  function buildAssignmentGraph(ast, adapter, sourceCode) {
910
- const graph = new Map();
1024
+ const ctx = {
1025
+ graph: new Map(),
1026
+ adapter,
1027
+ sourceCode,
1028
+ };
911
1029
  walkAST(ast.root, (node, parent) => {
912
- // const/let/var x = <expr>
913
- if (node.type === 'variable_declarator') {
914
- const { nameNode, valueNode } = splitVariableDeclarator(node, adapter);
915
- if (nameNode && valueNode) {
916
- const names = extractPatternNames(nameNode, adapter, sourceCode);
917
- const valueText = adapter.getNodeText(valueNode, sourceCode);
918
- for (const name of names) {
919
- if (!graph.has(name))
920
- graph.set(name, valueText);
921
- }
922
- }
923
- return;
924
- }
925
- // Parameter defaults: function foo(x = <expr>)
926
- if (node.type === 'assignment_pattern' &&
927
- parent?.type === 'formal_parameters') {
928
- const children = adapter.getChildren(node);
929
- if (children.length >= 2) {
930
- const leftNode = children[0];
931
- const rightNode = children[1];
932
- if (leftNode.type === 'identifier') {
933
- const paramName = adapter.getNodeText(leftNode, sourceCode);
934
- const valueText = adapter.getNodeText(rightNode, sourceCode);
935
- if (!graph.has(paramName))
936
- graph.set(paramName, valueText);
937
- }
938
- }
939
- return;
940
- }
941
- // Class field: fieldName = <expr>
942
- if (node.type === 'public_field_definition' ||
943
- node.type === 'field_definition') {
944
- const children = adapter.getChildren(node);
945
- const nameChild = children.find((c) => c.type === 'property_identifier');
946
- const valueChild = children.find((c) => c.type !== 'property_identifier' &&
947
- c.type !== 'decorator' &&
948
- c.type !== 'private' &&
949
- c.type !== 'public' &&
950
- c.type !== 'protected' &&
951
- c.type !== 'static' &&
952
- c.type !== 'readonly' &&
953
- c.type !== 'abstract' &&
954
- c.type !== '=');
955
- if (nameChild && valueChild) {
956
- const fieldName = adapter.getNodeText(nameChild, sourceCode);
957
- const valueText = adapter.getNodeText(valueChild, sourceCode);
958
- if (!graph.has(fieldName))
959
- graph.set(fieldName, valueText);
960
- }
961
- return;
962
- }
1030
+ recordVariableDeclarator(node, ctx);
1031
+ recordParameterDefault(node, parent, ctx);
1032
+ recordClassField(node, ctx);
963
1033
  });
964
- return graph;
1034
+ return ctx.graph;
1035
+ }
1036
+ /** Record `const/let/var x = <expr>` into the assignment graph. */
1037
+ function recordVariableDeclarator(node, ctx) {
1038
+ if (node.type !== 'variable_declarator')
1039
+ return;
1040
+ const { nameNode, valueNode } = splitVariableDeclarator(node, ctx.adapter);
1041
+ if (!nameNode || !valueNode)
1042
+ return;
1043
+ const names = extractPatternNames(nameNode, ctx.adapter, ctx.sourceCode);
1044
+ const valueText = ctx.adapter.getNodeText(valueNode, ctx.sourceCode);
1045
+ for (const name of names) {
1046
+ if (!ctx.graph.has(name))
1047
+ ctx.graph.set(name, valueText);
1048
+ }
1049
+ }
1050
+ /** Record parameter defaults (`function foo(x = <expr>)`) into the graph. */
1051
+ function recordParameterDefault(node, parent, ctx) {
1052
+ if (node.type !== 'assignment_pattern' || parent?.type !== 'formal_parameters') {
1053
+ return;
1054
+ }
1055
+ const children = ctx.adapter.getChildren(node);
1056
+ if (children.length < 2)
1057
+ return;
1058
+ const leftNode = children[0];
1059
+ const rightNode = children[1];
1060
+ if (leftNode.type !== 'identifier')
1061
+ return;
1062
+ const paramName = ctx.adapter.getNodeText(leftNode, ctx.sourceCode);
1063
+ const valueText = ctx.adapter.getNodeText(rightNode, ctx.sourceCode);
1064
+ if (!ctx.graph.has(paramName))
1065
+ ctx.graph.set(paramName, valueText);
1066
+ }
1067
+ /** Record class-field initializers (`fieldName = <expr>`) into the graph. */
1068
+ function recordClassField(node, ctx) {
1069
+ if (node.type !== 'public_field_definition' &&
1070
+ node.type !== 'field_definition') {
1071
+ return;
1072
+ }
1073
+ const children = ctx.adapter.getChildren(node);
1074
+ const nameChild = children.find((c) => c.type === 'property_identifier');
1075
+ const valueChild = children.find((c) => c.type !== 'property_identifier' &&
1076
+ c.type !== 'decorator' &&
1077
+ c.type !== 'private' &&
1078
+ c.type !== 'public' &&
1079
+ c.type !== 'protected' &&
1080
+ c.type !== 'static' &&
1081
+ c.type !== 'readonly' &&
1082
+ c.type !== 'abstract' &&
1083
+ c.type !== '=');
1084
+ if (!nameChild || !valueChild)
1085
+ return;
1086
+ const fieldName = ctx.adapter.getNodeText(nameChild, ctx.sourceCode);
1087
+ const valueText = ctx.adapter.getNodeText(valueChild, ctx.sourceCode);
1088
+ if (!ctx.graph.has(fieldName))
1089
+ ctx.graph.set(fieldName, valueText);
965
1090
  }
966
1091
  /**
967
1092
  * Trace a receiver identifier through the assignment graph to see if it
@@ -982,10 +1107,8 @@ function traceAssignmentChain(receiver, assignmentGraph, provenancedSet) {
982
1107
  reason: provenancedSet.get(current).source,
983
1108
  };
984
1109
  }
985
- // Also check if any identifier in the initializer text matches a provenanced source
986
1110
  const initText = assignmentGraph.get(current);
987
1111
  if (initText) {
988
- // Extract identifiers from the initializer text and check against provenanced set
989
1112
  const idents = extractTopLevelIdentifiers(initText);
990
1113
  for (const ident of idents) {
991
1114
  if (provenancedSet.has(ident)) {
@@ -998,27 +1121,28 @@ function traceAssignmentChain(receiver, assignmentGraph, provenancedSet) {
998
1121
  }
999
1122
  }
1000
1123
  }
1001
- // Move to the next link in the chain
1002
1124
  if (!initText || visited.has(initText))
1003
1125
  break;
1004
1126
  visited.add(initText);
1005
- // Try to find a next identifier to trace
1006
- const idents = extractTopLevelIdentifiers(initText);
1007
- let foundNext = false;
1008
- for (const ident of idents) {
1009
- if (assignmentGraph.has(ident) && !visited.has(ident)) {
1010
- chain.push(ident);
1011
- current = ident;
1012
- foundNext = true;
1013
- break;
1014
- }
1015
- }
1016
- if (!foundNext)
1127
+ const next = findNextTraceIdentifier(initText, assignmentGraph, visited);
1128
+ if (!next)
1017
1129
  break;
1130
+ chain.push(next);
1131
+ current = next;
1018
1132
  depth++;
1019
1133
  }
1020
1134
  return { found: false, chains: [], reason: '' };
1021
1135
  }
1136
+ /** Find the next assignment-graph identifier to trace, if any. */
1137
+ function findNextTraceIdentifier(initText, assignmentGraph, visited) {
1138
+ const idents = extractTopLevelIdentifiers(initText);
1139
+ for (const ident of idents) {
1140
+ if (assignmentGraph.has(ident) && !visited.has(ident)) {
1141
+ return ident;
1142
+ }
1143
+ }
1144
+ return null;
1145
+ }
1022
1146
  /**
1023
1147
  * Extract top-level identifier names from an expression text.
1024
1148
  * E.g., "db.prepare(sql)" → ["db"], "getConnection()" → ["getConnection"]