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,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple iterative PageRank over the symbol call graph.
|
|
3
|
+
* Follows the standard damping-factor formulation used by Aider's repo-map.
|
|
4
|
+
*/
|
|
5
|
+
export function computePageRank(
|
|
6
|
+
symbolIds: number[],
|
|
7
|
+
edges: Array<{ from: number; to: number }>,
|
|
8
|
+
iterations = 20,
|
|
9
|
+
damping = 0.85,
|
|
10
|
+
): Map<number, number> {
|
|
11
|
+
const n = symbolIds.length;
|
|
12
|
+
if (n === 0) return new Map();
|
|
13
|
+
|
|
14
|
+
const initial = 1.0 / n;
|
|
15
|
+
const ranks = new Map<number, number>(symbolIds.map(id => [id, initial]));
|
|
16
|
+
|
|
17
|
+
// outgoing edges per node (deduped — out-degree counts distinct targets)
|
|
18
|
+
const outgoing = new Map<number, Set<number>>();
|
|
19
|
+
for (const id of symbolIds) outgoing.set(id, new Set());
|
|
20
|
+
|
|
21
|
+
// incoming edges per node (deduped — each distinct (from→to) contributes once,
|
|
22
|
+
// regardless of how many call sites exist between the two symbols)
|
|
23
|
+
const incoming = new Map<number, Set<number>>();
|
|
24
|
+
for (const id of symbolIds) incoming.set(id, new Set());
|
|
25
|
+
|
|
26
|
+
for (const { from, to } of edges) {
|
|
27
|
+
if (outgoing.has(from) && incoming.has(to)) {
|
|
28
|
+
outgoing.get(from)!.add(to);
|
|
29
|
+
incoming.get(to)!.add(from);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const baseRank = (1 - damping) / n;
|
|
34
|
+
|
|
35
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
36
|
+
const next = new Map<number, number>();
|
|
37
|
+
|
|
38
|
+
for (const id of symbolIds) {
|
|
39
|
+
let rank = baseRank;
|
|
40
|
+
for (const fromId of incoming.get(id) ?? []) {
|
|
41
|
+
const outDeg = outgoing.get(fromId)!.size;
|
|
42
|
+
if (outDeg > 0) {
|
|
43
|
+
rank += damping * (ranks.get(fromId)! / outDeg);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
next.set(id, rank);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const [id, r] of next) ranks.set(id, r);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return ranks;
|
|
53
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { Store } from '../db/store.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Aggregate "what is this codebase?" snapshot. The view an agent should load
|
|
6
|
+
* BEFORE asking detailed questions — languages, packages, entry points,
|
|
7
|
+
* hotspots, deps, top-N symbols.
|
|
8
|
+
*
|
|
9
|
+
* Pure read-side: never mutates the DB. Cheap enough to run on every
|
|
10
|
+
* `seer_architecture` call (a few aggregate queries + some JS shaping).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface ArchitectureView {
|
|
14
|
+
workspace: string;
|
|
15
|
+
languages: Array<{ language: string; files: number; symbols: number }>;
|
|
16
|
+
fileRoles: { project: number; vendor: number; generated: number; test: number };
|
|
17
|
+
totals: {
|
|
18
|
+
files: number;
|
|
19
|
+
symbols: number;
|
|
20
|
+
edges: number;
|
|
21
|
+
routes: number;
|
|
22
|
+
externalDependencies: number;
|
|
23
|
+
configKeys: number;
|
|
24
|
+
};
|
|
25
|
+
/** Highest-PageRank symbols across the project (excludes vendor/generated). */
|
|
26
|
+
topSymbols: Array<{ name: string; qualifiedName: string | null; kind: string; file: string; pagerank: number }>;
|
|
27
|
+
/** Most-churned files. Empty if churn pass hasn't run. */
|
|
28
|
+
hotspots: Array<{ file: string; commits: number; lastCommit: number | null; topAuthor: string | null }>;
|
|
29
|
+
/** Probable entry points — top-PageRank symbols whose name matches common entry conventions. */
|
|
30
|
+
entryPoints: Array<{ name: string; qualifiedName: string | null; file: string; kind: string }>;
|
|
31
|
+
/** Aggregate of detected HTTP routes. */
|
|
32
|
+
routes: { total: number; byFramework: Record<string, number> };
|
|
33
|
+
/** Module / package boundaries — top-level directories under the workspace by file count. */
|
|
34
|
+
topModules: Array<{ name: string; files: number; symbols: number }>;
|
|
35
|
+
/** Most-depended-on external dependencies. */
|
|
36
|
+
externalDependencies: Array<{ ecosystem: string; name: string; versionRange: string | null }>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ENTRY_POINT_NAMES = new Set([
|
|
40
|
+
'main', 'Main', '__main__', 'run', 'start', 'serve', 'app',
|
|
41
|
+
'createServer', 'createApp', 'bootstrap', 'init', 'entry',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
export function buildArchitecture(workspace: string, store: Store): ArchitectureView {
|
|
45
|
+
const stats = store.getStats();
|
|
46
|
+
const langMap = new Map<string, { files: number; symbols: number }>();
|
|
47
|
+
for (const [lang, count] of Object.entries(stats.languages)) {
|
|
48
|
+
langMap.set(lang, { files: count, symbols: 0 });
|
|
49
|
+
}
|
|
50
|
+
// Symbol counts per language: one quick query.
|
|
51
|
+
const langSymRows = store.rawDb().prepare(`
|
|
52
|
+
SELECT f.language, COUNT(*) AS c
|
|
53
|
+
FROM symbols s JOIN files f ON f.id = s.file_id
|
|
54
|
+
GROUP BY f.language
|
|
55
|
+
`).all() as Array<{ language: string; c: number }>;
|
|
56
|
+
for (const r of langSymRows) {
|
|
57
|
+
const entry = langMap.get(String(r.language));
|
|
58
|
+
if (entry) entry.symbols = Number(r.c);
|
|
59
|
+
}
|
|
60
|
+
const languages = Array.from(langMap.entries())
|
|
61
|
+
.map(([language, v]) => ({ language, files: v.files, symbols: v.symbols }))
|
|
62
|
+
.sort((a, b) => b.files - a.files);
|
|
63
|
+
|
|
64
|
+
const topSymbols = store.getTopSymbols(15).map(s => ({
|
|
65
|
+
name: s.name, qualifiedName: s.qualifiedName, kind: s.kind,
|
|
66
|
+
file: s.filePath, pagerank: s.pagerank,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const hotspotRows = store.topChurnedFiles(15);
|
|
70
|
+
const hotspots = hotspotRows.map(h => ({
|
|
71
|
+
file: h.filePath, commits: h.commitCount, lastCommit: h.lastCommitAt,
|
|
72
|
+
topAuthor: h.topAuthor,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
// Entry-point heuristic: top-PageRank rankable symbols whose name matches
|
|
76
|
+
// a common entry-point convention. We pull more rows than we need and
|
|
77
|
+
// filter so the heuristic adapts naturally to bigger codebases.
|
|
78
|
+
const entryPointCandidates = store.getTopSymbols(200);
|
|
79
|
+
const entryPoints = entryPointCandidates
|
|
80
|
+
.filter(s => ENTRY_POINT_NAMES.has(s.name) || /^(?:_)?main_?$/.test(s.name))
|
|
81
|
+
.slice(0, 10)
|
|
82
|
+
.map(s => ({ name: s.name, qualifiedName: s.qualifiedName, file: s.filePath, kind: s.kind }));
|
|
83
|
+
|
|
84
|
+
// Module breakdown: bucket files by their top-level directory under the
|
|
85
|
+
// workspace. Helps an agent see "billing/ is 200 files, auth/ is 80".
|
|
86
|
+
const moduleMap = new Map<string, { files: number; symbols: number }>();
|
|
87
|
+
const allFiles = store.listFiles();
|
|
88
|
+
for (const f of allFiles) {
|
|
89
|
+
const top = topLevelDir(f.relPath);
|
|
90
|
+
if (!top) continue;
|
|
91
|
+
const e = moduleMap.get(top) ?? { files: 0, symbols: 0 };
|
|
92
|
+
e.files++;
|
|
93
|
+
moduleMap.set(top, e);
|
|
94
|
+
}
|
|
95
|
+
const modSymRows = store.rawDb().prepare(`
|
|
96
|
+
SELECT f.rel_path AS rel_path, COUNT(*) AS c
|
|
97
|
+
FROM symbols s JOIN files f ON f.id = s.file_id
|
|
98
|
+
GROUP BY f.id
|
|
99
|
+
`).all() as Array<{ rel_path: string; c: number }>;
|
|
100
|
+
for (const r of modSymRows) {
|
|
101
|
+
const top = topLevelDir(String(r.rel_path));
|
|
102
|
+
if (!top) continue;
|
|
103
|
+
const e = moduleMap.get(top);
|
|
104
|
+
if (e) e.symbols += Number(r.c);
|
|
105
|
+
}
|
|
106
|
+
const topModules = Array.from(moduleMap.entries())
|
|
107
|
+
.map(([name, v]) => ({ name, files: v.files, symbols: v.symbols }))
|
|
108
|
+
.sort((a, b) => b.symbols - a.symbols)
|
|
109
|
+
.slice(0, 12);
|
|
110
|
+
|
|
111
|
+
// Routes by framework.
|
|
112
|
+
const routesByFramework: Record<string, number> = {};
|
|
113
|
+
try {
|
|
114
|
+
const rows = store.rawDb().prepare('SELECT framework, COUNT(*) AS c FROM routes GROUP BY framework').all() as Array<{ framework: string; c: number }>;
|
|
115
|
+
for (const r of rows) routesByFramework[String(r.framework)] = Number(r.c);
|
|
116
|
+
} catch { /* */ }
|
|
117
|
+
|
|
118
|
+
const externalDependencies = store.listExternalDeps({ limit: 25 }).map(d => ({
|
|
119
|
+
ecosystem: d.ecosystem, name: d.name, versionRange: d.versionRange,
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
workspace,
|
|
124
|
+
languages,
|
|
125
|
+
fileRoles: stats.roles ?? { project: 0, vendor: 0, generated: 0, test: 0 },
|
|
126
|
+
totals: {
|
|
127
|
+
files: stats.files,
|
|
128
|
+
symbols: stats.symbols,
|
|
129
|
+
edges: stats.edges,
|
|
130
|
+
routes: stats.routes ?? 0,
|
|
131
|
+
externalDependencies: stats.externalDependencies ?? 0,
|
|
132
|
+
configKeys: stats.configKeys ?? 0,
|
|
133
|
+
},
|
|
134
|
+
topSymbols,
|
|
135
|
+
hotspots,
|
|
136
|
+
entryPoints,
|
|
137
|
+
routes: { total: stats.routes ?? 0, byFramework: routesByFramework },
|
|
138
|
+
topModules,
|
|
139
|
+
externalDependencies,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function topLevelDir(relPath: string): string | null {
|
|
144
|
+
const norm = relPath.replace(/\\/g, '/');
|
|
145
|
+
const idx = norm.indexOf('/');
|
|
146
|
+
if (idx <= 0) return null;
|
|
147
|
+
return norm.slice(0, idx);
|
|
148
|
+
}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Store } from '../db/store.js';
|
|
4
|
+
import type { SymbolRow } from '../types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tests-as-behavioral-spec, ranked.
|
|
8
|
+
*
|
|
9
|
+
* The Track-D `seer_behavior` returned raw 'tests' edges with no ranking,
|
|
10
|
+
* which made it hard for an agent to pick the test most worth reading.
|
|
11
|
+
* Track-E presents a ranked behavioral contract:
|
|
12
|
+
*
|
|
13
|
+
* - DIRECT coverage: synthesized 'tests' edges into the symbol
|
|
14
|
+
* - INDIRECT coverage: tests that call something the symbol transitively
|
|
15
|
+
* reaches (depth-limited)
|
|
16
|
+
* - NAMING-CONVENTION coverage: test symbols whose name contains the
|
|
17
|
+
* target's name (`testLogin`, `test_login`, `loginShouldSucceed`)
|
|
18
|
+
* - SAME-FILE coverage: tests in a file that maps by convention to the
|
|
19
|
+
* production file (`auth_service.test.ts` ↔ `auth_service.ts`,
|
|
20
|
+
* `Login.spec.tsx` ↔ `Login.tsx`)
|
|
21
|
+
*
|
|
22
|
+
* Each test gets:
|
|
23
|
+
* - relationship — which of the four signals matched (best match wins)
|
|
24
|
+
* - assertionCount — count of likely-assertion lines in the test body
|
|
25
|
+
* - graphDistance — BFS distance from the test caller to the target (1
|
|
26
|
+
* for direct, 2+ for indirect, null when found via naming/path only)
|
|
27
|
+
* - specificity — derived ranking score (higher = stronger contract)
|
|
28
|
+
* - recency — most recent file_churn last_commit_at when available
|
|
29
|
+
*
|
|
30
|
+
* Output is sorted by (specificity DESC, graphDistance ASC, file ASC).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export type BehaviorRelationship =
|
|
34
|
+
| 'direct-call'
|
|
35
|
+
| 'indirect-call'
|
|
36
|
+
| 'naming-convention'
|
|
37
|
+
| 'same-file';
|
|
38
|
+
|
|
39
|
+
export interface RankedBehaviorTest {
|
|
40
|
+
testSymbol: {
|
|
41
|
+
id: number;
|
|
42
|
+
name: string;
|
|
43
|
+
qualifiedName: string | null;
|
|
44
|
+
kind: string;
|
|
45
|
+
file: string;
|
|
46
|
+
lineStart: number;
|
|
47
|
+
lineEnd: number;
|
|
48
|
+
};
|
|
49
|
+
relationship: BehaviorRelationship;
|
|
50
|
+
graphDistance: number | null;
|
|
51
|
+
assertionCount: number;
|
|
52
|
+
specificity: number;
|
|
53
|
+
recentCommitAt: number | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BehaviorResult {
|
|
57
|
+
symbol: {
|
|
58
|
+
id: number;
|
|
59
|
+
name: string;
|
|
60
|
+
qualifiedName: string | null;
|
|
61
|
+
kind: string;
|
|
62
|
+
file: string;
|
|
63
|
+
};
|
|
64
|
+
total: number;
|
|
65
|
+
direct: number;
|
|
66
|
+
indirect: number;
|
|
67
|
+
namingMatches: number;
|
|
68
|
+
sameFileMatches: number;
|
|
69
|
+
tests: RankedBehaviorTest[];
|
|
70
|
+
source: 'tree-sitter';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ASSERTION_PATTERNS: RegExp[] = [
|
|
74
|
+
/\bexpect\s*\(/,
|
|
75
|
+
/\bassert(?:Equals?|True|False|Throws?|That)?\s*\(/i,
|
|
76
|
+
/\bshould\.(?:eq|equal|exist|throw|be)\b/i,
|
|
77
|
+
/\b(?:should|to)\.(?:be|equal|throw|deep|have)\b/i,
|
|
78
|
+
/\bassert!\s*\(/, // Rust `assert!` macro
|
|
79
|
+
/\bassert_eq!\s*\(/, // Rust `assert_eq!`
|
|
80
|
+
/\bassert\s+/, // Python `assert x == y`
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const TEST_FILENAME_SUFFIXES = ['.test.', '.spec.', '_test.', '.tests.'];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Compute the ranked behavioral contract for a target symbol.
|
|
87
|
+
*
|
|
88
|
+
* `nameOrId` may be a string (looked up via `getDefinition`) or a numeric id.
|
|
89
|
+
* When `nameOrId` matches multiple symbols, the highest-PageRank one is used.
|
|
90
|
+
*/
|
|
91
|
+
export function rankedBehavior(
|
|
92
|
+
store: Store,
|
|
93
|
+
nameOrId: string | number,
|
|
94
|
+
options: {
|
|
95
|
+
limit?: number;
|
|
96
|
+
indirectDepth?: number;
|
|
97
|
+
includeNamingConvention?: boolean;
|
|
98
|
+
includeSameFile?: boolean;
|
|
99
|
+
} = {},
|
|
100
|
+
): BehaviorResult | null {
|
|
101
|
+
const limit = options.limit ?? 30;
|
|
102
|
+
const indirectDepth = options.indirectDepth ?? 2;
|
|
103
|
+
const includeNaming = options.includeNamingConvention ?? true;
|
|
104
|
+
const includeSameFile = options.includeSameFile ?? true;
|
|
105
|
+
|
|
106
|
+
let target: SymbolRow | null = null;
|
|
107
|
+
if (typeof nameOrId === 'number') {
|
|
108
|
+
target = store.getSymbolById(nameOrId);
|
|
109
|
+
} else {
|
|
110
|
+
const candidates = store.getDefinition(nameOrId);
|
|
111
|
+
if (candidates.length === 0) {
|
|
112
|
+
// Try with includeDeclarations so we don't miss method-prototype targets.
|
|
113
|
+
const decl = store.getDefinition(nameOrId, { includeDeclarations: true });
|
|
114
|
+
target = decl[0] ?? null;
|
|
115
|
+
} else {
|
|
116
|
+
target = candidates[0];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!target) return null;
|
|
120
|
+
|
|
121
|
+
// ID-based lookup: never collapses same-short-name siblings. The
|
|
122
|
+
// synthesizeTestEdges pass preserves the original call edge's resolved
|
|
123
|
+
// to_id, so e.to_id == target.id is the correct id-scoped predicate.
|
|
124
|
+
const directRows = store.directTestEdgesForId(target.id, 500);
|
|
125
|
+
// Annotate with assertion counts (cheap: cached reads per file).
|
|
126
|
+
const fileCache = new Map<string, string[]>();
|
|
127
|
+
const readFileLines = (fp: string): string[] => {
|
|
128
|
+
const cached = fileCache.get(fp);
|
|
129
|
+
if (cached) return cached;
|
|
130
|
+
let lines: string[] = [];
|
|
131
|
+
try {
|
|
132
|
+
const text = fs.readFileSync(fp, 'utf8');
|
|
133
|
+
lines = text.split(/\r?\n/);
|
|
134
|
+
} catch { /* */ }
|
|
135
|
+
fileCache.set(fp, lines);
|
|
136
|
+
return lines;
|
|
137
|
+
};
|
|
138
|
+
const assertionsForRange = (filePath: string, start: number, end: number): number => {
|
|
139
|
+
const lines = readFileLines(filePath);
|
|
140
|
+
if (lines.length === 0) return 0;
|
|
141
|
+
let n = 0;
|
|
142
|
+
const lo = Math.max(0, start);
|
|
143
|
+
const hi = Math.min(lines.length - 1, end);
|
|
144
|
+
for (let i = lo; i <= hi; i++) {
|
|
145
|
+
const ln = lines[i];
|
|
146
|
+
for (const re of ASSERTION_PATTERNS) {
|
|
147
|
+
if (re.test(ln)) { n++; break; }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return n;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const seenCallerIds = new Set<number>();
|
|
154
|
+
const out: RankedBehaviorTest[] = [];
|
|
155
|
+
|
|
156
|
+
for (const r of directRows) {
|
|
157
|
+
if (seenCallerIds.has(r.callerId)) continue;
|
|
158
|
+
seenCallerIds.add(r.callerId);
|
|
159
|
+
const assertionCount = assertionsForRange(r.callerFile, r.callerLineStart, r.callerLineEnd);
|
|
160
|
+
const recent = recentCommitForFile(store, r.callerFile);
|
|
161
|
+
out.push({
|
|
162
|
+
testSymbol: {
|
|
163
|
+
id: r.callerId,
|
|
164
|
+
name: r.callerName,
|
|
165
|
+
qualifiedName: r.callerQualifiedName,
|
|
166
|
+
kind: r.callerKind,
|
|
167
|
+
file: r.callerFile,
|
|
168
|
+
lineStart: r.callerLineStart,
|
|
169
|
+
lineEnd: r.callerLineEnd,
|
|
170
|
+
},
|
|
171
|
+
relationship: 'direct-call',
|
|
172
|
+
graphDistance: 1,
|
|
173
|
+
assertionCount,
|
|
174
|
+
specificity: scoreSpecificity('direct-call', 1, assertionCount),
|
|
175
|
+
recentCommitAt: recent,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── INDIRECT coverage ────────────────────────────────────────────────────
|
|
180
|
+
// For each test symbol whose call graph reaches target within
|
|
181
|
+
// `indirectDepth`, record it once. We deliberately use the reverse-reachable
|
|
182
|
+
// walk from the target so we don't fan out from every test file.
|
|
183
|
+
if (indirectDepth > 0) {
|
|
184
|
+
const reverseHits = store.reverseReachableWithDepth(target.id, indirectDepth + 1);
|
|
185
|
+
// Filter to symbols that live in test files (role='test') AND haven't
|
|
186
|
+
// already been picked up as direct.
|
|
187
|
+
const candidateIds = reverseHits.map(h => h.id);
|
|
188
|
+
if (candidateIds.length > 0) {
|
|
189
|
+
const placeholders = candidateIds.map(() => '?').join(',');
|
|
190
|
+
const rows = store.rawDb().prepare(`
|
|
191
|
+
SELECT s.id, s.name, s.qualified_name AS qualifiedName, s.kind,
|
|
192
|
+
f.path AS file, s.line_start AS lineStart, s.line_end AS lineEnd,
|
|
193
|
+
f.role AS fileRole
|
|
194
|
+
FROM symbols s JOIN files f ON f.id = s.file_id
|
|
195
|
+
WHERE s.id IN (${placeholders}) AND f.role = 'test'
|
|
196
|
+
`).all(...candidateIds) as Array<{
|
|
197
|
+
id: unknown; name: unknown; qualifiedName: unknown; kind: unknown;
|
|
198
|
+
file: unknown; lineStart: unknown; lineEnd: unknown; fileRole: unknown;
|
|
199
|
+
}>;
|
|
200
|
+
const depthById = new Map(reverseHits.map(h => [h.id, h.depth]));
|
|
201
|
+
for (const r of rows) {
|
|
202
|
+
const id = Number(r.id);
|
|
203
|
+
if (seenCallerIds.has(id)) continue;
|
|
204
|
+
seenCallerIds.add(id);
|
|
205
|
+
const fp = String(r.file);
|
|
206
|
+
const start = Number(r.lineStart);
|
|
207
|
+
const end = Number(r.lineEnd);
|
|
208
|
+
const distance = depthById.get(id) ?? 2;
|
|
209
|
+
const assertionCount = assertionsForRange(fp, start, end);
|
|
210
|
+
out.push({
|
|
211
|
+
testSymbol: {
|
|
212
|
+
id,
|
|
213
|
+
name: String(r.name),
|
|
214
|
+
qualifiedName: r.qualifiedName == null ? null : String(r.qualifiedName),
|
|
215
|
+
kind: String(r.kind),
|
|
216
|
+
file: fp,
|
|
217
|
+
lineStart: start,
|
|
218
|
+
lineEnd: end,
|
|
219
|
+
},
|
|
220
|
+
relationship: 'indirect-call',
|
|
221
|
+
graphDistance: distance,
|
|
222
|
+
assertionCount,
|
|
223
|
+
specificity: scoreSpecificity('indirect-call', distance, assertionCount),
|
|
224
|
+
recentCommitAt: recentCommitForFile(store, fp),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── NAMING-CONVENTION coverage ──────────────────────────────────────────
|
|
231
|
+
// Test-file symbols whose name contains the target name (case-insensitive,
|
|
232
|
+
// word-boundary). Skips anything already seen via direct/indirect.
|
|
233
|
+
if (includeNaming) {
|
|
234
|
+
const needle = target.name;
|
|
235
|
+
if (needle && needle.length >= 3) {
|
|
236
|
+
const likes = [`%${needle}%`];
|
|
237
|
+
const rows = store.rawDb().prepare(`
|
|
238
|
+
SELECT s.id, s.name, s.qualified_name AS qualifiedName, s.kind,
|
|
239
|
+
f.path AS file, s.line_start AS lineStart, s.line_end AS lineEnd
|
|
240
|
+
FROM symbols s JOIN files f ON f.id = s.file_id
|
|
241
|
+
WHERE f.role = 'test'
|
|
242
|
+
AND s.kind IN ('function','method')
|
|
243
|
+
AND (s.name LIKE ? COLLATE NOCASE)
|
|
244
|
+
`).all(...likes) as Array<{
|
|
245
|
+
id: unknown; name: unknown; qualifiedName: unknown; kind: unknown;
|
|
246
|
+
file: unknown; lineStart: unknown; lineEnd: unknown;
|
|
247
|
+
}>;
|
|
248
|
+
for (const r of rows) {
|
|
249
|
+
const id = Number(r.id);
|
|
250
|
+
if (seenCallerIds.has(id)) continue;
|
|
251
|
+
const tname = String(r.name);
|
|
252
|
+
if (!nameMatches(tname, needle)) continue;
|
|
253
|
+
seenCallerIds.add(id);
|
|
254
|
+
const fp = String(r.file);
|
|
255
|
+
const start = Number(r.lineStart);
|
|
256
|
+
const end = Number(r.lineEnd);
|
|
257
|
+
const assertionCount = assertionsForRange(fp, start, end);
|
|
258
|
+
out.push({
|
|
259
|
+
testSymbol: {
|
|
260
|
+
id,
|
|
261
|
+
name: tname,
|
|
262
|
+
qualifiedName: r.qualifiedName == null ? null : String(r.qualifiedName),
|
|
263
|
+
kind: String(r.kind),
|
|
264
|
+
file: fp,
|
|
265
|
+
lineStart: start,
|
|
266
|
+
lineEnd: end,
|
|
267
|
+
},
|
|
268
|
+
relationship: 'naming-convention',
|
|
269
|
+
graphDistance: null,
|
|
270
|
+
assertionCount,
|
|
271
|
+
specificity: scoreSpecificity('naming-convention', null, assertionCount),
|
|
272
|
+
recentCommitAt: recentCommitForFile(store, fp),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── SAME-FILE coverage (path convention) ────────────────────────────────
|
|
279
|
+
if (includeSameFile) {
|
|
280
|
+
const targetFile = target.filePath;
|
|
281
|
+
const candidateTestFiles = candidateTestFilesForProduction(targetFile);
|
|
282
|
+
if (candidateTestFiles.length > 0) {
|
|
283
|
+
const placeholders = candidateTestFiles.map(() => '?').join(',');
|
|
284
|
+
const rows = store.rawDb().prepare(`
|
|
285
|
+
SELECT s.id, s.name, s.qualified_name AS qualifiedName, s.kind,
|
|
286
|
+
f.path AS file, s.line_start AS lineStart, s.line_end AS lineEnd
|
|
287
|
+
FROM symbols s JOIN files f ON f.id = s.file_id
|
|
288
|
+
WHERE f.role = 'test'
|
|
289
|
+
AND s.kind IN ('function','method')
|
|
290
|
+
AND (f.path IN (${placeholders}) OR f.rel_path IN (${placeholders}))
|
|
291
|
+
`).all(...candidateTestFiles, ...candidateTestFiles) as Array<{
|
|
292
|
+
id: unknown; name: unknown; qualifiedName: unknown; kind: unknown;
|
|
293
|
+
file: unknown; lineStart: unknown; lineEnd: unknown;
|
|
294
|
+
}>;
|
|
295
|
+
for (const r of rows) {
|
|
296
|
+
const id = Number(r.id);
|
|
297
|
+
if (seenCallerIds.has(id)) continue;
|
|
298
|
+
seenCallerIds.add(id);
|
|
299
|
+
const fp = String(r.file);
|
|
300
|
+
const start = Number(r.lineStart);
|
|
301
|
+
const end = Number(r.lineEnd);
|
|
302
|
+
const assertionCount = assertionsForRange(fp, start, end);
|
|
303
|
+
out.push({
|
|
304
|
+
testSymbol: {
|
|
305
|
+
id,
|
|
306
|
+
name: String(r.name),
|
|
307
|
+
qualifiedName: r.qualifiedName == null ? null : String(r.qualifiedName),
|
|
308
|
+
kind: String(r.kind),
|
|
309
|
+
file: fp,
|
|
310
|
+
lineStart: start,
|
|
311
|
+
lineEnd: end,
|
|
312
|
+
},
|
|
313
|
+
relationship: 'same-file',
|
|
314
|
+
graphDistance: null,
|
|
315
|
+
assertionCount,
|
|
316
|
+
specificity: scoreSpecificity('same-file', null, assertionCount),
|
|
317
|
+
recentCommitAt: recentCommitForFile(store, fp),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Sort and slice.
|
|
324
|
+
out.sort((a, b) => {
|
|
325
|
+
if (b.specificity !== a.specificity) return b.specificity - a.specificity;
|
|
326
|
+
const ad = a.graphDistance ?? 99;
|
|
327
|
+
const bd = b.graphDistance ?? 99;
|
|
328
|
+
if (ad !== bd) return ad - bd;
|
|
329
|
+
if (a.testSymbol.file !== b.testSymbol.file) {
|
|
330
|
+
return a.testSymbol.file < b.testSymbol.file ? -1 : 1;
|
|
331
|
+
}
|
|
332
|
+
return a.testSymbol.lineStart - b.testSymbol.lineStart;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const direct = out.filter(t => t.relationship === 'direct-call').length;
|
|
336
|
+
const indirect = out.filter(t => t.relationship === 'indirect-call').length;
|
|
337
|
+
const namingMatches = out.filter(t => t.relationship === 'naming-convention').length;
|
|
338
|
+
const sameFileMatches = out.filter(t => t.relationship === 'same-file').length;
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
symbol: {
|
|
342
|
+
id: target.id,
|
|
343
|
+
name: target.name,
|
|
344
|
+
qualifiedName: target.qualifiedName,
|
|
345
|
+
kind: target.kind,
|
|
346
|
+
file: target.filePath,
|
|
347
|
+
},
|
|
348
|
+
total: out.length,
|
|
349
|
+
direct,
|
|
350
|
+
indirect,
|
|
351
|
+
namingMatches,
|
|
352
|
+
sameFileMatches,
|
|
353
|
+
tests: out.slice(0, limit),
|
|
354
|
+
source: 'tree-sitter',
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
function scoreSpecificity(
|
|
361
|
+
relationship: BehaviorRelationship,
|
|
362
|
+
distance: number | null,
|
|
363
|
+
assertionCount: number,
|
|
364
|
+
): number {
|
|
365
|
+
// Base weights — direct > naming > same-file > indirect (distance > 1).
|
|
366
|
+
// Direct calls are the strongest behavioral contract because the test
|
|
367
|
+
// literally exercises the symbol; naming/same-file are good fallbacks;
|
|
368
|
+
// indirect calls fade quickly with distance.
|
|
369
|
+
let base: number;
|
|
370
|
+
switch (relationship) {
|
|
371
|
+
case 'direct-call': base = 100; break;
|
|
372
|
+
case 'naming-convention': base = 60; break;
|
|
373
|
+
case 'same-file': base = 40; break;
|
|
374
|
+
case 'indirect-call': base = Math.max(0, 50 - 10 * ((distance ?? 2) - 1)); break;
|
|
375
|
+
}
|
|
376
|
+
// Each assertion line adds a small boost (capped at 20). A test that
|
|
377
|
+
// exercises the symbol AND has assertions is a stronger contract than one
|
|
378
|
+
// that calls but doesn't assert.
|
|
379
|
+
const assertionBoost = Math.min(20, assertionCount * 4);
|
|
380
|
+
return base + assertionBoost;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function nameMatches(testName: string, target: string): boolean {
|
|
384
|
+
// Word-boundary-ish match: testName must contain the target as a contiguous
|
|
385
|
+
// case-insensitive substring AND have a non-alphanumeric (or end) on at
|
|
386
|
+
// least one side. Avoids `validate` matching `revalidateBackoff` etc.
|
|
387
|
+
if (testName.length < target.length) return false;
|
|
388
|
+
const lcT = testName.toLowerCase();
|
|
389
|
+
const lcN = target.toLowerCase();
|
|
390
|
+
let idx = lcT.indexOf(lcN);
|
|
391
|
+
while (idx !== -1) {
|
|
392
|
+
const before = idx === 0 ? '_' : lcT[idx - 1];
|
|
393
|
+
const after = idx + lcN.length >= lcT.length ? '_' : lcT[idx + lcN.length];
|
|
394
|
+
if (!/[a-z0-9]/.test(before) || !/[a-z0-9]/.test(after)) return true;
|
|
395
|
+
// PascalCase: prev char uppercase, this char uppercase boundary
|
|
396
|
+
idx = lcT.indexOf(lcN, idx + 1);
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* For a production file path, produce candidate paths where its tests might
|
|
403
|
+
* live. Covers the common conventions for the languages Seer indexes:
|
|
404
|
+
* - `src/auth_service.ts` → `src/auth_service.test.ts`, `tests/auth_service.test.ts`
|
|
405
|
+
* - `src/components/Login.tsx` → `src/components/Login.spec.tsx`, …
|
|
406
|
+
* - `pkg/auth/auth.go` → `pkg/auth/auth_test.go`
|
|
407
|
+
* - `lib/auth.py` → `tests/test_auth.py`, `tests/auth_test.py`
|
|
408
|
+
* - `src/Auth.java` → `src/test/java/AuthTest.java` (rough)
|
|
409
|
+
*/
|
|
410
|
+
function candidateTestFilesForProduction(prodPath: string): string[] {
|
|
411
|
+
const out: string[] = [];
|
|
412
|
+
const norm = prodPath.replace(/\\/g, '/');
|
|
413
|
+
const dir = path.posix.dirname(norm);
|
|
414
|
+
const ext = path.posix.extname(norm);
|
|
415
|
+
const base = path.posix.basename(norm, ext);
|
|
416
|
+
|
|
417
|
+
// JS/TS family: foo.ts → foo.test.ts, foo.spec.ts, foo.tests.ts
|
|
418
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
|
419
|
+
for (const tag of ['.test', '.spec', '.tests']) {
|
|
420
|
+
out.push(`${dir}/${base}${tag}${ext}`);
|
|
421
|
+
out.push(`${dir}/__tests__/${base}${tag}${ext}`);
|
|
422
|
+
out.push(`tests/${base}${tag}${ext}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Go: foo.go → foo_test.go
|
|
426
|
+
if (ext === '.go') {
|
|
427
|
+
out.push(`${dir}/${base}_test.go`);
|
|
428
|
+
}
|
|
429
|
+
// Python: foo.py → test_foo.py, foo_test.py
|
|
430
|
+
if (ext === '.py') {
|
|
431
|
+
out.push(`${dir}/test_${base}.py`);
|
|
432
|
+
out.push(`${dir}/${base}_test.py`);
|
|
433
|
+
out.push(`tests/test_${base}.py`);
|
|
434
|
+
}
|
|
435
|
+
// Java/C# — Test suffix
|
|
436
|
+
if (['.java', '.cs'].includes(ext)) {
|
|
437
|
+
out.push(`${dir}/${base}Test${ext}`);
|
|
438
|
+
out.push(`${dir}/${base}Tests${ext}`);
|
|
439
|
+
}
|
|
440
|
+
// C/C++ — test_foo.c etc.
|
|
441
|
+
if (['.c', '.cc', '.cpp', '.cxx', '.h', '.hpp'].includes(ext)) {
|
|
442
|
+
out.push(`${dir}/test_${base}${ext}`);
|
|
443
|
+
out.push(`${dir}/${base}_test${ext}`);
|
|
444
|
+
}
|
|
445
|
+
// Rust — same file with #[cfg(test)] handled by direct; sibling tests dir
|
|
446
|
+
if (ext === '.rs') {
|
|
447
|
+
out.push(`${dir}/${base}_test.rs`);
|
|
448
|
+
out.push(`tests/${base}.rs`);
|
|
449
|
+
}
|
|
450
|
+
return Array.from(new Set(out));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function recentCommitForFile(store: Store, filePath: string): number | null {
|
|
454
|
+
try {
|
|
455
|
+
const churn = store.getFileChurn(filePath);
|
|
456
|
+
return churn?.lastCommitAt ?? null;
|
|
457
|
+
} catch { return null; }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Lightweight predicate used by other Track-E features. */
|
|
461
|
+
export function isTestFilePath(p: string): boolean {
|
|
462
|
+
const lc = p.toLowerCase().replace(/\\/g, '/');
|
|
463
|
+
if (lc.includes('/tests/') || lc.startsWith('tests/') || lc.includes('/__tests__/')) return true;
|
|
464
|
+
for (const suf of TEST_FILENAME_SUFFIXES) if (lc.includes(suf)) return true;
|
|
465
|
+
return false;
|
|
466
|
+
}
|