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,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track G — service-link resolver and helpers.
|
|
3
|
+
*
|
|
4
|
+
* Two concerns live here, kept independent so they can be unit-tested in
|
|
5
|
+
* isolation:
|
|
6
|
+
*
|
|
7
|
+
* 1. `normalizeHttpTarget(raw)` — turn a string captured at a client call
|
|
8
|
+
* site (literal URL, template path, env-prefixed concat) into a
|
|
9
|
+
* `{ path, hostHint, envKey }` triple where each field is undefined when
|
|
10
|
+
* it can't be confidently recovered. Pure function; no DB access.
|
|
11
|
+
*
|
|
12
|
+
* 2. `routePatternsMatch(callPath, routePath)` — decide whether a literal
|
|
13
|
+
* caller path matches a (possibly parameterised) framework route.
|
|
14
|
+
* Returns a `{ matched, confidence, reason }` result so the resolver
|
|
15
|
+
* can ranke evidence without recomputing it.
|
|
16
|
+
*
|
|
17
|
+
* 3. `resolveServiceLinks(store)` — the actual post-index resolver.
|
|
18
|
+
* Wipes prior service_links, scans service_calls + routes, and inserts
|
|
19
|
+
* one row per confident rendezvous.
|
|
20
|
+
*
|
|
21
|
+
* Deterministic by construction: the resolver runs candidates in id order
|
|
22
|
+
* and only emits a top match (with ambiguity recorded as evidence) so two
|
|
23
|
+
* runs of the same DB produce the same service_links rows.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { Store } from '../db/store.js';
|
|
27
|
+
import type { ServiceHostMap } from './serviceHostScanner.js';
|
|
28
|
+
|
|
29
|
+
export type MatchKind =
|
|
30
|
+
| 'literal_path'
|
|
31
|
+
| 'env_base'
|
|
32
|
+
| 'service_host'
|
|
33
|
+
| 'route_pattern'
|
|
34
|
+
// v9 Track-H additions
|
|
35
|
+
| 'trpc_procedure'
|
|
36
|
+
| 'graphql_operation'
|
|
37
|
+
| 'grpc_method'
|
|
38
|
+
| 'topic_match'
|
|
39
|
+
| 'queue_match'
|
|
40
|
+
| 'exchange_match';
|
|
41
|
+
|
|
42
|
+
export interface NormalizedTarget {
|
|
43
|
+
/** /api/users — leading slash, no scheme, no query, no fragment, no trailing slash (except "/"). */
|
|
44
|
+
path?: string;
|
|
45
|
+
/** payment-service / billing.svc.cluster.local / etc. */
|
|
46
|
+
hostHint?: string;
|
|
47
|
+
/** PAYMENT_URL — populated by the extractor when it saw env reference. */
|
|
48
|
+
envKey?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Normalize an HTTP target captured at a client call site.
|
|
53
|
+
*
|
|
54
|
+
* Rules:
|
|
55
|
+
* - strip scheme + host when literal (`https://payment/api/charge` → `/api/charge`, host = `payment`)
|
|
56
|
+
* - keep route paths starting with '/'
|
|
57
|
+
* - drop query string and fragment
|
|
58
|
+
* - normalize trailing slash (but keep root '/'): '/api/users/' → '/api/users'
|
|
59
|
+
* - drop empty paths
|
|
60
|
+
* - do NOT collapse dynamic segments — that's the route-pattern matcher's job.
|
|
61
|
+
*/
|
|
62
|
+
export function normalizeHttpTarget(raw: string): NormalizedTarget {
|
|
63
|
+
if (!raw || typeof raw !== 'string') return {};
|
|
64
|
+
let s = raw.trim();
|
|
65
|
+
if (!s) return {};
|
|
66
|
+
|
|
67
|
+
let hostHint: string | undefined;
|
|
68
|
+
|
|
69
|
+
// Strip scheme + host if present.
|
|
70
|
+
const schemeMatch = s.match(/^(https?):\/\/([^/?#]+)(.*)$/i);
|
|
71
|
+
if (schemeMatch) {
|
|
72
|
+
hostHint = schemeMatch[2];
|
|
73
|
+
s = schemeMatch[3] || '/';
|
|
74
|
+
} else if (/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9_./%-]+/.test(s) && !s.startsWith('/')) {
|
|
75
|
+
// Bare host/path with no scheme (rare but seen in some libs).
|
|
76
|
+
const firstSlash = s.indexOf('/');
|
|
77
|
+
if (firstSlash > 0) {
|
|
78
|
+
hostHint = s.slice(0, firstSlash);
|
|
79
|
+
s = s.slice(firstSlash);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Strip query + fragment.
|
|
84
|
+
const q = s.indexOf('?');
|
|
85
|
+
if (q >= 0) s = s.slice(0, q);
|
|
86
|
+
const h = s.indexOf('#');
|
|
87
|
+
if (h >= 0) s = s.slice(0, h);
|
|
88
|
+
|
|
89
|
+
// Trailing slash normalization (keep root).
|
|
90
|
+
if (s.length > 1 && s.endsWith('/')) s = s.slice(0, -1);
|
|
91
|
+
|
|
92
|
+
let path: string | undefined;
|
|
93
|
+
if (s.startsWith('/')) path = s;
|
|
94
|
+
|
|
95
|
+
const out: NormalizedTarget = {};
|
|
96
|
+
if (path) out.path = path;
|
|
97
|
+
if (hostHint) out.hostHint = hostHint;
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface PatternMatch {
|
|
102
|
+
matched: boolean;
|
|
103
|
+
/** 1.0 for exact path+method, 0.95 path only, 0.85 parameterised */
|
|
104
|
+
confidence: number;
|
|
105
|
+
/** Human-readable why; surfaced in evidence_json. */
|
|
106
|
+
reason: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Decide whether `callPath` (literal) satisfies a (possibly parameterised)
|
|
111
|
+
* `routePath` like `/users/:id`, `/users/{id}`, `/items/<int:n>`.
|
|
112
|
+
*
|
|
113
|
+
* No method check here — the resolver applies method comparison around this
|
|
114
|
+
* with method-mismatch falling back to a lower confidence.
|
|
115
|
+
*
|
|
116
|
+
* Returns matched=false when the segment count differs OR a literal segment
|
|
117
|
+
* differs. Parameter segments (`:x`, `{x}`, `<...:x>`) match any one segment.
|
|
118
|
+
*/
|
|
119
|
+
export function routePatternsMatch(callPath: string, routePath: string): PatternMatch {
|
|
120
|
+
if (!callPath || !routePath) return { matched: false, confidence: 0, reason: 'empty' };
|
|
121
|
+
// Exact literal match.
|
|
122
|
+
if (callPath === routePath) {
|
|
123
|
+
return { matched: true, confidence: 0.95, reason: 'literal_path' };
|
|
124
|
+
}
|
|
125
|
+
const callSegs = callPath.split('/').filter(Boolean);
|
|
126
|
+
const routeSegs = routePath.split('/').filter(Boolean);
|
|
127
|
+
if (callSegs.length !== routeSegs.length) {
|
|
128
|
+
return { matched: false, confidence: 0, reason: 'segment_count' };
|
|
129
|
+
}
|
|
130
|
+
let paramHits = 0;
|
|
131
|
+
for (let i = 0; i < callSegs.length; i++) {
|
|
132
|
+
const r = routeSegs[i];
|
|
133
|
+
const c = callSegs[i];
|
|
134
|
+
if (isParamSegment(r)) { paramHits++; continue; }
|
|
135
|
+
if (r !== c) return { matched: false, confidence: 0, reason: 'segment_mismatch' };
|
|
136
|
+
}
|
|
137
|
+
if (paramHits === 0) {
|
|
138
|
+
// Same literal segment count but no params — earlier exact check should
|
|
139
|
+
// have caught this. Treat as no match so we don't accidentally rank
|
|
140
|
+
// /a/b == /a/b twice.
|
|
141
|
+
return { matched: false, confidence: 0, reason: 'duplicate_literal' };
|
|
142
|
+
}
|
|
143
|
+
return { matched: true, confidence: 0.85, reason: 'route_pattern' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isParamSegment(seg: string): boolean {
|
|
147
|
+
// Express :id, FastAPI {id}, Spring {id}, Flask <int:id>
|
|
148
|
+
if (seg.startsWith(':')) return true;
|
|
149
|
+
if (seg.startsWith('{') && seg.endsWith('}')) return true;
|
|
150
|
+
if (seg.startsWith('<') && seg.endsWith('>')) return true;
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Compare two HTTP methods, treating 'ANY' (client unknown method) and route's
|
|
156
|
+
* absence of method as wildcards. Returns:
|
|
157
|
+
* 1.0 — methods match exactly (including both 'ANY')
|
|
158
|
+
* 0.9 — caller method known + route is ANY (or vice versa)
|
|
159
|
+
* 0.0 — explicit mismatch (e.g. POST vs GET)
|
|
160
|
+
*/
|
|
161
|
+
export function methodMatchScore(callMethod: string | null | undefined,
|
|
162
|
+
routeMethod: string | null | undefined): number {
|
|
163
|
+
const cm = (callMethod ?? 'ANY').toUpperCase();
|
|
164
|
+
const rm = (routeMethod ?? 'ANY').toUpperCase();
|
|
165
|
+
if (cm === rm) return 1.0;
|
|
166
|
+
if (cm === 'ANY' || rm === 'ANY') return 0.9;
|
|
167
|
+
return 0.0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Resolver ────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/** Result returned by resolveServiceLinks. */
|
|
173
|
+
export interface ResolveResult {
|
|
174
|
+
/** Total service_calls considered (after dropping ones with no path). */
|
|
175
|
+
callsConsidered: number;
|
|
176
|
+
/** New service_links rows inserted. */
|
|
177
|
+
linksInserted: number;
|
|
178
|
+
/** Counts by match_kind. */
|
|
179
|
+
byKind: Record<MatchKind, number>;
|
|
180
|
+
/** v9 Track-H — number of calls whose candidate set was truncated at the
|
|
181
|
+
* ambiguity cap. Surfaced for telemetry / debug. */
|
|
182
|
+
truncated?: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// v9 Track-H — hard cap on how many candidate routes we consider for a single
|
|
186
|
+
// service_call. A symbol with 25+ matching routes is almost always ambiguous
|
|
187
|
+
// (think a generic /users/:id route in 30 microservices); we keep determinism
|
|
188
|
+
// by sorting first and then truncating, and record `truncated: true` in the
|
|
189
|
+
// link's evidence so the agent can see the cutoff happened.
|
|
190
|
+
const MAX_CANDIDATES_PER_CALL = 25;
|
|
191
|
+
// Cap on how many candidates we serialize into evidence_json. Smaller than the
|
|
192
|
+
// cap above so a link row stays compact even when many routes matched.
|
|
193
|
+
const MAX_EVIDENCE_CANDIDATES = 5;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Wipe service_links and rebuild from current service_calls + routes.
|
|
197
|
+
* Deterministic: orders candidates by id ascending and ties are broken by id.
|
|
198
|
+
*
|
|
199
|
+
* Match strategy:
|
|
200
|
+
* 1. literal_path — call.path == route.path, method compatible
|
|
201
|
+
* 2. route_pattern — call.path matches a parameterised route, method compatible
|
|
202
|
+
* 3. env_base — call carried an env key; we record the link only when
|
|
203
|
+
* the path *also* matches a route (env alone is not enough).
|
|
204
|
+
*
|
|
205
|
+
* S3 (service_host) — k8s/Docker host resolution — is provided by
|
|
206
|
+
* `scanServiceHosts()` and passed in as optional evidence.
|
|
207
|
+
*/
|
|
208
|
+
export function resolveServiceLinks(store: Store, options: { hostMap?: ServiceHostMap } = {}): ResolveResult {
|
|
209
|
+
const raw = store.rawDb();
|
|
210
|
+
raw.exec('DELETE FROM service_links');
|
|
211
|
+
|
|
212
|
+
type CallRow = {
|
|
213
|
+
id: number; symbol_id: number | null;
|
|
214
|
+
protocol: string; method: string | null;
|
|
215
|
+
raw_target: string; normalized_path: string | null;
|
|
216
|
+
host_hint: string | null; env_key: string | null;
|
|
217
|
+
framework: string;
|
|
218
|
+
operation: string | null;
|
|
219
|
+
topic: string | null; queue: string | null;
|
|
220
|
+
service: string | null;
|
|
221
|
+
};
|
|
222
|
+
// v9: detect whether the v9 generalized columns exist. listServiceCalls
|
|
223
|
+
// reads them but we use a raw SELECT here for speed.
|
|
224
|
+
const hasV9Cols = hasColumn(raw, 'service_calls', 'operation');
|
|
225
|
+
const v9Cols = hasV9Cols ? ', operation, topic, queue, service' : '';
|
|
226
|
+
const calls = raw.prepare(
|
|
227
|
+
`SELECT id, symbol_id, protocol, method, raw_target, normalized_path,
|
|
228
|
+
host_hint, env_key, framework ${v9Cols}
|
|
229
|
+
FROM service_calls
|
|
230
|
+
ORDER BY id ASC`
|
|
231
|
+
).all() as CallRow[];
|
|
232
|
+
|
|
233
|
+
// v9 Track-H — build a doc-identifier → field-name map from gql-doc sentinel
|
|
234
|
+
// rows. Lets us rewrite `client.query({ query: GET_USER })` whose operation
|
|
235
|
+
// is the const name to the operation field parsed from its gql body.
|
|
236
|
+
const gqlDocMap = new Map<string, { operation: string; method: string | null }>();
|
|
237
|
+
for (const c of calls) {
|
|
238
|
+
if (c.framework !== 'gql-doc' || !c.operation || !c.raw_target) continue;
|
|
239
|
+
gqlDocMap.set(c.raw_target, { operation: c.operation, method: c.method });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
type RouteRow = {
|
|
243
|
+
id: number; method: string | null; path: string; framework: string;
|
|
244
|
+
handler_id: number | null;
|
|
245
|
+
protocol: string | null;
|
|
246
|
+
operation: string | null;
|
|
247
|
+
topic: string | null;
|
|
248
|
+
queue: string | null;
|
|
249
|
+
service: string | null;
|
|
250
|
+
};
|
|
251
|
+
const hasV9RouteCols = hasColumn(raw, 'routes', 'protocol');
|
|
252
|
+
const v9RouteCols = hasV9RouteCols
|
|
253
|
+
? ', protocol, operation, topic, queue, service'
|
|
254
|
+
: '';
|
|
255
|
+
const routes = raw.prepare(
|
|
256
|
+
`SELECT id, method, path, framework, handler_id ${v9RouteCols}
|
|
257
|
+
FROM routes
|
|
258
|
+
ORDER BY id ASC`
|
|
259
|
+
).all() as RouteRow[];
|
|
260
|
+
// Backfill protocol/operation for pre-v9 row shape — every pre-v9 route is HTTP.
|
|
261
|
+
if (!hasV9RouteCols) {
|
|
262
|
+
for (const r of routes) {
|
|
263
|
+
r.protocol = 'http';
|
|
264
|
+
r.operation = null;
|
|
265
|
+
r.topic = null; r.queue = null; r.service = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Index routes by exact path for cheap lookup (HTTP only), and by operation
|
|
270
|
+
// (tRPC / GraphQL / gRPC), and keep the full list for parameterised matching.
|
|
271
|
+
const byExactPath = new Map<string, RouteRow[]>();
|
|
272
|
+
const byOperation = new Map<string, RouteRow[]>(); // key = `${protocol}:${operation}`
|
|
273
|
+
const byTopic = new Map<string, RouteRow[]>(); // key = topic
|
|
274
|
+
const byQueue = new Map<string, RouteRow[]>(); // key = queue
|
|
275
|
+
for (const r of routes) {
|
|
276
|
+
const proto = r.protocol ?? 'http';
|
|
277
|
+
if (proto === 'http' && r.path) {
|
|
278
|
+
const norm = normalizeRoutePath(r.path);
|
|
279
|
+
const list = byExactPath.get(norm);
|
|
280
|
+
if (list) list.push(r); else byExactPath.set(norm, [r]);
|
|
281
|
+
}
|
|
282
|
+
if (r.operation) {
|
|
283
|
+
const key = `${proto}:${r.operation}`;
|
|
284
|
+
const list = byOperation.get(key);
|
|
285
|
+
if (list) list.push(r); else byOperation.set(key, [r]);
|
|
286
|
+
}
|
|
287
|
+
if (r.topic) {
|
|
288
|
+
const list = byTopic.get(r.topic);
|
|
289
|
+
if (list) list.push(r); else byTopic.set(r.topic, [r]);
|
|
290
|
+
}
|
|
291
|
+
if (r.queue) {
|
|
292
|
+
const list = byQueue.get(r.queue);
|
|
293
|
+
if (list) list.push(r); else byQueue.set(r.queue, [r]);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const insertLink = store.makeServiceLinkInserter();
|
|
298
|
+
const byKind: Record<string, number> = {
|
|
299
|
+
literal_path: 0, env_base: 0, service_host: 0, route_pattern: 0,
|
|
300
|
+
trpc_procedure: 0, graphql_operation: 0, grpc_method: 0,
|
|
301
|
+
topic_match: 0, queue_match: 0, exchange_match: 0,
|
|
302
|
+
};
|
|
303
|
+
let considered = 0;
|
|
304
|
+
let inserted = 0;
|
|
305
|
+
let truncatedCalls = 0;
|
|
306
|
+
|
|
307
|
+
for (const c of calls) {
|
|
308
|
+
type Candidate = {
|
|
309
|
+
route: RouteRow;
|
|
310
|
+
confidence: number;
|
|
311
|
+
matchKind: MatchKind;
|
|
312
|
+
reason: string;
|
|
313
|
+
};
|
|
314
|
+
const candidates: Candidate[] = [];
|
|
315
|
+
|
|
316
|
+
if (c.protocol === 'http') {
|
|
317
|
+
if (!c.normalized_path) continue;
|
|
318
|
+
considered++;
|
|
319
|
+
// Known service host? Used as a confidence boost AND as the match_kind
|
|
320
|
+
// when the host carries strictly more signal than the path alone.
|
|
321
|
+
const knownHost = c.host_hint
|
|
322
|
+
? options.hostMap?.hosts.has(c.host_hint.toLowerCase())
|
|
323
|
+
: false;
|
|
324
|
+
const hostBoost = knownHost ? 1.05 : 1.0;
|
|
325
|
+
|
|
326
|
+
// Pass 1 — literal exact path matches (HTTP only).
|
|
327
|
+
const exacts = byExactPath.get(c.normalized_path) ?? [];
|
|
328
|
+
for (const r of exacts) {
|
|
329
|
+
// Only HTTP routes; tRPC operation could in theory collide with a path
|
|
330
|
+
// string but we filter by route.protocol to keep concerns separate.
|
|
331
|
+
if ((r.protocol ?? 'http') !== 'http') continue;
|
|
332
|
+
const ms = methodMatchScore(c.method, r.method);
|
|
333
|
+
if (ms === 0) continue;
|
|
334
|
+
const conf = Math.min(1.0, 0.95 * ms * hostBoost);
|
|
335
|
+
// Prefer 'service_host' when we have a known k8s/Docker host AND the
|
|
336
|
+
// call has no env_key — both the host name and a workspace route
|
|
337
|
+
// independently agree. Otherwise fall back to env_base / literal_path.
|
|
338
|
+
const matchKind: MatchKind = knownHost && !c.env_key
|
|
339
|
+
? 'service_host'
|
|
340
|
+
: (c.env_key ? 'env_base' : 'literal_path');
|
|
341
|
+
candidates.push({
|
|
342
|
+
route: r, confidence: conf, matchKind,
|
|
343
|
+
reason: knownHost ? 'literal_path+service_host' : 'literal_path',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
// Pass 2 — parameterised route matches. Only if no exact hit.
|
|
347
|
+
if (candidates.length === 0) {
|
|
348
|
+
for (const r of routes) {
|
|
349
|
+
if ((r.protocol ?? 'http') !== 'http') continue;
|
|
350
|
+
const pm = routePatternsMatch(c.normalized_path, normalizeRoutePath(r.path));
|
|
351
|
+
if (!pm.matched) continue;
|
|
352
|
+
const ms = methodMatchScore(c.method, r.method);
|
|
353
|
+
if (ms === 0) continue;
|
|
354
|
+
const conf = Math.min(1.0, pm.confidence * ms * hostBoost);
|
|
355
|
+
candidates.push({
|
|
356
|
+
route: r, confidence: conf,
|
|
357
|
+
matchKind: knownHost && !c.env_key ? 'service_host' : 'route_pattern',
|
|
358
|
+
reason: knownHost ? `${pm.reason}+service_host` : pm.reason,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} else if (c.protocol === 'trpc') {
|
|
363
|
+
if (!c.operation) continue;
|
|
364
|
+
considered++;
|
|
365
|
+
// tRPC clients carry the full nested procedure path like 'user.getById';
|
|
366
|
+
// server-side routes carry only their immediate procedure key like
|
|
367
|
+
// 'getById'. We match either way: prefer exact full-path equality, then
|
|
368
|
+
// fall back to last-segment match.
|
|
369
|
+
const exacts = byOperation.get(`trpc:${c.operation}`) ?? [];
|
|
370
|
+
for (const r of exacts) {
|
|
371
|
+
const ms = methodMatchScore(c.method, r.method);
|
|
372
|
+
// tRPC method match: query vs query etc.; fall back to ANY-as-wildcard.
|
|
373
|
+
const conf = 0.95 * (ms === 0 ? 0.85 : ms);
|
|
374
|
+
candidates.push({
|
|
375
|
+
route: r, confidence: conf,
|
|
376
|
+
matchKind: 'trpc_procedure', reason: 'exact_operation',
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (candidates.length === 0) {
|
|
380
|
+
// Last-segment fallback: client 'user.getById' matches server route
|
|
381
|
+
// whose operation is 'getById' (a procedure inside a sub-router).
|
|
382
|
+
const lastSeg = c.operation.split('.').pop()!;
|
|
383
|
+
const tail = byOperation.get(`trpc:${lastSeg}`) ?? [];
|
|
384
|
+
for (const r of tail) {
|
|
385
|
+
const ms = methodMatchScore(c.method, r.method);
|
|
386
|
+
const conf = 0.7 * (ms === 0 ? 0.85 : ms);
|
|
387
|
+
candidates.push({
|
|
388
|
+
route: r, confidence: conf,
|
|
389
|
+
matchKind: 'trpc_procedure', reason: 'last_segment_operation',
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} else if (c.protocol === 'graphql') {
|
|
394
|
+
// gql-doc sentinel rows: skip — they're document definitions, not calls.
|
|
395
|
+
if (c.framework === 'gql-doc') continue;
|
|
396
|
+
if (!c.operation) continue;
|
|
397
|
+
considered++;
|
|
398
|
+
// Resolve operation: prefer the parsed field name. If the operation is a
|
|
399
|
+
// known document-identifier (e.g. GET_USER) and we have a sentinel for
|
|
400
|
+
// it, use the sentinel's field name instead.
|
|
401
|
+
let effectiveOp = c.operation;
|
|
402
|
+
let effectiveMethod = c.method;
|
|
403
|
+
const doc = gqlDocMap.get(c.operation);
|
|
404
|
+
if (doc) {
|
|
405
|
+
effectiveOp = doc.operation;
|
|
406
|
+
if (!effectiveMethod && doc.method) effectiveMethod = doc.method;
|
|
407
|
+
}
|
|
408
|
+
const exacts = byOperation.get(`graphql:${effectiveOp}`) ?? [];
|
|
409
|
+
for (const r of exacts) {
|
|
410
|
+
const ms = methodMatchScore(effectiveMethod, r.method);
|
|
411
|
+
const conf = 0.9 * (ms === 0 ? 0.85 : ms);
|
|
412
|
+
candidates.push({
|
|
413
|
+
route: r, confidence: conf,
|
|
414
|
+
matchKind: 'graphql_operation',
|
|
415
|
+
reason: doc ? 'gql_doc_field' : 'exact_operation',
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
} else if (c.protocol === 'grpc') {
|
|
419
|
+
// Match by service + method, encoded as 'Service/Method' or just 'Method'.
|
|
420
|
+
const op = c.operation;
|
|
421
|
+
if (!op) continue;
|
|
422
|
+
considered++;
|
|
423
|
+
const exacts = byOperation.get(`grpc:${op}`) ?? [];
|
|
424
|
+
for (const r of exacts) {
|
|
425
|
+
candidates.push({
|
|
426
|
+
route: r, confidence: 0.95,
|
|
427
|
+
matchKind: 'grpc_method', reason: 'exact_grpc_method',
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
} else if (c.protocol === 'kafka' || c.protocol === 'sns' ||
|
|
431
|
+
c.protocol === 'nats' || c.protocol === 'redis_pubsub') {
|
|
432
|
+
if (!c.topic) continue;
|
|
433
|
+
considered++;
|
|
434
|
+
const consumers = byTopic.get(c.topic) ?? [];
|
|
435
|
+
for (const r of consumers) {
|
|
436
|
+
if ((r.protocol ?? '') !== c.protocol) continue;
|
|
437
|
+
candidates.push({
|
|
438
|
+
route: r, confidence: 0.9,
|
|
439
|
+
matchKind: 'topic_match', reason: 'topic_match',
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
} else if (c.protocol === 'sqs' || c.protocol === 'rabbitmq') {
|
|
443
|
+
if (!c.queue) continue;
|
|
444
|
+
considered++;
|
|
445
|
+
const consumers = byQueue.get(c.queue) ?? [];
|
|
446
|
+
for (const r of consumers) {
|
|
447
|
+
if ((r.protocol ?? '') !== c.protocol) continue;
|
|
448
|
+
candidates.push({
|
|
449
|
+
route: r, confidence: 0.9,
|
|
450
|
+
matchKind: 'queue_match', reason: 'queue_match',
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
// websocket / sse / unknown — skip for now; future plans can add them.
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (candidates.length === 0) continue;
|
|
459
|
+
|
|
460
|
+
// Sort by (confidence DESC, route_id ASC, match_kind ASC) so the top pick
|
|
461
|
+
// — and the ambiguity ordering — is deterministic across runs. match_kind
|
|
462
|
+
// is a tertiary tie-break in case two routes have identical id+confidence
|
|
463
|
+
// (shouldn't happen, but defensive).
|
|
464
|
+
candidates.sort((a, b) => {
|
|
465
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
466
|
+
if (a.route.id !== b.route.id) return a.route.id - b.route.id;
|
|
467
|
+
return a.matchKind < b.matchKind ? -1 : a.matchKind > b.matchKind ? 1 : 0;
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Cap candidates considered. Truncation is recorded in evidence so the
|
|
471
|
+
// agent can see the cutoff fired. Capping AFTER sorting preserves the
|
|
472
|
+
// best matches.
|
|
473
|
+
const totalCandidates = candidates.length;
|
|
474
|
+
let didTruncate = false;
|
|
475
|
+
if (candidates.length > MAX_CANDIDATES_PER_CALL) {
|
|
476
|
+
candidates.length = MAX_CANDIDATES_PER_CALL;
|
|
477
|
+
didTruncate = true;
|
|
478
|
+
truncatedCalls++;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const top = candidates[0];
|
|
482
|
+
const ambiguity = candidates.length > 1
|
|
483
|
+
? candidates.slice(1, 1 + MAX_EVIDENCE_CANDIDATES).map(x => ({
|
|
484
|
+
route_id: x.route.id,
|
|
485
|
+
confidence: Number(x.confidence.toFixed(3)),
|
|
486
|
+
reason: x.reason,
|
|
487
|
+
match_kind: x.matchKind,
|
|
488
|
+
}))
|
|
489
|
+
: [];
|
|
490
|
+
const evidence = JSON.stringify({
|
|
491
|
+
reason: top.reason,
|
|
492
|
+
method_call: c.method,
|
|
493
|
+
method_route: top.route.method,
|
|
494
|
+
env_key: c.env_key ?? null,
|
|
495
|
+
host_hint: c.host_hint ?? null,
|
|
496
|
+
raw_target: c.raw_target,
|
|
497
|
+
operation: c.operation ?? null,
|
|
498
|
+
topic: c.topic ?? null,
|
|
499
|
+
queue: c.queue ?? null,
|
|
500
|
+
service: c.service ?? null,
|
|
501
|
+
total_candidates: totalCandidates,
|
|
502
|
+
truncated: didTruncate,
|
|
503
|
+
ambiguity_candidates: ambiguity,
|
|
504
|
+
});
|
|
505
|
+
insertLink({
|
|
506
|
+
callId: c.id,
|
|
507
|
+
routeId: top.route.id,
|
|
508
|
+
callerSymbolId: c.symbol_id,
|
|
509
|
+
handlerSymbolId: top.route.handler_id,
|
|
510
|
+
protocol: c.protocol,
|
|
511
|
+
matchKind: top.matchKind,
|
|
512
|
+
confidence: Number(top.confidence.toFixed(3)),
|
|
513
|
+
evidenceJson: evidence,
|
|
514
|
+
});
|
|
515
|
+
byKind[top.matchKind] = (byKind[top.matchKind] ?? 0) + 1;
|
|
516
|
+
inserted++;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
callsConsidered: considered,
|
|
521
|
+
linksInserted: inserted,
|
|
522
|
+
byKind: byKind as Record<MatchKind, number>,
|
|
523
|
+
truncated: truncatedCalls,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Cheap PRAGMA-based column-existence check; used so the resolver can run
|
|
528
|
+
* unchanged against a pre-v9 DB shape. */
|
|
529
|
+
function hasColumn(raw: any, table: string, column: string): boolean {
|
|
530
|
+
try {
|
|
531
|
+
const rows = raw.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
532
|
+
return rows.some(r => r.name === column);
|
|
533
|
+
} catch { return false; }
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Apply the same normalization to a route path as to a call path so the
|
|
537
|
+
* byExactPath comparison is symmetric (strip trailing slash, etc). */
|
|
538
|
+
export function normalizeRoutePath(p: string): string {
|
|
539
|
+
if (!p) return '';
|
|
540
|
+
let s = p.trim();
|
|
541
|
+
if (s.length > 1 && s.endsWith('/')) s = s.slice(0, -1);
|
|
542
|
+
return s;
|
|
543
|
+
}
|