@mka-rainmaker/ama 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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/analyzers/baseline/analyzer.d.ts +47 -0
- package/dist/analyzers/baseline/analyzer.d.ts.map +1 -0
- package/dist/analyzers/baseline/analyzer.js +84 -0
- package/dist/analyzers/baseline/analyzer.js.map +1 -0
- package/dist/analyzers/baseline/c.d.ts +12 -0
- package/dist/analyzers/baseline/c.d.ts.map +1 -0
- package/dist/analyzers/baseline/c.js +56 -0
- package/dist/analyzers/baseline/c.js.map +1 -0
- package/dist/analyzers/baseline/config.d.ts +21 -0
- package/dist/analyzers/baseline/config.d.ts.map +1 -0
- package/dist/analyzers/baseline/config.js +32 -0
- package/dist/analyzers/baseline/config.js.map +1 -0
- package/dist/analyzers/baseline/csharp.d.ts +9 -0
- package/dist/analyzers/baseline/csharp.d.ts.map +1 -0
- package/dist/analyzers/baseline/csharp.js +107 -0
- package/dist/analyzers/baseline/csharp.js.map +1 -0
- package/dist/analyzers/baseline/go.d.ts +11 -0
- package/dist/analyzers/baseline/go.d.ts.map +1 -0
- package/dist/analyzers/baseline/go.js +66 -0
- package/dist/analyzers/baseline/go.js.map +1 -0
- package/dist/analyzers/baseline/java.d.ts +9 -0
- package/dist/analyzers/baseline/java.d.ts.map +1 -0
- package/dist/analyzers/baseline/java.js +50 -0
- package/dist/analyzers/baseline/java.js.map +1 -0
- package/dist/analyzers/baseline/javascript.d.ts +10 -0
- package/dist/analyzers/baseline/javascript.d.ts.map +1 -0
- package/dist/analyzers/baseline/javascript.js +55 -0
- package/dist/analyzers/baseline/javascript.js.map +1 -0
- package/dist/analyzers/baseline/kotlin.d.ts +11 -0
- package/dist/analyzers/baseline/kotlin.d.ts.map +1 -0
- package/dist/analyzers/baseline/kotlin.js +67 -0
- package/dist/analyzers/baseline/kotlin.js.map +1 -0
- package/dist/analyzers/baseline/paths.d.ts +6 -0
- package/dist/analyzers/baseline/paths.d.ts.map +1 -0
- package/dist/analyzers/baseline/paths.js +17 -0
- package/dist/analyzers/baseline/paths.js.map +1 -0
- package/dist/analyzers/baseline/php.d.ts +11 -0
- package/dist/analyzers/baseline/php.d.ts.map +1 -0
- package/dist/analyzers/baseline/php.js +76 -0
- package/dist/analyzers/baseline/php.js.map +1 -0
- package/dist/analyzers/baseline/python.d.ts +10 -0
- package/dist/analyzers/baseline/python.d.ts.map +1 -0
- package/dist/analyzers/baseline/python.js +63 -0
- package/dist/analyzers/baseline/python.js.map +1 -0
- package/dist/analyzers/baseline/rust.d.ts +10 -0
- package/dist/analyzers/baseline/rust.d.ts.map +1 -0
- package/dist/analyzers/baseline/rust.js +45 -0
- package/dist/analyzers/baseline/rust.js.map +1 -0
- package/dist/analyzers/baseline/swift.d.ts +11 -0
- package/dist/analyzers/baseline/swift.d.ts.map +1 -0
- package/dist/analyzers/baseline/swift.js +19 -0
- package/dist/analyzers/baseline/swift.js.map +1 -0
- package/dist/analyzers/baseline/treesitter.d.ts +11 -0
- package/dist/analyzers/baseline/treesitter.d.ts.map +1 -0
- package/dist/analyzers/baseline/treesitter.js +87 -0
- package/dist/analyzers/baseline/treesitter.js.map +1 -0
- package/dist/analyzers/baseline/walk.d.ts +26 -0
- package/dist/analyzers/baseline/walk.d.ts.map +1 -0
- package/dist/analyzers/baseline/walk.js +76 -0
- package/dist/analyzers/baseline/walk.js.map +1 -0
- package/dist/analyzers/registry.d.ts +19 -0
- package/dist/analyzers/registry.d.ts.map +1 -0
- package/dist/analyzers/registry.js +43 -0
- package/dist/analyzers/registry.js.map +1 -0
- package/dist/analyzers/sfc/analyzer.d.ts +17 -0
- package/dist/analyzers/sfc/analyzer.d.ts.map +1 -0
- package/dist/analyzers/sfc/analyzer.js +141 -0
- package/dist/analyzers/sfc/analyzer.js.map +1 -0
- package/dist/analyzers/sidecar/analyzer.d.ts +29 -0
- package/dist/analyzers/sidecar/analyzer.d.ts.map +1 -0
- package/dist/analyzers/sidecar/analyzer.js +114 -0
- package/dist/analyzers/sidecar/analyzer.js.map +1 -0
- package/dist/analyzers/sidecar/protocol.d.ts +508 -0
- package/dist/analyzers/sidecar/protocol.d.ts.map +1 -0
- package/dist/analyzers/sidecar/protocol.js +102 -0
- package/dist/analyzers/sidecar/protocol.js.map +1 -0
- package/dist/analyzers/types.d.ts +46 -0
- package/dist/analyzers/types.d.ts.map +1 -0
- package/dist/analyzers/types.js +2 -0
- package/dist/analyzers/types.js.map +1 -0
- package/dist/analyzers/typescript/analyzer.d.ts +126 -0
- package/dist/analyzers/typescript/analyzer.d.ts.map +1 -0
- package/dist/analyzers/typescript/analyzer.js +1600 -0
- package/dist/analyzers/typescript/analyzer.js.map +1 -0
- package/dist/cli/commands/cycles.d.ts +6 -0
- package/dist/cli/commands/cycles.d.ts.map +1 -0
- package/dist/cli/commands/cycles.js +27 -0
- package/dist/cli/commands/cycles.js.map +1 -0
- package/dist/cli/commands/files.d.ts +6 -0
- package/dist/cli/commands/files.d.ts.map +1 -0
- package/dist/cli/commands/files.js +33 -0
- package/dist/cli/commands/files.js.map +1 -0
- package/dist/cli/commands/impact.d.ts +18 -0
- package/dist/cli/commands/impact.d.ts.map +1 -0
- package/dist/cli/commands/impact.js +113 -0
- package/dist/cli/commands/impact.js.map +1 -0
- package/dist/cli/commands/lifecycle.d.ts +5 -0
- package/dist/cli/commands/lifecycle.d.ts.map +1 -0
- package/dist/cli/commands/lifecycle.js +83 -0
- package/dist/cli/commands/lifecycle.js.map +1 -0
- package/dist/cli/commands/query.d.ts +31 -0
- package/dist/cli/commands/query.d.ts.map +1 -0
- package/dist/cli/commands/query.js +187 -0
- package/dist/cli/commands/query.js.map +1 -0
- package/dist/cli/commands/search.d.ts +21 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +160 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/status.d.ts +6 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +63 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +6 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +57 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/emit.d.ts +9 -0
- package/dist/cli/emit.d.ts.map +1 -0
- package/dist/cli/emit.js +10 -0
- package/dist/cli/emit.js.map +1 -0
- package/dist/cli/index.d.ts +37 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +128 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/paths.d.ts +7 -0
- package/dist/cli/paths.d.ts.map +1 -0
- package/dist/cli/paths.js +10 -0
- package/dist/cli/paths.js.map +1 -0
- package/dist/cli/query-runner.d.ts +13 -0
- package/dist/cli/query-runner.d.ts.map +1 -0
- package/dist/cli/query-runner.js +33 -0
- package/dist/cli/query-runner.js.map +1 -0
- package/dist/graph/dispatch.d.ts +17 -0
- package/dist/graph/dispatch.d.ts.map +1 -0
- package/dist/graph/dispatch.js +82 -0
- package/dist/graph/dispatch.js.map +1 -0
- package/dist/graph/id.d.ts +19 -0
- package/dist/graph/id.d.ts.map +1 -0
- package/dist/graph/id.js +17 -0
- package/dist/graph/id.js.map +1 -0
- package/dist/graph/index.d.ts +6 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/types.d.ts +71 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +52 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/indexer/debouncer.d.ts +32 -0
- package/dist/indexer/debouncer.d.ts.map +1 -0
- package/dist/indexer/debouncer.js +81 -0
- package/dist/indexer/debouncer.js.map +1 -0
- package/dist/indexer/ignore.d.ts +55 -0
- package/dist/indexer/ignore.d.ts.map +1 -0
- package/dist/indexer/ignore.js +170 -0
- package/dist/indexer/ignore.js.map +1 -0
- package/dist/indexer/indexer.d.ts +112 -0
- package/dist/indexer/indexer.d.ts.map +1 -0
- package/dist/indexer/indexer.js +392 -0
- package/dist/indexer/indexer.js.map +1 -0
- package/dist/indexer/watcher.d.ts +50 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +86 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/mcp/build-info.d.ts +16 -0
- package/dist/mcp/build-info.d.ts.map +1 -0
- package/dist/mcp/build-info.js +54 -0
- package/dist/mcp/build-info.js.map +1 -0
- package/dist/mcp/http.d.ts +18 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.js +145 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/server.d.ts +22 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +401 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +155 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +319 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/query/service.d.ts +329 -0
- package/dist/query/service.d.ts.map +1 -0
- package/dist/query/service.js +959 -0
- package/dist/query/service.js.map +1 -0
- package/dist/runtime/entrypoint.d.ts +11 -0
- package/dist/runtime/entrypoint.d.ts.map +1 -0
- package/dist/runtime/entrypoint.js +22 -0
- package/dist/runtime/entrypoint.js.map +1 -0
- package/dist/runtime/quiet-sqlite-warning.d.ts +14 -0
- package/dist/runtime/quiet-sqlite-warning.d.ts.map +1 -0
- package/dist/runtime/quiet-sqlite-warning.js +26 -0
- package/dist/runtime/quiet-sqlite-warning.js.map +1 -0
- package/dist/runtime/wasm-tier.d.ts +2 -0
- package/dist/runtime/wasm-tier.d.ts.map +1 -0
- package/dist/runtime/wasm-tier.js +54 -0
- package/dist/runtime/wasm-tier.js.map +1 -0
- package/dist/store/memory.d.ts +54 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +210 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sqlite.d.ts +38 -0
- package/dist/store/sqlite.d.ts.map +1 -0
- package/dist/store/sqlite.js +298 -0
- package/dist/store/sqlite.js.map +1 -0
- package/dist/store/types.d.ts +76 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +2 -0
- package/dist/store/types.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Reverse-traversed by {@link QueryService.impactAnalysis}: every edge kind whose
|
|
5
|
+
* *source* depends on its *target*, so a change to the target can break the source.
|
|
6
|
+
* Excludes structural `Defines` and the file-level `Imports`/`ImportsType` (those are
|
|
7
|
+
* file-granularity — covered by {@link QueryService.affected}). (ama-8sw)
|
|
8
|
+
*/
|
|
9
|
+
const IMPACT_EDGE_KINDS = [
|
|
10
|
+
"Calls",
|
|
11
|
+
"References",
|
|
12
|
+
"UsesType",
|
|
13
|
+
"Returns",
|
|
14
|
+
"Instantiates",
|
|
15
|
+
"Implements",
|
|
16
|
+
"Inherits",
|
|
17
|
+
"Overrides",
|
|
18
|
+
];
|
|
19
|
+
// key:"quoted value" | key:bare | "quoted text" | bare-word.
|
|
20
|
+
const SEARCH_TOKEN = /(\w+):"([^"]*)"|(\w+):(\S+)|"([^"]+)"|(\S+)/g;
|
|
21
|
+
/**
|
|
22
|
+
* Parse a search string into free text plus `path:`/`kind:`/`lang:`/`name:`
|
|
23
|
+
* filters, honouring quotes for values with spaces. Unknown `key:value` tokens
|
|
24
|
+
* (e.g. a `http://…` URL) are kept verbatim as free text rather than dropped.
|
|
25
|
+
*/
|
|
26
|
+
export function parseSearchQuery(raw) {
|
|
27
|
+
const result = { text: "" };
|
|
28
|
+
const text = [];
|
|
29
|
+
for (const m of raw.matchAll(SEARCH_TOKEN)) {
|
|
30
|
+
const key = m[1] ?? m[3];
|
|
31
|
+
const value = m[2] ?? m[4];
|
|
32
|
+
if (key !== undefined && value !== undefined) {
|
|
33
|
+
switch (key.toLowerCase()) {
|
|
34
|
+
case "path":
|
|
35
|
+
result.path = value;
|
|
36
|
+
break;
|
|
37
|
+
case "kind":
|
|
38
|
+
result.kind = value;
|
|
39
|
+
break;
|
|
40
|
+
case "lang":
|
|
41
|
+
result.lang = value;
|
|
42
|
+
break;
|
|
43
|
+
case "name":
|
|
44
|
+
result.name = value;
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
text.push(`${key}:${value}`); // unknown filter — keep as text
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const bare = m[5] ?? m[6];
|
|
52
|
+
if (bare !== undefined)
|
|
53
|
+
text.push(bare);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
result.text = text.join(" ");
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
/** Source-file extension → language name, for the `lang:` search filter. A small
|
|
60
|
+
* presentation-layer map (the authoritative extension set lives in each analyzer,
|
|
61
|
+
* which the query layer must not import). */
|
|
62
|
+
const LANGUAGE_BY_EXT = {
|
|
63
|
+
".ts": "typescript",
|
|
64
|
+
".tsx": "typescript",
|
|
65
|
+
".mts": "typescript",
|
|
66
|
+
".cts": "typescript",
|
|
67
|
+
".js": "javascript",
|
|
68
|
+
".jsx": "javascript",
|
|
69
|
+
".mjs": "javascript",
|
|
70
|
+
".cjs": "javascript",
|
|
71
|
+
".py": "python",
|
|
72
|
+
".go": "go",
|
|
73
|
+
".rs": "rust",
|
|
74
|
+
".java": "java",
|
|
75
|
+
".cs": "csharp",
|
|
76
|
+
".php": "php",
|
|
77
|
+
".c": "c",
|
|
78
|
+
".cpp": "cpp",
|
|
79
|
+
".cc": "cpp",
|
|
80
|
+
".cxx": "cpp",
|
|
81
|
+
".hpp": "cpp",
|
|
82
|
+
".hh": "cpp",
|
|
83
|
+
".h": "cpp",
|
|
84
|
+
".kt": "kotlin",
|
|
85
|
+
".kts": "kotlin",
|
|
86
|
+
".swift": "swift",
|
|
87
|
+
};
|
|
88
|
+
function languageForFile(file) {
|
|
89
|
+
const dot = file.lastIndexOf(".");
|
|
90
|
+
return dot === -1 ? undefined : LANGUAGE_BY_EXT[file.slice(dot).toLowerCase()];
|
|
91
|
+
}
|
|
92
|
+
/** Relevance weight per node kind — top-level definitions outrank members and
|
|
93
|
+
* variables when match quality ties. */
|
|
94
|
+
const KIND_BONUS = {
|
|
95
|
+
Class: 8,
|
|
96
|
+
Interface: 8,
|
|
97
|
+
Function: 8,
|
|
98
|
+
Enum: 6,
|
|
99
|
+
TypeAlias: 6,
|
|
100
|
+
Module: 6,
|
|
101
|
+
Route: 4,
|
|
102
|
+
Method: 2,
|
|
103
|
+
Property: 1,
|
|
104
|
+
Variable: 1,
|
|
105
|
+
};
|
|
106
|
+
/** Whether a path looks like a test file — `tests/`/`__tests__/` dirs or a
|
|
107
|
+
* `.test.`/`.spec.` name. Used by test-impact filtering (ama-5gs.9) and search
|
|
108
|
+
* demotion. */
|
|
109
|
+
export function isTestFile(file) {
|
|
110
|
+
const f = file.toLowerCase();
|
|
111
|
+
return /(^|\/)(tests?|__tests__)\//.test(f) || /\.(test|spec)\./.test(f);
|
|
112
|
+
}
|
|
113
|
+
/** Test and generated/build files — real but rarely the symbol you searched for,
|
|
114
|
+
* so a match there is demoted below an equivalent match in source. */
|
|
115
|
+
function isDeprioritizedFile(file) {
|
|
116
|
+
const f = file.toLowerCase();
|
|
117
|
+
return (isTestFile(file) ||
|
|
118
|
+
f.endsWith(".d.ts") ||
|
|
119
|
+
/\.generated\./.test(f) ||
|
|
120
|
+
/(^|\/)(dist|build|coverage)\//.test(f));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A relevance score for a symbol against the free-text part of a search. Higher
|
|
124
|
+
* is better: exact name/qualified-name match dominates, then prefix, then
|
|
125
|
+
* substring; a brevity bonus favours the more specific (shorter) name; a kind
|
|
126
|
+
* bonus lifts top-level definitions; test/generated files are demoted. With no
|
|
127
|
+
* free text (a filters-only query) only the kind/demotion terms apply. (ama-m8k.2)
|
|
128
|
+
*/
|
|
129
|
+
function scoreSymbol(node, query) {
|
|
130
|
+
let score = KIND_BONUS[node.kind] ?? 0;
|
|
131
|
+
if (isDeprioritizedFile(node.file))
|
|
132
|
+
score -= 50;
|
|
133
|
+
if (query) {
|
|
134
|
+
const q = query.toLowerCase();
|
|
135
|
+
const name = node.name.toLowerCase();
|
|
136
|
+
const qn = node.qualifiedName.toLowerCase();
|
|
137
|
+
if (name === q || qn === q)
|
|
138
|
+
score += 100;
|
|
139
|
+
else if (name.startsWith(q))
|
|
140
|
+
score += 60;
|
|
141
|
+
else if (name.includes(q))
|
|
142
|
+
score += 30;
|
|
143
|
+
else if (qn.includes(q))
|
|
144
|
+
score += 12; // matched only via the qualified name
|
|
145
|
+
score += Math.max(0, 16 - name.length); // brevity: a shorter name is more specific
|
|
146
|
+
}
|
|
147
|
+
return score;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Order relationship results (callers, callees, importers, …) by relevance with no
|
|
151
|
+
* free-text term: the query-less `scoreSymbol` lifts top-level definitions over
|
|
152
|
+
* members/variables and demotes test/generated files, so a symbol's real source
|
|
153
|
+
* relationships surface above its test-file ones. Ties break alphabetically. (ama-bc2)
|
|
154
|
+
*/
|
|
155
|
+
function rankNodes(nodes) {
|
|
156
|
+
return [...nodes].sort((a, b) => scoreSymbol(b, "") - scoreSymbol(a, "") || a.qualifiedName.localeCompare(b.qualifiedName));
|
|
157
|
+
}
|
|
158
|
+
/** The edge kinds that mean "X invokes Y": a plain call and a `new Y()`
|
|
159
|
+
* construction. find_callers/find_callees report both, labelled by `via`. */
|
|
160
|
+
const CALL_EDGE_KINDS = ["Calls", "Instantiates"];
|
|
161
|
+
/** The edge kinds that mean "X uses type Y" — a param/property annotation and a
|
|
162
|
+
* return type. find_types_used/find_type_users report both; find_returns is the
|
|
163
|
+
* return half alone. (ama-37c) */
|
|
164
|
+
const TYPE_EDGE_KINDS = ["UsesType", "Returns"];
|
|
165
|
+
/** explore() output budget (ama-m8k.4): deep-dive the top this-many matches,
|
|
166
|
+
* scan up to this-many to report the true total, and show this-many blast-radius
|
|
167
|
+
* symbols. A broad term ("node") otherwise floods — 134k chars in one call. */
|
|
168
|
+
const EXPLORE_MATCH_LIMIT = 15;
|
|
169
|
+
const EXPLORE_SCAN_LIMIT = 200;
|
|
170
|
+
const EXPLORE_BLAST_LIMIT = 40;
|
|
171
|
+
/** Default result cap for search_symbol/search_code — shared so the MCP layer can
|
|
172
|
+
* request one extra and tell the agent when the result was truncated. (ama-b4q) */
|
|
173
|
+
export const DEFAULT_SEARCH_LIMIT = 50;
|
|
174
|
+
/** Cap on the secondary `dependents` preview in a file skeleton. The outline
|
|
175
|
+
* (symbols) stays complete; dependents is bounded so a foundational file's skeleton
|
|
176
|
+
* doesn't dwarf the file it summarizes — `dependentsTotal` carries the full count
|
|
177
|
+
* and find_importers gives the complete list. (ama-2by) */
|
|
178
|
+
export const SKELETON_DEPENDENTS_LIMIT = 25;
|
|
179
|
+
/** Question words and glue dropped before tokenizing an `explore` question, so its
|
|
180
|
+
* search terms are the content words (`baseline`, `import`), not `how`/`are`. */
|
|
181
|
+
const EXPLORE_STOPWORDS = new Set([
|
|
182
|
+
"how",
|
|
183
|
+
"what",
|
|
184
|
+
"where",
|
|
185
|
+
"why",
|
|
186
|
+
"when",
|
|
187
|
+
"who",
|
|
188
|
+
"which",
|
|
189
|
+
"the",
|
|
190
|
+
"are",
|
|
191
|
+
"was",
|
|
192
|
+
"were",
|
|
193
|
+
"does",
|
|
194
|
+
"did",
|
|
195
|
+
"and",
|
|
196
|
+
"for",
|
|
197
|
+
"with",
|
|
198
|
+
"this",
|
|
199
|
+
"that",
|
|
200
|
+
"its",
|
|
201
|
+
"into",
|
|
202
|
+
"from",
|
|
203
|
+
"use",
|
|
204
|
+
"used",
|
|
205
|
+
"uses",
|
|
206
|
+
"work",
|
|
207
|
+
"works",
|
|
208
|
+
"get",
|
|
209
|
+
"set",
|
|
210
|
+
"via",
|
|
211
|
+
"between",
|
|
212
|
+
"about",
|
|
213
|
+
"you",
|
|
214
|
+
]);
|
|
215
|
+
/** Content tokens of an `explore` question: lowercased words ≥ 3 chars that aren't
|
|
216
|
+
* stopwords, de-duplicated. A verbose NL question rarely matches a symbol name as
|
|
217
|
+
* one string, so explore searches these terms and unions the hits. (ama-30q) */
|
|
218
|
+
function exploreTerms(question) {
|
|
219
|
+
const terms = question
|
|
220
|
+
.toLowerCase()
|
|
221
|
+
.split(/[^a-z0-9_]+/)
|
|
222
|
+
.filter((t) => t.length >= 3 && !EXPLORE_STOPWORDS.has(t));
|
|
223
|
+
return [...new Set(terms)];
|
|
224
|
+
}
|
|
225
|
+
/** Pair a neighbour node with the metadata of the edge it was reached by. */
|
|
226
|
+
function neighbor(symbol, edge) {
|
|
227
|
+
const n = { symbol, via: edge.kind };
|
|
228
|
+
if (edge.at)
|
|
229
|
+
n.at = edge.at;
|
|
230
|
+
if (edge.sites)
|
|
231
|
+
n.sites = edge.sites;
|
|
232
|
+
if (edge.provenance)
|
|
233
|
+
n.provenance = edge.provenance;
|
|
234
|
+
return n;
|
|
235
|
+
}
|
|
236
|
+
/** {@link rankNodes} for edge neighbours — ranks by the neighbour symbol. */
|
|
237
|
+
function rankNeighbors(neighbors) {
|
|
238
|
+
return [...neighbors].sort((a, b) => scoreSymbol(b.symbol, "") - scoreSymbol(a.symbol, "") ||
|
|
239
|
+
a.symbol.qualifiedName.localeCompare(b.symbol.qualifiedName));
|
|
240
|
+
}
|
|
241
|
+
/** The 1-based start of a contiguous leading comment block immediately above
|
|
242
|
+
* `declLine` (a JSDoc block or line comments, no blank line between), or `declLine`
|
|
243
|
+
* itself when there's none — so a symbol's view reunites it with its documentation.
|
|
244
|
+
* The blank-line stop mirrors how TypeScript binds a doc comment to a declaration.
|
|
245
|
+
* Shared by get_code_snippet (to show the docs) and search_code (to search them).
|
|
246
|
+
* (ama-43e, ama-jxp) */
|
|
247
|
+
function commentAwareStart(lines, declLine) {
|
|
248
|
+
let start = declLine;
|
|
249
|
+
for (let i = declLine - 2; i >= 0; i--) {
|
|
250
|
+
const t = lines[i]?.trim() ?? "";
|
|
251
|
+
if (!(t.startsWith("//") || t.startsWith("/*") || t.startsWith("*") || t.endsWith("*/"))) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
start = i + 1;
|
|
255
|
+
}
|
|
256
|
+
return start;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Read-side of the graph: the four MVP questions an agent asks, answered from
|
|
260
|
+
* the store. A "symbol reference" is either an exact node id (e.g.
|
|
261
|
+
* "src/a.ts#Foo.bar") or a bare name ("bar"); names may resolve to several nodes.
|
|
262
|
+
*/
|
|
263
|
+
export class QueryService {
|
|
264
|
+
store;
|
|
265
|
+
root;
|
|
266
|
+
constructor(store,
|
|
267
|
+
/** Absolute repo root, used to read source for snippets. */
|
|
268
|
+
root) {
|
|
269
|
+
this.store = store;
|
|
270
|
+
this.root = root;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Symbols whose name matches `query`, answered by the store's name index
|
|
274
|
+
* (substring in-memory, FTS5 prefix in SQLite). Kind is filtered on top.
|
|
275
|
+
*/
|
|
276
|
+
searchSymbol(query, opts = {}) {
|
|
277
|
+
const { text, path: pathFilter, kind: kindFilter, lang, name } = parseSearchQuery(query);
|
|
278
|
+
const limit = opts.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
279
|
+
const kind = kindFilter ?? opts.kind;
|
|
280
|
+
// A completely empty query (no text and no filters) has nothing to match — don't
|
|
281
|
+
// fall through to allNodes() and hand back arbitrary symbols. A *filters-only*
|
|
282
|
+
// query (e.g. `kind:Class`) is still valid: it has a filter. (ama-k3d)
|
|
283
|
+
if (!text && !pathFilter && !kind && !lang && !name)
|
|
284
|
+
return [];
|
|
285
|
+
// Free text searches the name index (relevance-ordered); a filters-only query
|
|
286
|
+
// (e.g. `path:src/api kind:Class`) scans every node since there's no name term.
|
|
287
|
+
const candidates = text
|
|
288
|
+
? this.store.searchByName(text, Number.MAX_SAFE_INTEGER)
|
|
289
|
+
: this.store.allNodes();
|
|
290
|
+
const hits = [];
|
|
291
|
+
for (const node of candidates) {
|
|
292
|
+
if (kind && node.kind.toLowerCase() !== kind.toLowerCase())
|
|
293
|
+
continue;
|
|
294
|
+
if (pathFilter && !node.file.toLowerCase().includes(pathFilter.toLowerCase()))
|
|
295
|
+
continue;
|
|
296
|
+
if (lang && languageForFile(node.file) !== lang.toLowerCase())
|
|
297
|
+
continue;
|
|
298
|
+
if (name &&
|
|
299
|
+
!node.name.toLowerCase().includes(name.toLowerCase()) &&
|
|
300
|
+
!node.qualifiedName.toLowerCase().includes(name.toLowerCase())) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
hits.push(node);
|
|
304
|
+
}
|
|
305
|
+
// Rank by relevance (match quality, kind, test/generated demotion) then slice —
|
|
306
|
+
// so the best matches survive the limit, not just the first ones found. (ama-m8k.2)
|
|
307
|
+
const scored = hits.map((node) => ({ node, score: scoreSymbol(node, text) }));
|
|
308
|
+
scored.sort((a, b) => b.score - a.score || a.node.qualifiedName.localeCompare(b.node.qualifiedName));
|
|
309
|
+
return scored.slice(0, limit).map((s) => s.node);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* searchSymbol plus a confidence signal: low-confidence when there is a free-text
|
|
313
|
+
* term and results, but none is an exact or name-prefix match — the matches are
|
|
314
|
+
* loose substring/qualified hits, so the caller likely mistyped or should refine
|
|
315
|
+
* rather than trust the top hit. (ama-b79)
|
|
316
|
+
*/
|
|
317
|
+
searchSymbolWithConfidence(query, opts = {}) {
|
|
318
|
+
const results = this.searchSymbol(query, opts);
|
|
319
|
+
const t = parseSearchQuery(query).text.trim().toLowerCase();
|
|
320
|
+
const strong = results.some((r) => {
|
|
321
|
+
const name = r.name.toLowerCase();
|
|
322
|
+
return name === t || name.startsWith(t) || r.qualifiedName.toLowerCase() === t;
|
|
323
|
+
});
|
|
324
|
+
return { results, lowConfidence: t !== "" && results.length > 0 && !strong };
|
|
325
|
+
}
|
|
326
|
+
/** Every indexed file's metadata, sorted by repo-relative path. */
|
|
327
|
+
files() {
|
|
328
|
+
return [...this.store.allFiles()].sort((a, b) => a.path.localeCompare(b.path));
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Full-text search over symbol *bodies* (not names): symbols whose verbatim
|
|
332
|
+
* source contains `query`, case-insensitively. Each file is read once and
|
|
333
|
+
* sliced per symbol; File nodes are excluded so a hit points at the containing
|
|
334
|
+
* symbol. This is the in-memory tier — a plain substring scan, not an FTS
|
|
335
|
+
* index (the SQLite store can specialize it later).
|
|
336
|
+
*/
|
|
337
|
+
searchCode(query, opts = {}) {
|
|
338
|
+
return this.scanCode(query, opts).results;
|
|
339
|
+
}
|
|
340
|
+
/** search_code that also reports whether it fell back from an exact-phrase match to
|
|
341
|
+
* term-matching (the literal phrase wasn't found) — so the MCP layer can warn the
|
|
342
|
+
* agent that the hits match the query's words separately, not the phrase. (ama-dve) */
|
|
343
|
+
searchCodeWithConfidence(query, opts = {}) {
|
|
344
|
+
return this.scanCode(query, opts);
|
|
345
|
+
}
|
|
346
|
+
scanCode(query, opts) {
|
|
347
|
+
const needle = query.toLowerCase();
|
|
348
|
+
const terms = exploreTerms(query); // for the fallback when the literal phrase misses
|
|
349
|
+
// A blank query has nothing to find — `body.includes("")` is true for every body,
|
|
350
|
+
// so without this guard search_code returns arbitrary symbols. (ama-d36)
|
|
351
|
+
if (needle.trim() === "")
|
|
352
|
+
return { results: [], viaTerms: false };
|
|
353
|
+
const limit = opts.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
354
|
+
const byFile = new Map();
|
|
355
|
+
for (const node of this.store.allNodes()) {
|
|
356
|
+
if (!node.range || node.kind === "File")
|
|
357
|
+
continue;
|
|
358
|
+
const group = byFile.get(node.file) ?? [];
|
|
359
|
+
group.push(node);
|
|
360
|
+
byFile.set(node.file, group);
|
|
361
|
+
}
|
|
362
|
+
const phrase = [];
|
|
363
|
+
const byTerms = [];
|
|
364
|
+
for (const [file, nodes] of byFile) {
|
|
365
|
+
let lines;
|
|
366
|
+
try {
|
|
367
|
+
lines = fs.readFileSync(path.resolve(this.root, file), "utf8").split("\n");
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
continue; // a file that vanished since indexing — skip it
|
|
371
|
+
}
|
|
372
|
+
for (const node of nodes) {
|
|
373
|
+
if (!node.range)
|
|
374
|
+
continue;
|
|
375
|
+
// Include the leading doc comment so a concept search matches what a symbol
|
|
376
|
+
// documents, not only what its code spells out. (ama-jxp)
|
|
377
|
+
const body = lines
|
|
378
|
+
.slice(commentAwareStart(lines, node.range.startLine) - 1, node.range.endLine)
|
|
379
|
+
.join("\n")
|
|
380
|
+
.toLowerCase();
|
|
381
|
+
if (body.includes(needle)) {
|
|
382
|
+
phrase.push(node);
|
|
383
|
+
if (phrase.length >= limit)
|
|
384
|
+
return { results: phrase, viaTerms: false };
|
|
385
|
+
}
|
|
386
|
+
else if (terms.length >= 2) {
|
|
387
|
+
const hits = terms.reduce((n, t) => (body.includes(t) ? n + 1 : n), 0);
|
|
388
|
+
if (hits > 0)
|
|
389
|
+
byTerms.push({ node, hits });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Prefer a literal contiguous-phrase match (grep semantics, no regression). Only
|
|
394
|
+
// when there's none fall back to the symbols whose bodies mention the most query
|
|
395
|
+
// terms, so a conceptual multi-word query isn't a dead end. (ama-ejh)
|
|
396
|
+
if (phrase.length > 0)
|
|
397
|
+
return { results: phrase, viaTerms: false };
|
|
398
|
+
const results = byTerms
|
|
399
|
+
.sort((a, b) => b.hits - a.hits || a.node.qualifiedName.localeCompare(b.node.qualifiedName))
|
|
400
|
+
.slice(0, limit)
|
|
401
|
+
.map((m) => m.node);
|
|
402
|
+
return { results, viaTerms: results.length > 0 };
|
|
403
|
+
}
|
|
404
|
+
/** Symbols that call or construct the referenced symbol, each labelled by the
|
|
405
|
+
* edge kind (`Calls` / `Instantiates`) and its call-site location. */
|
|
406
|
+
findCallers(ref) {
|
|
407
|
+
const callers = new Map();
|
|
408
|
+
for (const target of this.resolve(ref)) {
|
|
409
|
+
for (const kind of CALL_EDGE_KINDS) {
|
|
410
|
+
for (const edge of this.store.edgesTo(target.id, kind)) {
|
|
411
|
+
const caller = this.store.getNode(edge.from);
|
|
412
|
+
if (caller && !callers.has(caller.id))
|
|
413
|
+
callers.set(caller.id, neighbor(caller, edge));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return rankNeighbors([...callers.values()]);
|
|
418
|
+
}
|
|
419
|
+
/** Symbols the referenced symbol calls or constructs, each labelled by the edge
|
|
420
|
+
* kind (`Calls` / `Instantiates`) and its call-site location. */
|
|
421
|
+
findCallees(ref) {
|
|
422
|
+
const callees = new Map();
|
|
423
|
+
for (const source of this.resolve(ref)) {
|
|
424
|
+
for (const kind of CALL_EDGE_KINDS) {
|
|
425
|
+
for (const edge of this.store.edgesFrom(source.id, kind)) {
|
|
426
|
+
const callee = this.store.getNode(edge.to);
|
|
427
|
+
if (callee && !callees.has(callee.id))
|
|
428
|
+
callees.set(callee.id, neighbor(callee, edge));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return rankNeighbors([...callees.values()]);
|
|
433
|
+
}
|
|
434
|
+
/** The handler symbols a route refers to (route → References → handler). */
|
|
435
|
+
findHandlers(ref) {
|
|
436
|
+
const handlers = new Map();
|
|
437
|
+
for (const route of this.resolve(ref)) {
|
|
438
|
+
for (const edge of this.store.edgesFrom(route.id, "References")) {
|
|
439
|
+
const handler = this.store.getNode(edge.to);
|
|
440
|
+
if (handler && !handlers.has(handler.id))
|
|
441
|
+
handlers.set(handler.id, neighbor(handler, edge));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return rankNeighbors([...handlers.values()]);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Everything that points at a symbol via a References edge: the readers of a
|
|
448
|
+
* module-level Variable (ama-6k0), the routes that map to a handler (rme.1), and
|
|
449
|
+
* any other dispatch reference. The general "who refers to this" — answers the
|
|
450
|
+
* question `find_callers` can't, since reads aren't calls. (ama-pfm)
|
|
451
|
+
*/
|
|
452
|
+
findReferrers(ref) {
|
|
453
|
+
const referrers = new Map();
|
|
454
|
+
for (const target of this.resolve(ref)) {
|
|
455
|
+
for (const edge of this.store.edgesTo(target.id, "References")) {
|
|
456
|
+
const referrer = this.store.getNode(edge.from);
|
|
457
|
+
if (referrer && !referrers.has(referrer.id)) {
|
|
458
|
+
referrers.set(referrer.id, neighbor(referrer, edge));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return rankNeighbors([...referrers.values()]);
|
|
463
|
+
}
|
|
464
|
+
/** The routes that map to a handler — the route-domain framing of
|
|
465
|
+
* {@link findReferrers} (a route References its handler). */
|
|
466
|
+
findRoutes(ref) {
|
|
467
|
+
return this.findReferrers(ref);
|
|
468
|
+
}
|
|
469
|
+
/** The supertype methods a method overrides or implements (method → super). */
|
|
470
|
+
findOverrides(ref) {
|
|
471
|
+
const result = new Map();
|
|
472
|
+
for (const source of this.resolve(ref)) {
|
|
473
|
+
for (const edge of this.store.edgesFrom(source.id, "Overrides")) {
|
|
474
|
+
const target = this.store.getNode(edge.to);
|
|
475
|
+
if (target && !result.has(target.id))
|
|
476
|
+
result.set(target.id, neighbor(target, edge));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return rankNeighbors([...result.values()]);
|
|
480
|
+
}
|
|
481
|
+
/** The subtype methods that override a method (who overrides this — incoming). */
|
|
482
|
+
findOverriddenBy(ref) {
|
|
483
|
+
const result = new Map();
|
|
484
|
+
for (const target of this.resolve(ref)) {
|
|
485
|
+
for (const edge of this.store.edgesTo(target.id, "Overrides")) {
|
|
486
|
+
const source = this.store.getNode(edge.from);
|
|
487
|
+
if (source && !result.has(source.id))
|
|
488
|
+
result.set(source.id, neighbor(source, edge));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return rankNeighbors([...result.values()]);
|
|
492
|
+
}
|
|
493
|
+
/** Classes that implement the referenced interface. */
|
|
494
|
+
findImplementations(ref) {
|
|
495
|
+
const targets = this.resolve(ref);
|
|
496
|
+
const implementers = new Map();
|
|
497
|
+
for (const target of targets) {
|
|
498
|
+
for (const edge of this.store.edgesTo(target.id, "Implements")) {
|
|
499
|
+
const cls = this.store.getNode(edge.from);
|
|
500
|
+
if (cls)
|
|
501
|
+
implementers.set(cls.id, cls);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return rankNodes([...implementers.values()]);
|
|
505
|
+
}
|
|
506
|
+
/** Interfaces the referenced class implements. */
|
|
507
|
+
findInterfaces(ref) {
|
|
508
|
+
const sources = this.resolve(ref);
|
|
509
|
+
const interfaces = new Map();
|
|
510
|
+
for (const source of sources) {
|
|
511
|
+
for (const edge of this.store.edgesFrom(source.id, "Implements")) {
|
|
512
|
+
const iface = this.store.getNode(edge.to);
|
|
513
|
+
if (iface)
|
|
514
|
+
interfaces.set(iface.id, iface);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return rankNodes([...interfaces.values()]);
|
|
518
|
+
}
|
|
519
|
+
/** Edges representing an import of/by `id` — value (`Imports`) and type-only
|
|
520
|
+
* (`ImportsType`) alike — so importer/dependent/affected queries still count an
|
|
521
|
+
* `import type` (it's a compile-time dependency). (ama-bhf) */
|
|
522
|
+
importEdgesTo(id) {
|
|
523
|
+
return [...this.store.edgesTo(id, "Imports"), ...this.store.edgesTo(id, "ImportsType")];
|
|
524
|
+
}
|
|
525
|
+
importEdgesFrom(id) {
|
|
526
|
+
return [...this.store.edgesFrom(id, "Imports"), ...this.store.edgesFrom(id, "ImportsType")];
|
|
527
|
+
}
|
|
528
|
+
/** Files that import (or re-export) the referenced symbol. */
|
|
529
|
+
findImporters(ref) {
|
|
530
|
+
const targets = this.resolve(ref);
|
|
531
|
+
const importers = new Map();
|
|
532
|
+
for (const target of targets) {
|
|
533
|
+
for (const edge of this.importEdgesTo(target.id)) {
|
|
534
|
+
const file = this.store.getNode(edge.from);
|
|
535
|
+
if (file)
|
|
536
|
+
importers.set(file.id, file);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return rankNodes([...importers.values()]);
|
|
540
|
+
}
|
|
541
|
+
/** Symbols the referenced file imports (or re-exports). */
|
|
542
|
+
findImports(ref) {
|
|
543
|
+
const sources = this.resolve(ref);
|
|
544
|
+
const imports = new Map();
|
|
545
|
+
for (const source of sources) {
|
|
546
|
+
for (const edge of this.importEdgesFrom(source.id)) {
|
|
547
|
+
const imported = this.store.getNode(edge.to);
|
|
548
|
+
if (imported)
|
|
549
|
+
imports.set(imported.id, imported);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return rankNodes([...imports.values()]);
|
|
553
|
+
}
|
|
554
|
+
/** Symbols that use the referenced type in a parameter, return, or property. */
|
|
555
|
+
findTypeUsers(ref) {
|
|
556
|
+
const users = new Map();
|
|
557
|
+
for (const target of this.resolve(ref)) {
|
|
558
|
+
for (const kind of TYPE_EDGE_KINDS) {
|
|
559
|
+
for (const edge of this.store.edgesTo(target.id, kind)) {
|
|
560
|
+
const user = this.store.getNode(edge.from);
|
|
561
|
+
if (user)
|
|
562
|
+
users.set(user.id, user);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return rankNodes([...users.values()]);
|
|
567
|
+
}
|
|
568
|
+
/** Types the referenced symbol uses in a parameter, return, or property. */
|
|
569
|
+
findTypesUsed(ref) {
|
|
570
|
+
const types = new Map();
|
|
571
|
+
for (const source of this.resolve(ref)) {
|
|
572
|
+
for (const kind of TYPE_EDGE_KINDS) {
|
|
573
|
+
for (const edge of this.store.edgesFrom(source.id, kind)) {
|
|
574
|
+
const type = this.store.getNode(edge.to);
|
|
575
|
+
if (type)
|
|
576
|
+
types.set(type.id, type);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return rankNodes([...types.values()]);
|
|
581
|
+
}
|
|
582
|
+
/** The named types a symbol returns (function/method → its return type). */
|
|
583
|
+
findReturns(ref) {
|
|
584
|
+
const types = new Map();
|
|
585
|
+
for (const source of this.resolve(ref)) {
|
|
586
|
+
for (const edge of this.store.edgesFrom(source.id, "Returns")) {
|
|
587
|
+
const type = this.store.getNode(edge.to);
|
|
588
|
+
if (type)
|
|
589
|
+
types.set(type.id, type);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return rankNodes([...types.values()]);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Everything about one node in a single call: its definition plus full
|
|
596
|
+
* source, callers, callees, and dependents — a higher-order composition of
|
|
597
|
+
* the individual query methods so an agent gets the whole picture at once.
|
|
598
|
+
* Undefined when the ref resolves to nothing.
|
|
599
|
+
*/
|
|
600
|
+
node(ref) {
|
|
601
|
+
const matches = this.resolve(ref);
|
|
602
|
+
const primary = matches[0];
|
|
603
|
+
if (!primary)
|
|
604
|
+
return undefined;
|
|
605
|
+
// Resolve once, then describe *that* node by its id. Querying relationships with
|
|
606
|
+
// the raw `ref` re-resolves it, and an ambiguous ref (e.g. "analyze") aggregates
|
|
607
|
+
// callers/callees across every same-named symbol — so the view showed one node
|
|
608
|
+
// (the interface) with another's callees (the implementations'). (ama-d5o)
|
|
609
|
+
const id = primary.id;
|
|
610
|
+
return {
|
|
611
|
+
node: primary,
|
|
612
|
+
snippet: this.getCodeSnippet(id),
|
|
613
|
+
members: this.definedBy(id),
|
|
614
|
+
callers: this.findCallers(id).map((c) => c.symbol),
|
|
615
|
+
callees: this.findCallees(id).map((c) => c.symbol),
|
|
616
|
+
referrers: this.findReferrers(id).map((c) => c.symbol),
|
|
617
|
+
dependents: this.findImporters(id),
|
|
618
|
+
overrides: this.findOverrides(id).map((c) => c.symbol),
|
|
619
|
+
overriddenBy: this.findOverriddenBy(id).map((c) => c.symbol),
|
|
620
|
+
interfaces: this.findInterfaces(id),
|
|
621
|
+
implementations: this.findImplementations(id),
|
|
622
|
+
alternatives: matches.slice(1),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
/** The symbols a node directly defines — its `Defines` targets (a class's
|
|
626
|
+
* methods/properties, an interface's members, …) — in source order. (ama-as5) */
|
|
627
|
+
definedBy(id) {
|
|
628
|
+
const members = [];
|
|
629
|
+
for (const edge of this.store.edgesFrom(id, "Defines")) {
|
|
630
|
+
const member = this.store.getNode(edge.to);
|
|
631
|
+
if (member)
|
|
632
|
+
members.push(member);
|
|
633
|
+
}
|
|
634
|
+
return members.sort((a, b) => (a.range?.startLine ?? 0) - (b.range?.startLine ?? 0));
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* A file's skeleton: the symbols it defines (its outline, in source order) plus
|
|
638
|
+
* the files that depend on it — so an agent grasps a file's shape and reverse
|
|
639
|
+
* dependencies from one call instead of reading the whole file. `ref` is a File
|
|
640
|
+
* id (repo-relative path) or basename; non-file matches are ignored.
|
|
641
|
+
*/
|
|
642
|
+
fileSkeleton(ref) {
|
|
643
|
+
const file = this.resolve(ref).find((n) => n.kind === "File");
|
|
644
|
+
if (!file)
|
|
645
|
+
return undefined;
|
|
646
|
+
const symbols = [...this.store.allNodes()]
|
|
647
|
+
.filter((n) => n.file === file.file && n.id !== file.id)
|
|
648
|
+
.sort((a, b) => (a.range?.startLine ?? 0) - (b.range?.startLine ?? 0));
|
|
649
|
+
// Dependents = files importing *any* symbol the file defines, plus the file
|
|
650
|
+
// itself (import * / export *). Imports edges target the imported declaration,
|
|
651
|
+
// not the file node, so importers of named symbols would otherwise be missed.
|
|
652
|
+
const dependents = new Map();
|
|
653
|
+
for (const target of [file, ...symbols]) {
|
|
654
|
+
for (const edge of this.store.edgesTo(target.id, "Imports")) {
|
|
655
|
+
const importer = this.store.getNode(edge.from);
|
|
656
|
+
if (importer && importer.id !== file.id)
|
|
657
|
+
dependents.set(importer.id, importer);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const allDependents = [...dependents.values()];
|
|
661
|
+
// Imports = the files this file depends on (outgoing). The Imports edge runs from
|
|
662
|
+
// the file to the imported declaration; dedup to that declaration's File node. (ama-1jv)
|
|
663
|
+
const imports = new Map();
|
|
664
|
+
for (const edge of this.store.edgesFrom(file.id, "Imports")) {
|
|
665
|
+
const target = this.store.getNode(edge.to);
|
|
666
|
+
const targetFile = target && this.store.getNode(target.file);
|
|
667
|
+
if (targetFile && targetFile.id !== file.id)
|
|
668
|
+
imports.set(targetFile.id, targetFile);
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
file,
|
|
672
|
+
symbols,
|
|
673
|
+
imports: [...imports.values()],
|
|
674
|
+
dependents: allDependents.slice(0, SKELETON_DEPENDENTS_LIMIT),
|
|
675
|
+
dependentsTotal: allDependents.length,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* The transitive blast radius of a symbol: everything affected by changing it,
|
|
680
|
+
* found by walking the reverse dependency edges breadth-first (callers, type
|
|
681
|
+
* users, referrers, implementers, subclasses, …). `maxDepth` bounds the traversal
|
|
682
|
+
* (default unbounded); a visited set makes cycles and recursion safe. The seed
|
|
683
|
+
* symbol(s) the ref resolves to are excluded. Following every {@link
|
|
684
|
+
* IMPACT_EDGE_KINDS} edge — not just `Calls` — is what lets the blast radius of a
|
|
685
|
+
* type/interface/constant surface, since those are referenced, not called. (ama-8sw)
|
|
686
|
+
*/
|
|
687
|
+
impactAnalysis(ref, maxDepth = Number.POSITIVE_INFINITY) {
|
|
688
|
+
const seen = new Set(this.resolve(ref).map((n) => n.id));
|
|
689
|
+
const affected = new Map();
|
|
690
|
+
let frontier = [...seen];
|
|
691
|
+
for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
|
|
692
|
+
const next = [];
|
|
693
|
+
for (const id of frontier) {
|
|
694
|
+
for (const kind of IMPACT_EDGE_KINDS) {
|
|
695
|
+
for (const edge of this.store.edgesTo(id, kind)) {
|
|
696
|
+
if (seen.has(edge.from))
|
|
697
|
+
continue;
|
|
698
|
+
seen.add(edge.from);
|
|
699
|
+
const dependent = this.store.getNode(edge.from);
|
|
700
|
+
if (dependent) {
|
|
701
|
+
affected.set(dependent.id, dependent);
|
|
702
|
+
next.push(dependent.id);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
frontier = next;
|
|
708
|
+
}
|
|
709
|
+
return [...affected.values()];
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* A census of the graph: how many nodes of each kind and edges of each kind
|
|
713
|
+
* the index holds. Each edge is counted once, at its source node.
|
|
714
|
+
*/
|
|
715
|
+
getGraphSchema() {
|
|
716
|
+
const nodes = {};
|
|
717
|
+
const edges = {};
|
|
718
|
+
const edgeProvenance = { resolved: 0, heuristic: 0, dispatch: 0 };
|
|
719
|
+
for (const node of this.store.allNodes()) {
|
|
720
|
+
nodes[node.kind] = (nodes[node.kind] ?? 0) + 1;
|
|
721
|
+
for (const edge of this.store.edgesFrom(node.id)) {
|
|
722
|
+
edges[edge.kind] = (edges[edge.kind] ?? 0) + 1;
|
|
723
|
+
edgeProvenance[edge.provenance ?? "resolved"]++;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return { nodes, edges, edgeProvenance };
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* The files affected by changing the given files: the transitive set of files
|
|
730
|
+
* that import from them — directly (a module import) or by importing a symbol
|
|
731
|
+
* they define — walked breadth-first. The input files are excluded. Answers
|
|
732
|
+
* "which files (and tests) should I recheck?". Non-file refs and unknowns
|
|
733
|
+
* contribute nothing.
|
|
734
|
+
*/
|
|
735
|
+
affected(refs, opts = {}) {
|
|
736
|
+
const seeds = new Set();
|
|
737
|
+
for (const ref of refs) {
|
|
738
|
+
for (const node of this.resolve(ref)) {
|
|
739
|
+
if (node.kind === "File")
|
|
740
|
+
seeds.add(node.id);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const seen = new Set(seeds);
|
|
744
|
+
const result = new Map();
|
|
745
|
+
let frontier = [...seeds];
|
|
746
|
+
while (frontier.length > 0) {
|
|
747
|
+
const next = [];
|
|
748
|
+
for (const fileId of frontier) {
|
|
749
|
+
for (const importer of this.fileImporters(fileId)) {
|
|
750
|
+
if (seen.has(importer.id))
|
|
751
|
+
continue;
|
|
752
|
+
seen.add(importer.id);
|
|
753
|
+
result.set(importer.id, importer);
|
|
754
|
+
next.push(importer.id);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
frontier = next;
|
|
758
|
+
}
|
|
759
|
+
const all = [...result.values()];
|
|
760
|
+
// Test-impact mode: keep only the affected test files — "which tests to run
|
|
761
|
+
// for this change". (ama-5gs.9)
|
|
762
|
+
return opts.testsOnly ? all.filter((n) => isTestFile(n.file)) : all;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Files that import from `fileId`: importers of the module itself (a star
|
|
766
|
+
* re-export or namespace import targets the File node) plus importers of each
|
|
767
|
+
* symbol the file defines.
|
|
768
|
+
*/
|
|
769
|
+
fileImporters(fileId) {
|
|
770
|
+
const importers = new Map();
|
|
771
|
+
const collect = (targetId) => {
|
|
772
|
+
for (const edge of this.importEdgesTo(targetId)) {
|
|
773
|
+
const file = this.store.getNode(edge.from);
|
|
774
|
+
if (file)
|
|
775
|
+
importers.set(file.id, file);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
collect(fileId);
|
|
779
|
+
for (const edge of this.store.edgesFrom(fileId, "Defines")) {
|
|
780
|
+
collect(edge.to);
|
|
781
|
+
}
|
|
782
|
+
return [...importers.values()];
|
|
783
|
+
}
|
|
784
|
+
/** The files this file imports: each Imports edge's target resolved to the file
|
|
785
|
+
* it lives in (a File node's id is its path, so that file is the target). */
|
|
786
|
+
fileImports(fileId) {
|
|
787
|
+
const deps = new Set();
|
|
788
|
+
for (const edge of this.store.edgesFrom(fileId, "Imports")) {
|
|
789
|
+
const target = this.store.getNode(edge.to);
|
|
790
|
+
if (target && target.file !== fileId)
|
|
791
|
+
deps.add(target.file);
|
|
792
|
+
}
|
|
793
|
+
return [...deps];
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* File-level import cycles, each a strongly-connected component of two or more
|
|
797
|
+
* files that (transitively) import each other — the high-signal answer for
|
|
798
|
+
* untangling a module graph. Tarjan's SCC over the file import graph; a
|
|
799
|
+
* single-node component (no self-import) is not a cycle and is omitted. (ama-m8k.7)
|
|
800
|
+
*/
|
|
801
|
+
circularImports() {
|
|
802
|
+
const files = [...this.store.allNodes()].filter((n) => n.kind === "File");
|
|
803
|
+
const adjacency = new Map(files.map((f) => [f.id, this.fileImports(f.id)]));
|
|
804
|
+
let counter = 0;
|
|
805
|
+
const index = new Map();
|
|
806
|
+
const low = new Map();
|
|
807
|
+
const onStack = new Set();
|
|
808
|
+
const stack = [];
|
|
809
|
+
const components = [];
|
|
810
|
+
const connect = (v) => {
|
|
811
|
+
const vIndex = counter++;
|
|
812
|
+
index.set(v, vIndex);
|
|
813
|
+
low.set(v, vIndex);
|
|
814
|
+
stack.push(v);
|
|
815
|
+
onStack.add(v);
|
|
816
|
+
for (const w of adjacency.get(v) ?? []) {
|
|
817
|
+
const wIndex = index.get(w);
|
|
818
|
+
if (wIndex === undefined) {
|
|
819
|
+
connect(w);
|
|
820
|
+
low.set(v, Math.min(low.get(v) ?? vIndex, low.get(w) ?? vIndex));
|
|
821
|
+
}
|
|
822
|
+
else if (onStack.has(w)) {
|
|
823
|
+
low.set(v, Math.min(low.get(v) ?? vIndex, wIndex));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (low.get(v) === index.get(v)) {
|
|
827
|
+
const component = [];
|
|
828
|
+
let w;
|
|
829
|
+
do {
|
|
830
|
+
w = stack.pop();
|
|
831
|
+
if (w === undefined)
|
|
832
|
+
break;
|
|
833
|
+
onStack.delete(w);
|
|
834
|
+
component.push(w);
|
|
835
|
+
} while (w !== v);
|
|
836
|
+
if (component.length > 1)
|
|
837
|
+
components.push(component);
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
for (const f of files)
|
|
841
|
+
if (!index.has(f.id))
|
|
842
|
+
connect(f.id);
|
|
843
|
+
const byId = new Map(files.map((f) => [f.id, f]));
|
|
844
|
+
return components
|
|
845
|
+
.map((component) => component
|
|
846
|
+
.flatMap((id) => {
|
|
847
|
+
const node = byId.get(id);
|
|
848
|
+
return node ? [node] : [];
|
|
849
|
+
})
|
|
850
|
+
.sort((a, b) => a.id.localeCompare(b.id)))
|
|
851
|
+
.sort((x, y) => (x[0]?.id ?? "").localeCompare(y[0]?.id ?? ""));
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* A one-call overview answering "what's going on around X?": symbols whose
|
|
855
|
+
* name matches `question`, grouped by file, each with its callers and callees,
|
|
856
|
+
* plus the combined transitive blast radius. Composes searchSymbol,
|
|
857
|
+
* findCallers/findCallees, and impactAnalysis — no new graph logic.
|
|
858
|
+
*/
|
|
859
|
+
explore(question, opts = {}) {
|
|
860
|
+
const limit = opts.limit ?? EXPLORE_MATCH_LIMIT;
|
|
861
|
+
// Rank generously to count how many matched, but only deep-dive the top
|
|
862
|
+
// `limit` — relationships and the blast radius are O(matches) and explode on
|
|
863
|
+
// broad terms (e.g. "node" matched hundreds, 134k chars of output). (ama-m8k.4)
|
|
864
|
+
// Tokenize the question and union per-term hits so a verbose NL question
|
|
865
|
+
// ("how are baseline import edges resolved") matches, not just a bare symbol
|
|
866
|
+
// name. An empty token set (all stopwords, or a filters-only query) falls back
|
|
867
|
+
// to the raw search so `path:`/`kind:` filters still work. (ama-30q)
|
|
868
|
+
const terms = exploreTerms(question);
|
|
869
|
+
const ranked = terms.length > 0
|
|
870
|
+
? this.searchByTerms(terms, EXPLORE_SCAN_LIMIT)
|
|
871
|
+
: this.searchSymbol(question, { limit: EXPLORE_SCAN_LIMIT });
|
|
872
|
+
const matches = ranked.slice(0, limit);
|
|
873
|
+
const byFile = {};
|
|
874
|
+
for (const match of matches) {
|
|
875
|
+
const group = byFile[match.file] ?? [];
|
|
876
|
+
group.push(match);
|
|
877
|
+
byFile[match.file] = group;
|
|
878
|
+
}
|
|
879
|
+
const relationships = matches.map((match) => ({
|
|
880
|
+
symbol: match.qualifiedName || match.name,
|
|
881
|
+
callers: this.findCallers(match.id).map((c) => c.symbol),
|
|
882
|
+
callees: this.findCallees(match.id).map((c) => c.symbol),
|
|
883
|
+
}));
|
|
884
|
+
const blast = new Map();
|
|
885
|
+
for (const match of matches) {
|
|
886
|
+
for (const affected of this.impactAnalysis(match.id))
|
|
887
|
+
blast.set(affected.id, affected);
|
|
888
|
+
}
|
|
889
|
+
const blastRanked = rankNodes([...blast.values()]);
|
|
890
|
+
return {
|
|
891
|
+
question,
|
|
892
|
+
byFile,
|
|
893
|
+
relationships,
|
|
894
|
+
blastRadius: blastRanked.slice(0, EXPLORE_BLAST_LIMIT),
|
|
895
|
+
totalMatches: ranked.length,
|
|
896
|
+
totalBlastRadius: blastRanked.length,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
/** Search each term and union the hits, ranked by how many distinct terms a
|
|
900
|
+
* symbol matches (a symbol hitting several is more on-topic), then by its best
|
|
901
|
+
* per-term relevance position. Each per-term `searchSymbol` keeps its own
|
|
902
|
+
* relevance order, so a single-term question behaves exactly as before. (ama-30q) */
|
|
903
|
+
searchByTerms(terms, limit) {
|
|
904
|
+
const byId = new Map();
|
|
905
|
+
for (const term of terms) {
|
|
906
|
+
this.searchSymbol(term, { limit }).forEach((node, i) => {
|
|
907
|
+
const entry = byId.get(node.id);
|
|
908
|
+
if (entry) {
|
|
909
|
+
entry.hits++;
|
|
910
|
+
entry.best = Math.min(entry.best, i);
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
byId.set(node.id, { node, hits: 1, best: i });
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
return [...byId.values()]
|
|
918
|
+
.sort((a, b) => b.hits - a.hits || a.best - b.best)
|
|
919
|
+
.map((entry) => entry.node);
|
|
920
|
+
}
|
|
921
|
+
/** Verbatim source for a symbol, or undefined if it has no known location. */
|
|
922
|
+
getCodeSnippet(ref) {
|
|
923
|
+
const node = this.resolve(ref).find((n) => n.range);
|
|
924
|
+
if (!node || !node.range)
|
|
925
|
+
return undefined;
|
|
926
|
+
let source;
|
|
927
|
+
try {
|
|
928
|
+
source = fs.readFileSync(path.resolve(this.root, node.file), "utf8");
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
return undefined; // the file vanished since indexing — no snippet, but no throw
|
|
932
|
+
}
|
|
933
|
+
const lines = source.split("\n");
|
|
934
|
+
// Carry the symbol's leading doc comment, usually its most useful part. (ama-43e)
|
|
935
|
+
const startLine = commentAwareStart(lines, node.range.startLine);
|
|
936
|
+
const text = lines.slice(startLine - 1, node.range.endLine).join("\n");
|
|
937
|
+
return {
|
|
938
|
+
id: node.id,
|
|
939
|
+
file: node.file,
|
|
940
|
+
startLine,
|
|
941
|
+
endLine: node.range.endLine,
|
|
942
|
+
text,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Resolve a reference to node(s), most-specific first: exact id, then simple
|
|
947
|
+
* name (e.g. "compute"), then dotted qualified name (e.g. "Service.compute").
|
|
948
|
+
*/
|
|
949
|
+
resolve(ref) {
|
|
950
|
+
const byId = this.store.getNode(ref);
|
|
951
|
+
if (byId)
|
|
952
|
+
return [byId];
|
|
953
|
+
const byName = this.store.nodesByName(ref);
|
|
954
|
+
if (byName.length)
|
|
955
|
+
return byName;
|
|
956
|
+
return [...this.store.allNodes()].filter((n) => n.qualifiedName === ref);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
//# sourceMappingURL=service.js.map
|