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,358 @@
1
+ import path from 'path';
2
+ import { Store } from '../db/store.js';
3
+
4
+ /**
5
+ * Track-E module clustering.
6
+ *
7
+ * We cluster the FILE graph (one node per indexed file) using Louvain
8
+ * modularity maximization. The edge weight between two files is a deterministic
9
+ * mix of:
10
+ * - cross-file call edges (weight 1 per call)
11
+ * - resolved import edges (weight 2 per import — imports are a stronger
12
+ * architectural signal than a single call)
13
+ * - synthesized test edges (weight 3 per edge — test→prod is a very strong
14
+ * cohesion signal: agents want tests grouped with the production code
15
+ * they exercise)
16
+ *
17
+ * The Louvain pass is deliberately deterministic: file ids are visited in
18
+ * ascending order, modularity-gain ties resolve to the lower-id community,
19
+ * and the final community labels are remapped to 0..K-1 in the order their
20
+ * representative file id was first encountered. Two builds against the same
21
+ * DB therefore produce identical module ids — which the test suite asserts.
22
+ *
23
+ * After clustering we compute:
24
+ * - label: dominant top-level directory of the module's files; if two
25
+ * modules share the same dominant dir we append a numeric suffix
26
+ * (`auth`, `auth#1`, …) so labels stay unique without inventing a name.
27
+ * - primary_language: most common files.language among members
28
+ * - cohesion: intra-module weight / total weight touching the module
29
+ * - centrality: sum of PageRank of rankable symbols in the module
30
+ *
31
+ * And we cache cross-module edge weights into `module_edges` so
32
+ * `seer_module_dependencies` is a single indexed lookup, not a join over
33
+ * the full symbols/edges graph.
34
+ */
35
+
36
+ export interface ModulesBuildResult {
37
+ modules: number;
38
+ files: number;
39
+ passes: number;
40
+ intraEdgesWeight: number;
41
+ totalEdgesWeight: number;
42
+ elapsedMs: number;
43
+ }
44
+
45
+ interface BuildOptions {
46
+ /**
47
+ * Cap on how many Louvain "move" sweeps we do at each level before
48
+ * declaring convergence. 20 is well past what real graphs need; the cap
49
+ * exists so a pathological graph can't burn the indexer's wall time.
50
+ */
51
+ maxSweeps?: number;
52
+ /**
53
+ * Minimum modularity gain that justifies recording another sweep. Below
54
+ * this we treat the level as converged.
55
+ */
56
+ minGain?: number;
57
+ }
58
+
59
+ interface WeightedEdge { from: number; to: number; weight: number; kind: 'call' | 'import' | 'tests' | 'service' }
60
+
61
+ /**
62
+ * Build (or rebuild) the modules / module_members / module_edges tables.
63
+ *
64
+ * Idempotent: re-running with the same DB state produces the same modules.
65
+ * Cheap to call after every full index pass — empty graphs short-circuit.
66
+ */
67
+ export function buildModules(store: Store, options: BuildOptions = {}): ModulesBuildResult {
68
+ const start = Date.now();
69
+ const maxSweeps = options.maxSweeps ?? 20;
70
+ const minGain = options.minGain ?? 1e-6;
71
+
72
+ const files = store.listFileSummaries();
73
+ if (files.length === 0) {
74
+ store.replaceModules([], []);
75
+ return { modules: 0, files: 0, passes: 0, intraEdgesWeight: 0, totalEdgesWeight: 0, elapsedMs: Date.now() - start };
76
+ }
77
+
78
+ // ── Collect weighted file-level edges ────────────────────────────────────
79
+ // Use a Map<string, edge> keyed by `${from}->${to}-${kind}` so duplicate
80
+ // weights (same files connected via both calls and tests) coexist.
81
+ const rawEdges: WeightedEdge[] = [];
82
+ for (const e of store.fileCallEdgeWeights()) {
83
+ rawEdges.push({ from: e.from, to: e.to, weight: e.weight, kind: 'call' });
84
+ }
85
+ for (const e of store.fileImportEdgeWeights()) {
86
+ rawEdges.push({ from: e.from, to: e.to, weight: e.weight * 2, kind: 'import' });
87
+ }
88
+ for (const e of store.fileTestEdgeWeights()) {
89
+ rawEdges.push({ from: e.from, to: e.to, weight: e.weight * 3, kind: 'tests' });
90
+ }
91
+ // v8 Track-G — service-link cross-file dependency. Same weight as tests
92
+ // because a confirmed cross-service client→handler link is an
93
+ // architecturally important coupling between modules.
94
+ for (const e of store.fileServiceLinkEdgeWeights()) {
95
+ rawEdges.push({ from: e.from, to: e.to, weight: e.weight * 3, kind: 'service' });
96
+ }
97
+
98
+ // ── Build symmetric adjacency for modularity. Louvain is defined on an
99
+ // undirected weighted graph, so collapse directed weights into a single
100
+ // weight per unordered pair. We keep the directed `rawEdges` around for
101
+ // the post-clustering module_edges aggregation.
102
+ const allFileIds = files.map(f => f.id).sort((a, b) => a - b);
103
+ const idIndex = new Map<number, number>();
104
+ allFileIds.forEach((id, i) => idIndex.set(id, i));
105
+
106
+ const n = allFileIds.length;
107
+ const adjMap: Array<Map<number, number>> = Array.from({ length: n }, () => new Map());
108
+ for (const e of rawEdges) {
109
+ const fi = idIndex.get(e.from); const ti = idIndex.get(e.to);
110
+ if (fi == null || ti == null || fi === ti) continue;
111
+ adjMap[fi].set(ti, (adjMap[fi].get(ti) ?? 0) + e.weight);
112
+ adjMap[ti].set(fi, (adjMap[ti].get(fi) ?? 0) + e.weight);
113
+ }
114
+ const adj = adjMap.map(m => Array.from(m.entries()).map(([j, w]) => ({ j, w })));
115
+ const nodeWeight = adj.map(arr => arr.reduce((acc, x) => acc + x.w, 0));
116
+ const totalWeight = nodeWeight.reduce((acc, x) => acc + x, 0);
117
+
118
+ // ── Louvain single-level pass. We run one level — multi-level helps on
119
+ // graphs with millions of nodes, but for code modules the single level
120
+ // already produces clean clusters and runs in O(N * avg_degree) per
121
+ // sweep. The caller can always force more by raising maxSweeps.
122
+ // Community assignment: starts at "every node its own community".
123
+ let community = allFileIds.map((_, i) => i);
124
+ let passes = 0;
125
+ if (totalWeight > 0) {
126
+ // Sum of weights inside each community (deg / inside).
127
+ let commTot = nodeWeight.slice();
128
+ let commIn = new Array<number>(n).fill(0);
129
+ for (let sweep = 0; sweep < maxSweeps; sweep++) {
130
+ passes++;
131
+ let totalGain = 0;
132
+ let movements = 0;
133
+ // Visit nodes in ascending file-id order (== ascending index because
134
+ // allFileIds is sorted). Deterministic.
135
+ for (let i = 0; i < n; i++) {
136
+ // Compute weights to neighboring communities.
137
+ const ki = nodeWeight[i];
138
+ const ciOld = community[i];
139
+ // Sum of weights from i to nodes in each community (including own).
140
+ const kiToComm = new Map<number, number>();
141
+ let selfLoop = 0;
142
+ for (const { j, w } of adj[i]) {
143
+ if (j === i) { selfLoop += w; continue; }
144
+ const c = community[j];
145
+ kiToComm.set(c, (kiToComm.get(c) ?? 0) + w);
146
+ }
147
+ // Remove i from its current community.
148
+ commTot[ciOld] -= ki;
149
+ commIn[ciOld] -= 2 * (kiToComm.get(ciOld) ?? 0) + selfLoop;
150
+ if (commIn[ciOld] < 0) commIn[ciOld] = 0;
151
+ // Find best community to insert i. Candidate set = neighbor
152
+ // communities + the singleton (own old community); ties favor the
153
+ // lower community id so the result is deterministic.
154
+ let bestComm = ciOld;
155
+ let bestGain = 0;
156
+ const candidates: number[] = Array.from(kiToComm.keys());
157
+ candidates.sort((a, b) => a - b);
158
+ // If ciOld isn't in candidates, include it so we can stay put.
159
+ if (!kiToComm.has(ciOld)) candidates.push(ciOld);
160
+ for (const c of candidates) {
161
+ const kiInC = kiToComm.get(c) ?? 0;
162
+ // ΔQ for moving i into community c (vs. its current isolation):
163
+ // gain = kiInC/m - (commTot[c] * ki) / (2 * m^2)
164
+ const gain = (kiInC / totalWeight) - (commTot[c] * ki) / (2 * totalWeight * totalWeight);
165
+ if (gain > bestGain + 1e-12 || (Math.abs(gain - bestGain) < 1e-12 && c < bestComm)) {
166
+ bestGain = gain;
167
+ bestComm = c;
168
+ }
169
+ }
170
+ // Insert i into bestComm.
171
+ commTot[bestComm] += ki;
172
+ commIn[bestComm] += 2 * (kiToComm.get(bestComm) ?? 0) + selfLoop;
173
+ if (bestComm !== ciOld) {
174
+ community[i] = bestComm;
175
+ movements++;
176
+ totalGain += bestGain;
177
+ }
178
+ }
179
+ if (movements === 0 || totalGain < minGain) break;
180
+ }
181
+ }
182
+
183
+ // ── Remap community labels to 0..K-1 in encounter order ─────────────────
184
+ // Encounter order = order of file-id ascending (= index order). Two builds
185
+ // of the same DB therefore produce the same label numbers.
186
+ const labelByOldComm = new Map<number, number>();
187
+ let nextLabel = 0;
188
+ const finalCluster = new Array<number>(n);
189
+ for (let i = 0; i < n; i++) {
190
+ const c = community[i];
191
+ let lab = labelByOldComm.get(c);
192
+ if (lab === undefined) {
193
+ lab = nextLabel++;
194
+ labelByOldComm.set(c, lab);
195
+ }
196
+ finalCluster[i] = lab;
197
+ }
198
+ const K = nextLabel;
199
+
200
+ // ── Build per-module metadata ──────────────────────────────────────────
201
+ const memberIds: number[][] = Array.from({ length: K }, () => []);
202
+ const memberPaths: string[][] = Array.from({ length: K }, () => []);
203
+ const memberLangs: string[][] = Array.from({ length: K }, () => []);
204
+ for (let i = 0; i < n; i++) {
205
+ const k = finalCluster[i];
206
+ memberIds[k].push(allFileIds[i]);
207
+ memberPaths[k].push(files[idIndex.get(allFileIds[i])!]?.relPath ?? '');
208
+ memberLangs[k].push(files[idIndex.get(allFileIds[i])!]?.language ?? '');
209
+ }
210
+
211
+ // PageRank centrality per file id — one query, partitioned in JS.
212
+ const prByFile = new Map<number, number>();
213
+ const prRows = store.rawDb().prepare(`
214
+ SELECT file_id AS fileId, SUM(pagerank) AS prSum
215
+ FROM symbols WHERE is_rankable = 1
216
+ GROUP BY file_id
217
+ `).all() as Array<{ fileId: unknown; prSum: unknown }>;
218
+ for (const r of prRows) {
219
+ prByFile.set(Number(r.fileId), Number(r.prSum ?? 0));
220
+ }
221
+
222
+ const symbolCountByFile = new Map<number, number>();
223
+ const symRows = store.rawDb().prepare(`
224
+ SELECT file_id AS fileId, COUNT(*) AS c
225
+ FROM symbols WHERE is_rankable = 1
226
+ GROUP BY file_id
227
+ `).all() as Array<{ fileId: unknown; c: unknown }>;
228
+ for (const r of symRows) {
229
+ symbolCountByFile.set(Number(r.fileId), Number(r.c));
230
+ }
231
+
232
+ // Module dominant directory → label. Two modules with the same dominant
233
+ // dir get numeric suffixes (#1, #2, …) so labels stay unique.
234
+ const labelUseCount = new Map<string, number>();
235
+ const moduleEntries: Array<{
236
+ label: string;
237
+ sizeFiles: number;
238
+ sizeSymbols: number;
239
+ primaryLanguage: string | null;
240
+ cohesion: number;
241
+ centrality: number;
242
+ fileIds: number[];
243
+ }> = [];
244
+
245
+ for (let k = 0; k < K; k++) {
246
+ const dominantDir = dominantTopLevelDir(memberPaths[k]) ?? `module-${k}`;
247
+ const used = labelUseCount.get(dominantDir) ?? 0;
248
+ const label = used === 0 ? dominantDir : `${dominantDir}#${used}`;
249
+ labelUseCount.set(dominantDir, used + 1);
250
+
251
+ const primaryLanguage = dominantString(memberLangs[k]);
252
+ let symCount = 0;
253
+ let centrality = 0;
254
+ for (const fid of memberIds[k]) {
255
+ symCount += symbolCountByFile.get(fid) ?? 0;
256
+ centrality += prByFile.get(fid) ?? 0;
257
+ }
258
+ moduleEntries.push({
259
+ label,
260
+ sizeFiles: memberIds[k].length,
261
+ sizeSymbols: symCount,
262
+ primaryLanguage,
263
+ cohesion: 0, // computed below once we have aggregated edges
264
+ centrality,
265
+ fileIds: memberIds[k],
266
+ });
267
+ }
268
+
269
+ // ── Aggregate cross-module edges (per kind) ───────────────────────────
270
+ // We use the directed `rawEdges` here so call vs import vs tests stay
271
+ // distinguishable in module_edges.
272
+ const moduleByFile = new Map<number, number>();
273
+ for (let i = 0; i < n; i++) moduleByFile.set(allFileIds[i], finalCluster[i]);
274
+
275
+ const edgeAgg = new Map<string, { fromIndex: number; toIndex: number; kind: string; weight: number }>();
276
+ const intraByModule = new Array<number>(K).fill(0);
277
+ const totalByModule = new Array<number>(K).fill(0);
278
+ for (const e of rawEdges) {
279
+ const fm = moduleByFile.get(e.from);
280
+ const tm = moduleByFile.get(e.to);
281
+ if (fm == null || tm == null) continue;
282
+ totalByModule[fm] += e.weight;
283
+ if (fm === tm) {
284
+ intraByModule[fm] += e.weight;
285
+ } else {
286
+ totalByModule[tm] += e.weight;
287
+ const key = `${fm}->${tm}:${e.kind}`;
288
+ const ex = edgeAgg.get(key);
289
+ if (ex) ex.weight += e.weight;
290
+ else edgeAgg.set(key, { fromIndex: fm, toIndex: tm, kind: e.kind, weight: e.weight });
291
+ }
292
+ }
293
+ let intraTotal = 0;
294
+ let allTotal = 0;
295
+ for (let k = 0; k < K; k++) {
296
+ const total = totalByModule[k];
297
+ const intra = intraByModule[k];
298
+ moduleEntries[k].cohesion = total > 0 ? intra / total : 1;
299
+ intraTotal += intra;
300
+ allTotal += total;
301
+ }
302
+
303
+ store.replaceModules(moduleEntries, Array.from(edgeAgg.values()));
304
+
305
+ return {
306
+ modules: K,
307
+ files: n,
308
+ passes,
309
+ intraEdgesWeight: intraTotal,
310
+ totalEdgesWeight: allTotal,
311
+ elapsedMs: Date.now() - start,
312
+ };
313
+ }
314
+
315
+ // ── helpers ─────────────────────────────────────────────────────────────────
316
+
317
+ /**
318
+ * Most-frequent top-level directory of a list of relative file paths.
319
+ * Files at the repo root return their basename's first identifier-like
320
+ * chunk so we never end up with empty labels.
321
+ */
322
+ function dominantTopLevelDir(paths: string[]): string | null {
323
+ if (paths.length === 0) return null;
324
+ const counts = new Map<string, number>();
325
+ for (const p of paths) {
326
+ const norm = p.replace(/\\/g, '/');
327
+ const slash = norm.indexOf('/');
328
+ const head = slash > 0 ? norm.slice(0, slash) : rootBasename(norm);
329
+ if (!head) continue;
330
+ counts.set(head, (counts.get(head) ?? 0) + 1);
331
+ }
332
+ if (counts.size === 0) return null;
333
+ // Deterministic tie-break: lexicographic.
334
+ const sorted = Array.from(counts.entries()).sort((a, b) =>
335
+ b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0),
336
+ );
337
+ return sorted[0][0];
338
+ }
339
+
340
+ function rootBasename(p: string): string {
341
+ const ext = path.extname(p);
342
+ const base = ext ? p.slice(0, p.length - ext.length) : p;
343
+ return base;
344
+ }
345
+
346
+ function dominantString(xs: string[]): string | null {
347
+ if (xs.length === 0) return null;
348
+ const counts = new Map<string, number>();
349
+ for (const x of xs) {
350
+ if (!x) continue;
351
+ counts.set(x, (counts.get(x) ?? 0) + 1);
352
+ }
353
+ if (counts.size === 0) return null;
354
+ const sorted = Array.from(counts.entries()).sort((a, b) =>
355
+ b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0),
356
+ );
357
+ return sorted[0][0];
358
+ }