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.
- package/.vscode/settings.json +3 -0
- package/LICENSE +176 -0
- package/README.md +272 -0
- package/README_dev.md +199 -0
- package/dist/bundle/ci.d.ts +47 -0
- package/dist/bundle/ci.d.ts.map +1 -0
- package/dist/bundle/ci.js +113 -0
- package/dist/bundle/ci.js.map +1 -0
- package/dist/bundle/contract.d.ts +111 -0
- package/dist/bundle/contract.d.ts.map +1 -0
- package/dist/bundle/contract.js +352 -0
- package/dist/bundle/contract.js.map +1 -0
- package/dist/bundle/export.d.ts +36 -0
- package/dist/bundle/export.d.ts.map +1 -0
- package/dist/bundle/export.js +152 -0
- package/dist/bundle/export.js.map +1 -0
- package/dist/bundle/external.d.ts +66 -0
- package/dist/bundle/external.d.ts.map +1 -0
- package/dist/bundle/external.js +238 -0
- package/dist/bundle/external.js.map +1 -0
- package/dist/bundle/format.d.ts +94 -0
- package/dist/bundle/format.d.ts.map +1 -0
- package/dist/bundle/format.js +42 -0
- package/dist/bundle/format.js.map +1 -0
- package/dist/bundle/import.d.ts +49 -0
- package/dist/bundle/import.d.ts.map +1 -0
- package/dist/bundle/import.js +116 -0
- package/dist/bundle/import.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1402 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +48 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +284 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/db/schema.d.ts +3 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +616 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/store.d.ts +1011 -0
- package/dist/db/store.d.ts.map +1 -0
- package/dist/db/store.js +3888 -0
- package/dist/db/store.js.map +1 -0
- package/dist/graph/pagerank.d.ts +9 -0
- package/dist/graph/pagerank.d.ts.map +1 -0
- package/dist/graph/pagerank.js +47 -0
- package/dist/graph/pagerank.js.map +1 -0
- package/dist/indexer/architecture.d.ts +72 -0
- package/dist/indexer/architecture.d.ts.map +1 -0
- package/dist/indexer/architecture.js +112 -0
- package/dist/indexer/architecture.js.map +1 -0
- package/dist/indexer/behavior.d.ts +75 -0
- package/dist/indexer/behavior.d.ts.map +1 -0
- package/dist/indexer/behavior.js +395 -0
- package/dist/indexer/behavior.js.map +1 -0
- package/dist/indexer/boundaries.d.ts +60 -0
- package/dist/indexer/boundaries.d.ts.map +1 -0
- package/dist/indexer/boundaries.js +366 -0
- package/dist/indexer/boundaries.js.map +1 -0
- package/dist/indexer/churn.d.ts +15 -0
- package/dist/indexer/churn.d.ts.map +1 -0
- package/dist/indexer/churn.js +49 -0
- package/dist/indexer/churn.js.map +1 -0
- package/dist/indexer/classify.d.ts +9 -0
- package/dist/indexer/classify.d.ts.map +1 -0
- package/dist/indexer/classify.js +90 -0
- package/dist/indexer/classify.js.map +1 -0
- package/dist/indexer/context.d.ts +176 -0
- package/dist/indexer/context.d.ts.map +1 -0
- package/dist/indexer/context.js +193 -0
- package/dist/indexer/context.js.map +1 -0
- package/dist/indexer/continuity.d.ts +67 -0
- package/dist/indexer/continuity.d.ts.map +1 -0
- package/dist/indexer/continuity.js +288 -0
- package/dist/indexer/continuity.js.map +1 -0
- package/dist/indexer/detectchanges.d.ts +32 -0
- package/dist/indexer/detectchanges.d.ts.map +1 -0
- package/dist/indexer/detectchanges.js +74 -0
- package/dist/indexer/detectchanges.js.map +1 -0
- package/dist/indexer/discovery.d.ts +37 -0
- package/dist/indexer/discovery.d.ts.map +1 -0
- package/dist/indexer/discovery.js +136 -0
- package/dist/indexer/discovery.js.map +1 -0
- package/dist/indexer/externaldeps.d.ts +18 -0
- package/dist/indexer/externaldeps.d.ts.map +1 -0
- package/dist/indexer/externaldeps.js +288 -0
- package/dist/indexer/externaldeps.js.map +1 -0
- package/dist/indexer/freshness.d.ts +48 -0
- package/dist/indexer/freshness.d.ts.map +1 -0
- package/dist/indexer/freshness.js +128 -0
- package/dist/indexer/freshness.js.map +1 -0
- package/dist/indexer/git.d.ts +144 -0
- package/dist/indexer/git.d.ts.map +1 -0
- package/dist/indexer/git.js +444 -0
- package/dist/indexer/git.js.map +1 -0
- package/dist/indexer/index.d.ts +145 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +930 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/modules.d.ts +62 -0
- package/dist/indexer/modules.d.ts.map +1 -0
- package/dist/indexer/modules.js +293 -0
- package/dist/indexer/modules.js.map +1 -0
- package/dist/indexer/preflight.d.ts +154 -0
- package/dist/indexer/preflight.d.ts.map +1 -0
- package/dist/indexer/preflight.js +399 -0
- package/dist/indexer/preflight.js.map +1 -0
- package/dist/indexer/protoScanner.d.ts +34 -0
- package/dist/indexer/protoScanner.d.ts.map +1 -0
- package/dist/indexer/protoScanner.js +133 -0
- package/dist/indexer/protoScanner.js.map +1 -0
- package/dist/indexer/risk.d.ts +115 -0
- package/dist/indexer/risk.d.ts.map +1 -0
- package/dist/indexer/risk.js +194 -0
- package/dist/indexer/risk.js.map +1 -0
- package/dist/indexer/serviceHostScanner.d.ts +25 -0
- package/dist/indexer/serviceHostScanner.d.ts.map +1 -0
- package/dist/indexer/serviceHostScanner.js +95 -0
- package/dist/indexer/serviceHostScanner.js.map +1 -0
- package/dist/indexer/serviceLinks.d.ts +105 -0
- package/dist/indexer/serviceLinks.d.ts.map +1 -0
- package/dist/indexer/serviceLinks.js +509 -0
- package/dist/indexer/serviceLinks.js.map +1 -0
- package/dist/indexer/shapehash.d.ts +98 -0
- package/dist/indexer/shapehash.d.ts.map +1 -0
- package/dist/indexer/shapehash.js +354 -0
- package/dist/indexer/shapehash.js.map +1 -0
- package/dist/indexer/skeleton.d.ts +15 -0
- package/dist/indexer/skeleton.d.ts.map +1 -0
- package/dist/indexer/skeleton.js +136 -0
- package/dist/indexer/skeleton.js.map +1 -0
- package/dist/indexer/symbolhistory.d.ts +41 -0
- package/dist/indexer/symbolhistory.d.ts.map +1 -0
- package/dist/indexer/symbolhistory.js +124 -0
- package/dist/indexer/symbolhistory.js.map +1 -0
- package/dist/indexer/watcher.d.ts +68 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +179 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/mcp/server.d.ts +80 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +1610 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/parser/index.d.ts +8 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +33 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/languages/cpp.d.ts +3 -0
- package/dist/parser/languages/cpp.d.ts.map +1 -0
- package/dist/parser/languages/cpp.js +350 -0
- package/dist/parser/languages/cpp.js.map +1 -0
- package/dist/parser/languages/csharp.d.ts +3 -0
- package/dist/parser/languages/csharp.d.ts.map +1 -0
- package/dist/parser/languages/csharp.js +239 -0
- package/dist/parser/languages/csharp.js.map +1 -0
- package/dist/parser/languages/go.d.ts +3 -0
- package/dist/parser/languages/go.d.ts.map +1 -0
- package/dist/parser/languages/go.js +259 -0
- package/dist/parser/languages/go.js.map +1 -0
- package/dist/parser/languages/java.d.ts +3 -0
- package/dist/parser/languages/java.d.ts.map +1 -0
- package/dist/parser/languages/java.js +391 -0
- package/dist/parser/languages/java.js.map +1 -0
- package/dist/parser/languages/python.d.ts +3 -0
- package/dist/parser/languages/python.d.ts.map +1 -0
- package/dist/parser/languages/python.js +396 -0
- package/dist/parser/languages/python.js.map +1 -0
- package/dist/parser/languages/rust.d.ts +3 -0
- package/dist/parser/languages/rust.d.ts.map +1 -0
- package/dist/parser/languages/rust.js +159 -0
- package/dist/parser/languages/rust.js.map +1 -0
- package/dist/parser/languages/typescript.d.ts +3 -0
- package/dist/parser/languages/typescript.d.ts.map +1 -0
- package/dist/parser/languages/typescript.js +1442 -0
- package/dist/parser/languages/typescript.js.map +1 -0
- package/dist/parser/parserContext.d.ts +77 -0
- package/dist/parser/parserContext.d.ts.map +1 -0
- package/dist/parser/parserContext.js +354 -0
- package/dist/parser/parserContext.js.map +1 -0
- package/dist/parser/walker.d.ts +81 -0
- package/dist/parser/walker.d.ts.map +1 -0
- package/dist/parser/walker.js +217 -0
- package/dist/parser/walker.js.map +1 -0
- package/dist/parser/worker.d.ts +66 -0
- package/dist/parser/worker.d.ts.map +1 -0
- package/dist/parser/worker.js +129 -0
- package/dist/parser/worker.js.map +1 -0
- package/dist/parser/workerpool.d.ts +107 -0
- package/dist/parser/workerpool.d.ts.map +1 -0
- package/dist/parser/workerpool.js +383 -0
- package/dist/parser/workerpool.js.map +1 -0
- package/dist/scip/format.d.ts +87 -0
- package/dist/scip/format.d.ts.map +1 -0
- package/dist/scip/format.js +31 -0
- package/dist/scip/format.js.map +1 -0
- package/dist/scip/import.d.ts +37 -0
- package/dist/scip/import.d.ts.map +1 -0
- package/dist/scip/import.js +180 -0
- package/dist/scip/import.js.map +1 -0
- package/dist/types.d.ts +392 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/docs/architecture.md +105 -0
- package/docs/benchmarks/methodology.md +134 -0
- package/docs/benchmarks/raw-results.md +71 -0
- package/docs/benchmarks.md +74 -0
- package/docs/cli.md +148 -0
- package/docs/examples/behavior-tests.md +70 -0
- package/docs/examples/change-history.md +85 -0
- package/docs/examples/pre-edit-context.md +81 -0
- package/docs/examples/service-links.md +88 -0
- package/docs/examples.md +80 -0
- package/docs/faq.md +70 -0
- package/docs/internals.md +104 -0
- package/docs/languages.md +70 -0
- package/docs/limits.md +52 -0
- package/docs/mcp.md +199 -0
- package/docs/quickstart.md +119 -0
- package/docs/testing.md +123 -0
- package/docs/tools.md +115 -0
- package/package.json +52 -0
- package/research-codebase.md +578 -0
- package/seer-cli-docs.md +326 -0
- package/seer-master-guide.md +246 -0
- package/src/bundle/ci.ts +141 -0
- package/src/bundle/contract.ts +387 -0
- package/src/bundle/export.ts +175 -0
- package/src/bundle/external.ts +285 -0
- package/src/bundle/format.ts +92 -0
- package/src/bundle/import.ts +157 -0
- package/src/cli/index.ts +1249 -0
- package/src/cli/init.ts +389 -0
- package/src/db/schema.ts +614 -0
- package/src/db/store.ts +4306 -0
- package/src/graph/pagerank.ts +53 -0
- package/src/indexer/architecture.ts +148 -0
- package/src/indexer/behavior.ts +466 -0
- package/src/indexer/boundaries.ts +374 -0
- package/src/indexer/churn.ts +58 -0
- package/src/indexer/classify.ts +96 -0
- package/src/indexer/context.ts +340 -0
- package/src/indexer/continuity.ts +322 -0
- package/src/indexer/detectchanges.ts +94 -0
- package/src/indexer/discovery.ts +176 -0
- package/src/indexer/externaldeps.ts +243 -0
- package/src/indexer/freshness.ts +166 -0
- package/src/indexer/git.ts +453 -0
- package/src/indexer/index.ts +1092 -0
- package/src/indexer/modules.ts +358 -0
- package/src/indexer/preflight.ts +548 -0
- package/src/indexer/protoScanner.ts +147 -0
- package/src/indexer/risk.ts +304 -0
- package/src/indexer/serviceHostScanner.ts +92 -0
- package/src/indexer/serviceLinks.ts +543 -0
- package/src/indexer/shapehash.ts +370 -0
- package/src/indexer/skeleton.ts +169 -0
- package/src/indexer/symbolhistory.ts +172 -0
- package/src/indexer/watcher.ts +206 -0
- package/src/mcp/server.ts +1659 -0
- package/src/parser/index.ts +37 -0
- package/src/parser/languages/cpp.ts +361 -0
- package/src/parser/languages/csharp.ts +235 -0
- package/src/parser/languages/go.ts +259 -0
- package/src/parser/languages/java.ts +382 -0
- package/src/parser/languages/python.ts +370 -0
- package/src/parser/languages/rust.ts +164 -0
- package/src/parser/languages/typescript.ts +1435 -0
- package/src/parser/parserContext.ts +392 -0
- package/src/parser/walker.ts +306 -0
- package/src/parser/worker.ts +181 -0
- package/src/parser/workerpool.ts +448 -0
- package/src/scip/format.ts +83 -0
- package/src/scip/import.ts +216 -0
- package/src/types.ts +457 -0
- package/tests/benchmark-service-links.ts +244 -0
- package/tests/bug-regressions.ts +626 -0
- package/tests/filters.ts +264 -0
- package/tests/fixtures/Counter.tsx +38 -0
- package/tests/fixtures/caller.ts +7 -0
- package/tests/fixtures/collisions.ts +23 -0
- package/tests/fixtures/local_helper.ts +5 -0
- package/tests/fixtures/overloads.java +17 -0
- package/tests/fixtures/remote_helper.ts +4 -0
- package/tests/fixtures/sample.c +15 -0
- package/tests/fixtures/sample.cpp +47 -0
- package/tests/fixtures/sample.cs +62 -0
- package/tests/fixtures/sample.go +68 -0
- package/tests/fixtures/sample.h +30 -0
- package/tests/fixtures/sample.java +85 -0
- package/tests/fixtures/sample.py +46 -0
- package/tests/fixtures/sample.rs +78 -0
- package/tests/fixtures/sample.ts +76 -0
- package/tests/fixtures-service/HttpClients.cs +30 -0
- package/tests/fixtures-service/HttpClients.java +24 -0
- package/tests/fixtures-service/billing.ts +15 -0
- package/tests/fixtures-service/docker-compose.yml +15 -0
- package/tests/fixtures-service/gateway.ts +10 -0
- package/tests/fixtures-service/get_user.ts +11 -0
- package/tests/fixtures-service/graphql_client.ts +63 -0
- package/tests/fixtures-service/graphql_server.ts +30 -0
- package/tests/fixtures-service/grpc_client.go +30 -0
- package/tests/fixtures-service/http_clients.go +23 -0
- package/tests/fixtures-service/http_clients.py +38 -0
- package/tests/fixtures-service/http_clients.ts +49 -0
- package/tests/fixtures-service/k8s/payment-service.yaml +22 -0
- package/tests/fixtures-service/k8s_calls.ts +20 -0
- package/tests/fixtures-service/messaging.ts +87 -0
- package/tests/fixtures-service/trpc_client.ts +39 -0
- package/tests/fixtures-service/trpc_server.ts +39 -0
- package/tests/fixtures-service/user_service.proto +33 -0
- package/tests/fixtures-trackcd/Cargo.toml +11 -0
- package/tests/fixtures-trackcd/SpringController.java +36 -0
- package/tests/fixtures-trackcd/auth_service.ts +19 -0
- package/tests/fixtures-trackcd/complex_module.py +50 -0
- package/tests/fixtures-trackcd/express_app.js +30 -0
- package/tests/fixtures-trackcd/fastapi_app.py +49 -0
- package/tests/fixtures-trackcd/fastify_object_routes.js +32 -0
- package/tests/fixtures-trackcd/go.mod +8 -0
- package/tests/fixtures-trackcd/package.json +15 -0
- package/tests/fixtures-trackcd/requirements.txt +4 -0
- package/tests/fixtures-trackcd/tests/auth_service.test.ts +13 -0
- package/tests/fixtures-tracke/auth/AuthService.ts +23 -0
- package/tests/fixtures-tracke/auth/crypto.ts +7 -0
- package/tests/fixtures-tracke/billing/Billing.ts +20 -0
- package/tests/fixtures-tracke/billing/Invoice.ts +10 -0
- package/tests/fixtures-tracke/billing/server.ts +17 -0
- package/tests/fixtures-tracke/package.json +7 -0
- package/tests/fixtures-tracke/tests/auth.test.ts +23 -0
- package/tests/fixtures-tracke/tests/billing.test.ts +14 -0
- package/tests/fixtures-trackf/package.json +5 -0
- package/tests/fixtures-trackf/src/auth.ts +26 -0
- package/tests/fixtures-trackf/src/handlers.ts +35 -0
- package/tests/fixtures-tracki/billing/routes.ts +12 -0
- package/tests/fixtures-tracki/gateway/client.ts +13 -0
- package/tests/git-features.ts +267 -0
- package/tests/init.ts +141 -0
- package/tests/mcp-jit.ts +130 -0
- package/tests/mcp-smoke.ts +191 -0
- package/tests/mcp-trackcd.ts +169 -0
- package/tests/mcp-tracke.ts +229 -0
- package/tests/mcp-trackf.ts +330 -0
- package/tests/mcp-trackg.ts +219 -0
- package/tests/mcp-tracki.ts +174 -0
- package/tests/mcp-watcher.ts +126 -0
- package/tests/optspec.ts +194 -0
- package/tests/parallel-index.ts +333 -0
- package/tests/parallel-read.ts +125 -0
- package/tests/parallel-recovery.ts +241 -0
- package/tests/perf-callers.ts +145 -0
- package/tests/query-parity.ts +184 -0
- package/tests/query-perf.ts +55 -0
- package/tests/scale-parallel-parity.ts +225 -0
- package/tests/scale-test.ts +523 -0
- package/tests/smoke.ts +396 -0
- package/tests/trackcd.ts +325 -0
- package/tests/tracke-collisions.ts +255 -0
- package/tests/tracke.ts +314 -0
- package/tests/trackf-bugs.ts +406 -0
- package/tests/trackf.ts +390 -0
- package/tests/trackg.ts +1372 -0
- package/tests/tracki-boundaries.ts +202 -0
- package/tests/tracki-continuity.ts +253 -0
- package/tests/tracki-contract-diff.ts +249 -0
- package/tests/tracki-external-bundles.ts +341 -0
- package/tests/tracki-preflight.ts +251 -0
- package/tests/verify-roles.ts +51 -0
- package/tests/worker-parity.ts +286 -0
- package/tests/worker-pool.ts +262 -0
- 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
|
+
}
|