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,333 @@
1
+ /**
2
+ * Parallel-indexing parity test (Step 4 of parallel parsing).
3
+ *
4
+ * The contract: indexing the same workspace with `parallel: true` must
5
+ * produce a DB byte-equivalent to the serial path. The check is row-level,
6
+ * not count-level — counts can match while routes/config-key/symbol_role
7
+ * resolution silently diverges.
8
+ *
9
+ * Covered:
10
+ * - Per-table row diffs (files, symbols, edges, file_imports, routes,
11
+ * config_keys, external_dependencies, FTS hits).
12
+ * - Top-K PageRank symbol IDs and names match.
13
+ * - jobs ∈ {1, 2, 4, 8} all produce the same DB as serial.
14
+ * - Cache-hit re-index: a second parallel pass over an unchanged tree
15
+ * reports `indexed=0`, `reusedFromCache=N`, `pagerankRecomputed=false`,
16
+ * and the DB is identical to the first pass.
17
+ * - One-file edit: only the edited file's row changes; PageRank recomputes.
18
+ * - Stale-file pruning: a deleted file is removed (FK cascade clears
19
+ * symbols/edges/imports/routes/config_keys).
20
+ *
21
+ * Run with: npm run test:parallel-index
22
+ */
23
+ import fs from 'fs';
24
+ import path from 'path';
25
+ import os from 'os';
26
+ import { Indexer } from '../src/indexer/index';
27
+ import { Store } from '../src/db/store';
28
+
29
+ const FIXTURES_DIR = path.join(__dirname, 'fixtures');
30
+ const FIXTURES_TRACKCD = path.join(__dirname, 'fixtures-trackcd');
31
+
32
+ let passed = 0;
33
+ let failed = 0;
34
+
35
+ function assert(condition: boolean, message: string): void {
36
+ if (condition) {
37
+ console.log(` ✓ ${message}`);
38
+ passed++;
39
+ } else {
40
+ console.error(` ✗ ${message}`);
41
+ failed++;
42
+ }
43
+ }
44
+
45
+ // ── Fixture set up ──────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Stage a temp workspace combining smoke + trackcd fixtures so we exercise:
49
+ * - all 9 language extractors (smoke fixtures)
50
+ * - routes, config keys, external dependencies (trackcd fixtures)
51
+ * - the C/C++ declaration-vs-definition split (sample.h + sample.cpp)
52
+ * - test-file classification (tests/ subdir from trackcd)
53
+ */
54
+ function stageFixtures(): string {
55
+ const root = path.join(os.tmpdir(), `seer-parallel-fixtures-${Date.now()}`);
56
+ fs.mkdirSync(root, { recursive: true });
57
+ for (const f of fs.readdirSync(FIXTURES_DIR)) {
58
+ const src = path.join(FIXTURES_DIR, f);
59
+ if (fs.statSync(src).isFile()) fs.copyFileSync(src, path.join(root, f));
60
+ }
61
+ // Bring in select trackcd fixtures (avoid name clashes by prefixing).
62
+ for (const f of fs.readdirSync(FIXTURES_TRACKCD)) {
63
+ const src = path.join(FIXTURES_TRACKCD, f);
64
+ if (fs.statSync(src).isFile()) {
65
+ fs.copyFileSync(src, path.join(root, `trackcd_${f}`));
66
+ } else if (fs.statSync(src).isDirectory() && f === 'tests') {
67
+ fs.mkdirSync(path.join(root, 'tests'), { recursive: true });
68
+ for (const sub of fs.readdirSync(src)) {
69
+ fs.copyFileSync(path.join(src, sub), path.join(root, 'tests', sub));
70
+ }
71
+ }
72
+ }
73
+ return root;
74
+ }
75
+
76
+ // ── DB dump helpers ─────────────────────────────────────────────────────────
77
+
78
+ interface DbSnapshot {
79
+ files: unknown[];
80
+ symbols: unknown[];
81
+ edges: unknown[];
82
+ file_imports: unknown[];
83
+ routes: unknown[];
84
+ config_keys: unknown[];
85
+ external_dependencies: unknown[];
86
+ fts_symbols_validate: unknown[];
87
+ fts_files_auth: unknown[];
88
+ pagerank_top: unknown[];
89
+ role_counts: unknown;
90
+ }
91
+
92
+ function dumpDb(store: Store): DbSnapshot {
93
+ const db = store.rawDb();
94
+ // `indexed_at` excluded — it's wall-clock, expected to differ across runs.
95
+ // Everything else is content-derived and must match exactly.
96
+ return {
97
+ files: db.prepare(`
98
+ SELECT id, rel_path, language, hash, lines, role, is_vendor, is_generated
99
+ FROM files ORDER BY rel_path
100
+ `).all(),
101
+ symbols: db.prepare(`
102
+ SELECT id, name, qualified_name, kind, file_id, line_start, line_end,
103
+ col_start, col_end, signature, is_rankable,
104
+ loc, cyclomatic, cognitive, max_nesting, symbol_key, symbol_role
105
+ FROM symbols ORDER BY file_id, line_start, line_end, name, qualified_name
106
+ `).all(),
107
+ edges: db.prepare(`
108
+ SELECT from_id, to_id, to_name, kind, line FROM edges
109
+ ORDER BY from_id, line, to_name, kind
110
+ `).all(),
111
+ file_imports: db.prepare(`
112
+ SELECT from_file_id, import_name, resolved_file_id FROM file_imports
113
+ ORDER BY from_file_id, import_name
114
+ `).all(),
115
+ routes: db.prepare(`
116
+ SELECT file_id, method, path, framework, handler_name, handler_id, line
117
+ FROM routes ORDER BY file_id, line, method, path
118
+ `).all(),
119
+ config_keys: db.prepare(`
120
+ SELECT key, source, file_id, symbol_id, line
121
+ FROM config_keys ORDER BY file_id, line, key
122
+ `).all(),
123
+ external_dependencies: db.prepare(`
124
+ SELECT name, version_range, ecosystem, manifest_path, is_dev FROM external_dependencies
125
+ ORDER BY ecosystem, name, manifest_path
126
+ `).all(),
127
+ // Two representative FTS queries — verify BM25-ranked hits match.
128
+ fts_symbols_validate: store.searchSymbolsFts('validate', { limit: 20 })
129
+ .map(r => ({ name: r.name, qualifiedName: r.qualifiedName, filePath: r.filePath, lineStart: r.lineStart })),
130
+ fts_files_auth: store.searchFilesFts('auth', 20)
131
+ .map(r => ({ relPath: r.relPath, language: r.language })),
132
+ // Top PageRank rows. ID, name, kind, filePath together — order must match.
133
+ pagerank_top: db.prepare(`
134
+ SELECT s.id, s.name, s.qualified_name, s.kind, f.rel_path, s.pagerank
135
+ FROM symbols s JOIN files f ON f.id = s.file_id
136
+ WHERE s.is_rankable = 1
137
+ ORDER BY s.pagerank DESC, s.id ASC
138
+ LIMIT 50
139
+ `).all(),
140
+ role_counts: store.getRoleCounts(),
141
+ };
142
+ }
143
+
144
+ function jcanon(v: unknown): string {
145
+ return JSON.stringify(v, (_k, x) => {
146
+ if (x && typeof x === 'object' && !Array.isArray(x)) {
147
+ const sorted: Record<string, unknown> = {};
148
+ for (const k of Object.keys(x as Record<string, unknown>).sort()) {
149
+ sorted[k] = (x as Record<string, unknown>)[k];
150
+ }
151
+ return sorted;
152
+ }
153
+ return x;
154
+ });
155
+ }
156
+
157
+ function diffSnapshots(a: DbSnapshot, b: DbSnapshot): string[] {
158
+ const out: string[] = [];
159
+ for (const k of Object.keys(a) as Array<keyof DbSnapshot>) {
160
+ const sa = jcanon(a[k]);
161
+ const sb = jcanon(b[k]);
162
+ if (sa !== sb) {
163
+ const lenA = Array.isArray(a[k]) ? (a[k] as unknown[]).length : 1;
164
+ const lenB = Array.isArray(b[k]) ? (b[k] as unknown[]).length : 1;
165
+ let firstDiffAt = -1;
166
+ for (let i = 0; i < Math.min(sa.length, sb.length); i++) {
167
+ if (sa[i] !== sb[i]) { firstDiffAt = i; break; }
168
+ }
169
+ out.push(
170
+ `${k}: serial len=${lenA}, parallel len=${lenB}, first diff at char ${firstDiffAt}\n` +
171
+ ` serial: …${sa.slice(Math.max(0, firstDiffAt - 60), firstDiffAt + 100)}…\n` +
172
+ ` parallel: …${sb.slice(Math.max(0, firstDiffAt - 60), firstDiffAt + 100)}…`,
173
+ );
174
+ }
175
+ }
176
+ return out;
177
+ }
178
+
179
+ // ── Indexing helper ─────────────────────────────────────────────────────────
180
+
181
+ async function indexInto(
182
+ dbPath: string, root: string, parallel: boolean, jobs?: number,
183
+ ): Promise<ReturnType<Indexer['indexDirectory']> extends Promise<infer R> ? R : never> {
184
+ const store = new Store(dbPath);
185
+ const indexer = new Indexer(store);
186
+ const result = await indexer.indexDirectory(root, {
187
+ quiet: true,
188
+ parallel,
189
+ jobs,
190
+ });
191
+ store.close();
192
+ return result;
193
+ }
194
+
195
+ function snapshotDb(dbPath: string): DbSnapshot {
196
+ const store = new Store(dbPath);
197
+ const snap = dumpDb(store);
198
+ store.close();
199
+ return snap;
200
+ }
201
+
202
+ // ── Run ─────────────────────────────────────────────────────────────────────
203
+
204
+ async function run(): Promise<void> {
205
+ console.log('\nParallel Indexing Parity Test (Step 4)');
206
+ console.log('========================================\n');
207
+
208
+ const root = stageFixtures();
209
+ console.log(` fixtures staged at ${root}\n`);
210
+
211
+ // ── 1. Build serial reference DB ──────────────────────────────────────────
212
+ const serialDb = path.join(os.tmpdir(), `seer-parallel-serial-${Date.now()}.db`);
213
+ const serialRes = await indexInto(serialDb, root, false);
214
+ console.log(`── Serial reference: ${serialRes.filesIndexed} indexed, ${serialRes.symbols} symbols, ${serialRes.edges} edges ──`);
215
+ const serialSnap = snapshotDb(serialDb);
216
+ assert(serialRes.filesIndexed > 0, 'serial run indexed at least one file');
217
+
218
+ // ── 2. Parallel DBs at multiple job counts must match serial exactly ─────
219
+ for (const jobs of [1, 2, 4, 8]) {
220
+ console.log(`\n── jobs=${jobs} parity ──`);
221
+ const dbPath = path.join(os.tmpdir(), `seer-parallel-${jobs}-${Date.now()}.db`);
222
+ const res = await indexInto(dbPath, root, true, jobs);
223
+ const snap = snapshotDb(dbPath);
224
+
225
+ assert(res.filesIndexed === serialRes.filesIndexed, `filesIndexed matches serial (${res.filesIndexed})`);
226
+ assert(res.symbols === serialRes.symbols, `symbols matches serial (${res.symbols})`);
227
+ assert(res.edges === serialRes.edges, `edges matches serial (${res.edges})`);
228
+ assert(res.resolvedEdges === serialRes.resolvedEdges, `resolvedEdges matches serial (${res.resolvedEdges})`);
229
+
230
+ const diffs = diffSnapshots(serialSnap, snap);
231
+ if (diffs.length > 0) {
232
+ for (const d of diffs) console.error(` diff: ${d}`);
233
+ }
234
+ assert(diffs.length === 0, `every DB table row-identical to serial (jobs=${jobs})`);
235
+
236
+ fs.unlinkSync(dbPath);
237
+ }
238
+
239
+ // ── 3. Cache-hit re-index: second pass is a no-op ─────────────────────────
240
+ console.log(`\n── Cache-hit re-index (parallel) ──`);
241
+ const cacheDb = path.join(os.tmpdir(), `seer-parallel-cache-${Date.now()}.db`);
242
+ const firstPass = await indexInto(cacheDb, root, true, 4);
243
+ const firstSnap = snapshotDb(cacheDb);
244
+ assert(firstPass.filesIndexed > 0, 'first parallel pass indexed files');
245
+ assert(firstPass.pagerankRecomputed === true, 'first pass recomputes PageRank');
246
+
247
+ const secondPass = await indexInto(cacheDb, root, true, 4);
248
+ const secondSnap = snapshotDb(cacheDb);
249
+ assert(secondPass.filesIndexed === 0, `second pass indexed=0 (got ${secondPass.filesIndexed})`);
250
+ assert(secondPass.filesReusedFromCache === firstPass.filesIndexed,
251
+ `second pass reusedFromCache=${secondPass.filesReusedFromCache} matches first pass indexed=${firstPass.filesIndexed}`);
252
+ assert(secondPass.pagerankRecomputed === false, 'second pass skips PageRank (graph unchanged)');
253
+ const cacheDiffs = diffSnapshots(firstSnap, secondSnap);
254
+ if (cacheDiffs.length > 0) {
255
+ for (const d of cacheDiffs) console.error(` cache-diff: ${d}`);
256
+ }
257
+ assert(cacheDiffs.length === 0, 'cache-hit pass DB identical to first pass (touchedFileIds includes cached files → no pruning)');
258
+
259
+ // ── 4. Stale-file pruning: delete a file, re-index, row should vanish ────
260
+ console.log(`\n── Stale-file pruning (parallel) ──`);
261
+ const beforePrune = secondSnap; // already captured
262
+ const victim = 'trackcd_complex_module.py';
263
+ fs.unlinkSync(path.join(root, victim));
264
+ const prunePass = await indexInto(cacheDb, root, true, 4);
265
+ const afterPrune = snapshotDb(cacheDb);
266
+ const filesNow = (afterPrune.files as Array<{ rel_path: string }>).map(r => r.rel_path);
267
+ assert(!filesNow.includes(victim), `deleted file ${victim} pruned from DB`);
268
+ assert(
269
+ (afterPrune.files as unknown[]).length === (beforePrune.files as unknown[]).length - 1,
270
+ `files count dropped by 1 (was ${(beforePrune.files as unknown[]).length}, now ${(afterPrune.files as unknown[]).length})`,
271
+ );
272
+ // The victim's symbols / config_keys must cascade-delete.
273
+ const victimSymsBefore = (beforePrune.symbols as Array<{ file_id: number }>)
274
+ .filter(s => {
275
+ const fid = (beforePrune.files as Array<{ id: number; rel_path: string }>)
276
+ .find(f => f.rel_path === victim)?.id;
277
+ return fid != null && s.file_id === fid;
278
+ }).length;
279
+ assert(victimSymsBefore > 0, `victim had >0 symbols before delete (sanity check, got ${victimSymsBefore})`);
280
+ const victimFidNow = (afterPrune.files as Array<{ id: number; rel_path: string }>)
281
+ .find(f => f.rel_path === victim)?.id;
282
+ assert(victimFidNow === undefined, 'victim file row gone from files table');
283
+ assert(prunePass.pagerankRecomputed === true, 'prune triggers PageRank recompute');
284
+
285
+ // ── 5. One-file edit: only edited file's symbols change ──────────────────
286
+ console.log(`\n── One-file edit (parallel) ──`);
287
+ // Restage so prior-test mutations don't carry over.
288
+ const editRoot = stageFixtures();
289
+ const editDb = path.join(os.tmpdir(), `seer-parallel-edit-${Date.now()}.db`);
290
+ await indexInto(editDb, editRoot, true, 4);
291
+ const beforeEdit = snapshotDb(editDb);
292
+
293
+ // Append a new function to caller.ts so its hash changes.
294
+ const callerPath = path.join(editRoot, 'caller.ts');
295
+ fs.appendFileSync(callerPath, '\nexport function freshlyAdded(): number { return 42; }\n');
296
+
297
+ const editRes = await indexInto(editDb, editRoot, true, 4);
298
+ const afterEdit = snapshotDb(editDb);
299
+ assert(editRes.filesIndexed === 1, `exactly 1 file reindexed (got ${editRes.filesIndexed})`);
300
+ assert(editRes.filesReusedFromCache === beforeEdit.files.length - 1,
301
+ `everything else cache-reused (got ${editRes.filesReusedFromCache} of ${beforeEdit.files.length - 1})`);
302
+
303
+ const addedSym = (afterEdit.symbols as Array<{ name: string }>).find(s => s.name === 'freshlyAdded');
304
+ assert(addedSym !== undefined, 'freshlyAdded symbol present after edit');
305
+
306
+ const callerFidBefore = (beforeEdit.files as Array<{ id: number; rel_path: string }>)
307
+ .find(f => f.rel_path === 'caller.ts')?.id;
308
+ const callerFidAfter = (afterEdit.files as Array<{ id: number; rel_path: string }>)
309
+ .find(f => f.rel_path === 'caller.ts')?.id;
310
+ assert(callerFidBefore === callerFidAfter, 'caller.ts file id stable across edit');
311
+ assert(editRes.pagerankRecomputed === true, 'edit triggers PageRank recompute');
312
+
313
+ // ── Cleanup ──────────────────────────────────────────────────────────────
314
+ try { fs.unlinkSync(serialDb); } catch { /* */ }
315
+ try { fs.unlinkSync(cacheDb); } catch { /* */ }
316
+ try { fs.unlinkSync(editDb); } catch { /* */ }
317
+ try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* */ }
318
+ try { fs.rmSync(editRoot, { recursive: true, force: true }); } catch { /* */ }
319
+
320
+ console.log('\n══════════════════════════════════════════════════════════════');
321
+ console.log(` Parallel-index results: ${passed} passed, ${failed} failed`);
322
+ if (failed > 0) {
323
+ console.error('\n PARALLEL-INDEX TESTS FAILED\n');
324
+ process.exit(1);
325
+ } else {
326
+ console.log('\n All parallel-index tests passed! ✓\n');
327
+ }
328
+ }
329
+
330
+ run().catch(err => {
331
+ console.error('parallel-index test threw:', err);
332
+ process.exit(1);
333
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Track A regression net: parallel read-only opens against a busy DB.
3
+ *
4
+ * The dogfood gap was that `Store` always ran SCHEMA_SQL + migrations on
5
+ * open, which took a write lock — and on Windows that lock would conflict
6
+ * with a concurrent indexer's transaction, surfacing as `database is locked`
7
+ * for everyone. The fix is:
8
+ * - Store.openReadOnly() opens with readOnly: true, skips schema setup,
9
+ * sets PRAGMA query_only=ON + busy_timeout.
10
+ * - The writer also sets busy_timeout so brief reader contention waits
11
+ * instead of failing.
12
+ *
13
+ * This test pounds the read path while the writer indexes, then confirms:
14
+ * 1. No `SQLITE_BUSY` errors during 100 parallel read opens.
15
+ * 2. The reader sees a consistent snapshot — symbol count never decreases.
16
+ * 3. Writes through a read-only Store are rejected with a clear error.
17
+ */
18
+
19
+ import path from 'path';
20
+ import fs from 'fs';
21
+ import os from 'os';
22
+ import { Store } from '../src/db/store';
23
+ import { Indexer } from '../src/indexer/index';
24
+ import { CURRENT_SCHEMA_VERSION } from '../src/db/schema';
25
+
26
+ const ROOT = path.resolve(__dirname, '..');
27
+ const FIXTURES = path.join(ROOT, 'tests/fixtures');
28
+ const TMP_DB = path.join(os.tmpdir(), `seer-parallel-${Date.now()}.db`);
29
+
30
+ let passed = 0;
31
+ let failed = 0;
32
+ function ok(label: string): void { passed++; console.log(` ✓ ${label}`); }
33
+ function bad(label: string, extra?: unknown): void {
34
+ failed++;
35
+ console.error(` ✗ ${label}` + (extra !== undefined ? ` :: ${JSON.stringify(extra)}` : ''));
36
+ }
37
+
38
+ async function main(): Promise<void> {
39
+ console.log('\nSeer Parallel-Read Test\n=========================\n');
40
+
41
+ // Seed the DB with an initial index.
42
+ const writer = new Store(TMP_DB);
43
+ await new Indexer(writer).indexDirectory(FIXTURES, { quiet: true });
44
+
45
+ // Verify schema version stored properly — pinned at the current build.
46
+ const sinfo = writer.schemaInfo();
47
+ if (sinfo.current && sinfo.dbVersion === CURRENT_SCHEMA_VERSION) ok(`schema_version pinned at ${sinfo.dbVersion}`);
48
+ else bad('schema_version not pinned', sinfo);
49
+
50
+ writer.close();
51
+
52
+ // 100 parallel read-only opens, each doing a couple of cheap queries.
53
+ // No SQLITE_BUSY allowed.
54
+ const N = 100;
55
+ const errors: Error[] = [];
56
+ const counts: number[] = [];
57
+ await Promise.all(Array.from({ length: N }, async () => {
58
+ try {
59
+ const s = Store.openReadOnly(TMP_DB);
60
+ counts.push(s.getStats().symbols);
61
+ void s.findCallers('process_payment', 5);
62
+ void s.findSymbols('AuthService', { limit: 10 });
63
+ void s.getTopSymbols(5);
64
+ s.close();
65
+ } catch (e) {
66
+ errors.push(e as Error);
67
+ }
68
+ }));
69
+ if (errors.length === 0) ok(`${N} parallel read-only opens completed without errors`);
70
+ else bad(`${errors.length}/${N} parallel reads failed`, errors[0]?.message);
71
+
72
+ if (new Set(counts).size === 1) ok(`all ${N} reads saw the same symbol count (${counts[0]})`);
73
+ else bad('symbol count drifted between parallel reads', new Set(counts));
74
+
75
+ // Concurrent reader + writer. A reindex pass should not lock out readers.
76
+ const w2 = new Store(TMP_DB);
77
+ const indexer = new Indexer(w2);
78
+
79
+ let raceErrors = 0;
80
+ const writerDone = indexer.indexDirectory(FIXTURES, { quiet: true });
81
+ // Hammer reads while the indexer runs.
82
+ for (let i = 0; i < 25; i++) {
83
+ try {
84
+ const r = Store.openReadOnly(TMP_DB);
85
+ r.getStats();
86
+ r.close();
87
+ } catch {
88
+ raceErrors++;
89
+ }
90
+ }
91
+ await writerDone;
92
+ w2.close();
93
+ if (raceErrors === 0) ok('25 reads while writer was active: no SQLITE_BUSY');
94
+ else bad(`${raceErrors} reads failed while writer was active`);
95
+
96
+ // Read-only Store must reject writes.
97
+ const ro = Store.openReadOnly(TMP_DB);
98
+ let writeBlocked = false;
99
+ try {
100
+ ro.upsertFile('/tmp/bogus.ts', 'bogus.ts', 'typescript', 'abc', 0);
101
+ } catch (e) {
102
+ writeBlocked = true;
103
+ // The exact message varies (sqlite says "attempt to write a readonly database"
104
+ // or "no such column"); we only require that it threw.
105
+ process.stdout.write(` (read-only write rejected: ${(e as Error).message.substring(0, 60)})\n`);
106
+ }
107
+ if (writeBlocked) ok('read-only Store rejects writes');
108
+ else bad('read-only Store allowed a write');
109
+ ro.close();
110
+
111
+ // Cleanup.
112
+ for (const ext of ['', '-wal', '-shm']) {
113
+ const p = TMP_DB + ext;
114
+ if (fs.existsSync(p)) fs.unlinkSync(p);
115
+ }
116
+
117
+ console.log(`\n══════════════════════════════════════════════════════════════`);
118
+ console.log(` Parallel-read results: ${passed} passed, ${failed} failed\n`);
119
+ if (failed > 0) process.exit(1);
120
+ }
121
+
122
+ main().catch(err => {
123
+ console.error('Parallel-read test crashed:', err);
124
+ process.exit(1);
125
+ });