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,340 @@
|
|
|
1
|
+
import { Store } from '../db/store.js';
|
|
2
|
+
import { rankedBehavior } from './behavior.js';
|
|
3
|
+
import { computeRisk, RiskResult } from './risk.js';
|
|
4
|
+
import type { SymbolRow, CallerRow, CalleeRow } from '../types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One compact, structured pre-edit packet for a symbol.
|
|
8
|
+
*
|
|
9
|
+
* The aim is workflow compression: an agent that's about to edit a symbol
|
|
10
|
+
* should be able to call ONE tool and get back the deterministic evidence
|
|
11
|
+
* it needs — definition, callers, callees, routes, config reads, behavioral
|
|
12
|
+
* tests, recent history, complexity, module, blast radius, and the
|
|
13
|
+
* decomposed risk score. The agent then decides which slices to expand
|
|
14
|
+
* (`seer_history` for full chain, `seer_callers` for everyone, etc.).
|
|
15
|
+
*
|
|
16
|
+
* This is intentionally NOT an explanation layer. Seer-Core stays
|
|
17
|
+
* deterministic facts only; any narrative about "why this matters" belongs
|
|
18
|
+
* outside Core.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface ContextPacket {
|
|
22
|
+
symbol: {
|
|
23
|
+
id: number;
|
|
24
|
+
name: string;
|
|
25
|
+
qualifiedName: string | null;
|
|
26
|
+
kind: string;
|
|
27
|
+
file: string;
|
|
28
|
+
lineStart: number;
|
|
29
|
+
lineEnd: number;
|
|
30
|
+
signature: string | null;
|
|
31
|
+
pagerank: number;
|
|
32
|
+
symbolRole: string | null;
|
|
33
|
+
};
|
|
34
|
+
module: { id: number; label: string } | null;
|
|
35
|
+
/** v10 — monorepo boundary the symbol's file belongs to (null when none). */
|
|
36
|
+
boundary: { id: number; label: string; kind: string; rootRelPath: string } | null;
|
|
37
|
+
complexity: {
|
|
38
|
+
loc: number | null;
|
|
39
|
+
cyclomatic: number | null;
|
|
40
|
+
cognitive: number | null;
|
|
41
|
+
maxNesting: number | null;
|
|
42
|
+
};
|
|
43
|
+
callers: {
|
|
44
|
+
total: number;
|
|
45
|
+
preview: Array<{
|
|
46
|
+
name: string; qualifiedName: string | null; kind: string;
|
|
47
|
+
file: string; line: number;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
callees: {
|
|
51
|
+
total: number;
|
|
52
|
+
preview: Array<{
|
|
53
|
+
name: string; kind: string | null;
|
|
54
|
+
file: string | null; line: number | null;
|
|
55
|
+
}>;
|
|
56
|
+
};
|
|
57
|
+
blastRadius: {
|
|
58
|
+
directCallers: number;
|
|
59
|
+
transitiveCallers: number;
|
|
60
|
+
/** Sample of the highest-PageRank reverse-reachable callers (capped). */
|
|
61
|
+
topAffected: Array<{ id: number; name: string; qualifiedName: string | null; file: string; pagerank: number }>;
|
|
62
|
+
maxDepth: number;
|
|
63
|
+
};
|
|
64
|
+
routes: Array<{ method: string; path: string; framework: string }>;
|
|
65
|
+
configKeys: Array<{ key: string; source: string; line: number }>;
|
|
66
|
+
/**
|
|
67
|
+
* v8 Track-G — outbound service calls this symbol makes (preview, capped).
|
|
68
|
+
* Empty array on Pre-Track-G DBs or symbols with no outgoing client calls.
|
|
69
|
+
*/
|
|
70
|
+
serviceCalls: Array<{
|
|
71
|
+
method: string | null;
|
|
72
|
+
path: string | null;
|
|
73
|
+
framework: string;
|
|
74
|
+
rawTarget: string;
|
|
75
|
+
line: number;
|
|
76
|
+
envKey: string | null;
|
|
77
|
+
hostHint: string | null;
|
|
78
|
+
confidence: number;
|
|
79
|
+
}>;
|
|
80
|
+
/**
|
|
81
|
+
* v8 Track-G — service-link evidence pointing at this symbol as the handler
|
|
82
|
+
* (inbound) and as the caller (outbound). Capped previews.
|
|
83
|
+
*/
|
|
84
|
+
serviceLinksInbound: Array<{
|
|
85
|
+
routePath: string | null;
|
|
86
|
+
method: string | null;
|
|
87
|
+
matchKind: string;
|
|
88
|
+
confidence: number;
|
|
89
|
+
callerName: string | null;
|
|
90
|
+
callerFile: string | null;
|
|
91
|
+
}>;
|
|
92
|
+
serviceLinksOutbound: Array<{
|
|
93
|
+
routePath: string | null;
|
|
94
|
+
method: string | null;
|
|
95
|
+
matchKind: string;
|
|
96
|
+
confidence: number;
|
|
97
|
+
handlerName: string | null;
|
|
98
|
+
handlerFile: string | null;
|
|
99
|
+
}>;
|
|
100
|
+
behavior: {
|
|
101
|
+
direct: number;
|
|
102
|
+
indirect: number;
|
|
103
|
+
namingMatches: number;
|
|
104
|
+
sameFileMatches: number;
|
|
105
|
+
preview: Array<{
|
|
106
|
+
name: string; qualifiedName: string | null; file: string; lineStart: number;
|
|
107
|
+
relationship: string; assertionCount: number; specificity: number;
|
|
108
|
+
}>;
|
|
109
|
+
};
|
|
110
|
+
recentHistory: {
|
|
111
|
+
total: number;
|
|
112
|
+
preview: Array<{
|
|
113
|
+
sha: string; author: string | null; email: string | null;
|
|
114
|
+
committedAt: number; message: string | null;
|
|
115
|
+
linesAdded: number; linesRemoved: number;
|
|
116
|
+
prNumber: number | null; prUrl: string | null;
|
|
117
|
+
confidence: number;
|
|
118
|
+
}>;
|
|
119
|
+
};
|
|
120
|
+
fileChurn: {
|
|
121
|
+
commitCount: number;
|
|
122
|
+
lastCommitAt: number | null;
|
|
123
|
+
topAuthor: string | null;
|
|
124
|
+
} | null;
|
|
125
|
+
risk: {
|
|
126
|
+
risk: 'low' | 'medium' | 'high';
|
|
127
|
+
score: number;
|
|
128
|
+
signals: RiskResult['signals'];
|
|
129
|
+
signalContributions: RiskResult['signalContributions'];
|
|
130
|
+
};
|
|
131
|
+
source: 'tree-sitter';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface ContextOptions {
|
|
135
|
+
filePath?: string;
|
|
136
|
+
callerLimit?: number;
|
|
137
|
+
calleeLimit?: number;
|
|
138
|
+
testLimit?: number;
|
|
139
|
+
historyLimit?: number;
|
|
140
|
+
callerDepth?: number;
|
|
141
|
+
affectedLimit?: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function buildContext(
|
|
145
|
+
store: Store,
|
|
146
|
+
nameOrId: string | number,
|
|
147
|
+
options: ContextOptions = {},
|
|
148
|
+
): ContextPacket | null {
|
|
149
|
+
const callerLimit = options.callerLimit ?? 10;
|
|
150
|
+
const calleeLimit = options.calleeLimit ?? 10;
|
|
151
|
+
const testLimit = options.testLimit ?? 10;
|
|
152
|
+
const historyLimit = options.historyLimit ?? 5;
|
|
153
|
+
const callerDepth = options.callerDepth ?? 3;
|
|
154
|
+
const affectedLimit = options.affectedLimit ?? 10;
|
|
155
|
+
|
|
156
|
+
let target: SymbolRow | null = null;
|
|
157
|
+
if (typeof nameOrId === 'number') {
|
|
158
|
+
target = store.getSymbolById(nameOrId);
|
|
159
|
+
} else {
|
|
160
|
+
const defs = store.getDefinition(nameOrId, { filePath: options.filePath });
|
|
161
|
+
if (defs.length === 0) return null;
|
|
162
|
+
target = defs[0];
|
|
163
|
+
}
|
|
164
|
+
if (!target) return null;
|
|
165
|
+
|
|
166
|
+
// Callers + callees use the id-based path so short-name siblings
|
|
167
|
+
// (Alpha.run vs Beta.run) don't share evidence. The legacy name-based
|
|
168
|
+
// APIs (findCallers / countCallers / findCallees) intentionally stay
|
|
169
|
+
// broad for the agent-facing CLI tools — Track E packets always have a
|
|
170
|
+
// resolved id and should never collapse.
|
|
171
|
+
const totalCallers = store.countCallersById(target.id);
|
|
172
|
+
const directCallers: CallerRow[] = store.findCallersById(target.id, callerLimit);
|
|
173
|
+
|
|
174
|
+
const allCallees: CalleeRow[] = store.findCalleesById(target.id);
|
|
175
|
+
const calleesPreview = allCallees.slice(0, calleeLimit);
|
|
176
|
+
|
|
177
|
+
// Blast radius.
|
|
178
|
+
const reverseHits = store.reverseReachableWithDepth(target.id, callerDepth);
|
|
179
|
+
const directRows = store.rawDb().prepare(
|
|
180
|
+
"SELECT DISTINCT from_id FROM edges WHERE to_id = ? AND kind = 'call'",
|
|
181
|
+
).all(target.id) as Array<{ from_id: unknown }>;
|
|
182
|
+
const directSet = new Set<number>(directRows.map(r => Number(r.from_id)));
|
|
183
|
+
const transitive = reverseHits.filter(h => !directSet.has(h.id));
|
|
184
|
+
let topAffected: ContextPacket['blastRadius']['topAffected'] = [];
|
|
185
|
+
if (reverseHits.length > 0) {
|
|
186
|
+
const ids = reverseHits.map(h => h.id);
|
|
187
|
+
const ph = ids.map(() => '?').join(',');
|
|
188
|
+
const rows = store.rawDb().prepare(`
|
|
189
|
+
SELECT s.id, s.name, s.qualified_name AS qualifiedName, f.path AS file, s.pagerank
|
|
190
|
+
FROM symbols s JOIN files f ON f.id = s.file_id
|
|
191
|
+
WHERE s.id IN (${ph}) AND s.is_rankable = 1
|
|
192
|
+
ORDER BY s.pagerank DESC
|
|
193
|
+
LIMIT ?
|
|
194
|
+
`).all(...ids, affectedLimit) as Array<{
|
|
195
|
+
id: unknown; name: unknown; qualifiedName: unknown; file: unknown; pagerank: unknown;
|
|
196
|
+
}>;
|
|
197
|
+
topAffected = rows.map(r => ({
|
|
198
|
+
id: Number(r.id),
|
|
199
|
+
name: String(r.name),
|
|
200
|
+
qualifiedName: r.qualifiedName == null ? null : String(r.qualifiedName),
|
|
201
|
+
file: String(r.file),
|
|
202
|
+
pagerank: Number(r.pagerank),
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Routes / config / behavior / history.
|
|
207
|
+
const routes = store.routesForHandler(target.id);
|
|
208
|
+
const configKeys = store.configKeysForSymbol(target.id);
|
|
209
|
+
const behavior = rankedBehavior(store, target.id, { limit: testLimit });
|
|
210
|
+
|
|
211
|
+
// v8 Track-G — service-link evidence. Capped previews so the packet stays
|
|
212
|
+
// compact even when the symbol is a hub.
|
|
213
|
+
const SERVICE_CALL_PREVIEW_CAP = 12;
|
|
214
|
+
const SERVICE_LINK_PREVIEW_CAP = 12;
|
|
215
|
+
const serviceCallsRows = store.listServiceCalls({
|
|
216
|
+
callerSymbolId: target.id, limit: SERVICE_CALL_PREVIEW_CAP,
|
|
217
|
+
});
|
|
218
|
+
const serviceLinksInbound = store.serviceLinksForHandler(target.id, { limit: SERVICE_LINK_PREVIEW_CAP });
|
|
219
|
+
const serviceLinksOutbound = store.serviceLinksForCaller(target.id, { limit: SERVICE_LINK_PREVIEW_CAP });
|
|
220
|
+
const history = store.getSymbolHistory(target.id, { limit: historyLimit });
|
|
221
|
+
const totalHistory = store.countSymbolHistory(target.id);
|
|
222
|
+
const fileChurn = (() => {
|
|
223
|
+
try {
|
|
224
|
+
const c = store.getFileChurn(target.filePath);
|
|
225
|
+
if (!c) return null;
|
|
226
|
+
return { commitCount: c.commitCount, lastCommitAt: c.lastCommitAt, topAuthor: c.topAuthor };
|
|
227
|
+
} catch { return null; }
|
|
228
|
+
})();
|
|
229
|
+
|
|
230
|
+
// Risk (reuses behavior + history + signals computed above; cheaper to
|
|
231
|
+
// recompute than to share through a back-channel.)
|
|
232
|
+
const risk = computeRisk(store, target.id, { callerDepth })!;
|
|
233
|
+
|
|
234
|
+
const moduleRow = store.moduleForFile(target.fileId);
|
|
235
|
+
const boundaryRow = store.boundaryForFile(target.fileId);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
symbol: {
|
|
239
|
+
id: target.id, name: target.name, qualifiedName: target.qualifiedName,
|
|
240
|
+
kind: target.kind, file: target.filePath,
|
|
241
|
+
lineStart: target.lineStart, lineEnd: target.lineEnd,
|
|
242
|
+
signature: target.signature, pagerank: target.pagerank,
|
|
243
|
+
symbolRole: target.symbolRole ?? null,
|
|
244
|
+
},
|
|
245
|
+
module: moduleRow,
|
|
246
|
+
boundary: boundaryRow,
|
|
247
|
+
complexity: {
|
|
248
|
+
loc: target.loc ?? null,
|
|
249
|
+
cyclomatic: target.cyclomatic ?? null,
|
|
250
|
+
cognitive: target.cognitive ?? null,
|
|
251
|
+
maxNesting: target.maxNesting ?? null,
|
|
252
|
+
},
|
|
253
|
+
callers: {
|
|
254
|
+
total: totalCallers,
|
|
255
|
+
preview: directCallers.map(c => ({
|
|
256
|
+
name: c.callerName, qualifiedName: c.callerQualifiedName, kind: c.callerKind,
|
|
257
|
+
file: c.callerFile, line: c.callerLine,
|
|
258
|
+
})),
|
|
259
|
+
},
|
|
260
|
+
callees: {
|
|
261
|
+
total: allCallees.length,
|
|
262
|
+
preview: calleesPreview.map(c => ({
|
|
263
|
+
name: c.calleeName, kind: c.calleeKind,
|
|
264
|
+
file: c.calleeFile, line: c.calleeLineStart,
|
|
265
|
+
})),
|
|
266
|
+
},
|
|
267
|
+
blastRadius: {
|
|
268
|
+
directCallers: directSet.size,
|
|
269
|
+
transitiveCallers: transitive.length,
|
|
270
|
+
topAffected,
|
|
271
|
+
maxDepth: callerDepth,
|
|
272
|
+
},
|
|
273
|
+
routes,
|
|
274
|
+
configKeys,
|
|
275
|
+
serviceCalls: serviceCallsRows.map(sc => ({
|
|
276
|
+
method: sc.method,
|
|
277
|
+
path: sc.normalizedPath,
|
|
278
|
+
framework: sc.framework,
|
|
279
|
+
rawTarget: sc.rawTarget,
|
|
280
|
+
line: sc.line,
|
|
281
|
+
envKey: sc.envKey,
|
|
282
|
+
hostHint: sc.hostHint,
|
|
283
|
+
confidence: sc.confidence,
|
|
284
|
+
})),
|
|
285
|
+
serviceLinksInbound: serviceLinksInbound.map(l => ({
|
|
286
|
+
routePath: l.routePath,
|
|
287
|
+
method: l.routeMethod ?? l.callMethod,
|
|
288
|
+
matchKind: l.matchKind,
|
|
289
|
+
confidence: l.confidence,
|
|
290
|
+
callerName: l.callerQualifiedName ?? l.callerName,
|
|
291
|
+
callerFile: l.callerFile,
|
|
292
|
+
})),
|
|
293
|
+
serviceLinksOutbound: serviceLinksOutbound.map(l => ({
|
|
294
|
+
routePath: l.routePath,
|
|
295
|
+
method: l.routeMethod ?? l.callMethod,
|
|
296
|
+
matchKind: l.matchKind,
|
|
297
|
+
confidence: l.confidence,
|
|
298
|
+
handlerName: l.handlerQualifiedName ?? l.handlerName,
|
|
299
|
+
handlerFile: l.handlerFile,
|
|
300
|
+
})),
|
|
301
|
+
behavior: {
|
|
302
|
+
direct: behavior?.direct ?? 0,
|
|
303
|
+
indirect: behavior?.indirect ?? 0,
|
|
304
|
+
namingMatches: behavior?.namingMatches ?? 0,
|
|
305
|
+
sameFileMatches: behavior?.sameFileMatches ?? 0,
|
|
306
|
+
preview: (behavior?.tests ?? []).map(t => ({
|
|
307
|
+
name: t.testSymbol.name,
|
|
308
|
+
qualifiedName: t.testSymbol.qualifiedName,
|
|
309
|
+
file: t.testSymbol.file,
|
|
310
|
+
lineStart: t.testSymbol.lineStart,
|
|
311
|
+
relationship: t.relationship,
|
|
312
|
+
assertionCount: t.assertionCount,
|
|
313
|
+
specificity: t.specificity,
|
|
314
|
+
})),
|
|
315
|
+
},
|
|
316
|
+
recentHistory: {
|
|
317
|
+
total: totalHistory,
|
|
318
|
+
preview: history.map(h => ({
|
|
319
|
+
sha: h.commitSha,
|
|
320
|
+
author: h.authorName,
|
|
321
|
+
email: h.authorEmail,
|
|
322
|
+
committedAt: h.committedAt,
|
|
323
|
+
message: h.message,
|
|
324
|
+
linesAdded: h.linesAdded,
|
|
325
|
+
linesRemoved: h.linesRemoved,
|
|
326
|
+
prNumber: h.prNumber,
|
|
327
|
+
prUrl: h.prUrl,
|
|
328
|
+
confidence: h.confidence,
|
|
329
|
+
})),
|
|
330
|
+
},
|
|
331
|
+
fileChurn,
|
|
332
|
+
risk: {
|
|
333
|
+
risk: risk.risk,
|
|
334
|
+
score: risk.score,
|
|
335
|
+
signals: risk.signals,
|
|
336
|
+
signalContributions: risk.signalContributions,
|
|
337
|
+
},
|
|
338
|
+
source: 'tree-sitter',
|
|
339
|
+
};
|
|
340
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v10 — Symbol Rename/Move Continuity heuristics.
|
|
3
|
+
*
|
|
4
|
+
* Goal: when exact `symbol_key` history walking terminates (because the
|
|
5
|
+
* function was renamed or moved), surface honest, confidence-labelled
|
|
6
|
+
* continuity evidence so the agent can decide whether to trust the link.
|
|
7
|
+
*
|
|
8
|
+
* Heuristics (current pass — opt-in, low-confidence by default):
|
|
9
|
+
* - shape_hash exact match: previous-symbol candidate has the same
|
|
10
|
+
* structural SimHash → strong (confidence 0.85+)
|
|
11
|
+
* - shape_hash close match: small Hamming distance (≤ 4) + similar name
|
|
12
|
+
* → medium (confidence 0.65)
|
|
13
|
+
* - signature similarity: same arity + same containing class/module
|
|
14
|
+
* → weak (confidence 0.5)
|
|
15
|
+
* - same file rename history: file was renamed in git history, the
|
|
16
|
+
* historical file had a same-shape function with a different name → boost
|
|
17
|
+
* (confidence 0.75)
|
|
18
|
+
*
|
|
19
|
+
* Stored on `symbol_history_continuity`. Never pretends rename continuity
|
|
20
|
+
* is certain.
|
|
21
|
+
*
|
|
22
|
+
* This module does NOT replace existing exact-key history. It only proposes
|
|
23
|
+
* additional links when buildSymbolHistory's exact-key walk ran out of
|
|
24
|
+
* commits. The Preflight / seer_history layers can read continuity rows
|
|
25
|
+
* alongside symbol_history.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Store } from '../db/store.js';
|
|
29
|
+
|
|
30
|
+
export interface ContinuityCandidate {
|
|
31
|
+
symbolId: number;
|
|
32
|
+
symbolKey: string;
|
|
33
|
+
previousSymbolKey: string;
|
|
34
|
+
previousName: string;
|
|
35
|
+
previousFile: string;
|
|
36
|
+
confidence: number;
|
|
37
|
+
matchReasons: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ContinuityResult {
|
|
41
|
+
candidatesConsidered: number;
|
|
42
|
+
inserted: number;
|
|
43
|
+
skipped: number;
|
|
44
|
+
elapsedMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run the continuity pass over every symbol whose recorded history has
|
|
49
|
+
* fewer than `historyThreshold` commits AND that has a shape_hash. For each
|
|
50
|
+
* such symbol we scan other symbols sharing a close shape_hash and propose
|
|
51
|
+
* the highest-confidence candidate (deduped per symbol_id).
|
|
52
|
+
*/
|
|
53
|
+
export function buildContinuity(
|
|
54
|
+
store: Store,
|
|
55
|
+
options: {
|
|
56
|
+
historyThreshold?: number;
|
|
57
|
+
maxHammingDistance?: number;
|
|
58
|
+
log?: (msg: string) => void;
|
|
59
|
+
/** When true, also create candidate links for symbols that have full history
|
|
60
|
+
* (useful for fixture tests and debugging). Default false. */
|
|
61
|
+
includeAllSymbols?: boolean;
|
|
62
|
+
} = {},
|
|
63
|
+
): ContinuityResult {
|
|
64
|
+
const start = Date.now();
|
|
65
|
+
const log = options.log ?? (() => { /* */ });
|
|
66
|
+
const historyThreshold = options.historyThreshold ?? 1;
|
|
67
|
+
const maxHamming = options.maxHammingDistance ?? 4;
|
|
68
|
+
|
|
69
|
+
// Pool of candidates: every symbol with a shape_hash.
|
|
70
|
+
const pool = store.listSymbolsWithShapeHash({ minLoc: 1, limit: 100000 });
|
|
71
|
+
if (pool.length === 0) {
|
|
72
|
+
log('no shape-hashed symbols; nothing to do');
|
|
73
|
+
return { candidatesConsidered: 0, inserted: 0, skipped: 0, elapsedMs: Date.now() - start };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Bucket by qualifiedName/name → list of candidates (so we can detect
|
|
77
|
+
// rename: same shape, different name).
|
|
78
|
+
const byHash = new Map<string, typeof pool>();
|
|
79
|
+
for (const s of pool) {
|
|
80
|
+
const k = s.shapeHash.toString();
|
|
81
|
+
const list = byHash.get(k) ?? [];
|
|
82
|
+
list.push(s);
|
|
83
|
+
byHash.set(k, list);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let considered = 0;
|
|
87
|
+
let inserted = 0;
|
|
88
|
+
let skipped = 0;
|
|
89
|
+
|
|
90
|
+
const raw = store.rawDb();
|
|
91
|
+
|
|
92
|
+
for (const s of pool) {
|
|
93
|
+
// Find the symbol's stored history count. If it's >= historyThreshold
|
|
94
|
+
// AND we're not in includeAllSymbols mode, skip — exact history is fine.
|
|
95
|
+
if (!options.includeAllSymbols) {
|
|
96
|
+
const cnt = raw.prepare(
|
|
97
|
+
'SELECT COUNT(*) AS c FROM symbol_history WHERE symbol_id = ?',
|
|
98
|
+
).get(s.id) as { c: number } | undefined;
|
|
99
|
+
if (cnt && cnt.c >= historyThreshold) continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Exact shape match candidates with a DIFFERENT (qualifiedName ?? name).
|
|
103
|
+
const exactMatches = (byHash.get(s.shapeHash.toString()) ?? [])
|
|
104
|
+
.filter(c => c.id !== s.id);
|
|
105
|
+
if (exactMatches.length > 0) {
|
|
106
|
+
const cand = pickBestCandidate(s, exactMatches);
|
|
107
|
+
if (cand) {
|
|
108
|
+
const sameClass = sharesContainingScope(s, cand);
|
|
109
|
+
const nameRelated = similarName(s.name, cand.name);
|
|
110
|
+
// How many OTHER symbols share this exact shape? A shape shared by many
|
|
111
|
+
// symbols (trivial getters, `return null;`, boilerplate) is NOT a
|
|
112
|
+
// reliable rename signal on its own. Only assert a high-confidence link
|
|
113
|
+
// when the shape is (near-)unique to this pair; otherwise require
|
|
114
|
+
// corroboration (same scope or a related name) and label the ambiguity
|
|
115
|
+
// honestly with a lower, capped confidence. Never pretend certainty.
|
|
116
|
+
const ambiguous = exactMatches.length >= 2;
|
|
117
|
+
if (ambiguous && !sameClass && !nameRelated) {
|
|
118
|
+
// Common shape, no corroborating evidence — do not invent a rename.
|
|
119
|
+
skipped++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
considered++;
|
|
123
|
+
const reasons = ['shape_hash_exact'];
|
|
124
|
+
if (sameClass) reasons.push('same_containing_scope');
|
|
125
|
+
if (nameRelated) reasons.push('similar_name');
|
|
126
|
+
let confidence: number;
|
|
127
|
+
if (ambiguous) {
|
|
128
|
+
reasons.push(`ambiguous_shape_bucket:n=${exactMatches.length + 1}`);
|
|
129
|
+
confidence = 0.6;
|
|
130
|
+
if (sameClass) confidence = Math.min(0.7, confidence + 0.05);
|
|
131
|
+
if (nameRelated) confidence = Math.min(0.7, confidence + 0.05);
|
|
132
|
+
} else {
|
|
133
|
+
confidence = 0.85;
|
|
134
|
+
if (sameClass) confidence = Math.min(0.95, confidence + 0.05);
|
|
135
|
+
if (nameRelated) confidence = Math.min(0.95, confidence + 0.05);
|
|
136
|
+
}
|
|
137
|
+
upsertContinuity(store, {
|
|
138
|
+
symbolId: s.id,
|
|
139
|
+
symbolKey: keyFor(s),
|
|
140
|
+
previousSymbolKey: keyFor(cand),
|
|
141
|
+
previousName: cand.name,
|
|
142
|
+
previousFile: cand.filePath,
|
|
143
|
+
confidence,
|
|
144
|
+
matchReasons: reasons,
|
|
145
|
+
});
|
|
146
|
+
inserted++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Close hash match.
|
|
152
|
+
let best: { peer: typeof pool[number]; distance: number } | null = null;
|
|
153
|
+
for (const peer of pool) {
|
|
154
|
+
if (peer.id === s.id) continue;
|
|
155
|
+
if ((peer.name === s.name) && (peer.qualifiedName === s.qualifiedName)) continue;
|
|
156
|
+
const d = hammingDistance(s.shapeHash, peer.shapeHash);
|
|
157
|
+
if (d > maxHamming) continue;
|
|
158
|
+
if (!best || d < best.distance) best = { peer, distance: d };
|
|
159
|
+
}
|
|
160
|
+
if (best && best.distance <= maxHamming) {
|
|
161
|
+
const cand = best.peer;
|
|
162
|
+
// Only act when names are at least loosely related (share a prefix or
|
|
163
|
+
// a suffix), to avoid pairing every short function in the codebase.
|
|
164
|
+
if (similarName(s.name, cand.name) || sharesContainingScope(s, cand)) {
|
|
165
|
+
considered++;
|
|
166
|
+
const reasons = [`shape_hash_close:d=${best.distance}`];
|
|
167
|
+
if (similarName(s.name, cand.name)) reasons.push('similar_name');
|
|
168
|
+
if (sharesContainingScope(s, cand)) reasons.push('same_containing_scope');
|
|
169
|
+
const confidence = best.distance === 0 ? 0.8
|
|
170
|
+
: best.distance <= 2 ? 0.6
|
|
171
|
+
: 0.4;
|
|
172
|
+
upsertContinuity(store, {
|
|
173
|
+
symbolId: s.id,
|
|
174
|
+
symbolKey: keyFor(s),
|
|
175
|
+
previousSymbolKey: keyFor(cand),
|
|
176
|
+
previousName: cand.name,
|
|
177
|
+
previousFile: cand.filePath,
|
|
178
|
+
confidence,
|
|
179
|
+
matchReasons: reasons,
|
|
180
|
+
});
|
|
181
|
+
inserted++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
skipped++;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
candidatesConsidered: considered,
|
|
191
|
+
inserted, skipped,
|
|
192
|
+
elapsedMs: Date.now() - start,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function upsertContinuity(
|
|
197
|
+
store: Store, c: ContinuityCandidate,
|
|
198
|
+
): void {
|
|
199
|
+
const raw = store.rawDb();
|
|
200
|
+
raw.prepare(`
|
|
201
|
+
INSERT INTO symbol_history_continuity
|
|
202
|
+
(symbol_id, symbol_key, previous_symbol_key, previous_name, previous_file,
|
|
203
|
+
bridging_sha, confidence, match_reasons, recorded_at)
|
|
204
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)
|
|
205
|
+
ON CONFLICT(symbol_id, previous_symbol_key) DO UPDATE SET
|
|
206
|
+
confidence = excluded.confidence,
|
|
207
|
+
match_reasons = excluded.match_reasons,
|
|
208
|
+
previous_name = excluded.previous_name,
|
|
209
|
+
previous_file = excluded.previous_file,
|
|
210
|
+
recorded_at = excluded.recorded_at
|
|
211
|
+
`).run(
|
|
212
|
+
c.symbolId, c.symbolKey, c.previousSymbolKey,
|
|
213
|
+
c.previousName, c.previousFile,
|
|
214
|
+
c.confidence, JSON.stringify(c.matchReasons),
|
|
215
|
+
Date.now(),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function keyFor(s: { kind: string; qualifiedName: string | null; name: string }): string {
|
|
220
|
+
return `${s.kind}:${s.qualifiedName ?? s.name}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function sharesContainingScope(
|
|
224
|
+
a: { qualifiedName: string | null; name: string; filePath: string },
|
|
225
|
+
b: { qualifiedName: string | null; name: string; filePath: string },
|
|
226
|
+
): boolean {
|
|
227
|
+
if (a.filePath === b.filePath) return true;
|
|
228
|
+
// Compare class/module prefix in the qualified name (e.g. `AuthService.foo`
|
|
229
|
+
// and `AuthService.bar` share `AuthService`).
|
|
230
|
+
const aQual = a.qualifiedName ?? '';
|
|
231
|
+
const bQual = b.qualifiedName ?? '';
|
|
232
|
+
if (!aQual.includes('.') || !bQual.includes('.')) return false;
|
|
233
|
+
const aPrefix = aQual.split('.').slice(0, -1).join('.');
|
|
234
|
+
const bPrefix = bQual.split('.').slice(0, -1).join('.');
|
|
235
|
+
return aPrefix.length > 0 && aPrefix === bPrefix;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function similarName(a: string, b: string): boolean {
|
|
239
|
+
if (!a || !b) return false;
|
|
240
|
+
if (a === b) return false; // we only flag potential RENAMES
|
|
241
|
+
const aL = a.toLowerCase();
|
|
242
|
+
const bL = b.toLowerCase();
|
|
243
|
+
// Same prefix of length >= 4 or same suffix of length >= 4.
|
|
244
|
+
const minLen = Math.min(aL.length, bL.length);
|
|
245
|
+
if (minLen < 4) return false;
|
|
246
|
+
let prefix = 0;
|
|
247
|
+
while (prefix < minLen && aL[prefix] === bL[prefix]) prefix++;
|
|
248
|
+
if (prefix >= 4) return true;
|
|
249
|
+
let suffix = 0;
|
|
250
|
+
while (suffix < minLen && aL[aL.length - 1 - suffix] === bL[bL.length - 1 - suffix]) suffix++;
|
|
251
|
+
if (suffix >= 4) return true;
|
|
252
|
+
// Names that differ by a verb prefix swap (validate → verify) — drop the
|
|
253
|
+
// first 4 characters and compare the rest.
|
|
254
|
+
if (aL.length >= 4 && bL.length >= 4 && aL.slice(4) === bL.slice(4)) return true;
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function pickBestCandidate<T extends { id: number; name: string; qualifiedName: string | null; filePath: string; kind: string }>(
|
|
259
|
+
target: T,
|
|
260
|
+
candidates: T[],
|
|
261
|
+
): T | null {
|
|
262
|
+
// Prefer same-file rename. Then same-class rename. Then any candidate.
|
|
263
|
+
const sameFile = candidates.filter(c => c.filePath === target.filePath);
|
|
264
|
+
if (sameFile.length > 0) return sameFile[0];
|
|
265
|
+
const sameClass = candidates.filter(c => sharesContainingScope(target, c));
|
|
266
|
+
if (sameClass.length > 0) return sameClass[0];
|
|
267
|
+
return candidates[0] ?? null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function hammingDistance(a: bigint, b: bigint): number {
|
|
271
|
+
let x = a ^ b;
|
|
272
|
+
let n = 0;
|
|
273
|
+
while (x !== 0n) {
|
|
274
|
+
x &= x - 1n;
|
|
275
|
+
n++;
|
|
276
|
+
}
|
|
277
|
+
return n;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Fetch continuity rows for a given symbol id, ordered by confidence desc.
|
|
282
|
+
*/
|
|
283
|
+
export function getContinuityForSymbol(
|
|
284
|
+
store: Store, symbolId: number,
|
|
285
|
+
): Array<{
|
|
286
|
+
previousSymbolKey: string;
|
|
287
|
+
previousName: string;
|
|
288
|
+
previousFile: string;
|
|
289
|
+
confidence: number;
|
|
290
|
+
matchReasons: string[];
|
|
291
|
+
}> {
|
|
292
|
+
if (!store.hasV10()) return [];
|
|
293
|
+
try {
|
|
294
|
+
const rows = store.rawDb().prepare(`
|
|
295
|
+
SELECT previous_symbol_key AS previousSymbolKey,
|
|
296
|
+
previous_name AS previousName,
|
|
297
|
+
previous_file AS previousFile,
|
|
298
|
+
confidence, match_reasons AS matchReasons
|
|
299
|
+
FROM symbol_history_continuity
|
|
300
|
+
WHERE symbol_id = ?
|
|
301
|
+
ORDER BY confidence DESC, id DESC
|
|
302
|
+
`).all(symbolId) as Array<{
|
|
303
|
+
previousSymbolKey: unknown; previousName: unknown; previousFile: unknown;
|
|
304
|
+
confidence: unknown; matchReasons: unknown;
|
|
305
|
+
}>;
|
|
306
|
+
return rows.map(r => ({
|
|
307
|
+
previousSymbolKey: String(r.previousSymbolKey),
|
|
308
|
+
previousName: String(r.previousName ?? ''),
|
|
309
|
+
previousFile: String(r.previousFile ?? ''),
|
|
310
|
+
confidence: Number(r.confidence ?? 0),
|
|
311
|
+
matchReasons: parseReasons(r.matchReasons),
|
|
312
|
+
}));
|
|
313
|
+
} catch { return []; }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function parseReasons(v: unknown): string[] {
|
|
317
|
+
if (typeof v !== 'string') return [];
|
|
318
|
+
try {
|
|
319
|
+
const parsed = JSON.parse(v);
|
|
320
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
321
|
+
} catch { return []; }
|
|
322
|
+
}
|