seer-mcp 0.1.0

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 (371) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/LICENSE +176 -0
  3. package/README.md +272 -0
  4. package/README_dev.md +199 -0
  5. package/dist/bundle/ci.d.ts +47 -0
  6. package/dist/bundle/ci.d.ts.map +1 -0
  7. package/dist/bundle/ci.js +113 -0
  8. package/dist/bundle/ci.js.map +1 -0
  9. package/dist/bundle/contract.d.ts +111 -0
  10. package/dist/bundle/contract.d.ts.map +1 -0
  11. package/dist/bundle/contract.js +352 -0
  12. package/dist/bundle/contract.js.map +1 -0
  13. package/dist/bundle/export.d.ts +36 -0
  14. package/dist/bundle/export.d.ts.map +1 -0
  15. package/dist/bundle/export.js +152 -0
  16. package/dist/bundle/export.js.map +1 -0
  17. package/dist/bundle/external.d.ts +66 -0
  18. package/dist/bundle/external.d.ts.map +1 -0
  19. package/dist/bundle/external.js +238 -0
  20. package/dist/bundle/external.js.map +1 -0
  21. package/dist/bundle/format.d.ts +94 -0
  22. package/dist/bundle/format.d.ts.map +1 -0
  23. package/dist/bundle/format.js +42 -0
  24. package/dist/bundle/format.js.map +1 -0
  25. package/dist/bundle/import.d.ts +49 -0
  26. package/dist/bundle/import.d.ts.map +1 -0
  27. package/dist/bundle/import.js +116 -0
  28. package/dist/bundle/import.js.map +1 -0
  29. package/dist/cli/index.d.ts +3 -0
  30. package/dist/cli/index.d.ts.map +1 -0
  31. package/dist/cli/index.js +1402 -0
  32. package/dist/cli/index.js.map +1 -0
  33. package/dist/cli/init.d.ts +48 -0
  34. package/dist/cli/init.d.ts.map +1 -0
  35. package/dist/cli/init.js +284 -0
  36. package/dist/cli/init.js.map +1 -0
  37. package/dist/db/schema.d.ts +3 -0
  38. package/dist/db/schema.d.ts.map +1 -0
  39. package/dist/db/schema.js +616 -0
  40. package/dist/db/schema.js.map +1 -0
  41. package/dist/db/store.d.ts +1011 -0
  42. package/dist/db/store.d.ts.map +1 -0
  43. package/dist/db/store.js +3888 -0
  44. package/dist/db/store.js.map +1 -0
  45. package/dist/graph/pagerank.d.ts +9 -0
  46. package/dist/graph/pagerank.d.ts.map +1 -0
  47. package/dist/graph/pagerank.js +47 -0
  48. package/dist/graph/pagerank.js.map +1 -0
  49. package/dist/indexer/architecture.d.ts +72 -0
  50. package/dist/indexer/architecture.d.ts.map +1 -0
  51. package/dist/indexer/architecture.js +112 -0
  52. package/dist/indexer/architecture.js.map +1 -0
  53. package/dist/indexer/behavior.d.ts +75 -0
  54. package/dist/indexer/behavior.d.ts.map +1 -0
  55. package/dist/indexer/behavior.js +395 -0
  56. package/dist/indexer/behavior.js.map +1 -0
  57. package/dist/indexer/boundaries.d.ts +60 -0
  58. package/dist/indexer/boundaries.d.ts.map +1 -0
  59. package/dist/indexer/boundaries.js +366 -0
  60. package/dist/indexer/boundaries.js.map +1 -0
  61. package/dist/indexer/churn.d.ts +15 -0
  62. package/dist/indexer/churn.d.ts.map +1 -0
  63. package/dist/indexer/churn.js +49 -0
  64. package/dist/indexer/churn.js.map +1 -0
  65. package/dist/indexer/classify.d.ts +9 -0
  66. package/dist/indexer/classify.d.ts.map +1 -0
  67. package/dist/indexer/classify.js +90 -0
  68. package/dist/indexer/classify.js.map +1 -0
  69. package/dist/indexer/context.d.ts +176 -0
  70. package/dist/indexer/context.d.ts.map +1 -0
  71. package/dist/indexer/context.js +193 -0
  72. package/dist/indexer/context.js.map +1 -0
  73. package/dist/indexer/continuity.d.ts +67 -0
  74. package/dist/indexer/continuity.d.ts.map +1 -0
  75. package/dist/indexer/continuity.js +288 -0
  76. package/dist/indexer/continuity.js.map +1 -0
  77. package/dist/indexer/detectchanges.d.ts +32 -0
  78. package/dist/indexer/detectchanges.d.ts.map +1 -0
  79. package/dist/indexer/detectchanges.js +74 -0
  80. package/dist/indexer/detectchanges.js.map +1 -0
  81. package/dist/indexer/discovery.d.ts +37 -0
  82. package/dist/indexer/discovery.d.ts.map +1 -0
  83. package/dist/indexer/discovery.js +136 -0
  84. package/dist/indexer/discovery.js.map +1 -0
  85. package/dist/indexer/externaldeps.d.ts +18 -0
  86. package/dist/indexer/externaldeps.d.ts.map +1 -0
  87. package/dist/indexer/externaldeps.js +288 -0
  88. package/dist/indexer/externaldeps.js.map +1 -0
  89. package/dist/indexer/freshness.d.ts +48 -0
  90. package/dist/indexer/freshness.d.ts.map +1 -0
  91. package/dist/indexer/freshness.js +128 -0
  92. package/dist/indexer/freshness.js.map +1 -0
  93. package/dist/indexer/git.d.ts +144 -0
  94. package/dist/indexer/git.d.ts.map +1 -0
  95. package/dist/indexer/git.js +444 -0
  96. package/dist/indexer/git.js.map +1 -0
  97. package/dist/indexer/index.d.ts +145 -0
  98. package/dist/indexer/index.d.ts.map +1 -0
  99. package/dist/indexer/index.js +930 -0
  100. package/dist/indexer/index.js.map +1 -0
  101. package/dist/indexer/modules.d.ts +62 -0
  102. package/dist/indexer/modules.d.ts.map +1 -0
  103. package/dist/indexer/modules.js +293 -0
  104. package/dist/indexer/modules.js.map +1 -0
  105. package/dist/indexer/preflight.d.ts +154 -0
  106. package/dist/indexer/preflight.d.ts.map +1 -0
  107. package/dist/indexer/preflight.js +399 -0
  108. package/dist/indexer/preflight.js.map +1 -0
  109. package/dist/indexer/protoScanner.d.ts +34 -0
  110. package/dist/indexer/protoScanner.d.ts.map +1 -0
  111. package/dist/indexer/protoScanner.js +133 -0
  112. package/dist/indexer/protoScanner.js.map +1 -0
  113. package/dist/indexer/risk.d.ts +115 -0
  114. package/dist/indexer/risk.d.ts.map +1 -0
  115. package/dist/indexer/risk.js +194 -0
  116. package/dist/indexer/risk.js.map +1 -0
  117. package/dist/indexer/serviceHostScanner.d.ts +25 -0
  118. package/dist/indexer/serviceHostScanner.d.ts.map +1 -0
  119. package/dist/indexer/serviceHostScanner.js +95 -0
  120. package/dist/indexer/serviceHostScanner.js.map +1 -0
  121. package/dist/indexer/serviceLinks.d.ts +105 -0
  122. package/dist/indexer/serviceLinks.d.ts.map +1 -0
  123. package/dist/indexer/serviceLinks.js +509 -0
  124. package/dist/indexer/serviceLinks.js.map +1 -0
  125. package/dist/indexer/shapehash.d.ts +98 -0
  126. package/dist/indexer/shapehash.d.ts.map +1 -0
  127. package/dist/indexer/shapehash.js +354 -0
  128. package/dist/indexer/shapehash.js.map +1 -0
  129. package/dist/indexer/skeleton.d.ts +15 -0
  130. package/dist/indexer/skeleton.d.ts.map +1 -0
  131. package/dist/indexer/skeleton.js +136 -0
  132. package/dist/indexer/skeleton.js.map +1 -0
  133. package/dist/indexer/symbolhistory.d.ts +41 -0
  134. package/dist/indexer/symbolhistory.d.ts.map +1 -0
  135. package/dist/indexer/symbolhistory.js +124 -0
  136. package/dist/indexer/symbolhistory.js.map +1 -0
  137. package/dist/indexer/watcher.d.ts +68 -0
  138. package/dist/indexer/watcher.d.ts.map +1 -0
  139. package/dist/indexer/watcher.js +179 -0
  140. package/dist/indexer/watcher.js.map +1 -0
  141. package/dist/mcp/server.d.ts +80 -0
  142. package/dist/mcp/server.d.ts.map +1 -0
  143. package/dist/mcp/server.js +1610 -0
  144. package/dist/mcp/server.js.map +1 -0
  145. package/dist/parser/index.d.ts +8 -0
  146. package/dist/parser/index.d.ts.map +1 -0
  147. package/dist/parser/index.js +33 -0
  148. package/dist/parser/index.js.map +1 -0
  149. package/dist/parser/languages/cpp.d.ts +3 -0
  150. package/dist/parser/languages/cpp.d.ts.map +1 -0
  151. package/dist/parser/languages/cpp.js +350 -0
  152. package/dist/parser/languages/cpp.js.map +1 -0
  153. package/dist/parser/languages/csharp.d.ts +3 -0
  154. package/dist/parser/languages/csharp.d.ts.map +1 -0
  155. package/dist/parser/languages/csharp.js +239 -0
  156. package/dist/parser/languages/csharp.js.map +1 -0
  157. package/dist/parser/languages/go.d.ts +3 -0
  158. package/dist/parser/languages/go.d.ts.map +1 -0
  159. package/dist/parser/languages/go.js +259 -0
  160. package/dist/parser/languages/go.js.map +1 -0
  161. package/dist/parser/languages/java.d.ts +3 -0
  162. package/dist/parser/languages/java.d.ts.map +1 -0
  163. package/dist/parser/languages/java.js +391 -0
  164. package/dist/parser/languages/java.js.map +1 -0
  165. package/dist/parser/languages/python.d.ts +3 -0
  166. package/dist/parser/languages/python.d.ts.map +1 -0
  167. package/dist/parser/languages/python.js +396 -0
  168. package/dist/parser/languages/python.js.map +1 -0
  169. package/dist/parser/languages/rust.d.ts +3 -0
  170. package/dist/parser/languages/rust.d.ts.map +1 -0
  171. package/dist/parser/languages/rust.js +159 -0
  172. package/dist/parser/languages/rust.js.map +1 -0
  173. package/dist/parser/languages/typescript.d.ts +3 -0
  174. package/dist/parser/languages/typescript.d.ts.map +1 -0
  175. package/dist/parser/languages/typescript.js +1442 -0
  176. package/dist/parser/languages/typescript.js.map +1 -0
  177. package/dist/parser/parserContext.d.ts +77 -0
  178. package/dist/parser/parserContext.d.ts.map +1 -0
  179. package/dist/parser/parserContext.js +354 -0
  180. package/dist/parser/parserContext.js.map +1 -0
  181. package/dist/parser/walker.d.ts +81 -0
  182. package/dist/parser/walker.d.ts.map +1 -0
  183. package/dist/parser/walker.js +217 -0
  184. package/dist/parser/walker.js.map +1 -0
  185. package/dist/parser/worker.d.ts +66 -0
  186. package/dist/parser/worker.d.ts.map +1 -0
  187. package/dist/parser/worker.js +129 -0
  188. package/dist/parser/worker.js.map +1 -0
  189. package/dist/parser/workerpool.d.ts +107 -0
  190. package/dist/parser/workerpool.d.ts.map +1 -0
  191. package/dist/parser/workerpool.js +383 -0
  192. package/dist/parser/workerpool.js.map +1 -0
  193. package/dist/scip/format.d.ts +87 -0
  194. package/dist/scip/format.d.ts.map +1 -0
  195. package/dist/scip/format.js +31 -0
  196. package/dist/scip/format.js.map +1 -0
  197. package/dist/scip/import.d.ts +37 -0
  198. package/dist/scip/import.d.ts.map +1 -0
  199. package/dist/scip/import.js +180 -0
  200. package/dist/scip/import.js.map +1 -0
  201. package/dist/types.d.ts +392 -0
  202. package/dist/types.d.ts.map +1 -0
  203. package/dist/types.js +4 -0
  204. package/dist/types.js.map +1 -0
  205. package/docs/architecture.md +105 -0
  206. package/docs/benchmarks/methodology.md +134 -0
  207. package/docs/benchmarks/raw-results.md +71 -0
  208. package/docs/benchmarks.md +74 -0
  209. package/docs/cli.md +148 -0
  210. package/docs/examples/behavior-tests.md +70 -0
  211. package/docs/examples/change-history.md +85 -0
  212. package/docs/examples/pre-edit-context.md +81 -0
  213. package/docs/examples/service-links.md +88 -0
  214. package/docs/examples.md +80 -0
  215. package/docs/faq.md +70 -0
  216. package/docs/internals.md +104 -0
  217. package/docs/languages.md +70 -0
  218. package/docs/limits.md +52 -0
  219. package/docs/mcp.md +199 -0
  220. package/docs/quickstart.md +119 -0
  221. package/docs/testing.md +123 -0
  222. package/docs/tools.md +115 -0
  223. package/package.json +52 -0
  224. package/research-codebase.md +578 -0
  225. package/seer-cli-docs.md +326 -0
  226. package/seer-master-guide.md +246 -0
  227. package/src/bundle/ci.ts +141 -0
  228. package/src/bundle/contract.ts +387 -0
  229. package/src/bundle/export.ts +175 -0
  230. package/src/bundle/external.ts +285 -0
  231. package/src/bundle/format.ts +92 -0
  232. package/src/bundle/import.ts +157 -0
  233. package/src/cli/index.ts +1249 -0
  234. package/src/cli/init.ts +389 -0
  235. package/src/db/schema.ts +614 -0
  236. package/src/db/store.ts +4306 -0
  237. package/src/graph/pagerank.ts +53 -0
  238. package/src/indexer/architecture.ts +148 -0
  239. package/src/indexer/behavior.ts +466 -0
  240. package/src/indexer/boundaries.ts +374 -0
  241. package/src/indexer/churn.ts +58 -0
  242. package/src/indexer/classify.ts +96 -0
  243. package/src/indexer/context.ts +340 -0
  244. package/src/indexer/continuity.ts +322 -0
  245. package/src/indexer/detectchanges.ts +94 -0
  246. package/src/indexer/discovery.ts +176 -0
  247. package/src/indexer/externaldeps.ts +243 -0
  248. package/src/indexer/freshness.ts +166 -0
  249. package/src/indexer/git.ts +453 -0
  250. package/src/indexer/index.ts +1092 -0
  251. package/src/indexer/modules.ts +358 -0
  252. package/src/indexer/preflight.ts +548 -0
  253. package/src/indexer/protoScanner.ts +147 -0
  254. package/src/indexer/risk.ts +304 -0
  255. package/src/indexer/serviceHostScanner.ts +92 -0
  256. package/src/indexer/serviceLinks.ts +543 -0
  257. package/src/indexer/shapehash.ts +370 -0
  258. package/src/indexer/skeleton.ts +169 -0
  259. package/src/indexer/symbolhistory.ts +172 -0
  260. package/src/indexer/watcher.ts +206 -0
  261. package/src/mcp/server.ts +1659 -0
  262. package/src/parser/index.ts +37 -0
  263. package/src/parser/languages/cpp.ts +361 -0
  264. package/src/parser/languages/csharp.ts +235 -0
  265. package/src/parser/languages/go.ts +259 -0
  266. package/src/parser/languages/java.ts +382 -0
  267. package/src/parser/languages/python.ts +370 -0
  268. package/src/parser/languages/rust.ts +164 -0
  269. package/src/parser/languages/typescript.ts +1435 -0
  270. package/src/parser/parserContext.ts +392 -0
  271. package/src/parser/walker.ts +306 -0
  272. package/src/parser/worker.ts +181 -0
  273. package/src/parser/workerpool.ts +448 -0
  274. package/src/scip/format.ts +83 -0
  275. package/src/scip/import.ts +216 -0
  276. package/src/types.ts +457 -0
  277. package/tests/benchmark-service-links.ts +244 -0
  278. package/tests/bug-regressions.ts +626 -0
  279. package/tests/filters.ts +264 -0
  280. package/tests/fixtures/Counter.tsx +38 -0
  281. package/tests/fixtures/caller.ts +7 -0
  282. package/tests/fixtures/collisions.ts +23 -0
  283. package/tests/fixtures/local_helper.ts +5 -0
  284. package/tests/fixtures/overloads.java +17 -0
  285. package/tests/fixtures/remote_helper.ts +4 -0
  286. package/tests/fixtures/sample.c +15 -0
  287. package/tests/fixtures/sample.cpp +47 -0
  288. package/tests/fixtures/sample.cs +62 -0
  289. package/tests/fixtures/sample.go +68 -0
  290. package/tests/fixtures/sample.h +30 -0
  291. package/tests/fixtures/sample.java +85 -0
  292. package/tests/fixtures/sample.py +46 -0
  293. package/tests/fixtures/sample.rs +78 -0
  294. package/tests/fixtures/sample.ts +76 -0
  295. package/tests/fixtures-service/HttpClients.cs +30 -0
  296. package/tests/fixtures-service/HttpClients.java +24 -0
  297. package/tests/fixtures-service/billing.ts +15 -0
  298. package/tests/fixtures-service/docker-compose.yml +15 -0
  299. package/tests/fixtures-service/gateway.ts +10 -0
  300. package/tests/fixtures-service/get_user.ts +11 -0
  301. package/tests/fixtures-service/graphql_client.ts +63 -0
  302. package/tests/fixtures-service/graphql_server.ts +30 -0
  303. package/tests/fixtures-service/grpc_client.go +30 -0
  304. package/tests/fixtures-service/http_clients.go +23 -0
  305. package/tests/fixtures-service/http_clients.py +38 -0
  306. package/tests/fixtures-service/http_clients.ts +49 -0
  307. package/tests/fixtures-service/k8s/payment-service.yaml +22 -0
  308. package/tests/fixtures-service/k8s_calls.ts +20 -0
  309. package/tests/fixtures-service/messaging.ts +87 -0
  310. package/tests/fixtures-service/trpc_client.ts +39 -0
  311. package/tests/fixtures-service/trpc_server.ts +39 -0
  312. package/tests/fixtures-service/user_service.proto +33 -0
  313. package/tests/fixtures-trackcd/Cargo.toml +11 -0
  314. package/tests/fixtures-trackcd/SpringController.java +36 -0
  315. package/tests/fixtures-trackcd/auth_service.ts +19 -0
  316. package/tests/fixtures-trackcd/complex_module.py +50 -0
  317. package/tests/fixtures-trackcd/express_app.js +30 -0
  318. package/tests/fixtures-trackcd/fastapi_app.py +49 -0
  319. package/tests/fixtures-trackcd/fastify_object_routes.js +32 -0
  320. package/tests/fixtures-trackcd/go.mod +8 -0
  321. package/tests/fixtures-trackcd/package.json +15 -0
  322. package/tests/fixtures-trackcd/requirements.txt +4 -0
  323. package/tests/fixtures-trackcd/tests/auth_service.test.ts +13 -0
  324. package/tests/fixtures-tracke/auth/AuthService.ts +23 -0
  325. package/tests/fixtures-tracke/auth/crypto.ts +7 -0
  326. package/tests/fixtures-tracke/billing/Billing.ts +20 -0
  327. package/tests/fixtures-tracke/billing/Invoice.ts +10 -0
  328. package/tests/fixtures-tracke/billing/server.ts +17 -0
  329. package/tests/fixtures-tracke/package.json +7 -0
  330. package/tests/fixtures-tracke/tests/auth.test.ts +23 -0
  331. package/tests/fixtures-tracke/tests/billing.test.ts +14 -0
  332. package/tests/fixtures-trackf/package.json +5 -0
  333. package/tests/fixtures-trackf/src/auth.ts +26 -0
  334. package/tests/fixtures-trackf/src/handlers.ts +35 -0
  335. package/tests/fixtures-tracki/billing/routes.ts +12 -0
  336. package/tests/fixtures-tracki/gateway/client.ts +13 -0
  337. package/tests/git-features.ts +267 -0
  338. package/tests/init.ts +141 -0
  339. package/tests/mcp-jit.ts +130 -0
  340. package/tests/mcp-smoke.ts +191 -0
  341. package/tests/mcp-trackcd.ts +169 -0
  342. package/tests/mcp-tracke.ts +229 -0
  343. package/tests/mcp-trackf.ts +330 -0
  344. package/tests/mcp-trackg.ts +219 -0
  345. package/tests/mcp-tracki.ts +174 -0
  346. package/tests/mcp-watcher.ts +126 -0
  347. package/tests/optspec.ts +194 -0
  348. package/tests/parallel-index.ts +333 -0
  349. package/tests/parallel-read.ts +125 -0
  350. package/tests/parallel-recovery.ts +241 -0
  351. package/tests/perf-callers.ts +145 -0
  352. package/tests/query-parity.ts +184 -0
  353. package/tests/query-perf.ts +55 -0
  354. package/tests/scale-parallel-parity.ts +225 -0
  355. package/tests/scale-test.ts +523 -0
  356. package/tests/smoke.ts +396 -0
  357. package/tests/trackcd.ts +325 -0
  358. package/tests/tracke-collisions.ts +255 -0
  359. package/tests/tracke.ts +314 -0
  360. package/tests/trackf-bugs.ts +406 -0
  361. package/tests/trackf.ts +390 -0
  362. package/tests/trackg.ts +1372 -0
  363. package/tests/tracki-boundaries.ts +202 -0
  364. package/tests/tracki-continuity.ts +253 -0
  365. package/tests/tracki-contract-diff.ts +249 -0
  366. package/tests/tracki-external-bundles.ts +341 -0
  367. package/tests/tracki-preflight.ts +251 -0
  368. package/tests/verify-roles.ts +51 -0
  369. package/tests/worker-parity.ts +286 -0
  370. package/tests/worker-pool.ts +262 -0
  371. package/tsconfig.json +20 -0
@@ -0,0 +1,626 @@
1
+ /**
2
+ * Regression tests for the audit and scale bugs found after Track C/D.
3
+ * Each section reproduces the original failure mode against the fixed code
4
+ * so a future regression in any of these areas trips immediately.
5
+ *
6
+ * 1. v3→v4 migration leaves cached files without symbol_key + FTS rows
7
+ * 2. churn-before-history makes buildSymbolHistory skip
8
+ * 3. Spring class-level @RequestMapping("/api") emits bogus route + drops prefix
9
+ * 4. Symbol history drops author email
10
+ * 5. File rename: --follow finds commits but diff lookup misses them
11
+ * 6. Fastify object-style app.route({ method, url, handler }) not extracted
12
+ * 7. Large repos spend minutes in test-edge duplicate checks without a
13
+ * composite edges(from_id, to_id, kind) index
14
+ * 8. Same-file edge resolution needs symbols(file_id, name) for Godot scale
15
+ * 9. FastAPI @app.post/@app.get decorators mis-detected as outbound
16
+ * http-client service_calls (route emitted on the parent decorator node, so
17
+ * the walker's node-level de-dup missed it), producing route self-links
18
+ * 10. TS template-literal path params (`/api/users/${id}`) dropped the dynamic
19
+ * segment, collapsing to `/api/users` and breaking route-pattern matching
20
+ * 11. C++ out-of-line method defs (`Vec<T>::dot`) dropped the class scope from
21
+ * the qualified name (`geo.dot` instead of `geo.Vec.dot`) — enhancement
22
+ * 12. getDefinition `file` filter was exact-match only; a basename couldn't
23
+ * disambiguate (`svc.ts` vs `src/svc.ts`) — enhancement
24
+ *
25
+ * Run with: npx tsx tests/bug-regressions.ts
26
+ */
27
+
28
+ import path from 'path';
29
+ import fs from 'fs';
30
+ import os from 'os';
31
+ import { DatabaseSync } from 'node:sqlite';
32
+ import { spawnSync } from 'child_process';
33
+ import { Store } from '../src/db/store';
34
+ import { Indexer } from '../src/indexer/index';
35
+ import { collectChurn } from '../src/indexer/churn';
36
+ import { buildSymbolHistory } from '../src/indexer/symbolhistory';
37
+ import { parseFollowLog } from '../src/indexer/git';
38
+ import { resolveServiceLinks } from '../src/indexer/serviceLinks';
39
+
40
+ let passed = 0;
41
+ let failed = 0;
42
+
43
+ function assert(cond: boolean, msg: string): void {
44
+ if (cond) { console.log(` ✓ ${msg}`); passed++; }
45
+ else { console.error(` ✗ ${msg}`); failed++; }
46
+ }
47
+
48
+ function git(repo: string, ...args: string[]): { stdout: string; status: number } {
49
+ const r = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
50
+ return { stdout: r.stdout ?? '', status: r.status ?? 1 };
51
+ }
52
+
53
+ function makeGitRepo(repo: string): void {
54
+ fs.mkdirSync(repo, { recursive: true });
55
+ const initRes = spawnSync('git', ['-C', repo, 'init', '-q', '-b', 'main'], { encoding: 'utf8' });
56
+ if (initRes.status !== 0) spawnSync('git', ['-C', repo, 'init', '-q'], { encoding: 'utf8' });
57
+ git(repo, 'config', 'commit.gpgsign', 'false');
58
+ git(repo, 'config', 'user.email', 'tester@example.com');
59
+ git(repo, 'config', 'user.name', 'Tester');
60
+ }
61
+
62
+ function commit(repo: string, msg: string): string {
63
+ spawnSync('git', ['-C', repo, 'add', '.'], { encoding: 'utf8' });
64
+ const r = spawnSync('git', ['-C', repo, 'commit', '-m', msg, '--no-gpg-sign'], { encoding: 'utf8' });
65
+ if (r.status !== 0) throw new Error(`git commit failed: ${r.stderr}`);
66
+ return git(repo, 'rev-parse', 'HEAD').stdout.trim();
67
+ }
68
+
69
+ // ── Bug 1: v3→v4 migration backfills symbol_key + FTS ──────────────────────
70
+ async function bug1_v3MigrationBackfill(): Promise<void> {
71
+ console.log('\n── Bug 1: v3→v4 migration backfills symbol_key + FTS rows ──');
72
+ const tmp = path.join(os.tmpdir(), `seer-bug1-${Date.now()}.db`);
73
+ // Hand-build a "v3" DB: schema version 3, no v4 columns/tables/FTS.
74
+ const db = new DatabaseSync(tmp);
75
+ db.exec(`
76
+ CREATE TABLE _schema_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
77
+ INSERT INTO _schema_meta (key, value) VALUES ('schema_version', '3');
78
+ CREATE TABLE files (
79
+ id INTEGER PRIMARY KEY, path TEXT NOT NULL UNIQUE, rel_path TEXT NOT NULL,
80
+ language TEXT NOT NULL, hash TEXT NOT NULL, lines INTEGER NOT NULL DEFAULT 0,
81
+ indexed_at INTEGER NOT NULL,
82
+ role TEXT NOT NULL DEFAULT 'project',
83
+ is_vendor INTEGER NOT NULL DEFAULT 0, is_generated INTEGER NOT NULL DEFAULT 0
84
+ );
85
+ CREATE TABLE symbols (
86
+ id INTEGER PRIMARY KEY, name TEXT NOT NULL, qualified_name TEXT, kind TEXT NOT NULL,
87
+ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
88
+ line_start INTEGER NOT NULL DEFAULT 0, line_end INTEGER NOT NULL DEFAULT 0,
89
+ col_start INTEGER NOT NULL DEFAULT 0, col_end INTEGER NOT NULL DEFAULT 0,
90
+ signature TEXT, pagerank REAL NOT NULL DEFAULT 0.15,
91
+ is_rankable INTEGER NOT NULL DEFAULT 1
92
+ );
93
+ CREATE TABLE edges (
94
+ id INTEGER PRIMARY KEY, from_id INTEGER NOT NULL REFERENCES symbols(id) ON DELETE CASCADE,
95
+ to_name TEXT NOT NULL, to_id INTEGER REFERENCES symbols(id) ON DELETE SET NULL,
96
+ kind TEXT NOT NULL DEFAULT 'call', line INTEGER NOT NULL DEFAULT 0
97
+ );
98
+ CREATE TABLE file_imports (
99
+ id INTEGER PRIMARY KEY, from_file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
100
+ import_name TEXT NOT NULL, resolved_file_id INTEGER REFERENCES files(id) ON DELETE SET NULL
101
+ );
102
+ INSERT INTO files (path, rel_path, language, hash, lines, indexed_at)
103
+ VALUES ('/x/a.ts', 'a.ts', 'typescript', 'fakehash', 10, 0);
104
+ INSERT INTO symbols (name, qualified_name, kind, file_id, line_start, line_end)
105
+ VALUES ('foo', 'foo', 'function', 1, 0, 5),
106
+ ('bar', 'Klass.bar', 'method', 1, 6, 9);
107
+ `);
108
+ db.close();
109
+
110
+ // Open through Store — runs v3→v4 migration including the new backfill.
111
+ const s = new Store(tmp);
112
+ const raw = s.rawDb();
113
+ const nullKeys = raw.prepare('SELECT COUNT(*) AS c FROM symbols WHERE symbol_key IS NULL').get() as { c: number };
114
+ const ftsSyms = raw.prepare('SELECT COUNT(*) AS c FROM symbols_fts').get() as { c: number };
115
+ const ftsFiles = raw.prepare('SELECT COUNT(*) AS c FROM files_fts').get() as { c: number };
116
+
117
+ assert(s.schemaInfo().dbVersion === 10, `schema migrated to v9 (got ${s.schemaInfo().dbVersion})`);
118
+ assert(nullKeys.c === 0, `symbol_key backfilled for every pre-v4 symbol (got ${nullKeys.c} NULL)`);
119
+ assert(ftsSyms.c === 2, `symbols_fts rebuilt from existing symbols (got ${ftsSyms.c} rows, expected 2)`);
120
+ assert(ftsFiles.c === 1, `files_fts rebuilt from existing files (got ${ftsFiles.c} rows, expected 1)`);
121
+
122
+ // The keys should match makeSymbolKey() format: `kind:qualified_name`.
123
+ const sample = raw.prepare("SELECT name, symbol_key FROM symbols WHERE name = 'bar'").get() as { name: string; symbol_key: string };
124
+ assert(sample.symbol_key === 'method:Klass.bar', `bar.symbol_key = method:Klass.bar (got ${sample.symbol_key})`);
125
+
126
+ // FTS query should actually find the migrated symbols (full integration).
127
+ const ftsHits = raw.prepare(`SELECT rowid FROM symbols_fts WHERE symbols_fts MATCH ?`).all('"foo"*') as Array<{ rowid: number }>;
128
+ assert(ftsHits.length === 1, `FTS query 'foo*' finds the migrated symbol (got ${ftsHits.length} hits)`);
129
+
130
+ s.close();
131
+ fs.unlinkSync(tmp);
132
+ }
133
+
134
+ // ── Bug 2: churn before history must not poison the history skip guard ────
135
+ async function bug2_churnHistoryClash(): Promise<void> {
136
+ console.log('\n── Bug 2: churn before history does NOT make history skip ──');
137
+ const tmp = path.join(os.tmpdir(), `seer-bug2-${Date.now()}`);
138
+ const repo = path.join(tmp, 'repo');
139
+ makeGitRepo(repo);
140
+ fs.writeFileSync(path.join(repo, 'a.ts'), 'export function foo() { return 1; }\n');
141
+ commit(repo, 'init');
142
+
143
+ const dbPath = path.join(tmp, 'g.db');
144
+ const store = new Store(dbPath);
145
+ const indexer = new Indexer(store);
146
+ await indexer.indexDirectory(repo, { quiet: true });
147
+
148
+ // The original bug: running churn first stamps git_index_state.last_head_sha,
149
+ // and a subsequent symbol-history build sees "HEAD unchanged" and skips —
150
+ // even though history has never been built. The fix is a separate
151
+ // last_history_head_sha column that only history touches.
152
+ const ch = await collectChurn(repo, store);
153
+ assert(ch.headSha !== null, 'churn stamped git_index_state.last_head_sha');
154
+
155
+ const sh1 = await buildSymbolHistory(repo, store, { log: () => {} });
156
+ assert(sh1.skipped === false, `history must NOT skip after churn (skipped=${sh1.skipped})`);
157
+ assert(sh1.historyRowsInserted >= 1, `history inserted ≥1 row (got ${sh1.historyRowsInserted})`);
158
+
159
+ // Now run again — this time it SHOULD skip because history already ran
160
+ // against the same HEAD.
161
+ const sh2 = await buildSymbolHistory(repo, store, { log: () => {} });
162
+ assert(sh2.skipped === true, `second history call skips because HEAD unchanged`);
163
+
164
+ // And running churn AGAIN after history must not undo the history-skip
165
+ // signal — i.e. churn's stamp is separate.
166
+ await collectChurn(repo, store);
167
+ const sh3 = await buildSymbolHistory(repo, store, { log: () => {} });
168
+ assert(sh3.skipped === true, `history still skips after a fresh churn pass`);
169
+
170
+ store.close();
171
+ fs.rmSync(tmp, { recursive: true, force: true });
172
+ }
173
+
174
+ // ── Bug 3: Spring class-level @RequestMapping prefixes method routes ──────
175
+ async function bug3_springClassPrefix(): Promise<void> {
176
+ console.log('\n── Bug 3: Spring class-level @RequestMapping("/api") prefix ──');
177
+ const tmp = path.join(os.tmpdir(), `seer-bug3-${Date.now()}`);
178
+ fs.mkdirSync(tmp, { recursive: true });
179
+ fs.writeFileSync(path.join(tmp, 'A.java'), `
180
+ package x;
181
+ import org.springframework.web.bind.annotation.*;
182
+
183
+ @RestController
184
+ @RequestMapping("/api")
185
+ class A {
186
+ @GetMapping("/users") String list() { return "u"; }
187
+ @PostMapping(value = "/users") String create() { return "c"; }
188
+ @RequestMapping(value = "/items", method = RequestMethod.GET) String items() { return "i"; }
189
+ }
190
+ `.trimStart());
191
+ // Class with NO @RequestMapping — method paths should pass through unchanged.
192
+ fs.writeFileSync(path.join(tmp, 'B.java'), `
193
+ package x;
194
+ import org.springframework.web.bind.annotation.*;
195
+
196
+ @RestController
197
+ class B {
198
+ @GetMapping("/bare") String bare() { return "b"; }
199
+ }
200
+ `.trimStart());
201
+
202
+ const store = new Store(path.join(tmp, 'g.db'));
203
+ const indexer = new Indexer(store);
204
+ await indexer.indexDirectory(tmp, { quiet: true });
205
+
206
+ const routes = store.listRoutes({ framework: 'spring', limit: 50 });
207
+ const paths = routes.map(r => `${r.method} ${r.path}`).sort();
208
+
209
+ assert(!routes.some(r => r.path === '/api'),
210
+ `class-level @RequestMapping does NOT emit a bare /api route (paths: ${JSON.stringify(paths)})`);
211
+ assert(routes.some(r => r.method === 'GET' && r.path === '/api/users'),
212
+ 'GET /api/users (class prefix + method path)');
213
+ assert(routes.some(r => r.method === 'POST' && r.path === '/api/users'),
214
+ 'POST /api/users (class prefix + method path)');
215
+ assert(routes.some(r => r.method === 'GET' && r.path === '/api/items'),
216
+ 'GET /api/items (RequestMapping with method= and class prefix)');
217
+ assert(routes.some(r => r.method === 'GET' && r.path === '/bare'),
218
+ 'GET /bare (no class prefix — pass-through unchanged)');
219
+
220
+ // Handler resolution should still work — routes link back to the methods.
221
+ const listRoute = routes.find(r => r.path === '/api/users' && r.method === 'GET');
222
+ assert(listRoute?.handlerName === 'list', `class-prefixed route still resolves handler (got ${listRoute?.handlerName})`);
223
+
224
+ store.close();
225
+ fs.rmSync(tmp, { recursive: true, force: true });
226
+ }
227
+
228
+ // ── Bug 4 + 5: rename history + author email ──────────────────────────────
229
+ async function bug4and5_renameAndEmail(): Promise<void> {
230
+ console.log('\n── Bugs 4 + 5: file rename + author email in symbol history ──');
231
+ const tmp = path.join(os.tmpdir(), `seer-bug45-${Date.now()}`);
232
+ const repo = path.join(tmp, 'repo');
233
+ makeGitRepo(repo);
234
+
235
+ // sha1: create old.ts
236
+ fs.writeFileSync(path.join(repo, 'old.ts'), 'export function helper() { return 1; }\n');
237
+ const sha1 = commit(repo, 'init old.ts');
238
+
239
+ // sha2: rename old.ts → new.ts (no content change)
240
+ spawnSync('git', ['-C', repo, 'mv', 'old.ts', 'new.ts'], { encoding: 'utf8' });
241
+ const sha2 = commit(repo, 'rename to new.ts');
242
+
243
+ // sha3: modify helper in new.ts
244
+ fs.writeFileSync(path.join(repo, 'new.ts'), 'export function helper() { return 2; }\n');
245
+ const sha3 = commit(repo, 'change return value');
246
+
247
+ const dbPath = path.join(tmp, 'g.db');
248
+ const store = new Store(dbPath);
249
+ const indexer = new Indexer(store);
250
+ await indexer.indexDirectory(repo, { quiet: true });
251
+
252
+ const r = await buildSymbolHistory(repo, store, { log: () => {} });
253
+ assert(r.historyRowsInserted >= 2,
254
+ `history has ≥2 rows (got ${r.historyRowsInserted})`);
255
+
256
+ const helperDef = store.getDefinition('helper')[0];
257
+ assert(helperDef !== undefined, 'helper symbol indexed in new.ts');
258
+ const hist = store.getSymbolHistory(helperDef.id, { limit: 20 });
259
+
260
+ // Bug 4: every commit must carry the real author email — never ''.
261
+ for (const h of hist) {
262
+ assert(h.authorEmail !== null && h.authorEmail !== '',
263
+ `commit ${h.commitSha.slice(0,8)} has non-empty author email (got '${h.authorEmail}')`);
264
+ }
265
+
266
+ // Bug 5: pre-rename commit must appear in history.
267
+ assert(hist.some(h => h.commitSha === sha1),
268
+ `pre-rename sha1=${sha1.slice(0,8)} appears in history (got [${hist.map(h => h.commitSha.slice(0,8)).join(',')}])`);
269
+ // Post-rename change should also appear.
270
+ assert(hist.some(h => h.commitSha === sha3),
271
+ `post-rename sha3=${sha3.slice(0,8)} appears in history`);
272
+
273
+ store.close();
274
+ fs.rmSync(tmp, { recursive: true, force: true });
275
+ }
276
+
277
+ // ── parseFollowLog unit test: messages with quirky content ────────────────
278
+ function bug5_parseFollowLogEdgeCases(): void {
279
+ console.log('\n── Bug 5 (extra): parseFollowLog handles messy commit content ──');
280
+ // Synthetic git log output exercising:
281
+ // - multi-line messages (with blank lines and a literal "__C__" substring)
282
+ // - rename entry (R100) — pathAtCommit should be the NEW path
283
+ // - modify entry (M)
284
+ // - root commit added entry (A)
285
+ const buf = [
286
+ '__C__aaaaaa\tAlice\talice@x\t2026-05-28T10:00:00Z',
287
+ 'tweak helper',
288
+ '',
289
+ 'Fixes __C__ in formatting',
290
+ '__BODY_END__',
291
+ '',
292
+ 'M\tnew.ts',
293
+ '',
294
+ '__C__bbbbbb\tBob\tbob@x\t2026-05-27T10:00:00Z',
295
+ 'rename',
296
+ '__BODY_END__',
297
+ '',
298
+ 'R100\told.ts\tnew.ts',
299
+ '',
300
+ '__C__cccccc\tAlice\talice@x\t2026-05-26T10:00:00Z',
301
+ 'init',
302
+ '__BODY_END__',
303
+ '',
304
+ 'A\told.ts',
305
+ ].join('\n');
306
+ const commits = parseFollowLog(buf);
307
+ assert(commits.length === 3, `parsed 3 commits (got ${commits.length})`);
308
+ assert(commits[0].sha === 'aaaaaa', 'commit 0 sha');
309
+ assert(commits[0].authorEmail === 'alice@x', `commit 0 email (got ${commits[0].authorEmail})`);
310
+ assert(commits[0].pathAtCommit === 'new.ts', `commit 0 modify pathAtCommit = new.ts (got ${commits[0].pathAtCommit})`);
311
+ assert(commits[0].message.includes('Fixes __C__ in formatting'),
312
+ `commit 0 message preserves literal __C__ inside body (got: ${JSON.stringify(commits[0].message)})`);
313
+ assert(commits[1].pathAtCommit === 'new.ts',
314
+ `commit 1 rename pathAtCommit = NEW path (got ${commits[1].pathAtCommit})`);
315
+ assert(commits[2].pathAtCommit === 'old.ts',
316
+ `commit 2 add pathAtCommit = old.ts (got ${commits[2].pathAtCommit})`);
317
+ }
318
+
319
+ // ── Bug 6: Fastify object-style routes ────────────────────────────────────
320
+ async function bug6_fastifyObjectRoutes(): Promise<void> {
321
+ console.log('\n── Bug 6: Fastify object-style app.route({ method, url, handler }) ──');
322
+ const tmp = path.join(os.tmpdir(), `seer-bug6-${Date.now()}`);
323
+ fs.mkdirSync(tmp, { recursive: true });
324
+ fs.copyFileSync(
325
+ path.join(__dirname, 'fixtures-trackcd', 'fastify_object_routes.js'),
326
+ path.join(tmp, 'fastify_object_routes.js'),
327
+ );
328
+ const store = new Store(path.join(tmp, 'g.db'));
329
+ const indexer = new Indexer(store);
330
+ await indexer.indexDirectory(tmp, { quiet: true });
331
+
332
+ const routes = store.listRoutes({ framework: 'fastify', limit: 50 });
333
+ const summary = routes.map(r => `${r.method} ${r.path}`).sort();
334
+
335
+ assert(routes.some(r => r.method === 'GET' && r.path === '/things/:id'),
336
+ `Fastify object-style GET extracted (paths: ${JSON.stringify(summary)})`);
337
+ assert(routes.some(r => r.method === 'PUT' && r.path === '/things/:id'),
338
+ `Fastify object-style method=['PUT','PATCH'] emits PUT route`);
339
+ assert(routes.some(r => r.method === 'PATCH' && r.path === '/things/:id'),
340
+ `Fastify object-style method=['PUT','PATCH'] also emits PATCH route`);
341
+ assert(routes.some(r => r.method === 'DELETE' && r.path === '/things/:id'),
342
+ `Fastify object-style with fields out-of-order still extracts (handler-first form)`);
343
+
344
+ // Handler names should be resolved across all object-style routes.
345
+ const getR = routes.find(r => r.method === 'GET' && r.path === '/things/:id');
346
+ assert(getR?.handlerName === 'getThing', `Fastify GET handler resolved (got ${getR?.handlerName})`);
347
+ const putR = routes.find(r => r.method === 'PUT' && r.path === '/things/:id');
348
+ assert(putR?.handlerName === 'upsertThing', `Fastify PUT handler resolved (got ${putR?.handlerName})`);
349
+
350
+ // Negative: app.route() with non-literal url must NOT emit a route. We
351
+ // verify by adding a second fixture inline and re-indexing.
352
+ fs.writeFileSync(path.join(tmp, 'dynamic.js'), `
353
+ const fastify = require('fastify')();
354
+ const url = computeUrl();
355
+ fastify.route({ method: 'GET', url: url, handler: doStuff });
356
+ `.trimStart());
357
+ store.close();
358
+ const store2 = new Store(path.join(tmp, 'g.db'));
359
+ const indexer2 = new Indexer(store2);
360
+ await indexer2.indexDirectory(tmp, { quiet: true });
361
+ const dynRoutes = store2.listRoutes({ framework: 'fastify', limit: 50 })
362
+ .filter(r => !r.handlerName || r.handlerName === 'doStuff');
363
+ assert(!dynRoutes.some(r => r.handlerName === 'doStuff'),
364
+ `Fastify object-style with dynamic url is NOT extracted (would be a false positive)`);
365
+
366
+ store2.close();
367
+ fs.rmSync(tmp, { recursive: true, force: true });
368
+ }
369
+
370
+ // ── Bug 7: test-edge duplicate check needs a composite edge index ───────────
371
+ function bug7_testEdgeDuplicateIndex(): void {
372
+ console.log('\n── Bug 7: test-edge duplicate check uses composite edge index ──');
373
+ const tmp = path.join(os.tmpdir(), `seer-bug7-${Date.now()}.db`);
374
+ const store = new Store(tmp);
375
+ const raw = store.rawDb();
376
+
377
+ const indexCols = raw.prepare(`PRAGMA index_info(idx_edges_from_to_kind)`).all() as Array<{ name: string }>;
378
+ const colNames = indexCols.map(c => c.name).join(',');
379
+ assert(colNames === 'from_id,to_id,kind',
380
+ `idx_edges_from_to_kind columns are from_id,to_id,kind (got ${colNames})`);
381
+
382
+ const plan = raw.prepare(`
383
+ EXPLAIN QUERY PLAN
384
+ SELECT e.from_id, e.to_id, e.to_name, e.line
385
+ FROM edges e
386
+ JOIN symbols s ON s.id = e.from_id
387
+ JOIN files fs ON fs.id = s.file_id
388
+ JOIN symbols t ON t.id = e.to_id
389
+ JOIN files ft ON ft.id = t.file_id
390
+ WHERE e.kind = 'call'
391
+ AND fs.role = 'test'
392
+ AND ft.role <> 'test'
393
+ AND NOT EXISTS (
394
+ SELECT 1 FROM edges e2
395
+ WHERE e2.from_id = e.from_id
396
+ AND e2.to_id = e.to_id
397
+ AND e2.kind = 'tests'
398
+ )
399
+ `).all() as Array<{ detail: string }>;
400
+
401
+ const details = plan.map(p => p.detail).join(' | ');
402
+ assert(details.includes('idx_edges_from_to_kind') && details.includes('from_id=?') && details.includes('to_id=?') && details.includes('kind=?'),
403
+ `duplicate check probes idx_edges_from_to_kind (plan: ${details})`);
404
+
405
+ store.close();
406
+ fs.unlinkSync(tmp);
407
+ }
408
+
409
+ // ── Bug 8: same-file edge resolution needs a composite symbol lookup index ──
410
+ function bug8_sameFileResolutionIndex(): void {
411
+ console.log('\n── Bug 8: same-file edge resolution uses symbols(file_id, name) index ──');
412
+ const tmp = path.join(os.tmpdir(), `seer-bug8-${Date.now()}.db`);
413
+ const store = new Store(tmp);
414
+ const raw = store.rawDb();
415
+
416
+ const indexCols = raw.prepare(`PRAGMA index_info(idx_symbols_file_name)`).all() as Array<{ name: string }>;
417
+ const colNames = indexCols.map(c => c.name).join(',');
418
+ assert(colNames === 'file_id,name',
419
+ `idx_symbols_file_name columns are file_id,name (got ${colNames})`);
420
+
421
+ const plan = raw.prepare(`
422
+ EXPLAIN QUERY PLAN
423
+ UPDATE edges
424
+ SET to_id = (
425
+ SELECT t.id
426
+ FROM symbols t, symbols s
427
+ WHERE s.id = edges.from_id
428
+ AND t.name = edges.to_name
429
+ AND t.file_id = s.file_id
430
+ LIMIT 1
431
+ )
432
+ WHERE to_id IS NULL
433
+ AND EXISTS (
434
+ SELECT 1
435
+ FROM symbols t, symbols s
436
+ WHERE s.id = edges.from_id
437
+ AND t.name = edges.to_name
438
+ AND t.file_id = s.file_id
439
+ );
440
+ `).all() as Array<{ detail: string }>;
441
+
442
+ const details = plan.map(p => p.detail).join(' | ');
443
+ assert(details.includes('idx_symbols_file_name') && details.includes('file_id=?') && details.includes('name=?'),
444
+ `same-file resolution probes idx_symbols_file_name (plan: ${details})`);
445
+
446
+ store.close();
447
+ fs.unlinkSync(tmp);
448
+ }
449
+
450
+ // ── Bug 9: FastAPI @app.post(...) decorator mis-detected as outbound call ───
451
+ // The Python service-call extractor fired on the inner `call` of a decorator
452
+ // (`@app.post("/x")`), emitting a phantom http-client service_call. The route
453
+ // was emitted on the parent `decorator` node, so the walker's node-level
454
+ // route/service-call de-dup never suppressed it. The resolver then self-linked
455
+ // the service's own route to a phantom client call.
456
+ async function bug9_fastapiDecoratorNotServiceCall(): Promise<void> {
457
+ console.log('\n── Bug 9: FastAPI route decorators are not outbound service calls ──');
458
+ const tmp = path.join(os.tmpdir(), `seer-bug9-${Date.now()}`);
459
+ fs.mkdirSync(tmp, { recursive: true });
460
+ fs.writeFileSync(path.join(tmp, 'api.py'), [
461
+ 'from fastapi import FastAPI',
462
+ 'import requests',
463
+ 'app = FastAPI()',
464
+ '',
465
+ '@app.post("/api/payments/charge")',
466
+ 'def charge_handler(amount: int):',
467
+ ' requests.post("http://notify/notify", json={})',
468
+ ' return {"ok": True}',
469
+ '',
470
+ '@app.get("/api/payments/{payment_id}")',
471
+ 'def get_payment(payment_id: str):',
472
+ ' return {"id": payment_id}',
473
+ '',
474
+ ].join('\n'));
475
+ const store = new Store(path.join(tmp, 'g.db'));
476
+ const indexer = new Indexer(store);
477
+ await indexer.indexDirectory(tmp, { quiet: true });
478
+
479
+ const calls = store.listServiceCalls({ limit: 100 });
480
+ const targets = calls.map(c => `${c.framework}:${c.normalizedPath ?? c.rawTarget}`);
481
+ // The only legitimate outbound call is requests.post -> /notify.
482
+ assert(calls.length === 1 && calls[0].framework === 'requests',
483
+ `exactly one outbound call (requests.post), got ${JSON.stringify(targets)}`);
484
+ assert(!calls.some(c => c.framework === 'http-client'),
485
+ `no phantom http-client service_call from @app.post/@app.get decorators`);
486
+ // Routes must still be detected.
487
+ const routes = store.listRoutes({ framework: 'fastapi', limit: 50 });
488
+ assert(routes.some(r => r.method === 'POST' && r.path === '/api/payments/charge'),
489
+ `FastAPI POST route still extracted`);
490
+ // No self-link: a route should never link to its own registration site.
491
+ resolveServiceLinks(store);
492
+ const links = store.listServiceLinks({ limit: 100 });
493
+ assert(!links.some(l => /payments\/charge/.test(l.routePath ?? '') && /payments\/charge/.test(l.callRawTarget ?? '')),
494
+ `no self-link of /api/payments/charge route to a phantom client call`);
495
+
496
+ store.close();
497
+ fs.rmSync(tmp, { recursive: true, force: true });
498
+ }
499
+
500
+ // ── Bug 10: TS template-literal path param dropped, breaking segment match ──
501
+ // `axios.get(`http://svc/api/users/${id}`)` collapsed to `/api/users` (the
502
+ // `${id}` segment vanished), so it could not match a `/api/users/:id` route.
503
+ // Non-env substitutions now emit a `:param` placeholder segment.
504
+ async function bug10_tsTemplatePathParam(): Promise<void> {
505
+ console.log('\n── Bug 10: TS template path param preserved as a segment ──');
506
+ const tmp = path.join(os.tmpdir(), `seer-bug10-${Date.now()}`);
507
+ fs.mkdirSync(tmp, { recursive: true });
508
+ fs.writeFileSync(path.join(tmp, 'client.ts'), [
509
+ 'import axios from "axios";',
510
+ 'export async function getUser(id: string) {',
511
+ ' return axios.get(`http://svc/api/users/${id}`);',
512
+ '}',
513
+ 'export async function charge() {',
514
+ ' return fetch(`${process.env.PAY_URL}/charge`, { method: "POST" });',
515
+ '}',
516
+ ].join('\n'));
517
+ const store = new Store(path.join(tmp, 'g.db'));
518
+ const indexer = new Indexer(store);
519
+ await indexer.indexDirectory(tmp, { quiet: true });
520
+
521
+ const calls = store.listServiceCalls({ limit: 100 });
522
+ const user = calls.find(c => /users/.test(c.normalizedPath ?? c.rawTarget));
523
+ assert(!!user && user.normalizedPath === '/api/users/:param',
524
+ `template param preserved: /api/users/:param (got ${user?.normalizedPath})`);
525
+ // env-base substitution still recovers the env key and the literal tail only.
526
+ const pay = calls.find(c => /charge/.test(c.normalizedPath ?? c.rawTarget));
527
+ assert(!!pay && pay.normalizedPath === '/charge' && pay.envKey === 'PAY_URL',
528
+ `env-base substitution still yields /charge + envKey=PAY_URL (got ${pay?.normalizedPath}, ${pay?.envKey})`);
529
+
530
+ store.close();
531
+ fs.rmSync(tmp, { recursive: true, force: true });
532
+ }
533
+
534
+ // ── Bug 11: C++ out-of-line method definitions reconstruct class scope ──────
535
+ // `T Vec<T>::dot(...)` defined at namespace scope used to qualify as `geo.dot`
536
+ // (reads like a free function). It now folds the owner scope from the
537
+ // qualified_identifier declarator into the qualified name -> `geo.Vec.dot`,
538
+ // and keeps `Foo::bar` / `Baz::bar` distinct instead of collapsing to bar/bar#1.
539
+ async function bug11_cppOutOfLineScope(): Promise<void> {
540
+ console.log('\n── Bug 11: C++ out-of-line method qualified-name class scope ──');
541
+ const tmp = path.join(os.tmpdir(), `seer-bug11-${Date.now()}`);
542
+ fs.mkdirSync(tmp, { recursive: true });
543
+ fs.writeFileSync(path.join(tmp, 'shapes.cpp'), [
544
+ 'namespace geo {',
545
+ 'template <typename T> class Vec {',
546
+ 'public:',
547
+ ' T dot(const Vec& o) const;', // in-class declaration
548
+ '};',
549
+ 'template <typename T> T Vec<T>::dot(const Vec& o) const { return o.x; }', // out-of-line def
550
+ 'struct Mat { double det() const; };',
551
+ 'double Mat::det() const { return 0.0; }', // distinct owner, same idea
552
+ '}',
553
+ '',
554
+ ].join('\n'));
555
+ const store = new Store(path.join(tmp, 'g.db'));
556
+ const indexer = new Indexer(store);
557
+ await indexer.indexDirectory(tmp, { quiet: true });
558
+
559
+ const dot = store.getDefinition('dot', { includeDeclarations: true });
560
+ assert(dot.length >= 1 && dot.every(d => d.qualifiedName === 'geo.Vec.dot'),
561
+ `out-of-line Vec::dot qualified geo.Vec.dot (got ${JSON.stringify(dot.map(d => d.qualifiedName))})`);
562
+ const det = store.getDefinition('det', { includeDeclarations: true });
563
+ assert(det.some(d => d.qualifiedName === 'geo.Mat.det'),
564
+ `out-of-line Mat::det qualified geo.Mat.det (got ${JSON.stringify(det.map(d => d.qualifiedName))})`);
565
+ // The two distinct owners must NOT collapse into bar / bar#1 style siblings.
566
+ assert(!dot.some(d => /#/.test(d.qualifiedName ?? '')) && !det.some(d => /#/.test(d.qualifiedName ?? '')),
567
+ `distinct owners do not collapse into #N overload siblings`);
568
+
569
+ store.close();
570
+ fs.rmSync(tmp, { recursive: true, force: true });
571
+ }
572
+
573
+ // ── Bug 12: getDefinition file filter accepts a path-suffix on a boundary ───
574
+ // The `file` disambiguator was exact-match only; a basename like `svc.ts`
575
+ // could not match rel_path `src/svc.ts`, silently returning nothing. It now
576
+ // matches a trailing fragment on a `/` boundary, with LIKE metachars escaped.
577
+ async function bug12_fileFilterSuffixMatch(): Promise<void> {
578
+ console.log('\n── Bug 12: getDefinition file filter accepts path-suffix ──');
579
+ const tmp = path.join(os.tmpdir(), `seer-bug12-${Date.now()}`);
580
+ fs.mkdirSync(path.join(tmp, 'src'), { recursive: true });
581
+ fs.writeFileSync(path.join(tmp, 'src/svc.ts'), 'export function handler() { return 1; }\n');
582
+ fs.writeFileSync(path.join(tmp, 'src/o_auth.ts'), 'export function tokenize() { return 2; }\n');
583
+ const store = new Store(path.join(tmp, 'g.db'));
584
+ const indexer = new Indexer(store);
585
+ await indexer.indexDirectory(tmp, { quiet: true });
586
+
587
+ assert(store.getDefinition('handler', { filePath: 'svc.ts' }).length === 1,
588
+ `basename 'svc.ts' resolves to src/svc.ts`);
589
+ assert(store.getDefinition('handler', { filePath: 'src/svc.ts' }).length === 1,
590
+ `exact rel_path still resolves`);
591
+ assert(store.getDefinition('handler', { filePath: 'vc.ts' }).length === 0,
592
+ `non-boundary partial 'vc.ts' does NOT match (segment boundary enforced)`);
593
+ // `_` must not behave as a LIKE wildcard: 'oXauth.ts' must not match o_auth.ts.
594
+ assert(store.getDefinition('tokenize', { filePath: 'o_auth.ts' }).length === 1,
595
+ `underscore filename matches literally (o_auth.ts)`);
596
+ assert(store.getDefinition('tokenize', { filePath: 'oXauth.ts' }).length === 0,
597
+ `underscore is escaped, not a single-char wildcard`);
598
+
599
+ store.close();
600
+ fs.rmSync(tmp, { recursive: true, force: true });
601
+ }
602
+
603
+ async function run(): Promise<void> {
604
+ console.log('\nSeer Bug-Regression Tests');
605
+ console.log('===========================');
606
+ console.log('Each section asserts an audit/scale bug stays fixed.');
607
+ await bug1_v3MigrationBackfill();
608
+ await bug2_churnHistoryClash();
609
+ await bug3_springClassPrefix();
610
+ await bug4and5_renameAndEmail();
611
+ bug5_parseFollowLogEdgeCases();
612
+ await bug6_fastifyObjectRoutes();
613
+ bug7_testEdgeDuplicateIndex();
614
+ bug8_sameFileResolutionIndex();
615
+ await bug9_fastapiDecoratorNotServiceCall();
616
+ await bug10_tsTemplatePathParam();
617
+ await bug11_cppOutOfLineScope();
618
+ await bug12_fileFilterSuffixMatch();
619
+
620
+ console.log(`\n══════════════════════════════════════════════════════════════`);
621
+ console.log(` Regression results: ${passed} passed, ${failed} failed`);
622
+ if (failed > 0) { console.error('\n BUG-REGRESSION TESTS FAILED\n'); process.exit(1); }
623
+ else { console.log('\n All bug-regression tests passed! ✓\n'); }
624
+ }
625
+
626
+ run().catch(err => { console.error('bug-regressions crashed:', err); process.exit(1); });