@lbroth/rothunter 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/dist/adapters/llm.d.ts +68 -0
  4. package/dist/adapters/llm.d.ts.map +1 -0
  5. package/dist/adapters/llm.js +189 -0
  6. package/dist/adapters/llm.js.map +1 -0
  7. package/dist/config.d.ts +37 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +81 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/detector-registry.d.ts +32 -0
  12. package/dist/detector-registry.d.ts.map +1 -0
  13. package/dist/detector-registry.js +74 -0
  14. package/dist/detector-registry.js.map +1 -0
  15. package/dist/detectors/api-race.d.ts +6 -0
  16. package/dist/detectors/api-race.d.ts.map +1 -0
  17. package/dist/detectors/api-race.js +222 -0
  18. package/dist/detectors/api-race.js.map +1 -0
  19. package/dist/detectors/bad-config.d.ts +6 -0
  20. package/dist/detectors/bad-config.d.ts.map +1 -0
  21. package/dist/detectors/bad-config.js +529 -0
  22. package/dist/detectors/bad-config.js.map +1 -0
  23. package/dist/detectors/console-log-prod.d.ts +6 -0
  24. package/dist/detectors/console-log-prod.d.ts.map +1 -0
  25. package/dist/detectors/console-log-prod.js +72 -0
  26. package/dist/detectors/console-log-prod.js.map +1 -0
  27. package/dist/detectors/dead-api.d.ts +10 -0
  28. package/dist/detectors/dead-api.d.ts.map +1 -0
  29. package/dist/detectors/dead-api.js +115 -0
  30. package/dist/detectors/dead-api.js.map +1 -0
  31. package/dist/detectors/dead-export.d.ts +12 -0
  32. package/dist/detectors/dead-export.d.ts.map +1 -0
  33. package/dist/detectors/dead-export.js +140 -0
  34. package/dist/detectors/dead-export.js.map +1 -0
  35. package/dist/detectors/dead-handler.d.ts +12 -0
  36. package/dist/detectors/dead-handler.d.ts.map +1 -0
  37. package/dist/detectors/dead-handler.js +40 -0
  38. package/dist/detectors/dead-handler.js.map +1 -0
  39. package/dist/detectors/dead-module.d.ts +14 -0
  40. package/dist/detectors/dead-module.d.ts.map +1 -0
  41. package/dist/detectors/dead-module.js +50 -0
  42. package/dist/detectors/dead-module.js.map +1 -0
  43. package/dist/detectors/deep-nesting.d.ts +12 -0
  44. package/dist/detectors/deep-nesting.d.ts.map +1 -0
  45. package/dist/detectors/deep-nesting.js +133 -0
  46. package/dist/detectors/deep-nesting.js.map +1 -0
  47. package/dist/detectors/duplicate-function.d.ts +9 -0
  48. package/dist/detectors/duplicate-function.d.ts.map +1 -0
  49. package/dist/detectors/duplicate-function.js +199 -0
  50. package/dist/detectors/duplicate-function.js.map +1 -0
  51. package/dist/detectors/duplicate-type.d.ts +9 -0
  52. package/dist/detectors/duplicate-type.d.ts.map +1 -0
  53. package/dist/detectors/duplicate-type.js +166 -0
  54. package/dist/detectors/duplicate-type.js.map +1 -0
  55. package/dist/detectors/hot-hub-file.d.ts +11 -0
  56. package/dist/detectors/hot-hub-file.d.ts.map +1 -0
  57. package/dist/detectors/hot-hub-file.js +42 -0
  58. package/dist/detectors/hot-hub-file.js.map +1 -0
  59. package/dist/detectors/long-file.d.ts +12 -0
  60. package/dist/detectors/long-file.d.ts.map +1 -0
  61. package/dist/detectors/long-file.js +82 -0
  62. package/dist/detectors/long-file.js.map +1 -0
  63. package/dist/detectors/long-function.d.ts +12 -0
  64. package/dist/detectors/long-function.d.ts.map +1 -0
  65. package/dist/detectors/long-function.js +45 -0
  66. package/dist/detectors/long-function.js.map +1 -0
  67. package/dist/detectors/magic-numbers.d.ts +10 -0
  68. package/dist/detectors/magic-numbers.d.ts.map +1 -0
  69. package/dist/detectors/magic-numbers.js +332 -0
  70. package/dist/detectors/magic-numbers.js.map +1 -0
  71. package/dist/detectors/mutable-globals.d.ts +6 -0
  72. package/dist/detectors/mutable-globals.d.ts.map +1 -0
  73. package/dist/detectors/mutable-globals.js +95 -0
  74. package/dist/detectors/mutable-globals.js.map +1 -0
  75. package/dist/detectors/mutation.d.ts +11 -0
  76. package/dist/detectors/mutation.d.ts.map +1 -0
  77. package/dist/detectors/mutation.js +397 -0
  78. package/dist/detectors/mutation.js.map +1 -0
  79. package/dist/detectors/public-any.d.ts +6 -0
  80. package/dist/detectors/public-any.d.ts.map +1 -0
  81. package/dist/detectors/public-any.js +52 -0
  82. package/dist/detectors/public-any.js.map +1 -0
  83. package/dist/detectors/race-condition.d.ts +6 -0
  84. package/dist/detectors/race-condition.d.ts.map +1 -0
  85. package/dist/detectors/race-condition.js +608 -0
  86. package/dist/detectors/race-condition.js.map +1 -0
  87. package/dist/detectors/shared-db-write.d.ts +6 -0
  88. package/dist/detectors/shared-db-write.d.ts.map +1 -0
  89. package/dist/detectors/shared-db-write.js +656 -0
  90. package/dist/detectors/shared-db-write.js.map +1 -0
  91. package/dist/detectors/silent-catch.d.ts +6 -0
  92. package/dist/detectors/silent-catch.d.ts.map +1 -0
  93. package/dist/detectors/silent-catch.js +167 -0
  94. package/dist/detectors/silent-catch.js.map +1 -0
  95. package/dist/detectors/similar-functions.d.ts +15 -0
  96. package/dist/detectors/similar-functions.d.ts.map +1 -0
  97. package/dist/detectors/similar-functions.js +334 -0
  98. package/dist/detectors/similar-functions.js.map +1 -0
  99. package/dist/detectors/skip-tests.d.ts +6 -0
  100. package/dist/detectors/skip-tests.d.ts.map +1 -0
  101. package/dist/detectors/skip-tests.js +69 -0
  102. package/dist/detectors/skip-tests.js.map +1 -0
  103. package/dist/detectors/todo-comments.d.ts +29 -0
  104. package/dist/detectors/todo-comments.d.ts.map +1 -0
  105. package/dist/detectors/todo-comments.js +154 -0
  106. package/dist/detectors/todo-comments.js.map +1 -0
  107. package/dist/detectors/unused-deps.d.ts +8 -0
  108. package/dist/detectors/unused-deps.d.ts.map +1 -0
  109. package/dist/detectors/unused-deps.js +115 -0
  110. package/dist/detectors/unused-deps.js.map +1 -0
  111. package/dist/extraction/api-race-confirmer.d.ts +31 -0
  112. package/dist/extraction/api-race-confirmer.d.ts.map +1 -0
  113. package/dist/extraction/api-race-confirmer.js +110 -0
  114. package/dist/extraction/api-race-confirmer.js.map +1 -0
  115. package/dist/extraction/llm-confirmer.d.ts +25 -0
  116. package/dist/extraction/llm-confirmer.d.ts.map +1 -0
  117. package/dist/extraction/llm-confirmer.js +118 -0
  118. package/dist/extraction/llm-confirmer.js.map +1 -0
  119. package/dist/extraction/mutation-confirmer.d.ts +30 -0
  120. package/dist/extraction/mutation-confirmer.d.ts.map +1 -0
  121. package/dist/extraction/mutation-confirmer.js +73 -0
  122. package/dist/extraction/mutation-confirmer.js.map +1 -0
  123. package/dist/extraction/prompt-chunking.d.ts +37 -0
  124. package/dist/extraction/prompt-chunking.d.ts.map +1 -0
  125. package/dist/extraction/prompt-chunking.js +61 -0
  126. package/dist/extraction/prompt-chunking.js.map +1 -0
  127. package/dist/extraction/race-confirmer.d.ts +28 -0
  128. package/dist/extraction/race-confirmer.d.ts.map +1 -0
  129. package/dist/extraction/race-confirmer.js +68 -0
  130. package/dist/extraction/race-confirmer.js.map +1 -0
  131. package/dist/extraction/shared-db-write-confirmer.d.ts +31 -0
  132. package/dist/extraction/shared-db-write-confirmer.d.ts.map +1 -0
  133. package/dist/extraction/shared-db-write-confirmer.js +141 -0
  134. package/dist/extraction/shared-db-write-confirmer.js.map +1 -0
  135. package/dist/extraction/triage-confirmer.d.ts +59 -0
  136. package/dist/extraction/triage-confirmer.d.ts.map +1 -0
  137. package/dist/extraction/triage-confirmer.js +104 -0
  138. package/dist/extraction/triage-confirmer.js.map +1 -0
  139. package/dist/graph/cfg.d.ts +45 -0
  140. package/dist/graph/cfg.d.ts.map +1 -0
  141. package/dist/graph/cfg.js +198 -0
  142. package/dist/graph/cfg.js.map +1 -0
  143. package/dist/graph/decorator-entries.d.ts +2 -0
  144. package/dist/graph/decorator-entries.d.ts.map +1 -0
  145. package/dist/graph/decorator-entries.js +89 -0
  146. package/dist/graph/decorator-entries.js.map +1 -0
  147. package/dist/graph/entry-points.d.ts +12 -0
  148. package/dist/graph/entry-points.d.ts.map +1 -0
  149. package/dist/graph/entry-points.js +282 -0
  150. package/dist/graph/entry-points.js.map +1 -0
  151. package/dist/graph/handler-conventions.d.ts +2 -0
  152. package/dist/graph/handler-conventions.d.ts.map +1 -0
  153. package/dist/graph/handler-conventions.js +26 -0
  154. package/dist/graph/handler-conventions.js.map +1 -0
  155. package/dist/graph/iac-entries.d.ts +2 -0
  156. package/dist/graph/iac-entries.d.ts.map +1 -0
  157. package/dist/graph/iac-entries.js +123 -0
  158. package/dist/graph/iac-entries.js.map +1 -0
  159. package/dist/graph/import-graph.d.ts +48 -0
  160. package/dist/graph/import-graph.d.ts.map +1 -0
  161. package/dist/graph/import-graph.js +86 -0
  162. package/dist/graph/import-graph.js.map +1 -0
  163. package/dist/graph/monorepo-detect.d.ts +3 -0
  164. package/dist/graph/monorepo-detect.d.ts.map +1 -0
  165. package/dist/graph/monorepo-detect.js +166 -0
  166. package/dist/graph/monorepo-detect.js.map +1 -0
  167. package/dist/graph/tsconfig-paths.d.ts +23 -0
  168. package/dist/graph/tsconfig-paths.d.ts.map +1 -0
  169. package/dist/graph/tsconfig-paths.js +217 -0
  170. package/dist/graph/tsconfig-paths.js.map +1 -0
  171. package/dist/multi-workspace-scanner.d.ts +13 -0
  172. package/dist/multi-workspace-scanner.d.ts.map +1 -0
  173. package/dist/multi-workspace-scanner.js +130 -0
  174. package/dist/multi-workspace-scanner.js.map +1 -0
  175. package/dist/normalizers/type-normalizer.d.ts +16 -0
  176. package/dist/normalizers/type-normalizer.d.ts.map +1 -0
  177. package/dist/normalizers/type-normalizer.js +189 -0
  178. package/dist/normalizers/type-normalizer.js.map +1 -0
  179. package/dist/parsers/typescript-parser.d.ts +57 -0
  180. package/dist/parsers/typescript-parser.d.ts.map +1 -0
  181. package/dist/parsers/typescript-parser.js +502 -0
  182. package/dist/parsers/typescript-parser.js.map +1 -0
  183. package/dist/reporter/json-reporter.d.ts +12 -0
  184. package/dist/reporter/json-reporter.d.ts.map +1 -0
  185. package/dist/reporter/json-reporter.js +28 -0
  186. package/dist/reporter/json-reporter.js.map +1 -0
  187. package/dist/reporter/markdown-reporter.d.ts +11 -0
  188. package/dist/reporter/markdown-reporter.d.ts.map +1 -0
  189. package/dist/reporter/markdown-reporter.js +77 -0
  190. package/dist/reporter/markdown-reporter.js.map +1 -0
  191. package/dist/rothunter.d.ts +125 -0
  192. package/dist/rothunter.d.ts.map +1 -0
  193. package/dist/rothunter.js +1038 -0
  194. package/dist/rothunter.js.map +1 -0
  195. package/dist/server/false-positives.d.ts +34 -0
  196. package/dist/server/false-positives.d.ts.map +1 -0
  197. package/dist/server/false-positives.js +85 -0
  198. package/dist/server/false-positives.js.map +1 -0
  199. package/dist/server/index.d.ts +2 -0
  200. package/dist/server/index.d.ts.map +1 -0
  201. package/dist/server/index.js +1529 -0
  202. package/dist/server/index.js.map +1 -0
  203. package/dist/server/marked-to-fix.d.ts +16 -0
  204. package/dist/server/marked-to-fix.d.ts.map +1 -0
  205. package/dist/server/marked-to-fix.js +36 -0
  206. package/dist/server/marked-to-fix.js.map +1 -0
  207. package/dist/server/scan-store.d.ts +147 -0
  208. package/dist/server/scan-store.d.ts.map +1 -0
  209. package/dist/server/scan-store.js +291 -0
  210. package/dist/server/scan-store.js.map +1 -0
  211. package/dist/server/settings-store.d.ts +28 -0
  212. package/dist/server/settings-store.d.ts.map +1 -0
  213. package/dist/server/settings-store.js +46 -0
  214. package/dist/server/settings-store.js.map +1 -0
  215. package/dist/server/workspace-store.d.ts +39 -0
  216. package/dist/server/workspace-store.d.ts.map +1 -0
  217. package/dist/server/workspace-store.js +108 -0
  218. package/dist/server/workspace-store.js.map +1 -0
  219. package/dist/types/detector-input.d.ts +37 -0
  220. package/dist/types/detector-input.d.ts.map +1 -0
  221. package/dist/types/detector-input.js +2 -0
  222. package/dist/types/detector-input.js.map +1 -0
  223. package/dist/types.d.ts +110 -0
  224. package/dist/types.d.ts.map +1 -0
  225. package/dist/types.js +2 -0
  226. package/dist/types.js.map +1 -0
  227. package/dist/utils/clustering.d.ts +14 -0
  228. package/dist/utils/clustering.d.ts.map +1 -0
  229. package/dist/utils/clustering.js +56 -0
  230. package/dist/utils/clustering.js.map +1 -0
  231. package/dist/utils/gitignore.d.ts +32 -0
  232. package/dist/utils/gitignore.d.ts.map +1 -0
  233. package/dist/utils/gitignore.js +122 -0
  234. package/dist/utils/gitignore.js.map +1 -0
  235. package/dist/utils/hash.d.ts +11 -0
  236. package/dist/utils/hash.d.ts.map +1 -0
  237. package/dist/utils/hash.js +14 -0
  238. package/dist/utils/hash.js.map +1 -0
  239. package/dist/utils/ignore-annotation.d.ts +28 -0
  240. package/dist/utils/ignore-annotation.d.ts.map +1 -0
  241. package/dist/utils/ignore-annotation.js +46 -0
  242. package/dist/utils/ignore-annotation.js.map +1 -0
  243. package/dist/utils/llm-json.d.ts +2 -0
  244. package/dist/utils/llm-json.d.ts.map +1 -0
  245. package/dist/utils/llm-json.js +53 -0
  246. package/dist/utils/llm-json.js.map +1 -0
  247. package/dist/utils/logger.d.ts +3 -0
  248. package/dist/utils/logger.d.ts.map +1 -0
  249. package/dist/utils/logger.js +4 -0
  250. package/dist/utils/logger.js.map +1 -0
  251. package/dist/utils/project-conventions.d.ts +2 -0
  252. package/dist/utils/project-conventions.d.ts.map +1 -0
  253. package/dist/utils/project-conventions.js +108 -0
  254. package/dist/utils/project-conventions.js.map +1 -0
  255. package/dist/utils/regex.d.ts +9 -0
  256. package/dist/utils/regex.d.ts.map +1 -0
  257. package/dist/utils/regex.js +11 -0
  258. package/dist/utils/regex.js.map +1 -0
  259. package/dist/utils/snippet.d.ts +20 -0
  260. package/dist/utils/snippet.d.ts.map +1 -0
  261. package/dist/utils/snippet.js +28 -0
  262. package/dist/utils/snippet.js.map +1 -0
  263. package/dist/utils/source-reader.d.ts +19 -0
  264. package/dist/utils/source-reader.d.ts.map +1 -0
  265. package/dist/utils/source-reader.js +32 -0
  266. package/dist/utils/source-reader.js.map +1 -0
  267. package/logo.png +0 -0
  268. package/package.json +92 -0
  269. package/scripts/start-llm.mjs +161 -0
@@ -0,0 +1,656 @@
1
+ import * as path from 'node:path';
2
+ import { stableHash } from '../utils/hash.js';
3
+ import { trimSnippet, trimEnclosingSource } from '../utils/snippet.js';
4
+ import { Project, SyntaxKind, } from 'ts-morph';
5
+ // Cross-flow DB write detector. Indexes (entity, column) tuples for ORM /
6
+ // SQL-builder writes (Prisma, Sequelize, TypeORM, Mongoose, Knex, Drizzle,
7
+ // raw SQL). Clusters with ≥2 caller files become findings. MED, 0.7 — LLM
8
+ // LLM filters trivial cases. Instance-style writes + Prisma relation
9
+ // writes deferred (need flow analysis).
10
+ export function detectSharedDbWrites(input) {
11
+ let project;
12
+ if (input.project) {
13
+ project = input.project;
14
+ }
15
+ else {
16
+ project = new Project({
17
+ skipAddingFilesFromTsConfig: true,
18
+ skipFileDependencyResolution: true,
19
+ });
20
+ for (const rel of input.files) {
21
+ project.addSourceFileAtPathIfExists(path.join(input.workspaceRoot, rel));
22
+ }
23
+ }
24
+ const writes = [];
25
+ for (const sf of project.getSourceFiles()) {
26
+ const relativeFile = path.relative(input.workspaceRoot, sf.getFilePath());
27
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
28
+ const matched = matchAdapters(call);
29
+ if (!matched)
30
+ continue;
31
+ const enclosing = findEnclosingFunction(call);
32
+ const enclosingSource = enclosing
33
+ ? trimEnclosingSource(enclosing.getText())
34
+ : trimSnippet(call.getText());
35
+ const enclosingName = enclosing?.getName?.() ?? undefined;
36
+ const snippet = trimSnippet(call.getText());
37
+ const line = call.getStartLineNumber();
38
+ const endLine = call.getEndLineNumber();
39
+ for (const column of matched.columns) {
40
+ writes.push({
41
+ entity: matched.entity.toLowerCase(),
42
+ column,
43
+ adapter: matched.adapter,
44
+ file: relativeFile,
45
+ line,
46
+ endLine,
47
+ snippet,
48
+ enclosingName,
49
+ enclosingSource,
50
+ });
51
+ }
52
+ }
53
+ }
54
+ // Cluster by entity.column; emit when ≥ 2 distinct files write.
55
+ const byKey = new Map();
56
+ for (const w of writes) {
57
+ const key = `${w.entity}.${w.column}`;
58
+ const list = byKey.get(key) ?? [];
59
+ list.push(w);
60
+ byKey.set(key, list);
61
+ }
62
+ const findings = [];
63
+ for (const [key, list] of byKey) {
64
+ const distinctFiles = new Set(list.map((w) => w.file));
65
+ if (distinctFiles.size < 2)
66
+ continue;
67
+ const adapters = [...new Set(list.map((w) => w.adapter))];
68
+ const fileCount = distinctFiles.size;
69
+ const callCount = list.length;
70
+ const exampleFiles = [...distinctFiles].slice(0, 6).join(', ');
71
+ findings.push({
72
+ detectorId: 'shared-db-write',
73
+ severity: 'medium',
74
+ confidence: 0.7,
75
+ layer: 1,
76
+ title: `Shared DB column write: \`${key}\` across ${fileCount} files (${callCount} call sites, adapters: ${adapters.join('+')})`,
77
+ description: [
78
+ `Multiple functions write the same database column \`${key}\`.`,
79
+ `If any two of these can execute concurrently (HTTP handler + background worker, two webhook handlers, parallel job consumers, two services in a multi-workspace group), the second write may stomp the first — the lost-update class of distributed race.`,
80
+ ``,
81
+ `Locations:`,
82
+ ...list.map((w) => `- ${w.file}:${w.line} (${w.adapter}) \`${w.snippet}\``),
83
+ ``,
84
+ `Files involved: ${exampleFiles}${distinctFiles.size > 6 ? ', …' : ''}`,
85
+ ].join('\n'),
86
+ evidence: list.slice(0, 8).map((w) => ({
87
+ file: w.file,
88
+ range: { startLine: w.line, endLine: w.endLine },
89
+ snippet: w.enclosingSource,
90
+ note: JSON.stringify({
91
+ entity: w.entity,
92
+ column: w.column,
93
+ adapter: w.adapter,
94
+ enclosingName: w.enclosingName ?? '',
95
+ }),
96
+ })),
97
+ suggestion: 'Coordinate the writes via a single owner (one service is the source of truth, others publish events), wrap concurrent paths in an optimistic-locking version check, or merge into a single transactional update. If the writes are guaranteed serialised (queue, mutex, single-instance worker), document the synchronisation and mark this finding as a false positive.',
98
+ fingerprint: `shared-db-write:${stableHash(key)}`,
99
+ });
100
+ }
101
+ return findings;
102
+ }
103
+ /** Try each adapter in turn; return the first match. */
104
+ function matchAdapters(call) {
105
+ return (matchPrisma(call) ||
106
+ matchSequelize(call) ||
107
+ matchTypeOrm(call) ||
108
+ matchMongoose(call) ||
109
+ matchKnex(call) ||
110
+ matchDrizzle(call) ||
111
+ matchRawSql(call));
112
+ }
113
+ // ---------- Prisma --------------------------------------------------------
114
+ const PRISMA_WRITE_METHODS = new Set([
115
+ 'update',
116
+ 'updateMany',
117
+ 'upsert',
118
+ 'create',
119
+ 'createMany',
120
+ ]);
121
+ function matchPrisma(call) {
122
+ const callee = call.getExpression();
123
+ if (callee.getKind() !== SyntaxKind.PropertyAccessExpression)
124
+ return null;
125
+ const methodAccess = callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
126
+ const method = methodAccess.getName();
127
+ if (!PRISMA_WRITE_METHODS.has(method))
128
+ return null;
129
+ const entityAccess = methodAccess.getExpression();
130
+ if (entityAccess.getKind() !== SyntaxKind.PropertyAccessExpression)
131
+ return null;
132
+ const entityNode = entityAccess;
133
+ const head = entityNode.getExpression().getText();
134
+ // Heuristic: only match when the chain head looks like a Prisma client — common
135
+ // names are `prisma`, `db`, `tx`, `client`, or anything ending in `Prisma`/`Client`.
136
+ if (!/^(prisma|db|tx|client|.*Prisma|.*Client)$/i.test(head))
137
+ return null;
138
+ const entity = entityNode.getName();
139
+ const [arg] = call.getArguments();
140
+ if (!arg || arg.getKind() !== SyntaxKind.ObjectLiteralExpression)
141
+ return null;
142
+ const cols = new Set();
143
+ if (method === 'upsert') {
144
+ for (const propName of ['update', 'create']) {
145
+ const sub = pickObjectProperty(arg, propName);
146
+ if (sub)
147
+ collectPropertyNames(sub, cols);
148
+ }
149
+ }
150
+ else {
151
+ const data = pickObjectProperty(arg, 'data');
152
+ if (data) {
153
+ if (data.getKind() === SyntaxKind.ObjectLiteralExpression) {
154
+ collectPropertyNames(data, cols);
155
+ }
156
+ else if (data.getKind() === SyntaxKind.ArrayLiteralExpression) {
157
+ for (const row of data.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)) {
158
+ collectPropertyNames(row, cols);
159
+ }
160
+ }
161
+ }
162
+ }
163
+ if (cols.size === 0)
164
+ return null;
165
+ return { adapter: 'prisma', entity, columns: cols };
166
+ }
167
+ // ---------- Sequelize -----------------------------------------------------
168
+ // Sequelize: Model.update/upsert/create/bulkCreate + instance.update.
169
+ // Identifier head — non-ORM FPs filtered by cluster + LLM.
170
+ /**
171
+ * Suffix blacklist for the Sequelize PascalCase heuristic. Real-world
172
+ * codebases use `<Something>.create({...})` heavily for factories (JWT
173
+ * verifiers, HTTP clients, builders) — surfaced as a smoke FP on pixadyx-be:
174
+ * `CognitoJwtVerifier.create(...)` was treated as a Sequelize model write.
175
+ * If the receiver name matches any of these patterns we skip the match.
176
+ */
177
+ const NON_ORM_PASCAL_SUFFIXES = [
178
+ /Verifier$/,
179
+ /Builder$/,
180
+ /Factory$/,
181
+ /Client$/,
182
+ /Service$/,
183
+ /Manager$/,
184
+ /Adapter$/,
185
+ /Provider$/,
186
+ /Logger$/,
187
+ /Validator$/,
188
+ /Parser$/,
189
+ /Handler$/,
190
+ /Helper$/,
191
+ /Strategy$/,
192
+ /Resolver$/,
193
+ /Renderer$/,
194
+ /Listener$/,
195
+ ];
196
+ function matchSequelize(call) {
197
+ const callee = call.getExpression();
198
+ if (callee.getKind() !== SyntaxKind.PropertyAccessExpression)
199
+ return null;
200
+ const pa = callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
201
+ const method = pa.getName();
202
+ if (method !== 'update' && method !== 'upsert' && method !== 'create' && method !== 'bulkCreate')
203
+ return null;
204
+ const head = pa.getExpression();
205
+ if (head.getKind() !== SyntaxKind.Identifier)
206
+ return null;
207
+ const entity = head.getText();
208
+ // Sequelize models look like PascalCase identifiers. Skip non-PascalCase
209
+ // to dodge most `someService.update(...)` false positives.
210
+ if (!/^[A-Z][\w]*$/.test(entity))
211
+ return null;
212
+ // Skip well-known non-ORM PascalCase patterns (factories, builders, ...).
213
+ if (NON_ORM_PASCAL_SUFFIXES.some((re) => re.test(entity)))
214
+ return null;
215
+ const args = call.getArguments();
216
+ if (args.length === 0)
217
+ return null;
218
+ const cols = new Set();
219
+ // M.update({ a:1 }, { where: ... }) — Sequelize requires `where`. Without
220
+ // the second-arg options object we treat this as a non-Sequelize call.
221
+ if (method === 'update') {
222
+ if (args.length < 2)
223
+ return null;
224
+ const opts = args[1];
225
+ if (opts.getKind() !== SyntaxKind.ObjectLiteralExpression)
226
+ return null;
227
+ const optsObj = opts;
228
+ // Must look like a Sequelize options object: `where`, `transaction`,
229
+ // `returning`, `paranoid`, ... at minimum `where`.
230
+ if (!pickObjectProperty(optsObj, 'where'))
231
+ return null;
232
+ const valueObj = args[0];
233
+ if (valueObj.getKind() === SyntaxKind.ObjectLiteralExpression) {
234
+ collectPropertyNames(valueObj, cols);
235
+ }
236
+ }
237
+ else if (method === 'upsert' || method === 'create') {
238
+ // Require a second argument with Sequelize options (`transaction`,
239
+ // `returning`, `where`, `defaults`, …) — this disambiguates from
240
+ // factory `.create({ ... })` calls that take no options.
241
+ if (args.length < 2)
242
+ return null;
243
+ const opts = args[1];
244
+ if (opts.getKind() !== SyntaxKind.ObjectLiteralExpression)
245
+ return null;
246
+ const optsObj = opts;
247
+ const isSequelizeOpts = !!pickObjectProperty(optsObj, 'transaction') ||
248
+ !!pickObjectProperty(optsObj, 'returning') ||
249
+ !!pickObjectProperty(optsObj, 'where') ||
250
+ !!pickObjectProperty(optsObj, 'defaults') ||
251
+ !!pickObjectProperty(optsObj, 'individualHooks') ||
252
+ !!pickObjectProperty(optsObj, 'validate') ||
253
+ !!pickObjectProperty(optsObj, 'logging');
254
+ if (!isSequelizeOpts)
255
+ return null;
256
+ const valueObj = args[0];
257
+ if (valueObj.getKind() === SyntaxKind.ObjectLiteralExpression) {
258
+ collectPropertyNames(valueObj, cols);
259
+ }
260
+ }
261
+ else if (method === 'bulkCreate') {
262
+ // bulkCreate always passes an array as first arg and is rarely confused
263
+ // with non-ORM factories. Accept regardless of options.
264
+ const arr = args[0];
265
+ if (arr.getKind() === SyntaxKind.ArrayLiteralExpression) {
266
+ for (const row of arr.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)) {
267
+ collectPropertyNames(row, cols);
268
+ }
269
+ }
270
+ }
271
+ else {
272
+ return null;
273
+ }
274
+ if (cols.size === 0)
275
+ return null;
276
+ return { adapter: 'sequelize', entity, columns: cols };
277
+ }
278
+ // ---------- TypeORM ------------------------------------------------------
279
+ const TYPEORM_REPO_SUFFIX = /^(.+?)(Repo|Repository|repository)$/;
280
+ /**
281
+ * TypeORM:
282
+ * - repo.update(<criteria>, { <cols> })
283
+ * - repo.save({ id, <cols> })
284
+ * - getRepository(E).update(<criteria>, { <cols> })
285
+ *
286
+ * Receiver must look like a repository handle (`*Repo` / `*Repository` /
287
+ * literally `repository`) OR a `getRepository(E).method(...)` chain.
288
+ */
289
+ function matchTypeOrm(call) {
290
+ const callee = call.getExpression();
291
+ if (callee.getKind() !== SyntaxKind.PropertyAccessExpression)
292
+ return null;
293
+ const pa = callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
294
+ const method = pa.getName();
295
+ if (method !== 'update' && method !== 'save')
296
+ return null;
297
+ const head = pa.getExpression();
298
+ let entity = null;
299
+ if (head.getKind() === SyntaxKind.Identifier) {
300
+ const name = head.getText();
301
+ const m = TYPEORM_REPO_SUFFIX.exec(name);
302
+ if (m)
303
+ entity = m[1];
304
+ else if (/^repository$/i.test(name))
305
+ entity = name;
306
+ else
307
+ return null;
308
+ }
309
+ else {
310
+ const inner = head.asKind(SyntaxKind.CallExpression);
311
+ if (!inner)
312
+ return null;
313
+ const innerName = inner.getExpression().getText();
314
+ if (!/(^|\.)getRepository$|getMongoRepository$|getCustomRepository$/.test(innerName))
315
+ return null;
316
+ const innerArgs = inner.getArguments();
317
+ if (innerArgs.length === 0 || innerArgs[0].getKind() !== SyntaxKind.Identifier)
318
+ return null;
319
+ entity = innerArgs[0].getText();
320
+ }
321
+ if (!entity)
322
+ return null;
323
+ const args = call.getArguments();
324
+ if (args.length === 0)
325
+ return null;
326
+ const cols = new Set();
327
+ if (method === 'update' && args.length >= 2) {
328
+ const obj = args[1];
329
+ if (obj.getKind() === SyntaxKind.ObjectLiteralExpression)
330
+ collectPropertyNames(obj, cols);
331
+ }
332
+ else if (method === 'save') {
333
+ const obj = args[0];
334
+ if (obj.getKind() === SyntaxKind.ObjectLiteralExpression)
335
+ collectPropertyNames(obj, cols);
336
+ }
337
+ else {
338
+ return null;
339
+ }
340
+ if (cols.size === 0)
341
+ return null;
342
+ return { adapter: 'typeorm', entity, columns: cols };
343
+ }
344
+ // ---------- Mongoose -----------------------------------------------------
345
+ /**
346
+ * Mongoose write methods. Each has a known argument shape:
347
+ * - `args[1]`-shape (filter/id first, update/replacement second):
348
+ * updateOne, updateMany, findOneAndUpdate, findOneAndReplace,
349
+ * findByIdAndUpdate, findByIdAndReplace, replaceOne
350
+ * - `args[0]`-shape (write doc / docs first):
351
+ * create, insertMany
352
+ */
353
+ const MONGOOSE_ARGS1_METHODS = new Set([
354
+ 'updateOne',
355
+ 'updateMany',
356
+ 'findOneAndUpdate',
357
+ 'findOneAndReplace',
358
+ 'findByIdAndUpdate',
359
+ 'findByIdAndReplace',
360
+ 'replaceOne',
361
+ ]);
362
+ const MONGOOSE_ARGS0_METHODS = new Set(['create', 'insertMany']);
363
+ // Mongoose updateOne/findOneAndUpdate/create/insertMany. Receiver: Pascal
364
+ // ident, camel-Model, or this.<id>Model. $set unwrapped, $-ops stripped.
365
+ function matchMongoose(call) {
366
+ const callee = call.getExpression();
367
+ if (callee.getKind() !== SyntaxKind.PropertyAccessExpression)
368
+ return null;
369
+ const pa = callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
370
+ const method = pa.getName();
371
+ const isArgs1 = MONGOOSE_ARGS1_METHODS.has(method);
372
+ const isArgs0 = MONGOOSE_ARGS0_METHODS.has(method);
373
+ if (!isArgs1 && !isArgs0)
374
+ return null;
375
+ const head = pa.getExpression();
376
+ const entity = resolveMongooseEntity(head);
377
+ if (!entity)
378
+ return null;
379
+ const args = call.getArguments();
380
+ const cols = new Set();
381
+ if (isArgs1) {
382
+ if (args.length < 2)
383
+ return null;
384
+ const obj = args[1];
385
+ if (obj.getKind() !== SyntaxKind.ObjectLiteralExpression)
386
+ return null;
387
+ const setBlock = pickObjectProperty(obj, '$set');
388
+ if (setBlock && setBlock.getKind() === SyntaxKind.ObjectLiteralExpression) {
389
+ collectPropertyNames(setBlock, cols);
390
+ }
391
+ else {
392
+ collectPropertyNames(obj, cols);
393
+ for (const c of [...cols])
394
+ if (c.startsWith('$'))
395
+ cols.delete(c);
396
+ }
397
+ }
398
+ else {
399
+ if (args.length === 0)
400
+ return null;
401
+ const obj = args[0];
402
+ if (obj.getKind() === SyntaxKind.ObjectLiteralExpression) {
403
+ collectPropertyNames(obj, cols);
404
+ }
405
+ else if (obj.getKind() === SyntaxKind.ArrayLiteralExpression) {
406
+ for (const row of obj.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)) {
407
+ collectPropertyNames(row, cols);
408
+ }
409
+ }
410
+ else {
411
+ return null;
412
+ }
413
+ for (const c of [...cols])
414
+ if (c.startsWith('$'))
415
+ cols.delete(c);
416
+ }
417
+ if (cols.size === 0)
418
+ return null;
419
+ return { adapter: 'mongoose', entity, columns: cols };
420
+ }
421
+ /**
422
+ * Resolve the entity name from a Mongoose-style receiver:
423
+ * - `Cat` → `cat`
424
+ * - `catModel` → `cat`
425
+ * - `this.catModel` → `cat`
426
+ *
427
+ * Returns null when the receiver doesn't look like a Mongoose model handle.
428
+ */
429
+ function resolveMongooseEntity(head) {
430
+ if (head.getKind() === SyntaxKind.Identifier) {
431
+ const name = head.getText();
432
+ // PascalCase model class — `Cat`, `User`.
433
+ if (/^[A-Z][\w]*$/.test(name)) {
434
+ if (NON_ORM_PASCAL_SUFFIXES.some((re) => re.test(name)))
435
+ return null;
436
+ return name;
437
+ }
438
+ // camelCase identifier ending in `Model` — `catModel`, `userModel`.
439
+ const m = /^([a-z][\w]*?)Model$/.exec(name);
440
+ if (m)
441
+ return m[1];
442
+ return null;
443
+ }
444
+ if (head.getKind() === SyntaxKind.PropertyAccessExpression) {
445
+ const accessed = head;
446
+ if (accessed.getExpression().getKind() !== SyntaxKind.ThisKeyword)
447
+ return null;
448
+ const propName = accessed.getName();
449
+ const m = /^([a-z][\w]*?)Model$/.exec(propName);
450
+ if (m)
451
+ return m[1];
452
+ return null;
453
+ }
454
+ return null;
455
+ }
456
+ // ---------- Knex ---------------------------------------------------------
457
+ // Knex update/insert. Walks back through the chain to the
458
+ // knex('t')/db('t')/.from('t')/.into('t') head for the table name.
459
+ function matchKnex(call) {
460
+ const callee = call.getExpression();
461
+ if (callee.getKind() !== SyntaxKind.PropertyAccessExpression)
462
+ return null;
463
+ const pa = callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
464
+ const method = pa.getName();
465
+ if (method !== 'update' && method !== 'insert')
466
+ return null;
467
+ const args = call.getArguments();
468
+ if (args.length === 0)
469
+ return null;
470
+ const obj = args[0];
471
+ if (obj.getKind() !== SyntaxKind.ObjectLiteralExpression)
472
+ return null;
473
+ const tableName = findKnexTable(pa.getExpression());
474
+ if (!tableName)
475
+ return null;
476
+ const cols = new Set();
477
+ collectPropertyNames(obj, cols);
478
+ if (cols.size === 0)
479
+ return null;
480
+ return { adapter: 'knex', entity: tableName, columns: cols };
481
+ }
482
+ function findKnexTable(node) {
483
+ let cur = node;
484
+ for (let i = 0; cur && i < 12; i++) {
485
+ if (cur.getKind() === SyntaxKind.CallExpression) {
486
+ const c = cur;
487
+ const callExpr = c.getExpression();
488
+ const calleeText = callExpr.getText();
489
+ if (/^(knex|db|trx)$/.test(calleeText) || /\.(from|table|into)$/.test(calleeText)) {
490
+ const [first] = c.getArguments();
491
+ if (first &&
492
+ (first.getKind() === SyntaxKind.StringLiteral ||
493
+ first.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral)) {
494
+ const lit = first.asKind(SyntaxKind.StringLiteral) ?? first.asKind(SyntaxKind.NoSubstitutionTemplateLiteral);
495
+ if (lit)
496
+ return lit.getLiteralText();
497
+ }
498
+ }
499
+ }
500
+ cur = cur.getExpression?.();
501
+ }
502
+ return null;
503
+ }
504
+ // ---------- Drizzle ------------------------------------------------------
505
+ // Drizzle: db.update(t).set({...}) and db.insert(t).values({...}|[...]).
506
+ function matchDrizzle(call) {
507
+ const callee = call.getExpression();
508
+ if (callee.getKind() !== SyntaxKind.PropertyAccessExpression)
509
+ return null;
510
+ const pa = callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
511
+ const method = pa.getName();
512
+ if (method !== 'set' && method !== 'values')
513
+ return null;
514
+ const args = call.getArguments();
515
+ if (args.length === 0)
516
+ return null;
517
+ let table = null;
518
+ let cur = pa.getExpression();
519
+ for (let i = 0; cur && i < 10; i++) {
520
+ if (cur.getKind() === SyntaxKind.CallExpression) {
521
+ const c = cur;
522
+ const inner = c.getExpression();
523
+ const innerText = inner.getText();
524
+ if (/\.(update|insert)$/.test(innerText) || /^(update|insert)$/.test(innerText)) {
525
+ const [first] = c.getArguments();
526
+ if (first && first.getKind() === SyntaxKind.Identifier) {
527
+ table = first.getText();
528
+ break;
529
+ }
530
+ }
531
+ }
532
+ cur = cur.getExpression?.();
533
+ }
534
+ if (!table)
535
+ return null;
536
+ const cols = new Set();
537
+ const obj = args[0];
538
+ if (obj.getKind() === SyntaxKind.ObjectLiteralExpression) {
539
+ collectPropertyNames(obj, cols);
540
+ }
541
+ else if (obj.getKind() === SyntaxKind.ArrayLiteralExpression) {
542
+ for (const row of obj.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)) {
543
+ collectPropertyNames(row, cols);
544
+ }
545
+ }
546
+ if (cols.size === 0)
547
+ return null;
548
+ return { adapter: 'drizzle', entity: table, columns: cols };
549
+ }
550
+ // ---------- Raw SQL ------------------------------------------------------
551
+ // Raw SQL: .raw / .query / .$executeRaw[Unsafe] on UPDATE/INSERT shapes.
552
+ // Tiny regex grammar, not a SQL parser — comments + subqueries fall through.
553
+ function matchRawSql(call) {
554
+ const callee = call.getExpression();
555
+ let sqlText = null;
556
+ // .raw('SQL', ...) / .query('SQL', ...) / .$executeRawUnsafe('SQL', ...).
557
+ if (callee.getKind() === SyntaxKind.PropertyAccessExpression) {
558
+ const pa = callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
559
+ const m = pa.getName();
560
+ if (m !== 'raw' && m !== 'query' && m !== '$executeRaw' && m !== '$executeRawUnsafe') {
561
+ return null;
562
+ }
563
+ const [first] = call.getArguments();
564
+ if (!first)
565
+ return null;
566
+ const stringLit = first.asKind(SyntaxKind.StringLiteral) ?? first.asKind(SyntaxKind.NoSubstitutionTemplateLiteral);
567
+ if (stringLit) {
568
+ sqlText = stringLit.getLiteralText();
569
+ }
570
+ else if (first.getKind() === SyntaxKind.TemplateExpression) {
571
+ sqlText = first.getText();
572
+ }
573
+ }
574
+ if (!sqlText)
575
+ return null;
576
+ const parsed = parseSqlWrite(sqlText);
577
+ if (!parsed)
578
+ return null;
579
+ return { adapter: 'raw-sql', entity: parsed.table.toLowerCase(), columns: new Set(parsed.columns) };
580
+ }
581
+ /**
582
+ * Parse a SQL string for UPDATE or INSERT writes. Returns `{ table, columns }`
583
+ * or null. The grammar is intentionally narrow:
584
+ * UPDATE <table> SET <col>=…, <col>=…
585
+ * INSERT INTO <table> (<col>, <col>, …) VALUES (…)
586
+ */
587
+ function parseSqlWrite(sql) {
588
+ const cleaned = sql.replace(/--[^\n]*/g, '').replace(/\s+/g, ' ').trim();
589
+ // UPDATE ... SET col=..., col=...
590
+ const upd = /^update\s+([\w."]+)\s+set\s+(.+?)(?:\swhere\s|$)/i.exec(cleaned);
591
+ if (upd) {
592
+ const table = stripQuotes(upd[1]).split('.').pop();
593
+ const setBlock = upd[2];
594
+ const cols = [];
595
+ for (const part of setBlock.split(',')) {
596
+ const m = /^\s*"?([\w.]+)"?\s*=/.exec(part);
597
+ if (m)
598
+ cols.push(stripQuotes(m[1]).split('.').pop());
599
+ }
600
+ if (cols.length === 0)
601
+ return null;
602
+ return { table, columns: cols };
603
+ }
604
+ // INSERT INTO <table> (col, col, ...) VALUES
605
+ const ins = /^insert\s+into\s+([\w."]+)\s*\(([^)]+)\)\s*values/i.exec(cleaned);
606
+ if (ins) {
607
+ const table = stripQuotes(ins[1]).split('.').pop();
608
+ const cols = ins[2]
609
+ .split(',')
610
+ .map((c) => stripQuotes(c.trim()).split('.').pop())
611
+ .filter(Boolean);
612
+ if (cols.length === 0)
613
+ return null;
614
+ return { table, columns: cols };
615
+ }
616
+ return null;
617
+ }
618
+ function stripQuotes(s) {
619
+ return s.replace(/^["'`]+|["'`]+$/g, '');
620
+ }
621
+ // ---------- shared helpers -----------------------------------------------
622
+ function pickObjectProperty(obj, name) {
623
+ const olit = obj.asKind(SyntaxKind.ObjectLiteralExpression);
624
+ if (!olit)
625
+ return null;
626
+ const prop = olit.getProperty(name);
627
+ if (!prop || prop.getKind() !== SyntaxKind.PropertyAssignment)
628
+ return null;
629
+ return prop.getInitializer() ?? null;
630
+ }
631
+ function collectPropertyNames(obj, into) {
632
+ const olit = obj.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
633
+ for (const prop of olit.getProperties()) {
634
+ if (prop.getKind() === SyntaxKind.PropertyAssignment ||
635
+ prop.getKind() === SyntaxKind.ShorthandPropertyAssignment) {
636
+ const named = prop;
637
+ if (typeof named.getName === 'function')
638
+ into.add(named.getName());
639
+ }
640
+ }
641
+ }
642
+ function findEnclosingFunction(node) {
643
+ let cur = node.getParent();
644
+ while (cur) {
645
+ const k = cur.getKind();
646
+ if (k === SyntaxKind.FunctionDeclaration ||
647
+ k === SyntaxKind.MethodDeclaration ||
648
+ k === SyntaxKind.ArrowFunction ||
649
+ k === SyntaxKind.FunctionExpression) {
650
+ return cur;
651
+ }
652
+ cur = cur.getParent();
653
+ }
654
+ return null;
655
+ }
656
+ //# sourceMappingURL=shared-db-write.js.map