homegraph 1.2.0 → 1.4.1
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/dist/bin/homegraph.js +3 -0
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +85 -0
- package/dist/context/index.js.map +1 -1
- package/dist/db/index.d.ts +64 -5
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +178 -14
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +19 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +117 -2
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +299 -6
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +32 -1
- package/dist/db/wal-valve.d.ts +106 -0
- package/dist/db/wal-valve.d.ts.map +1 -0
- package/dist/db/wal-valve.js +208 -0
- package/dist/db/wal-valve.js.map +1 -0
- package/dist/directory.d.ts +9 -5
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +210 -19
- package/dist/directory.js.map +1 -1
- package/dist/extraction/cfml-extractor.d.ts +107 -0
- package/dist/extraction/cfml-extractor.d.ts.map +1 -0
- package/dist/extraction/cfml-extractor.js +494 -0
- package/dist/extraction/cfml-extractor.js.map +1 -0
- package/dist/extraction/grammars.d.ts +25 -1
- package/dist/extraction/grammars.d.ts.map +1 -1
- package/dist/extraction/grammars.js +177 -24
- package/dist/extraction/grammars.js.map +1 -1
- package/dist/extraction/index.d.ts +43 -4
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +308 -5
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/c-cpp.d.ts +42 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +369 -4
- package/dist/extraction/languages/c-cpp.js.map +1 -1
- package/dist/extraction/languages/cfquery.d.ts +12 -0
- package/dist/extraction/languages/cfquery.d.ts.map +1 -0
- package/dist/extraction/languages/cfquery.js +28 -0
- package/dist/extraction/languages/cfquery.js.map +1 -0
- package/dist/extraction/languages/cfscript.d.ts +3 -0
- package/dist/extraction/languages/cfscript.d.ts.map +1 -0
- package/dist/extraction/languages/cfscript.js +73 -0
- package/dist/extraction/languages/cfscript.js.map +1 -0
- package/dist/extraction/languages/cobol.d.ts +33 -0
- package/dist/extraction/languages/cobol.d.ts.map +1 -0
- package/dist/extraction/languages/cobol.js +499 -0
- package/dist/extraction/languages/cobol.js.map +1 -0
- package/dist/extraction/languages/erlang.d.ts +3 -0
- package/dist/extraction/languages/erlang.d.ts.map +1 -0
- package/dist/extraction/languages/erlang.js +350 -0
- package/dist/extraction/languages/erlang.js.map +1 -0
- package/dist/extraction/languages/index.d.ts.map +1 -1
- package/dist/extraction/languages/index.js +16 -0
- package/dist/extraction/languages/index.js.map +1 -1
- package/dist/extraction/languages/nix.d.ts +3 -0
- package/dist/extraction/languages/nix.d.ts.map +1 -0
- package/dist/extraction/languages/nix.js +294 -0
- package/dist/extraction/languages/nix.js.map +1 -0
- package/dist/extraction/languages/solidity.d.ts +3 -0
- package/dist/extraction/languages/solidity.d.ts.map +1 -0
- package/dist/extraction/languages/solidity.js +293 -0
- package/dist/extraction/languages/solidity.js.map +1 -0
- package/dist/extraction/languages/terraform.d.ts +3 -0
- package/dist/extraction/languages/terraform.d.ts.map +1 -0
- package/dist/extraction/languages/terraform.js +641 -0
- package/dist/extraction/languages/terraform.js.map +1 -0
- package/dist/extraction/languages/vbnet.d.ts +11 -0
- package/dist/extraction/languages/vbnet.d.ts.map +1 -0
- package/dist/extraction/languages/vbnet.js +141 -0
- package/dist/extraction/languages/vbnet.js.map +1 -0
- package/dist/extraction/mybatis-extractor.d.ts +30 -10
- package/dist/extraction/mybatis-extractor.d.ts.map +1 -1
- package/dist/extraction/mybatis-extractor.js +140 -38
- package/dist/extraction/mybatis-extractor.js.map +1 -1
- package/dist/extraction/parse-pool.d.ts +27 -1
- package/dist/extraction/parse-pool.d.ts.map +1 -1
- package/dist/extraction/parse-pool.js +75 -7
- package/dist/extraction/parse-pool.js.map +1 -1
- package/dist/extraction/parse-worker.js +9 -2
- package/dist/extraction/parse-worker.js.map +1 -1
- package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/dist/extraction/tree-sitter-helpers.js +1 -0
- package/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/dist/extraction/tree-sitter-types.d.ts +3 -1
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +38 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +754 -10
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/dist/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +1 -1
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +60 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +348 -5
- package/dist/index.js.map +1 -1
- package/dist/installer/index.d.ts +42 -0
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +97 -2
- package/dist/installer/index.js.map +1 -1
- package/dist/mcp/daemon.d.ts +25 -3
- package/dist/mcp/daemon.d.ts.map +1 -1
- package/dist/mcp/daemon.js +62 -8
- package/dist/mcp/daemon.js.map +1 -1
- package/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
- package/dist/mcp/dynamic-boundaries.js +2 -1
- package/dist/mcp/dynamic-boundaries.js.map +1 -1
- package/dist/mcp/early-ppid.d.ts +26 -0
- package/dist/mcp/early-ppid.d.ts.map +1 -0
- package/dist/mcp/early-ppid.js +29 -0
- package/dist/mcp/early-ppid.js.map +1 -0
- package/dist/mcp/engine.d.ts +4 -5
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +27 -1
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +41 -6
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/liveness-watchdog.d.ts +18 -1
- package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
- package/dist/mcp/liveness-watchdog.js +73 -13
- package/dist/mcp/liveness-watchdog.js.map +1 -1
- package/dist/mcp/memory-budget.d.ts +30 -0
- package/dist/mcp/memory-budget.d.ts.map +1 -0
- package/dist/mcp/memory-budget.js +123 -0
- package/dist/mcp/memory-budget.js.map +1 -0
- package/dist/mcp/proxy.d.ts.map +1 -1
- package/dist/mcp/proxy.js +88 -4
- package/dist/mcp/proxy.js.map +1 -1
- package/dist/mcp/query-cache.d.ts +35 -4
- package/dist/mcp/query-cache.d.ts.map +1 -1
- package/dist/mcp/query-cache.js +89 -19
- package/dist/mcp/query-cache.js.map +1 -1
- package/dist/mcp/query-pool.d.ts +46 -12
- package/dist/mcp/query-pool.d.ts.map +1 -1
- package/dist/mcp/query-pool.js +100 -23
- package/dist/mcp/query-pool.js.map +1 -1
- package/dist/mcp/server-instructions.d.ts +4 -27
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +39 -84
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/session.d.ts +14 -0
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +28 -1
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/startup-handshake.d.ts +44 -0
- package/dist/mcp/startup-handshake.d.ts.map +1 -0
- package/dist/mcp/startup-handshake.js +73 -0
- package/dist/mcp/startup-handshake.js.map +1 -0
- package/dist/mcp/tools.d.ts +104 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +2390 -191
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/transport.d.ts.map +1 -1
- package/dist/mcp/transport.js +18 -0
- package/dist/mcp/transport.js.map +1 -1
- package/dist/project-config.d.ts +38 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +101 -2
- package/dist/project-config.js.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.js +178 -146
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts +9 -1
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +1164 -176
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- package/dist/resolution/frameworks/cics.d.ts +20 -0
- package/dist/resolution/frameworks/cics.d.ts.map +1 -0
- package/dist/resolution/frameworks/cics.js +90 -0
- package/dist/resolution/frameworks/cics.js.map +1 -0
- package/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/dist/resolution/frameworks/index.js +3 -0
- package/dist/resolution/frameworks/index.js.map +1 -1
- package/dist/resolution/frameworks/java.d.ts.map +1 -1
- package/dist/resolution/frameworks/java.js +14 -6
- package/dist/resolution/frameworks/java.js.map +1 -1
- package/dist/resolution/frameworks/terraform.d.ts +38 -0
- package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
- package/dist/resolution/frameworks/terraform.js +277 -0
- package/dist/resolution/frameworks/terraform.js.map +1 -0
- package/dist/resolution/goframe-synthesizer.d.ts +2 -1
- package/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
- package/dist/resolution/goframe-synthesizer.js +8 -3
- package/dist/resolution/goframe-synthesizer.js.map +1 -1
- package/dist/resolution/import-resolver.d.ts +7 -0
- package/dist/resolution/import-resolver.d.ts.map +1 -1
- package/dist/resolution/import-resolver.js +129 -4
- package/dist/resolution/import-resolver.js.map +1 -1
- package/dist/resolution/index.d.ts +60 -7
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +430 -80
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/name-matcher.d.ts +0 -3
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +354 -33
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/resolution/strip-comments.d.ts +1 -1
- package/dist/resolution/strip-comments.d.ts.map +1 -1
- package/dist/resolution/strip-comments.js +49 -0
- package/dist/resolution/strip-comments.js.map +1 -1
- package/dist/resolution/types.d.ts +29 -0
- package/dist/resolution/types.d.ts.map +1 -1
- package/dist/resolution/workspace-packages.d.ts +10 -0
- package/dist/resolution/workspace-packages.d.ts.map +1 -1
- package/dist/resolution/workspace-packages.js +142 -4
- package/dist/resolution/workspace-packages.js.map +1 -1
- package/dist/search/identifier-segments.d.ts +60 -0
- package/dist/search/identifier-segments.d.ts.map +1 -0
- package/dist/search/identifier-segments.js +176 -0
- package/dist/search/identifier-segments.js.map +1 -0
- package/dist/search/query-utils.d.ts +201 -0
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +919 -2
- package/dist/search/query-utils.js.map +1 -1
- package/dist/sync/git-hooks.d.ts.map +1 -1
- package/dist/sync/git-hooks.js +2 -0
- package/dist/sync/git-hooks.js.map +1 -1
- package/dist/sync/watcher.d.ts +10 -5
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +51 -14
- package/dist/sync/watcher.js.map +1 -1
- package/dist/sync/worktree.d.ts.map +1 -1
- package/dist/sync/worktree.js +5 -0
- package/dist/sync/worktree.js.map +1 -1
- package/dist/types.d.ts +19 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/dist/upgrade/index.d.ts +32 -0
- package/dist/upgrade/index.d.ts.map +1 -1
- package/dist/upgrade/index.js +162 -14
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/remove-binary.d.ts +87 -0
- package/dist/upgrade/remove-binary.d.ts.map +1 -0
- package/dist/upgrade/remove-binary.js +289 -0
- package/dist/upgrade/remove-binary.js.map +1 -0
- package/dist/upgrade/update-check.d.ts +92 -0
- package/dist/upgrade/update-check.d.ts.map +1 -0
- package/dist/upgrade/update-check.js +258 -0
- package/dist/upgrade/update-check.js.map +1 -0
- package/package.json +2 -2
- package/scripts/agent-eval/run-all.sh +6 -0
- package/scripts/build-bundle.sh +5 -0
- package/scripts/npm-shim.js +8 -1
- package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/data/agents.json +0 -109
- package/scripts/exp_boundary_eval/data/experiments.json +0 -140
- package/scripts/qa_eval/README.md +0 -407
- package/scripts/qa_eval/_test_deveco_probe.py +0 -41
- package/scripts/qa_eval/agent_runner.py +0 -526
- package/scripts/qa_eval/data/.gitignore +0 -4
- package/scripts/qa_eval/data/test-set.jsonl +0 -2
- package/scripts/qa_eval/eval_metrics.py +0 -274
- package/scripts/qa_eval/external_agent.py +0 -976
- package/scripts/qa_eval/llm_config.py +0 -92
- package/scripts/qa_eval/memory_monitor.py +0 -132
- package/scripts/qa_eval/my_answer_accuracy.py +0 -187
- package/scripts/qa_eval/requirements.txt +0 -2
- package/scripts/qa_eval/run_pipeline.py +0 -804
- package/scripts/qa_eval/stats_efficiency.py +0 -279
- package/scripts/qa_eval/stats_scores.py +0 -207
package/dist/mcp/tools.js
CHANGED
|
@@ -6,12 +6,16 @@
|
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.ToolHandler = exports.tools = exports.PathRefusalError = exports.NotIndexedError = void 0;
|
|
9
|
+
exports.normalizeQuerySpelling = normalizeQuerySpelling;
|
|
9
10
|
exports.getExploreBudget = getExploreBudget;
|
|
10
11
|
exports.getExploreOutputBudget = getExploreOutputBudget;
|
|
12
|
+
exports.tightenExploreBudgetForQuery = tightenExploreBudgetForQuery;
|
|
11
13
|
exports.formatStaleBanner = formatStaleBanner;
|
|
12
14
|
exports.formatStaleFooter = formatStaleFooter;
|
|
13
15
|
exports.formatDegradedBanner = formatDegradedBanner;
|
|
14
16
|
exports.getStaticTools = getStaticTools;
|
|
17
|
+
const query_pool_1 = require("./query-pool");
|
|
18
|
+
const memory_budget_1 = require("./memory-budget");
|
|
15
19
|
const directory_1 = require("../directory");
|
|
16
20
|
// Lazy-load the heavy HomeGraph chain off the MCP startup path — see the same
|
|
17
21
|
// helper in engine.ts. ToolHandler must load to answer tools/list (static
|
|
@@ -96,6 +100,25 @@ function lastQualifierPart(symbol) {
|
|
|
96
100
|
const parts = symbol.split(/::|[./]/).filter((p) => p.length > 0);
|
|
97
101
|
return parts[parts.length - 1] ?? symbol;
|
|
98
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Normalize Erlang-native symbol spellings in an explore query into the shapes
|
|
105
|
+
* the rest of the pipeline already understands. Agents working Erlang code
|
|
106
|
+
* name symbols the way the language spells them — `mod:fn/3`, `init/2` — and
|
|
107
|
+
* those tokens previously died in both consumers: the flow-builder's token
|
|
108
|
+
* filter rejects `:` and `/arity` outright, and the search-side field parser
|
|
109
|
+
* eats `mod:fn` as an unknown `field:value`.
|
|
110
|
+
*
|
|
111
|
+
* - `fn/3` → `fn` (arity tail after an identifier)
|
|
112
|
+
* - `mod:fn` → `mod.fn` (exactly one colon between identifiers)
|
|
113
|
+
*
|
|
114
|
+
* Safe cross-language: Lua's `t:m` spelling maps to the same `t.m` its
|
|
115
|
+
* qualified names use.
|
|
116
|
+
*/
|
|
117
|
+
function normalizeQuerySpelling(query) {
|
|
118
|
+
return query
|
|
119
|
+
.replace(/\b([A-Za-z_][\w@]*)\/(\d{1,3})(?=$|[\s,()[\]/])/g, '$1')
|
|
120
|
+
.replace(/(^|[\s,()[\]])(?!(?:kind|lang|language|path|name):)([a-z_][\w@]*):([A-Za-z_][\w@]*)(?=$|[\s,()[\]])/g, '$1$2.$3');
|
|
121
|
+
}
|
|
99
122
|
/**
|
|
100
123
|
* Calculate the recommended number of homegraph_explore calls based on project size.
|
|
101
124
|
* Larger codebases need more exploration calls to cover their surface area,
|
|
@@ -211,6 +234,46 @@ function getExploreOutputBudget(fileCount) {
|
|
|
211
234
|
excludeLowValueFiles: false,
|
|
212
235
|
};
|
|
213
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Shrink explore ceilings for local-detail / no-flow named-symbol questions.
|
|
239
|
+
* Large repos otherwise dump ~24K related source that the agent then still
|
|
240
|
+
* greps/reads — the main token regression vs without-homegraph.
|
|
241
|
+
*/
|
|
242
|
+
function tightenExploreBudgetForQuery(budget, query, opts) {
|
|
243
|
+
const hasFlow = opts?.hasFlowPath === true;
|
|
244
|
+
if (hasFlow && ((0, query_utils_1.queryAsMechanismSurvey)(query) || (0, query_utils_1.queryAsCrossModuleFlowSurvey)(query))) {
|
|
245
|
+
return budget;
|
|
246
|
+
}
|
|
247
|
+
const local = (0, query_utils_1.queryAsLocalSymbolDetail)(query);
|
|
248
|
+
const compact = (0, query_utils_1.shouldUseCompactExploreBudget)(query);
|
|
249
|
+
if (!local && !compact && hasFlow)
|
|
250
|
+
return budget;
|
|
251
|
+
if (!local && !compact && hasFlow === false) {
|
|
252
|
+
// Generic no-flow explore: still trim meta + file count a bit.
|
|
253
|
+
return {
|
|
254
|
+
...budget,
|
|
255
|
+
maxOutputChars: Math.min(budget.maxOutputChars, 14000),
|
|
256
|
+
defaultMaxFiles: Math.min(budget.defaultMaxFiles, 3),
|
|
257
|
+
maxCharsPerFile: Math.min(budget.maxCharsPerFile, 5000),
|
|
258
|
+
includeRelationships: false,
|
|
259
|
+
includeAdditionalFiles: false,
|
|
260
|
+
includeCompletenessSignal: false,
|
|
261
|
+
includeBudgetNote: false,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (!local && !compact)
|
|
265
|
+
return budget;
|
|
266
|
+
return {
|
|
267
|
+
...budget,
|
|
268
|
+
maxOutputChars: Math.min(budget.maxOutputChars, local ? 9000 : 12000),
|
|
269
|
+
defaultMaxFiles: Math.min(budget.defaultMaxFiles, local ? 2 : 3),
|
|
270
|
+
maxCharsPerFile: Math.min(budget.maxCharsPerFile, local ? 4000 : 5000),
|
|
271
|
+
includeRelationships: false,
|
|
272
|
+
includeAdditionalFiles: false,
|
|
273
|
+
includeCompletenessSignal: false,
|
|
274
|
+
includeBudgetNote: false,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
214
277
|
/**
|
|
215
278
|
* Whether `homegraph_explore` should prefix source lines with their line
|
|
216
279
|
* numbers (cat -n style: `<num>\t<code>`).
|
|
@@ -329,18 +392,26 @@ function fileSectionHeader(filePath, suffix) {
|
|
|
329
392
|
* The agent uses this to fall back to Read for those specific files
|
|
330
393
|
* without waiting for the debounced sync (issue #403).
|
|
331
394
|
*/
|
|
332
|
-
function formatStaleBanner(stale) {
|
|
395
|
+
function formatStaleBanner(stale, opts) {
|
|
333
396
|
const now = Date.now();
|
|
334
397
|
const lines = stale.map((p) => {
|
|
335
398
|
const ageMs = Math.max(0, now - p.lastSeenMs);
|
|
336
399
|
const label = p.indexing ? 'indexing in progress' : 'pending sync';
|
|
337
400
|
return ` - ${p.path} (edited ${ageMs}ms ago, ${label})`;
|
|
338
401
|
});
|
|
402
|
+
// On large indexes catch-up is skipped for RSS — "Read them" teaches the agent
|
|
403
|
+
// to abandon homegraph after every save and burn tokens on whole-file Reads.
|
|
404
|
+
const guidance = opts?.catchUpDeferred
|
|
405
|
+
? 'Callers / defs / explore locations above are still usable for structural answers. ' +
|
|
406
|
+
'Read a listed file only if you need byte-exact content that may have changed mid-session — ' +
|
|
407
|
+
'do not re-read the whole file just because of this notice.'
|
|
408
|
+
: 'For accurate content of those specific files, Read them directly. ' +
|
|
409
|
+
'The rest of this response is fresh.';
|
|
339
410
|
return ('⚠️ Some files referenced below were edited since the last index sync — ' +
|
|
340
411
|
'their homegraph entries may be stale:\n' +
|
|
341
412
|
lines.join('\n') +
|
|
342
|
-
'\
|
|
343
|
-
|
|
413
|
+
'\n' +
|
|
414
|
+
guidance);
|
|
344
415
|
}
|
|
345
416
|
/**
|
|
346
417
|
* Compact footer listing pending files that are NOT referenced in this
|
|
@@ -409,7 +480,7 @@ const READ_ONLY_ANNOTATIONS = {
|
|
|
409
480
|
exports.tools = [
|
|
410
481
|
{
|
|
411
482
|
name: 'homegraph_search',
|
|
412
|
-
description: '
|
|
483
|
+
description: 'LAST RESORT spelling lookup — locations only, no source. Prefer homegraph_explore whenever the question already names a symbol/file/@kit. If you do call search with a bare name, HomeGraph may answer with a compact explore result instead of locations.',
|
|
413
484
|
inputSchema: {
|
|
414
485
|
type: 'object',
|
|
415
486
|
properties: {
|
|
@@ -435,7 +506,7 @@ exports.tools = [
|
|
|
435
506
|
},
|
|
436
507
|
{
|
|
437
508
|
name: 'homegraph_callers',
|
|
438
|
-
description: '
|
|
509
|
+
description: 'Compact caller list for a NAMED in-repo symbol (no bodies). Use after you know the exact name. For full flows use homegraph_explore. DO NOT call for SDK catalogs, one-function semantics, or "what if X fails" hypothetics — Read that function instead. Prefer one explore over parallel callers+callees+node.',
|
|
439
510
|
inputSchema: {
|
|
440
511
|
type: 'object',
|
|
441
512
|
properties: {
|
|
@@ -460,7 +531,7 @@ exports.tools = [
|
|
|
460
531
|
},
|
|
461
532
|
{
|
|
462
533
|
name: 'homegraph_callees',
|
|
463
|
-
description: '
|
|
534
|
+
description: 'Compact callee list for a NAMED in-repo symbol (no bodies). For full flows use homegraph_explore. DO NOT use for out-of-repo SDK internals or counterfactual analysis. Prefer one explore over parallel node+callers+callees.',
|
|
464
535
|
inputSchema: {
|
|
465
536
|
type: 'object',
|
|
466
537
|
properties: {
|
|
@@ -485,7 +556,7 @@ exports.tools = [
|
|
|
485
556
|
},
|
|
486
557
|
{
|
|
487
558
|
name: 'homegraph_impact',
|
|
488
|
-
description: '
|
|
559
|
+
description: 'Blast radius for a NAMED in-repo symbol before a refactor. Not for SDK docs, permission judgments, or hypothetical failure effects — those need Read/Grep, not impact.',
|
|
489
560
|
inputSchema: {
|
|
490
561
|
type: 'object',
|
|
491
562
|
properties: {
|
|
@@ -510,7 +581,7 @@ exports.tools = [
|
|
|
510
581
|
},
|
|
511
582
|
{
|
|
512
583
|
name: 'homegraph_node',
|
|
513
|
-
description: '
|
|
584
|
+
description: 'Depth on ONE known in-repo symbol or indexed file — not a survey tool. (1) FILE: pass `file` alone → bounded line-numbered source + dependents. (2) SYMBOL: body (includeCode) + short trail; overloads return every body. USE after explore named the symbol and you still need one body. DO NOT call repeatedly to crawl a feature (prefer one explore). DO NOT call for: @kit/SDK catalogs, broad "how does X work", or after explore already returned that symbol\'s source. Treat returned source as already Read — do not grep/read the same path.',
|
|
514
585
|
inputSchema: {
|
|
515
586
|
type: 'object',
|
|
516
587
|
properties: {
|
|
@@ -552,13 +623,13 @@ exports.tools = [
|
|
|
552
623
|
},
|
|
553
624
|
{
|
|
554
625
|
name: 'homegraph_explore',
|
|
555
|
-
description: 'PRIMARY
|
|
626
|
+
description: 'PRIMARY tool for THIS REPO\'s symbol graph (call paths + often line-numbered source). CALL when you need in-repo structure: how a named feature/component is wired, A→B path, callers/callees, Type.member → who uses it, click/handler flow, in-repo @kit import usages — put concrete symbol/file/@kit names in query; skip search. If the question does not need that graph, do not call. One explore; answer from returned Source + trail; treat as already Read; do not re-grep/search the same names. Busy/partial → retry same explore once.',
|
|
556
627
|
inputSchema: {
|
|
557
628
|
type: 'object',
|
|
558
629
|
properties: {
|
|
559
630
|
query: {
|
|
560
631
|
type: 'string',
|
|
561
|
-
description: '
|
|
632
|
+
description: 'In-repo symbols, file basenames, or @kit names for USAGE questions (import/call sites), not for asking the kit\'s official full API list. For flows, name both endpoints. Prefer concrete names from the question.',
|
|
562
633
|
},
|
|
563
634
|
maxFiles: {
|
|
564
635
|
type: 'number',
|
|
@@ -584,7 +655,7 @@ exports.tools = [
|
|
|
584
655
|
},
|
|
585
656
|
{
|
|
586
657
|
name: 'homegraph_files',
|
|
587
|
-
description: 'Indexed
|
|
658
|
+
description: 'Indexed directory tree (paths and symbol counts only — NO source code). Do NOT use to answer where/what/how code questions; use homegraph_explore. Only for coarse folder layout when explore cannot help.',
|
|
588
659
|
inputSchema: {
|
|
589
660
|
type: 'object',
|
|
590
661
|
properties: {
|
|
@@ -1064,6 +1135,7 @@ class ToolHandler {
|
|
|
1064
1135
|
freshen(cg) {
|
|
1065
1136
|
try {
|
|
1066
1137
|
if (cg.reopenIfReplaced()) {
|
|
1138
|
+
(0, query_cache_1.getMcpQueryCacheIndex)(cg.getProjectRoot()).reset();
|
|
1067
1139
|
process.stderr.write('[HomeGraph MCP] The index was replaced on disk (e.g. a git worktree ' +
|
|
1068
1140
|
'recreated at the same path); reopened the live database in place.\n');
|
|
1069
1141
|
}
|
|
@@ -1270,7 +1342,18 @@ class ToolHandler {
|
|
|
1270
1342
|
}
|
|
1271
1343
|
let banner = '';
|
|
1272
1344
|
if (inResponse.length > 0) {
|
|
1273
|
-
|
|
1345
|
+
let dbPath = null;
|
|
1346
|
+
try {
|
|
1347
|
+
// Large indexes skip catch-up — soft banner so agents don't abandon HG for Read.
|
|
1348
|
+
const root = cg.getProjectRoot();
|
|
1349
|
+
dbPath = (0, path_1.resolve)(root, '.homegraph', 'homegraph.db');
|
|
1350
|
+
}
|
|
1351
|
+
catch {
|
|
1352
|
+
dbPath = null;
|
|
1353
|
+
}
|
|
1354
|
+
banner = formatStaleBanner(inResponse, {
|
|
1355
|
+
catchUpDeferred: (0, memory_budget_1.shouldSkipCatchUpSync)(dbPath),
|
|
1356
|
+
});
|
|
1274
1357
|
}
|
|
1275
1358
|
let footer = '';
|
|
1276
1359
|
if (elsewhere.length > 0) {
|
|
@@ -1299,6 +1382,11 @@ class ToolHandler {
|
|
|
1299
1382
|
this.catchUpGate = null;
|
|
1300
1383
|
await this.awaitCatchUpGate(gate);
|
|
1301
1384
|
}
|
|
1385
|
+
// Hard process RSS ceiling — any tool/path that already pushed us over must
|
|
1386
|
+
// stop with success-shaped Partial (never OOM / multi-GB growth).
|
|
1387
|
+
if ((0, memory_budget_1.isOverRssBudget)()) {
|
|
1388
|
+
return (0, memory_budget_1.rssBudgetPartialResult)(toolName);
|
|
1389
|
+
}
|
|
1302
1390
|
// Honor the optional tool allowlist (HOMEGRAPH_MCP_TOOLS): a trimmed
|
|
1303
1391
|
// surface rejects ablated tools defensively even if a client cached them.
|
|
1304
1392
|
if (!this.isToolAllowed(toolName)) {
|
|
@@ -1328,11 +1416,13 @@ class ToolHandler {
|
|
|
1328
1416
|
const cacheEnabled = (0, query_cache_1.isMcpQueryCacheEnabled)() && (0, query_cache_1.isCacheableMcpTool)(toolName);
|
|
1329
1417
|
let cacheKey;
|
|
1330
1418
|
let cacheQueries;
|
|
1419
|
+
let cacheIndex;
|
|
1331
1420
|
if (cacheEnabled) {
|
|
1332
1421
|
try {
|
|
1333
1422
|
const cacheCg = this.getHomeGraph(projectPath);
|
|
1334
1423
|
cacheQueries = cacheCg.getQueryBuilder();
|
|
1335
|
-
(0, query_cache_1.
|
|
1424
|
+
cacheIndex = (0, query_cache_1.getMcpQueryCacheIndex)(cacheCg.getProjectRoot());
|
|
1425
|
+
cacheIndex.ensureValid(cacheQueries, () => cacheCg.getLastIndexedAt());
|
|
1336
1426
|
let fileCount;
|
|
1337
1427
|
try {
|
|
1338
1428
|
fileCount = cacheCg.getStats().fileCount;
|
|
@@ -1341,7 +1431,7 @@ class ToolHandler {
|
|
|
1341
1431
|
fileCount = undefined;
|
|
1342
1432
|
}
|
|
1343
1433
|
cacheKey = (0, query_cache_1.buildMcpQueryCacheKey)(toolName, args, fileCount);
|
|
1344
|
-
const cached =
|
|
1434
|
+
const cached = cacheIndex.getEntry(cacheQueries, cacheKey);
|
|
1345
1435
|
if (cached) {
|
|
1346
1436
|
const withWorktree = this.withWorktreeNotice(cached, projectPath);
|
|
1347
1437
|
return this.withStalenessNotice(withWorktree, projectPath);
|
|
@@ -1359,20 +1449,43 @@ class ToolHandler {
|
|
|
1359
1449
|
if (toolName === 'homegraph_status') {
|
|
1360
1450
|
return await this.handleStatus(args);
|
|
1361
1451
|
}
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1452
|
+
// Every read tool races a deadline ≪ MCP client ~60s hard timeout and
|
|
1453
|
+
// Named-member / local-compact questions finish in tens of ms on the warm
|
|
1454
|
+
// main connection. Serving them here — before the query-pool offload —
|
|
1455
|
+
// avoids cold-worker / wedged-daemon paths that otherwise surface as empty
|
|
1456
|
+
// MCP client `-32001` (the handler itself is fine; the transport times out).
|
|
1457
|
+
if (toolName === 'homegraph_explore' || toolName === 'homegraph_search') {
|
|
1458
|
+
const q = typeof args.query === 'string' ? args.query : '';
|
|
1459
|
+
if (q) {
|
|
1460
|
+
try {
|
|
1461
|
+
const cgFast = this.getHomeGraph(projectPath);
|
|
1462
|
+
const rootFast = cgFast.getProjectRoot();
|
|
1463
|
+
const fast = (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
|
|
1464
|
+
? this.tryFastInventoryExplore(cgFast, q, rootFast)
|
|
1465
|
+
: null)
|
|
1466
|
+
?? this.tryCompactLocalSymbolExplore(cgFast, q, rootFast)
|
|
1467
|
+
?? (toolName === 'homegraph_explore'
|
|
1468
|
+
? this.tryLightMechanismExplore(cgFast, q, rootFast)
|
|
1469
|
+
: null);
|
|
1470
|
+
if (fast) {
|
|
1471
|
+
if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !fast.isError) {
|
|
1472
|
+
cacheIndex.setEntry(cacheQueries, cacheKey, toolName, fast);
|
|
1473
|
+
}
|
|
1474
|
+
const withWorktree = this.withWorktreeNotice(fast, projectPath);
|
|
1475
|
+
return this.withStalenessNotice(withWorktree, projectPath);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
catch {
|
|
1479
|
+
// Not indexed / path issue — fall through to normal dispatch.
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
// prefers the query pool so sync SQLite/CPU cannot freeze the transport
|
|
1484
|
+
// (a frozen main loop prevents setTimeout deadlines from firing → empty
|
|
1485
|
+
// `-32001`). Fast-path surveys run inside the worker via executeReadTool.
|
|
1486
|
+
const result = await this.runReadToolWithDeadline(toolName, args);
|
|
1487
|
+
if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !result.isError) {
|
|
1488
|
+
cacheIndex.setEntry(cacheQueries, cacheKey, toolName, result);
|
|
1376
1489
|
}
|
|
1377
1490
|
const withWorktree = this.withWorktreeNotice(result, projectPath);
|
|
1378
1491
|
return this.withStalenessNotice(withWorktree, projectPath);
|
|
@@ -1393,6 +1506,64 @@ class ToolHandler {
|
|
|
1393
1506
|
'continue without homegraph for this task.');
|
|
1394
1507
|
}
|
|
1395
1508
|
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Dispatch a read tool with a hard deadline under the typical ~60s MCP client
|
|
1511
|
+
* timeout.
|
|
1512
|
+
*
|
|
1513
|
+
* - **Heavy** tools (explore/impact) → query pool (keeps transport free).
|
|
1514
|
+
* - **Light** tools (search/node/callers/…) → warm main connection (pool cold
|
|
1515
|
+
* open of a large WAL index routinely outruns the client; search used to
|
|
1516
|
+
* finish in <1s on the already-open main DB).
|
|
1517
|
+
*
|
|
1518
|
+
* Soft/deadline replies are **static busy text only** — never FTS / explore
|
|
1519
|
+
* on the main thread. A previous path called `searchNodes` from the timeout
|
|
1520
|
+
* callback with the full natural-language query, freezing the event loop so
|
|
1521
|
+
* the success-shaped reply never flushed → empty client `-32001`.
|
|
1522
|
+
*/
|
|
1523
|
+
async runReadToolWithDeadline(toolName, args) {
|
|
1524
|
+
const deadlineMs = (0, query_pool_1.resolveToolDeadlineMs)();
|
|
1525
|
+
const light = toolName === 'homegraph_search'
|
|
1526
|
+
|| toolName === 'homegraph_node'
|
|
1527
|
+
|| toolName === 'homegraph_callers'
|
|
1528
|
+
|| toolName === 'homegraph_callees'
|
|
1529
|
+
|| toolName === 'homegraph_files';
|
|
1530
|
+
const work = () => {
|
|
1531
|
+
if (!light && this.queryPool && this.queryPool.healthy) {
|
|
1532
|
+
return this.queryPool.run(toolName, args, {
|
|
1533
|
+
// Static Partial only — never DB/FTS on the soft-timeout callback
|
|
1534
|
+
// (any sync work here can freeze the MCP transport → empty -32001).
|
|
1535
|
+
onSoftTimeout: () => this.deadlineBusyResult(deadlineMs),
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
return (async () => {
|
|
1539
|
+
await new Promise((r) => setImmediate(r));
|
|
1540
|
+
return this.executeReadTool(toolName, args);
|
|
1541
|
+
})();
|
|
1542
|
+
};
|
|
1543
|
+
let timer;
|
|
1544
|
+
const deadline = new Promise((resolve) => {
|
|
1545
|
+
// Keep this timer ref'd so a blocked sync stretch still flushes Partial
|
|
1546
|
+
// once the event loop runs again — do not call wrapPartialBusyResult here.
|
|
1547
|
+
timer = setTimeout(() => {
|
|
1548
|
+
resolve(this.deadlineBusyResult(deadlineMs));
|
|
1549
|
+
}, deadlineMs);
|
|
1550
|
+
});
|
|
1551
|
+
try {
|
|
1552
|
+
return await Promise.race([work(), deadline]);
|
|
1553
|
+
}
|
|
1554
|
+
finally {
|
|
1555
|
+
if (timer)
|
|
1556
|
+
clearTimeout(timer);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
/** Success-shaped busy note — no DB, never blocks the transport. */
|
|
1560
|
+
deadlineBusyResult(deadlineMs) {
|
|
1561
|
+
const secs = Math.max(1, Math.round(deadlineMs / 1000));
|
|
1562
|
+
return this.textResult(`⚠️ **Partial result** — HomeGraph hit its ${secs}s response deadline / is busy ` +
|
|
1563
|
+
`(MCP clients typically kill the call at ~60s with an empty timeout). ` +
|
|
1564
|
+
`This is NOT an error. Retry ONE \`homegraph_explore\` with concrete symbol/file names from the question — ` +
|
|
1565
|
+
`do not fire search+explore or node+callers+callees in parallel, and do not grep/read symbols you already named.`);
|
|
1566
|
+
}
|
|
1396
1567
|
/**
|
|
1397
1568
|
* Run a single read tool to completion and return its raw {@link ToolResult},
|
|
1398
1569
|
* classifying expected failures the same way {@link execute}'s catch does so
|
|
@@ -1408,6 +1579,12 @@ class ToolHandler {
|
|
|
1408
1579
|
*/
|
|
1409
1580
|
async executeReadTool(toolName, args) {
|
|
1410
1581
|
try {
|
|
1582
|
+
// Compact inventory / one-symbol surveys — safe on the worker (keeps the
|
|
1583
|
+
// daemon main loop free). Never run these unprotected on the MCP transport
|
|
1584
|
+
// thread: they can block long enough for the client to emit empty `-32001`.
|
|
1585
|
+
const fastPath = this.tryFastPathResult(toolName, args);
|
|
1586
|
+
if (fastPath)
|
|
1587
|
+
return fastPath;
|
|
1411
1588
|
return await this.dispatchTool(toolName, args);
|
|
1412
1589
|
}
|
|
1413
1590
|
catch (err) {
|
|
@@ -1443,14 +1620,31 @@ class ToolHandler {
|
|
|
1443
1620
|
default: return this.errorResult(`Unknown tool: ${toolName}`);
|
|
1444
1621
|
}
|
|
1445
1622
|
}
|
|
1446
|
-
/**
|
|
1447
|
-
* Handle homegraph_search
|
|
1448
|
-
*/
|
|
1449
1623
|
async handleSearch(args) {
|
|
1450
1624
|
const query = this.validateString(args.query, 'query');
|
|
1451
1625
|
if (typeof query !== 'string')
|
|
1452
1626
|
return query;
|
|
1453
1627
|
const cg = this.getHomeGraph(args.projectPath);
|
|
1628
|
+
// Explore redirect is best-effort — incomplete/faked graphs (or missing
|
|
1629
|
+
// getProjectRoot) must fall through to FTS search rather than error.
|
|
1630
|
+
try {
|
|
1631
|
+
const projectRoot = cg.getProjectRoot();
|
|
1632
|
+
if ((0, query_utils_1.queryShouldPreferExploreOverSearch)(query)) {
|
|
1633
|
+
const exploreRedirect = this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
1634
|
+
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot)
|
|
1635
|
+
?? this.tryLightMechanismExplore(cg, query, projectRoot);
|
|
1636
|
+
if (exploreRedirect)
|
|
1637
|
+
return exploreRedirect;
|
|
1638
|
+
}
|
|
1639
|
+
const exploreRedirect = this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
1640
|
+
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot)
|
|
1641
|
+
?? this.tryLightMechanismExplore(cg, query, projectRoot);
|
|
1642
|
+
if (exploreRedirect)
|
|
1643
|
+
return exploreRedirect;
|
|
1644
|
+
}
|
|
1645
|
+
catch {
|
|
1646
|
+
// Fall through to FTS search.
|
|
1647
|
+
}
|
|
1454
1648
|
const rawKind = args.kind;
|
|
1455
1649
|
// The schema enum says 'type' (what agents naturally reach for); the
|
|
1456
1650
|
// NodeKind is 'type_alias'. Without the mapping, kind: "type" silently
|
|
@@ -1474,7 +1668,9 @@ class ToolHandler {
|
|
|
1474
1668
|
return aGen - bGen;
|
|
1475
1669
|
});
|
|
1476
1670
|
const formatted = this.formatSearchResults(ranked);
|
|
1477
|
-
|
|
1671
|
+
const steer = '\n\n> Locations only. For source / callers / how it works, call `homegraph_explore` ' +
|
|
1672
|
+
'with these symbol names next — do not re-search or grep the same names.';
|
|
1673
|
+
return this.textResult(this.truncateOutput(formatted + steer));
|
|
1478
1674
|
}
|
|
1479
1675
|
/**
|
|
1480
1676
|
* Group symbol matches into DISTINCT DEFINITIONS — one group per
|
|
@@ -1536,6 +1732,8 @@ class ToolHandler {
|
|
|
1536
1732
|
const callers = [];
|
|
1537
1733
|
const labels = new Map();
|
|
1538
1734
|
for (const node of defNodes) {
|
|
1735
|
+
if ((0, memory_budget_1.isOverRssBudget)())
|
|
1736
|
+
break;
|
|
1539
1737
|
for (const c of cg.getCallers(node.id)) {
|
|
1540
1738
|
if (!seen.has(c.node.id)) {
|
|
1541
1739
|
seen.add(c.node.id);
|
|
@@ -1544,6 +1742,8 @@ class ToolHandler {
|
|
|
1544
1742
|
if (label)
|
|
1545
1743
|
labels.set(c.node.id, label);
|
|
1546
1744
|
}
|
|
1745
|
+
if (callers.length >= limit)
|
|
1746
|
+
return { callers, labels };
|
|
1547
1747
|
}
|
|
1548
1748
|
}
|
|
1549
1749
|
return { callers, labels };
|
|
@@ -1557,7 +1757,9 @@ class ToolHandler {
|
|
|
1557
1757
|
// A successful `file` narrowing makes the multi-symbol aggregation note
|
|
1558
1758
|
// stale — suppress it.
|
|
1559
1759
|
const note = fileFilter && !filteredOut ? '' : allMatches.note;
|
|
1560
|
-
const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels)
|
|
1760
|
+
const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels)
|
|
1761
|
+
+ '\n\n> Caller listing complete — answer from this list; no read/grep needed.'
|
|
1762
|
+
+ note + filterNote;
|
|
1561
1763
|
return this.textResult(this.truncateOutput(formatted));
|
|
1562
1764
|
}
|
|
1563
1765
|
// Multiple DISTINCT definitions (#764): one section per definition so an
|
|
@@ -1579,7 +1781,7 @@ class ToolHandler {
|
|
|
1579
1781
|
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
|
|
1580
1782
|
}
|
|
1581
1783
|
}
|
|
1582
|
-
return this.textResult(this.truncateOutput(lines.join('\n') + filterNote));
|
|
1784
|
+
return this.textResult(this.truncateOutput(lines.join('\n') + filterNote + '\n\n> Caller listing complete — answer from this list; no read/grep needed.'));
|
|
1583
1785
|
}
|
|
1584
1786
|
/**
|
|
1585
1787
|
* Handle homegraph_callees
|
|
@@ -1603,6 +1805,12 @@ class ToolHandler {
|
|
|
1603
1805
|
const seen = new Set();
|
|
1604
1806
|
const callees = [];
|
|
1605
1807
|
const labels = new Map();
|
|
1808
|
+
const defPaths = defNodes.map((n) => n.filePath);
|
|
1809
|
+
const affinity = (fp) => defPaths.some((dp) => {
|
|
1810
|
+
const a = dp.replace(/\\/g, '/').split('/');
|
|
1811
|
+
const b = fp.replace(/\\/g, '/').split('/');
|
|
1812
|
+
return a.length >= 2 && b.length >= 2 && a[0] === b[0] && a[1] === b[1];
|
|
1813
|
+
});
|
|
1606
1814
|
for (const node of defNodes) {
|
|
1607
1815
|
for (const c of cg.getCallees(node.id)) {
|
|
1608
1816
|
if (!seen.has(c.node.id)) {
|
|
@@ -1614,7 +1822,10 @@ class ToolHandler {
|
|
|
1614
1822
|
}
|
|
1615
1823
|
}
|
|
1616
1824
|
}
|
|
1617
|
-
|
|
1825
|
+
// Prefer same-package callees when present — cross-package logError/logInfo
|
|
1826
|
+
// homonyms poison upstream/downstream answers.
|
|
1827
|
+
const near = callees.filter((n) => affinity(n.filePath));
|
|
1828
|
+
return { callees: near.length > 0 ? near : callees, labels };
|
|
1618
1829
|
};
|
|
1619
1830
|
if (groups.length === 1) {
|
|
1620
1831
|
const { callees, labels } = collect(groups[0]);
|
|
@@ -2275,130 +2486,1704 @@ class ToolHandler {
|
|
|
2275
2486
|
supers.set(s.id, e);
|
|
2276
2487
|
}
|
|
2277
2488
|
}
|
|
2278
|
-
// Pick the supertype with the most TRUE implementers (graph-wide), among
|
|
2279
|
-
// those genuinely shared by the token's definers.
|
|
2280
|
-
let best = null;
|
|
2281
|
-
for (const { node, count, targets } of supers.values()) {
|
|
2282
|
-
if (count < MIN_SUPPORT)
|
|
2489
|
+
// Pick the supertype with the most TRUE implementers (graph-wide), among
|
|
2490
|
+
// those genuinely shared by the token's definers.
|
|
2491
|
+
let best = null;
|
|
2492
|
+
for (const { node, count, targets } of supers.values()) {
|
|
2493
|
+
if (count < MIN_SUPPORT)
|
|
2494
|
+
continue;
|
|
2495
|
+
let impl = 0;
|
|
2496
|
+
try {
|
|
2497
|
+
impl = cg.getIncomingEdges(node.id).filter((e) => e.kind === 'implements' || e.kind === 'extends').length;
|
|
2498
|
+
}
|
|
2499
|
+
catch { /* leave 0 — gated out below */ }
|
|
2500
|
+
if (impl < MIN_IMPL)
|
|
2501
|
+
continue;
|
|
2502
|
+
if (!best || impl > best.impl)
|
|
2503
|
+
best = { node, impl, targets };
|
|
2504
|
+
}
|
|
2505
|
+
if (!best || seenSuper.has(best.node.id))
|
|
2506
|
+
continue;
|
|
2507
|
+
seenSuper.add(best.node.id);
|
|
2508
|
+
const namedNames = new Set([...named.values()].map((n) => n.name));
|
|
2509
|
+
const eg = best.targets.slice(0, 4).map((m) => {
|
|
2510
|
+
const cont = containerOf(m);
|
|
2511
|
+
const disp = cont ? `${cont.name}.${m.name}` : (m.qualifiedName || m.name);
|
|
2512
|
+
const mark = cont && namedNames.has(cont.name) ? ' ← you named this' : '';
|
|
2513
|
+
return `\`${disp}\` (${rel(m.filePath)}:${m.startLine})${mark}`;
|
|
2514
|
+
});
|
|
2515
|
+
const more = best.impl > eg.length ? ` +${best.impl - eg.length} more` : '';
|
|
2516
|
+
notes.push(`- \`${token}\` → runtime dispatch to **${best.impl}** types implementing \`${best.node.name}\` — the static path ends here, the target is chosen at runtime. e.g. ${eg.join(', ')}${more}`);
|
|
2517
|
+
}
|
|
2518
|
+
if (notes.length === 0)
|
|
2519
|
+
return '';
|
|
2520
|
+
return [
|
|
2521
|
+
'**Interface dispatch (a named method has many implementations)**',
|
|
2522
|
+
'',
|
|
2523
|
+
...notes,
|
|
2524
|
+
'',
|
|
2525
|
+
'> The method above is dispatched at runtime to one of the listed implementations (a registry / plugin / strategy interface) — there is no single static caller→callee edge; the implementations ARE the continuations. To follow one, run homegraph_explore on a listed target.',
|
|
2526
|
+
'',
|
|
2527
|
+
].join('\n');
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* Shortlist candidate runtime targets for a dispatch key surfaced by
|
|
2531
|
+
* {@link buildDynamicBoundaries}. Exact conventional names first (`save` →
|
|
2532
|
+
* `onSave`/`handleSave`; `CreateCmd` → `CreateCmdHandler`), then FTS, with a
|
|
2533
|
+
* normalized-containment post-filter (FTS camel-splitting is fuzzier than a
|
|
2534
|
+
* candidate list should be). Symbols the agent already named sort first and
|
|
2535
|
+
* are marked — that's the "you were right, here's the wiring" case.
|
|
2536
|
+
*/
|
|
2537
|
+
boundaryCandidates(cg, key, keyIsType, named, selfId) {
|
|
2538
|
+
const CALLABLE = new Set(['method', 'function', 'component', 'constructor', 'class']);
|
|
2539
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
2540
|
+
const keyNorm = norm(key);
|
|
2541
|
+
if (keyNorm.length < 3)
|
|
2542
|
+
return '';
|
|
2543
|
+
const cands = new Map();
|
|
2544
|
+
const consider = (n) => {
|
|
2545
|
+
if (!n || n.id === selfId || !CALLABLE.has(n.kind) || cands.has(n.id))
|
|
2546
|
+
return;
|
|
2547
|
+
const nameNorm = norm(n.name || '');
|
|
2548
|
+
if (nameNorm.length < 3)
|
|
2549
|
+
return;
|
|
2550
|
+
if (!nameNorm.includes(keyNorm) && !keyNorm.includes(nameNorm))
|
|
2551
|
+
return;
|
|
2552
|
+
cands.set(n.id, n);
|
|
2553
|
+
};
|
|
2554
|
+
const cap = key.charAt(0).toUpperCase() + key.slice(1);
|
|
2555
|
+
const probes = keyIsType
|
|
2556
|
+
? [`${key}Handler`, key]
|
|
2557
|
+
: [key, `on${cap}`, `handle${cap}`, `${key}Handler`, `handle_${key}`];
|
|
2558
|
+
for (const p of probes) {
|
|
2559
|
+
try {
|
|
2560
|
+
for (const n of cg.getNodesByName(p))
|
|
2561
|
+
consider(n);
|
|
2562
|
+
}
|
|
2563
|
+
catch { /* exact probe miss is fine */ }
|
|
2564
|
+
}
|
|
2565
|
+
let raw = 0;
|
|
2566
|
+
try {
|
|
2567
|
+
const results = cg.searchNodes(key, { limit: 12 });
|
|
2568
|
+
raw = results.length;
|
|
2569
|
+
for (const r of results)
|
|
2570
|
+
consider(r.node);
|
|
2571
|
+
}
|
|
2572
|
+
catch { /* FTS syntax edge — exact probes already ran */ }
|
|
2573
|
+
if (cands.size === 0) {
|
|
2574
|
+
return raw >= 12 && key.length < 5 ? `key \`${key}\` is too generic to shortlist (${raw}+ matches)` : '';
|
|
2575
|
+
}
|
|
2576
|
+
// A constructor candidate duplicates its class: extractors emit ctors as
|
|
2577
|
+
// METHOD nodes named like the class (C#/Java `Foo::Foo`) — keep the class.
|
|
2578
|
+
const all = [...cands.values()];
|
|
2579
|
+
const classKey = new Set(all.filter((n) => n.kind === 'class').map((n) => `${n.name}|${n.filePath}`));
|
|
2580
|
+
const namedNames = new Set([...named.values()].map((n) => n.name));
|
|
2581
|
+
const isNamed = (n) => named.has(n.id) || namedNames.has(n.name); // the flow's named set holds callables only — transfer the mark to the class
|
|
2582
|
+
const list = all
|
|
2583
|
+
.filter((n) => !(n.kind !== 'class' && classKey.has(`${n.name}|${n.filePath}`)))
|
|
2584
|
+
.sort((a, b) => (isNamed(b) ? 1 : 0) - (isNamed(a) ? 1 : 0))
|
|
2585
|
+
.slice(0, 4)
|
|
2586
|
+
.map((n) => {
|
|
2587
|
+
// Typed-bus convention: the runtime target is the candidate class's
|
|
2588
|
+
// Handle/Execute/Consume method — name the exact node, not just the class.
|
|
2589
|
+
let display = n.qualifiedName || n.name;
|
|
2590
|
+
let at = `${n.filePath}:${n.startLine}`;
|
|
2591
|
+
if (keyIsType && n.kind === 'class') {
|
|
2592
|
+
try {
|
|
2593
|
+
const HANDLER_METHODS = /^(handle|handleAsync|execute|executeAsync|consume|consumeAsync|run|__invoke)$/i;
|
|
2594
|
+
const method = cg.getOutgoingEdges(n.id)
|
|
2595
|
+
.filter((e) => e.kind === 'contains')
|
|
2596
|
+
.map((e) => { try {
|
|
2597
|
+
return cg.getNode(e.target);
|
|
2598
|
+
}
|
|
2599
|
+
catch {
|
|
2600
|
+
return null;
|
|
2601
|
+
} })
|
|
2602
|
+
.find((c) => !!c && c.kind === 'method' && HANDLER_METHODS.test(c.name));
|
|
2603
|
+
if (method) {
|
|
2604
|
+
display = `${n.name}.${method.name}`;
|
|
2605
|
+
at = `${method.filePath}:${method.startLine}`;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
catch { /* class without resolvable members — show the class itself */ }
|
|
2609
|
+
}
|
|
2610
|
+
return `\`${display}\` (${at})${isNamed(n) ? ' ← you named this' : ''}`;
|
|
2611
|
+
});
|
|
2612
|
+
return `candidates for key \`${key}\`: ${list.join(', ')}`;
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Import sites for @kit.* / *Kit module names — surfaces full `import { … } from '@kit.X'`
|
|
2616
|
+
* lines. When the query also names a symbol (taskpool), only matching imports are listed.
|
|
2617
|
+
*/
|
|
2618
|
+
buildImportSitesSection(cg, query, projectRoot) {
|
|
2619
|
+
const kitTerms = (0, query_utils_1.extractKitModuleNamesFromQuery)(query);
|
|
2620
|
+
const depSymbols = (0, query_utils_1.extractDependencySymbolsFromQuery)(query);
|
|
2621
|
+
const kitSearchTerms = (0, query_utils_1.extractImportSearchTerms)(query);
|
|
2622
|
+
const seen = new Set();
|
|
2623
|
+
const sites = [];
|
|
2624
|
+
const tryAdd = (node, lineText) => {
|
|
2625
|
+
const lineLc = lineText.toLowerCase();
|
|
2626
|
+
if (kitTerms.length > 0) {
|
|
2627
|
+
const matchesKit = kitTerms.some((k) => lineLc.includes(`@kit.${k.toLowerCase()}`));
|
|
2628
|
+
if (!matchesKit)
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
if (depSymbols.length > 0) {
|
|
2632
|
+
const matchesSym = depSymbols.some((s) => lineLc.includes(s.toLowerCase()));
|
|
2633
|
+
if (!matchesSym)
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
if (depSymbols.includes('taskpool')) {
|
|
2637
|
+
if (!lineLc.includes('taskpool') || !lineLc.includes('@kit.arkts'))
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
const key = `${node.filePath}:${node.startLine}`;
|
|
2641
|
+
if (seen.has(key))
|
|
2642
|
+
return;
|
|
2643
|
+
seen.add(key);
|
|
2644
|
+
sites.push({ file: node.filePath, line: node.startLine, lineText });
|
|
2645
|
+
};
|
|
2646
|
+
const resolveImportLine = (node) => (0, query_utils_1.resolveImportLineFromNode)(node, projectRoot);
|
|
2647
|
+
const importLimit = depSymbols.includes('taskpool') ? 200
|
|
2648
|
+
: depSymbols.length > 0 ? 60 : 20;
|
|
2649
|
+
// Symbol-first search: "taskpool" hits `import { taskpool } from '@kit.ArkTS'`.
|
|
2650
|
+
for (const sym of depSymbols) {
|
|
2651
|
+
let hits = [];
|
|
2652
|
+
try {
|
|
2653
|
+
hits = cg.searchNodes(sym, { kinds: ['import'], limit: importLimit });
|
|
2654
|
+
}
|
|
2655
|
+
catch {
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
for (const r of hits) {
|
|
2659
|
+
tryAdd(r.node, resolveImportLine(r.node));
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
// Kit-module search (when no symbol filter, or to catch re-exports).
|
|
2663
|
+
if (sites.length === 0 || depSymbols.length === 0) {
|
|
2664
|
+
for (const term of kitSearchTerms) {
|
|
2665
|
+
const termLc = term.toLowerCase().replace(/^@kit\./, '');
|
|
2666
|
+
let hits = [];
|
|
2667
|
+
try {
|
|
2668
|
+
hits = cg.searchNodes(term, { kinds: ['import'], limit: importLimit });
|
|
2669
|
+
}
|
|
2670
|
+
catch {
|
|
2671
|
+
continue;
|
|
2672
|
+
}
|
|
2673
|
+
for (const r of hits) {
|
|
2674
|
+
const lineText = resolveImportLine(r.node);
|
|
2675
|
+
if (!lineText.toLowerCase().includes(termLc))
|
|
2676
|
+
continue;
|
|
2677
|
+
if (depSymbols.length > 0) {
|
|
2678
|
+
const matchesSym = depSymbols.some((s) => lineText.toLowerCase().includes(s.toLowerCase()));
|
|
2679
|
+
if (!matchesSym)
|
|
2680
|
+
continue;
|
|
2681
|
+
}
|
|
2682
|
+
tryAdd(r.node, lineText);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
if (sites.length === 0) {
|
|
2687
|
+
return { section: '', siteCount: 0, compactListing: false };
|
|
2688
|
+
}
|
|
2689
|
+
sites.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
2690
|
+
const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
|
|
2691
|
+
const taskpoolInventory = depSymbols.includes('taskpool');
|
|
2692
|
+
const compactListing = taskpoolInventory || (0, query_utils_1.shouldCompactImportListing)(sites.length, importInventoryFilter);
|
|
2693
|
+
const cap = compactListing ? (taskpoolInventory ? sites.length : 40) : 15;
|
|
2694
|
+
const lines = compactListing
|
|
2695
|
+
? ['**Dependency list**', '', `Files importing the queried symbol(s) (${sites.length} total):`, '']
|
|
2696
|
+
: ['**Import sites**', ''];
|
|
2697
|
+
for (const s of sites.slice(0, cap)) {
|
|
2698
|
+
if (compactListing) {
|
|
2699
|
+
lines.push(`- \`${s.file}\` (line ${s.line})`);
|
|
2700
|
+
}
|
|
2701
|
+
else {
|
|
2702
|
+
lines.push(`- \`${s.file}:${s.line}\` — \`${s.lineText}\``);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
if (sites.length > cap) {
|
|
2706
|
+
lines.push(`- … and ${sites.length - cap} more`);
|
|
2707
|
+
}
|
|
2708
|
+
if (compactListing) {
|
|
2709
|
+
lines.push('');
|
|
2710
|
+
if (taskpoolInventory) {
|
|
2711
|
+
lines.push(`> Complete list of **${sites.length}** \`import { taskpool } from '@kit.ArkTS'\` site(s) — answer from this section; no grep/read needed.`);
|
|
2712
|
+
}
|
|
2713
|
+
else {
|
|
2714
|
+
lines.push('> Listing complete — answer from this section; no grep/read needed for the dependency set.');
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
lines.push('');
|
|
2718
|
+
return { section: lines.join('\n'), siteCount: sites.length, compactListing };
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* External-caller inventory for a named class — methods → who calls them (paths only).
|
|
2722
|
+
*/
|
|
2723
|
+
buildCallerListingSection(cg, query) {
|
|
2724
|
+
const typeNames = (0, query_utils_1.extractCallerSurveySymbols)(query);
|
|
2725
|
+
if (typeNames.length === 0)
|
|
2726
|
+
return '';
|
|
2727
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
2728
|
+
// Only filter to other files when the user asked for "external" callers.
|
|
2729
|
+
// Applying this to every caller-survey query wiped same-file call sites
|
|
2730
|
+
// (common in C++/ARK .cpp units) and fell through to a fat full explore.
|
|
2731
|
+
const externalOnly = /\bexternal\b/i.test(query) || /外部/.test(query);
|
|
2732
|
+
const lines = ['**Caller inventory**', ''];
|
|
2733
|
+
let substantive = 0;
|
|
2734
|
+
// Function / method symbols named directly (e.g. SortWidgets).
|
|
2735
|
+
for (const sym of typeNames.slice(0, 4)) {
|
|
2736
|
+
const funcs = cg.getNodesByName(sym).filter((n) => (n.kind === 'function' || n.kind === 'method') && !(0, query_utils_1.isTestFile)(n.filePath));
|
|
2737
|
+
for (const fn of funcs.slice(0, 3)) {
|
|
2738
|
+
let callers = [];
|
|
2739
|
+
try {
|
|
2740
|
+
callers = cg.getCallers(fn.id);
|
|
2741
|
+
}
|
|
2742
|
+
catch {
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
const uniq = new Map();
|
|
2746
|
+
for (const c of callers) {
|
|
2747
|
+
if (!c?.node)
|
|
2748
|
+
continue;
|
|
2749
|
+
if (externalOnly && c.node.filePath === fn.filePath)
|
|
2750
|
+
continue;
|
|
2751
|
+
if ((0, query_utils_1.isTestFile)(c.node.filePath))
|
|
2752
|
+
continue;
|
|
2753
|
+
uniq.set(c.node.id, c.node);
|
|
2754
|
+
}
|
|
2755
|
+
if (uniq.size === 0)
|
|
2756
|
+
continue;
|
|
2757
|
+
const callerList = [...uniq.values()].slice(0, 12)
|
|
2758
|
+
.map((n) => `\`${rel(n.filePath)}:${n.startLine}\` (\`${n.name}\`)`)
|
|
2759
|
+
.join(', ');
|
|
2760
|
+
const more = uniq.size > 12 ? ` +${uniq.size - 12} more` : '';
|
|
2761
|
+
lines.push(`- \`${fn.name}\` (\`${rel(fn.filePath)}:${fn.startLine}\`) ← ${callerList}${more}`);
|
|
2762
|
+
substantive++;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
for (const typeName of typeNames.slice(0, 3)) {
|
|
2766
|
+
const classes = cg.getNodesByName(typeName)
|
|
2767
|
+
.filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'component') && !(0, query_utils_1.isTestFile)(n.filePath));
|
|
2768
|
+
for (const cls of classes.slice(0, 2)) {
|
|
2769
|
+
const methods = [];
|
|
2770
|
+
for (const e of cg.getOutgoingEdges(cls.id)) {
|
|
2771
|
+
if (e.kind !== 'contains')
|
|
2772
|
+
continue;
|
|
2773
|
+
const m = cg.getNode(e.target);
|
|
2774
|
+
if (m && (m.kind === 'method' || m.kind === 'function'))
|
|
2775
|
+
methods.push(m);
|
|
2776
|
+
}
|
|
2777
|
+
if (methods.length === 0) {
|
|
2778
|
+
for (const n of cg.getNodesByName(typeName)) {
|
|
2779
|
+
if (n.filePath === cls.filePath && (n.kind === 'method' || n.kind === 'function')) {
|
|
2780
|
+
methods.push(n);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
const methodLines = [];
|
|
2785
|
+
for (const method of methods.slice(0, 25)) {
|
|
2786
|
+
let callers = [];
|
|
2787
|
+
try {
|
|
2788
|
+
callers = cg.getCallers(method.id);
|
|
2789
|
+
}
|
|
2790
|
+
catch {
|
|
2791
|
+
continue;
|
|
2792
|
+
}
|
|
2793
|
+
const uniq = new Map();
|
|
2794
|
+
for (const c of callers) {
|
|
2795
|
+
if (!c?.node)
|
|
2796
|
+
continue;
|
|
2797
|
+
if (externalOnly && c.node.filePath === cls.filePath)
|
|
2798
|
+
continue;
|
|
2799
|
+
if ((0, query_utils_1.isTestFile)(c.node.filePath))
|
|
2800
|
+
continue;
|
|
2801
|
+
uniq.set(c.node.id, c.node);
|
|
2802
|
+
}
|
|
2803
|
+
if (uniq.size === 0)
|
|
2804
|
+
continue;
|
|
2805
|
+
const callerList = [...uniq.values()].slice(0, 8)
|
|
2806
|
+
.map((n) => `\`${rel(n.filePath)}:${n.startLine}\` (\`${n.name}\`)`)
|
|
2807
|
+
.join(', ');
|
|
2808
|
+
const more = uniq.size > 8 ? ` +${uniq.size - 8} more` : '';
|
|
2809
|
+
methodLines.push(`- \`${method.name}\` ← ${callerList}${more}`);
|
|
2810
|
+
substantive++;
|
|
2811
|
+
}
|
|
2812
|
+
if (methodLines.length === 0)
|
|
2813
|
+
continue;
|
|
2814
|
+
lines.push(`### \`${cls.name}\` (\`${rel(cls.filePath)}\`)`);
|
|
2815
|
+
lines.push(...methodLines);
|
|
2816
|
+
lines.push('');
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
if (substantive === 0)
|
|
2820
|
+
return '';
|
|
2821
|
+
lines.push('> Caller inventory complete — answer from this section.');
|
|
2822
|
+
lines.push('');
|
|
2823
|
+
return lines.join('\n');
|
|
2824
|
+
}
|
|
2825
|
+
/**
|
|
2826
|
+
* Upstream data-source survey — which services/symbols feed a Manager (e.g. BadgeManager → notification).
|
|
2827
|
+
*/
|
|
2828
|
+
buildDataSourceSection(cg, query) {
|
|
2829
|
+
const anchors = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((n) => /Manager|Service|Handler|Store|Provider|Controller/i.test(n));
|
|
2830
|
+
if (anchors.length === 0)
|
|
2831
|
+
return { section: '', edgeCount: 0 };
|
|
2832
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
2833
|
+
// Prefer system/SDK / service APIs — not local UI helpers (getBadgeOffsetX).
|
|
2834
|
+
const SERVICE_RE = /notification|@ohos|ans|subscribe|publish|bundle|ability|vibrator|telephony|NumBadge|notificationManager|badgeManager|wantAgent|distributed/i;
|
|
2835
|
+
const lines = ['**Data sources / upstream services**', ''];
|
|
2836
|
+
let edgeCount = 0;
|
|
2837
|
+
const addEdge = (bucket, node, via, rank) => {
|
|
2838
|
+
if ((0, query_utils_1.isTestFile)(node.filePath))
|
|
2839
|
+
return;
|
|
2840
|
+
const sig = `${node.name} ${node.filePath} ${node.signature || ''}`;
|
|
2841
|
+
if (!SERVICE_RE.test(sig))
|
|
2842
|
+
return;
|
|
2843
|
+
const prev = bucket.get(node.id);
|
|
2844
|
+
if (!prev || rank < prev.rank) {
|
|
2845
|
+
bucket.set(node.id, { name: node.name, file: rel(node.filePath), line: node.startLine, via, rank });
|
|
2846
|
+
if (!prev)
|
|
2847
|
+
edgeCount++;
|
|
2848
|
+
}
|
|
2849
|
+
};
|
|
2850
|
+
for (const name of anchors.slice(0, 3)) {
|
|
2851
|
+
const classes = cg.getNodesByName(name).filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'component') && !(0, query_utils_1.isTestFile)(n.filePath));
|
|
2852
|
+
for (const cls of classes.slice(0, 2)) {
|
|
2853
|
+
const upstream = new Map();
|
|
2854
|
+
const methods = [];
|
|
2855
|
+
for (const e of cg.getOutgoingEdges(cls.id)) {
|
|
2856
|
+
if (e.kind !== 'contains')
|
|
2857
|
+
continue;
|
|
2858
|
+
const m = cg.getNode(e.target);
|
|
2859
|
+
if (m && (m.kind === 'method' || m.kind === 'function'))
|
|
2860
|
+
methods.push(m);
|
|
2861
|
+
}
|
|
2862
|
+
if (methods.length === 0) {
|
|
2863
|
+
for (const n of cg.getNodesByName(name)) {
|
|
2864
|
+
if (n.filePath === cls.filePath && (n.kind === 'method' || n.kind === 'function'))
|
|
2865
|
+
methods.push(n);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
// 1) Same-file @ohos / kit imports — the usual system-service surface.
|
|
2869
|
+
try {
|
|
2870
|
+
for (const r of cg.searchNodes('@ohos', { kinds: ['import'], limit: 40 })) {
|
|
2871
|
+
if (r.node.filePath !== cls.filePath)
|
|
2872
|
+
continue;
|
|
2873
|
+
addEdge(upstream, r.node, 'import', 0);
|
|
2874
|
+
}
|
|
2875
|
+
for (const term of ['notification', 'badge', 'NumBadge', 'subscribe', 'bundle', 'ability']) {
|
|
2876
|
+
for (const r of cg.searchNodes(term, { kinds: ['import'], limit: 30 })) {
|
|
2877
|
+
if (r.node.filePath !== cls.filePath)
|
|
2878
|
+
continue;
|
|
2879
|
+
addEdge(upstream, r.node, 'import', 1);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
catch { /* */ }
|
|
2884
|
+
// 2) Callees that look like services/APIs.
|
|
2885
|
+
for (const method of methods.slice(0, 25)) {
|
|
2886
|
+
let callees = [];
|
|
2887
|
+
try {
|
|
2888
|
+
callees = cg.getCallees(method.id);
|
|
2889
|
+
}
|
|
2890
|
+
catch {
|
|
2891
|
+
continue;
|
|
2892
|
+
}
|
|
2893
|
+
for (const c of callees) {
|
|
2894
|
+
if (c?.node)
|
|
2895
|
+
addEdge(upstream, c.node, method.name, 2);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
if (upstream.size === 0)
|
|
2899
|
+
continue;
|
|
2900
|
+
lines.push(`### \`${cls.name}\` (\`${rel(cls.filePath)}\`)`);
|
|
2901
|
+
const ranked = [...upstream.values()].sort((a, b) => a.rank - b.rank || a.name.localeCompare(b.name));
|
|
2902
|
+
for (const u of ranked.slice(0, 15)) {
|
|
2903
|
+
lines.push(`- \`${u.name}\` at \`${u.file}:${u.line}\` (via \`${u.via}\`)`);
|
|
2904
|
+
}
|
|
2905
|
+
if (ranked.length > 15)
|
|
2906
|
+
lines.push(`- … and ${ranked.length - 15} more upstream symbol(s)`);
|
|
2907
|
+
lines.push('');
|
|
2908
|
+
lines.push('> Prefer `@ohos.*` / notification / bundle imports as the system-service answer; local helpers are secondary.');
|
|
2909
|
+
lines.push('');
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
if (edgeCount === 0)
|
|
2913
|
+
return { section: '', edgeCount: 0 };
|
|
2914
|
+
lines.push('> Data-source survey — answer from upstream symbols above; source bodies omitted.');
|
|
2915
|
+
lines.push('');
|
|
2916
|
+
return { section: lines.join('\n'), edgeCount };
|
|
2917
|
+
}
|
|
2918
|
+
/**
|
|
2919
|
+
* Main-thread fast path for inventory surveys — skips the worker queue.
|
|
2920
|
+
*/
|
|
2921
|
+
tryFastPathResult(toolName, args) {
|
|
2922
|
+
const query = args.query;
|
|
2923
|
+
if (typeof query !== 'string')
|
|
2924
|
+
return null;
|
|
2925
|
+
try {
|
|
2926
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
2927
|
+
const projectRoot = cg.getProjectRoot();
|
|
2928
|
+
if (toolName === 'homegraph_explore') {
|
|
2929
|
+
return this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
2930
|
+
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot)
|
|
2931
|
+
?? this.tryLightMechanismExplore(cg, query, projectRoot);
|
|
2932
|
+
}
|
|
2933
|
+
if (toolName === 'homegraph_search') {
|
|
2934
|
+
return this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
2935
|
+
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot)
|
|
2936
|
+
?? this.tryLightMechanismExplore(cg, query, projectRoot);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
catch {
|
|
2940
|
+
return null;
|
|
2941
|
+
}
|
|
2942
|
+
return null;
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Fast inventory-only explore — skips findRelevantContext for survey/caller/dependency queries.
|
|
2946
|
+
*/
|
|
2947
|
+
tryFastInventoryExplore(cg, query, projectRoot) {
|
|
2948
|
+
if (!(0, query_utils_1.shouldTryFastInventoryExplore)(query))
|
|
2949
|
+
return null;
|
|
2950
|
+
const lines = [`**Exploration: ${query}**`, '', ''];
|
|
2951
|
+
const summaryLineIdx = 2;
|
|
2952
|
+
const inheritanceOnly = (0, query_utils_1.shouldBuildInheritanceSurvey)(query);
|
|
2953
|
+
const typeSurface = inheritanceOnly
|
|
2954
|
+
|| (0, query_utils_1.shouldBuildCallerInventory)(query)
|
|
2955
|
+
|| (0, query_utils_1.queryIsTypeNameFocus)(query);
|
|
2956
|
+
// Type / hierarchy / method-caller surveys: return inventory first — never a
|
|
2957
|
+
// definition dump or full-graph explore. Agent already named the type.
|
|
2958
|
+
// Exception: caller + co-named "definition visible" needs bodies (compact path).
|
|
2959
|
+
if (typeSurface && !(0, query_utils_1.queryNeedsCoNamedUseBridge)(query)) {
|
|
2960
|
+
const inheritanceSection = this.buildInheritanceSurveySection(cg, query);
|
|
2961
|
+
const callerSection = (0, query_utils_1.shouldBuildCallerInventory)(query)
|
|
2962
|
+
? this.buildCallerListingSection(cg, query)
|
|
2963
|
+
: '';
|
|
2964
|
+
const inheritanceListed = inheritanceSection
|
|
2965
|
+
? inheritanceSection.split('\n').filter((l) => l.startsWith('- `')).length > 0
|
|
2966
|
+
: false;
|
|
2967
|
+
const callerBulletCount = callerSection
|
|
2968
|
+
? callerSection.split('\n').filter((l) => l.startsWith('- ') && l.includes(' ← ')).length
|
|
2969
|
+
: 0;
|
|
2970
|
+
if (inheritanceListed || callerBulletCount > 0) {
|
|
2971
|
+
const parts = [
|
|
2972
|
+
`**Exploration: ${query}**`,
|
|
2973
|
+
'',
|
|
2974
|
+
inheritanceListed && callerBulletCount > 0
|
|
2975
|
+
? 'Type surface: inheritance + method caller inventory below — answer from it; do not grep `extends` and do not `homegraph_callers` each method.'
|
|
2976
|
+
: inheritanceListed
|
|
2977
|
+
? 'Inheritance survey above lists all direct subtypes found.'
|
|
2978
|
+
: 'Caller inventory lists method→caller sites — answer from it; do not fan out `homegraph_callers` per method.',
|
|
2979
|
+
];
|
|
2980
|
+
if (inheritanceSection)
|
|
2981
|
+
parts.push(inheritanceSection);
|
|
2982
|
+
if (callerSection)
|
|
2983
|
+
parts.push(callerSection);
|
|
2984
|
+
return this.textResult(parts.join('\n'));
|
|
2985
|
+
}
|
|
2986
|
+
// Nothing graph-visible — fall through; import/domain may still help.
|
|
2987
|
+
}
|
|
2988
|
+
const importResult = (inheritanceOnly || (0, query_utils_1.queryIsTypeNameFocus)(query) || (0, query_utils_1.queryAsInheritanceSurvey)(query))
|
|
2989
|
+
? { section: '', siteCount: 0, compactListing: false }
|
|
2990
|
+
: this.buildImportSitesSection(cg, query, projectRoot);
|
|
2991
|
+
if (importResult.section)
|
|
2992
|
+
lines.push(importResult.section);
|
|
2993
|
+
const kitUsageResult = (0, query_utils_1.shouldBuildKitModuleUsageSurvey)(query)
|
|
2994
|
+
? this.buildKitModuleUsageSection(cg, query, projectRoot)
|
|
2995
|
+
: { section: '', symbolCount: 0 };
|
|
2996
|
+
if (kitUsageResult.section)
|
|
2997
|
+
lines.push(kitUsageResult.section);
|
|
2998
|
+
const domainFileResult = (0, query_utils_1.shouldBuildDomainFileSurvey)(query)
|
|
2999
|
+
? this.buildDomainFileSurveySection(cg, query)
|
|
3000
|
+
: { section: '', fileCount: 0 };
|
|
3001
|
+
if (domainFileResult.section)
|
|
3002
|
+
lines.push(domainFileResult.section);
|
|
3003
|
+
const apiUsageResult = (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
|
|
3004
|
+
? this.buildApiUsageSection(cg, query, projectRoot)
|
|
3005
|
+
: { section: '', fileCount: 0 };
|
|
3006
|
+
if (apiUsageResult.section)
|
|
3007
|
+
lines.push(apiUsageResult.section);
|
|
3008
|
+
const dataSourceResult = (0, query_utils_1.queryAsDataSourceSurvey)(query)
|
|
3009
|
+
? this.buildDataSourceSection(cg, query)
|
|
3010
|
+
: { section: '', edgeCount: 0 };
|
|
3011
|
+
if (dataSourceResult.section)
|
|
3012
|
+
lines.push(dataSourceResult.section);
|
|
3013
|
+
const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
|
|
3014
|
+
const multiAnchor = (0, query_utils_1.queryNamesMultipleExploreAnchors)(query);
|
|
3015
|
+
// Type / caller surveys: skip expensive named-symbol flow synthesize —
|
|
3016
|
+
// it wanders unrelated dynamic edges and burns tokens.
|
|
3017
|
+
const skipFlow = typeSurface || (0, query_utils_1.queryAsCallerOrMethodSurvey)(query);
|
|
3018
|
+
const flow = skipFlow
|
|
3019
|
+
? { pathNodeIds: new Set(), text: '' }
|
|
3020
|
+
: this.buildFlowFromNamedSymbols(cg, query);
|
|
3021
|
+
const hasFlowPath = flow.pathNodeIds.size > 0;
|
|
3022
|
+
const inheritanceSection = !hasFlowPath && !multiAnchor
|
|
3023
|
+
? this.buildInheritanceSurveySection(cg, query) : '';
|
|
3024
|
+
const callerSection = !hasFlowPath && !multiAnchor && (0, query_utils_1.shouldBuildCallerInventory)(query)
|
|
3025
|
+
? this.buildCallerListingSection(cg, query) : '';
|
|
3026
|
+
const memberSection = !hasFlowPath && !multiAnchor && (0, query_utils_1.shouldBuildMemberSurvey)(query)
|
|
3027
|
+
? this.buildMemberSurveySection(cg, query, projectRoot) : '';
|
|
3028
|
+
const configSection = (0, query_utils_1.shouldBuildConfigSection)(query)
|
|
3029
|
+
? this.buildConfigFileSection(cg, query, projectRoot) : '';
|
|
3030
|
+
if (inheritanceSection)
|
|
3031
|
+
lines.push(inheritanceSection);
|
|
3032
|
+
if (callerSection)
|
|
3033
|
+
lines.push(callerSection);
|
|
3034
|
+
if (memberSection)
|
|
3035
|
+
lines.push(memberSection);
|
|
3036
|
+
if (configSection)
|
|
3037
|
+
lines.push(configSection);
|
|
3038
|
+
const finishCompact = (summary) => {
|
|
3039
|
+
lines[summaryLineIdx] = summary;
|
|
3040
|
+
return this.textResult(lines.join('\n'));
|
|
3041
|
+
};
|
|
3042
|
+
const memberFileCount = memberSection
|
|
3043
|
+
? memberSection.split('\n').filter((l) => l.startsWith('- ')).length
|
|
3044
|
+
: 0;
|
|
3045
|
+
const callerBulletCount = callerSection
|
|
3046
|
+
? callerSection.split('\n').filter((l) => l.startsWith('- ') && l.includes(' ← ')).length
|
|
3047
|
+
: 0;
|
|
3048
|
+
const inheritanceListed = inheritanceSection
|
|
3049
|
+
? inheritanceSection.split('\n').filter((l) => l.startsWith('- `')).length > 0
|
|
3050
|
+
: false;
|
|
3051
|
+
const hasAnySection = importResult.section || kitUsageResult.section || domainFileResult.section
|
|
3052
|
+
|| apiUsageResult.section || dataSourceResult.section || inheritanceSection
|
|
3053
|
+
|| callerSection || memberSection || configSection;
|
|
3054
|
+
if (!hasAnySection)
|
|
3055
|
+
return null;
|
|
3056
|
+
const omitSource = (0, query_utils_1.shouldOmitSourceBodies)({
|
|
3057
|
+
importSiteCount: importResult.siteCount,
|
|
3058
|
+
hasFilteredImports: importInventoryFilter && importResult.siteCount > 0,
|
|
3059
|
+
callerBulletCount,
|
|
3060
|
+
memberFileCount,
|
|
3061
|
+
apiUsageFileCount: apiUsageResult.fileCount,
|
|
3062
|
+
configRendered: !!configSection,
|
|
3063
|
+
kitModuleSurveyRendered: !!kitUsageResult.section,
|
|
3064
|
+
inheritanceListed,
|
|
3065
|
+
domainFileCount: domainFileResult.fileCount,
|
|
3066
|
+
dataSourceEdgeCount: dataSourceResult.edgeCount,
|
|
3067
|
+
}, hasFlowPath, multiAnchor);
|
|
3068
|
+
if (!omitSource)
|
|
3069
|
+
return null;
|
|
3070
|
+
if (configSection)
|
|
3071
|
+
return finishCompact('Config/manifest content above — answer from it directly.');
|
|
3072
|
+
if (kitUsageResult.section) {
|
|
3073
|
+
return finishCompact(`Kit module usage survey — **${kitUsageResult.symbolCount}** imported symbol(s) across the repo.`);
|
|
3074
|
+
}
|
|
3075
|
+
if (domainFileResult.fileCount > 0) {
|
|
3076
|
+
return finishCompact(`Domain file survey — **${domainFileResult.fileCount}** related file(s) listed above. ` +
|
|
3077
|
+
'This is the exhaustive related-file inventory; no glob/search needed.');
|
|
3078
|
+
}
|
|
3079
|
+
if (apiUsageResult.fileCount > 0) {
|
|
3080
|
+
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s) referencing the queried symbol(s).`);
|
|
3081
|
+
}
|
|
3082
|
+
if (dataSourceResult.edgeCount > 0) {
|
|
3083
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s) listed above.`);
|
|
3084
|
+
}
|
|
3085
|
+
if (inheritanceListed)
|
|
3086
|
+
return finishCompact('Inheritance survey above lists all direct subtypes found.');
|
|
3087
|
+
if (importResult.compactListing) {
|
|
3088
|
+
return finishCompact(`Listed **${importResult.siteCount}** import site(s) for the queried symbol(s).`);
|
|
3089
|
+
}
|
|
3090
|
+
if (callerBulletCount >= 1) {
|
|
3091
|
+
return finishCompact(`Caller inventory lists **${callerBulletCount}** call site(s) — answer from the section above.`);
|
|
3092
|
+
}
|
|
3093
|
+
if (memberFileCount >= 2) {
|
|
3094
|
+
return finishCompact(`Member/pattern usage in **${memberFileCount}** file(s) — answer from the inventory above.`);
|
|
3095
|
+
}
|
|
3096
|
+
return finishCompact('Inventory sections above are complete for this query.');
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Render a compact symbol-bounded slice of one file (lightweight mechanism path).
|
|
3100
|
+
*/
|
|
3101
|
+
renderLightMechanismSource(projectRoot, filePath, nodes, maxChars) {
|
|
3102
|
+
const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, filePath);
|
|
3103
|
+
if (!absPath || !(0, fs_1.existsSync)(absPath))
|
|
3104
|
+
return null;
|
|
3105
|
+
const relevant = nodes.filter((n) => n.startLine > 0 && n.kind !== 'import' && n.kind !== 'export');
|
|
3106
|
+
if (relevant.length === 0)
|
|
3107
|
+
return null;
|
|
3108
|
+
let content;
|
|
3109
|
+
try {
|
|
3110
|
+
content = (0, fs_1.readFileSync)(absPath, 'utf-8');
|
|
3111
|
+
}
|
|
3112
|
+
catch {
|
|
3113
|
+
return null;
|
|
3114
|
+
}
|
|
3115
|
+
const fileLines = content.split('\n');
|
|
3116
|
+
const start = Math.max(1, Math.min(...relevant.map((n) => n.startLine)) - 8);
|
|
3117
|
+
const end = Math.min(fileLines.length, Math.max(...relevant.map((n) => n.endLine ?? n.startLine)) + 8);
|
|
3118
|
+
const lang = relevant[0]?.language || '';
|
|
3119
|
+
const names = [...new Set(relevant.map((n) => n.name))].slice(0, 4).join(', ');
|
|
3120
|
+
const out = [fileSectionHeader(filePath, names), '', '```' + lang];
|
|
3121
|
+
for (let i = start; i <= end; i++) {
|
|
3122
|
+
out.push(`${i}\t${fileLines[i - 1] ?? ''}`);
|
|
3123
|
+
}
|
|
3124
|
+
out.push('```', '');
|
|
3125
|
+
let text = out.join('\n');
|
|
3126
|
+
if (text.length > maxChars) {
|
|
3127
|
+
text = `${text.slice(0, maxChars)}\n... (section trimmed — another explore with a specific symbol is cheaper than grep/read)\n\`\`\`\n`;
|
|
3128
|
+
}
|
|
3129
|
+
return text;
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Lightweight mechanism explore — seed entry symbols + flow spine, skip
|
|
3133
|
+
* findRelevantContext. Fast enough for MCP budget; complete enough to avoid
|
|
3134
|
+
* agent grep/read loops (token savings).
|
|
3135
|
+
*/
|
|
3136
|
+
tryLightMechanismExplore(cg, query, projectRoot) {
|
|
3137
|
+
if (!(0, query_utils_1.shouldTryLightMechanismExplore)(query))
|
|
3138
|
+
return null;
|
|
3139
|
+
const STRUCTURE_KINDS = new Set(['class', 'struct', 'interface', 'component', 'method', 'function']);
|
|
3140
|
+
const isTestPath = (p) => /(^|\/)(tests?|spec)\//i.test(p) || /\.(test|spec)\./i.test(p);
|
|
3141
|
+
const fileNodes = new Map();
|
|
3142
|
+
const seedIds = new Set();
|
|
3143
|
+
const addNode = (n) => {
|
|
3144
|
+
if (isTestPath(n.filePath))
|
|
3145
|
+
return;
|
|
3146
|
+
if (n.kind !== 'import' && !STRUCTURE_KINDS.has(n.kind))
|
|
3147
|
+
return;
|
|
3148
|
+
seedIds.add(n.id);
|
|
3149
|
+
const list = fileNodes.get(n.filePath) ?? [];
|
|
3150
|
+
if (!list.some((x) => x.id === n.id))
|
|
3151
|
+
list.push(n);
|
|
3152
|
+
fileNodes.set(n.filePath, list);
|
|
3153
|
+
};
|
|
3154
|
+
for (const seed of (0, query_utils_1.extractMechanismEntrySeeds)(query)) {
|
|
3155
|
+
if (seed.startsWith('@')) {
|
|
3156
|
+
let hits = [];
|
|
3157
|
+
try {
|
|
3158
|
+
hits = cg.searchNodes(seed, { kinds: ['import'], limit: 12 });
|
|
3159
|
+
}
|
|
3160
|
+
catch {
|
|
3161
|
+
continue;
|
|
3162
|
+
}
|
|
3163
|
+
for (const r of hits)
|
|
3164
|
+
addNode(r.node);
|
|
3165
|
+
continue;
|
|
3166
|
+
}
|
|
3167
|
+
for (const n of cg.getNodesByName(seed)) {
|
|
3168
|
+
if (STRUCTURE_KINDS.has(n.kind))
|
|
3169
|
+
addNode(n);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
if (seedIds.size < 2) {
|
|
3173
|
+
const domainTerms = (0, query_utils_1.extractDomainSearchTerms)(query);
|
|
3174
|
+
const asciiTerms = domainTerms.filter((t) => /^[\x00-\x7F]+$/.test(t));
|
|
3175
|
+
for (const term of asciiTerms.slice(0, 8)) {
|
|
3176
|
+
let hits = [];
|
|
3177
|
+
try {
|
|
3178
|
+
hits = cg.searchNodes(term, {
|
|
3179
|
+
kinds: ['class', 'struct', 'interface', 'function', 'method', 'import'],
|
|
3180
|
+
limit: 16,
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
catch {
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3186
|
+
for (const r of hits) {
|
|
3187
|
+
// English FTS expansions from Chinese concepts need not be *Manager.
|
|
3188
|
+
const nameHit = new RegExp(term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i').test(r.node.name);
|
|
3189
|
+
const entryOk = (0, query_utils_1.isImplementationEntrySymbol)(r.node.name, domainTerms)
|
|
3190
|
+
|| r.node.kind === 'import'
|
|
3191
|
+
|| ((r.node.kind === 'function' || r.node.kind === 'method') && nameHit);
|
|
3192
|
+
if (!entryOk)
|
|
3193
|
+
continue;
|
|
3194
|
+
addNode(r.node);
|
|
3195
|
+
if (seedIds.size >= 10)
|
|
3196
|
+
break;
|
|
3197
|
+
}
|
|
3198
|
+
if (seedIds.size >= 10)
|
|
3199
|
+
break;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
if (seedIds.size === 0)
|
|
3203
|
+
return null;
|
|
3204
|
+
const seeds = (0, query_utils_1.extractMechanismEntrySeeds)(query);
|
|
3205
|
+
const flow = this.buildFlowFromNamedSymbols(cg, `${query} ${seeds.join(' ')}`);
|
|
3206
|
+
const lines = [
|
|
3207
|
+
`**Exploration: ${query}**`,
|
|
3208
|
+
'',
|
|
3209
|
+
`Mechanism anchors: **${seedIds.size}** symbol(s) — lightweight explore (seed + flow spine).`,
|
|
3210
|
+
'',
|
|
3211
|
+
];
|
|
3212
|
+
const importResult = this.buildImportSitesSection(cg, query, projectRoot);
|
|
3213
|
+
if (importResult.section)
|
|
3214
|
+
lines.push(importResult.section);
|
|
3215
|
+
if (flow.text)
|
|
3216
|
+
lines.push(flow.text);
|
|
3217
|
+
const fileScores = new Map();
|
|
3218
|
+
for (const [fp, nodes] of fileNodes) {
|
|
3219
|
+
let score = 0;
|
|
3220
|
+
for (const n of nodes) {
|
|
3221
|
+
if (flow.pathNodeIds.has(n.id))
|
|
3222
|
+
score += 20;
|
|
3223
|
+
else if (flow.uniqueNamedNodeIds.has(n.id))
|
|
3224
|
+
score += 10;
|
|
3225
|
+
else
|
|
3226
|
+
score += 5;
|
|
3227
|
+
}
|
|
3228
|
+
fileScores.set(fp, score);
|
|
3229
|
+
}
|
|
3230
|
+
const sortedFiles = [...fileScores.entries()]
|
|
3231
|
+
.filter(([, s]) => s > 0)
|
|
3232
|
+
.sort((a, b) => b[1] - a[1])
|
|
3233
|
+
.slice(0, 4);
|
|
3234
|
+
lines.push('**Source Code**', '');
|
|
3235
|
+
lines.push('> Line-numbered source below — treat as already Read. Do not grep/read these files again; ' +
|
|
3236
|
+
'another `homegraph_explore` with more symbol names is cheaper if you need more.');
|
|
3237
|
+
lines.push('');
|
|
3238
|
+
let totalChars = lines.join('\n').length;
|
|
3239
|
+
let filesRendered = 0;
|
|
3240
|
+
for (const [fp] of sortedFiles) {
|
|
3241
|
+
if (filesRendered >= 4 || totalChars > 14_000)
|
|
3242
|
+
break;
|
|
3243
|
+
const chunk = this.renderLightMechanismSource(projectRoot, fp, fileNodes.get(fp) ?? [], 4000);
|
|
3244
|
+
if (!chunk)
|
|
3245
|
+
continue;
|
|
3246
|
+
lines.push(chunk);
|
|
3247
|
+
totalChars += chunk.length;
|
|
3248
|
+
filesRendered++;
|
|
3249
|
+
}
|
|
3250
|
+
if (filesRendered === 0 && !importResult.section && !flow.text)
|
|
3251
|
+
return null;
|
|
3252
|
+
lines.push('---');
|
|
3253
|
+
lines.push('> **Mechanism explore complete** for anchored symbols — answer from sections above. ' +
|
|
3254
|
+
'Retry explore with more names before grep/read (grep duplicates token cost).');
|
|
3255
|
+
lines.push('');
|
|
3256
|
+
return this.textResult(lines.join('\n'));
|
|
3257
|
+
}
|
|
3258
|
+
/**
|
|
3259
|
+
* Compact explore for local-symbol behavior questions — skips findRelevantContext
|
|
3260
|
+
* and caps to 1–2 defining files (avoids the ~24K related-file dump).
|
|
3261
|
+
*/
|
|
3262
|
+
tryCompactLocalSymbolExplore(cg, query, projectRoot) {
|
|
3263
|
+
// Inventory runs *before* this on the call sites. Do not refuse compact
|
|
3264
|
+
// merely because inventory *intent* matched — empty inventory must fall
|
|
3265
|
+
// through here (bare callbacks like OnSurfaceChangedCB).
|
|
3266
|
+
// Hard rule: 1–3 named anchors → compact for local-detail / bare-id shapes.
|
|
3267
|
+
// Multi-anchor flow bags (routeSave/onSave, aboutToAppear/build, thunk→thunk)
|
|
3268
|
+
// must fall through to full explore so Flow / Dynamic-dispatch / adaptive
|
|
3269
|
+
// sizing still surface — compact trail is not a substitute.
|
|
3270
|
+
const bareId = /^[A-Za-z_][\w]*$/.test(query.trim());
|
|
3271
|
+
if (!(0, query_utils_1.queryAsLocalSymbolDetail)(query) && !bareId && !(0, query_utils_1.queryHasFocusedNamedAnchors)(query)) {
|
|
3272
|
+
return null;
|
|
3273
|
+
}
|
|
3274
|
+
const STRUCTURE_KINDS = new Set([
|
|
3275
|
+
'class', 'struct', 'interface', 'component', 'method', 'function', 'constant', 'variable', 'property', 'field',
|
|
3276
|
+
]);
|
|
3277
|
+
const isTestPath = (p) => /(^|\/)(tests?|spec)\//i.test(p) || /\.(test|spec)\./i.test(p);
|
|
3278
|
+
const names = (0, query_utils_1.extractLocalDetailAnchors)(query).slice(0, 4);
|
|
3279
|
+
if (names.length === 0)
|
|
3280
|
+
return null;
|
|
3281
|
+
// Multi-anchor bags that form a Flow / Dynamic-dispatch section must use
|
|
3282
|
+
// full explore (synth notes, adaptive sizing, boundary announcements).
|
|
3283
|
+
// Compact trail alone loses those sections. Probe using the raw query —
|
|
3284
|
+
// extractLocalDetailAnchors can drop snake_case tokens (handle_save) that
|
|
3285
|
+
// buildFlowFromNamedSymbols still resolves.
|
|
3286
|
+
const rawTokens = query.split(/[\s,()[\]]+/).filter((t) => t.length >= 3 && /^[A-Za-z_][\w]*$/.test(t));
|
|
3287
|
+
if (names.length >= 2 || rawTokens.length >= 2) {
|
|
3288
|
+
try {
|
|
3289
|
+
const flow = this.buildFlowFromNamedSymbols(cg, query);
|
|
3290
|
+
if (flow.text.length > 0)
|
|
3291
|
+
return null;
|
|
3292
|
+
}
|
|
3293
|
+
catch {
|
|
3294
|
+
// Probe is best-effort — stay on compact if flow build fails.
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
const fileNodes = new Map();
|
|
3298
|
+
const seedIds = new Set();
|
|
3299
|
+
const addNode = (n) => {
|
|
3300
|
+
if (isTestPath(n.filePath) && !(0, query_utils_1.queryAsTestOnlyInterpretation)(query))
|
|
3301
|
+
return;
|
|
3302
|
+
if (!STRUCTURE_KINDS.has(n.kind))
|
|
3303
|
+
return;
|
|
3304
|
+
seedIds.add(n.id);
|
|
3305
|
+
const list = fileNodes.get(n.filePath) ?? [];
|
|
3306
|
+
if (!list.some((x) => x.id === n.id))
|
|
3307
|
+
list.push(n);
|
|
3308
|
+
fileNodes.set(n.filePath, list);
|
|
3309
|
+
};
|
|
3310
|
+
for (const seed of names) {
|
|
3311
|
+
for (const n of cg.getNodesByName(seed)) {
|
|
3312
|
+
addNode(n);
|
|
3313
|
+
if (seedIds.size >= 12)
|
|
3314
|
+
break;
|
|
3315
|
+
}
|
|
3316
|
+
if (seedIds.size >= 12)
|
|
3317
|
+
break;
|
|
3318
|
+
}
|
|
3319
|
+
if (seedIds.size === 0)
|
|
3320
|
+
return null;
|
|
3321
|
+
const pathAffinity = (seedPath, otherPath) => {
|
|
3322
|
+
const a = seedPath.replace(/\\/g, '/').split('/');
|
|
3323
|
+
const b = otherPath.replace(/\\/g, '/').split('/');
|
|
3324
|
+
// Same package only (e.g. feature/foldeffect/…). Do NOT fallback to
|
|
3325
|
+
// top-level alone — that keeps cross-feature logError homonyms.
|
|
3326
|
+
return a.length >= 2 && b.length >= 2 && a[0] === b[0] && a[1] === b[1];
|
|
3327
|
+
};
|
|
3328
|
+
const seedPaths = [...seedIds].map((id) => {
|
|
3329
|
+
try {
|
|
3330
|
+
return cg.getNode(id)?.filePath;
|
|
3331
|
+
}
|
|
3332
|
+
catch {
|
|
3333
|
+
return undefined;
|
|
3334
|
+
}
|
|
3335
|
+
}).filter((p) => !!p);
|
|
3336
|
+
const nearSeed = (fp) => seedPaths.some((sp) => sp === fp || pathAffinity(sp, fp));
|
|
3337
|
+
// Neighbor policy (agents often search("Foo") alone — do not dump 11 callees):
|
|
3338
|
+
// - bareId / caller-bridge: callers of PRIMARY name only, no callee fan-out
|
|
3339
|
+
// - UI action / pinpoint local-detail: callers + callees
|
|
3340
|
+
const bridge = (0, query_utils_1.queryNeedsCoNamedUseBridge)(query);
|
|
3341
|
+
const callersOnly = bareId
|
|
3342
|
+
|| bridge
|
|
3343
|
+
|| ((0, query_utils_1.shouldBuildCallerInventory)(query) && !(0, query_utils_1.queryAsNamedComponentAction)(query));
|
|
3344
|
+
const primaryName = names[0];
|
|
3345
|
+
const expandIds = [...seedIds].filter((id) => {
|
|
3346
|
+
try {
|
|
3347
|
+
return cg.getNode(id)?.name === primaryName;
|
|
3348
|
+
}
|
|
3349
|
+
catch {
|
|
3350
|
+
return false;
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
// Pull one-hop neighbors into the focus set so the answer is graph-complete
|
|
3354
|
+
// without dumping an import inventory for isExpired/onClick.
|
|
3355
|
+
const neighborIds = new Set();
|
|
3356
|
+
for (const id of expandIds) {
|
|
3357
|
+
let seedPath = '';
|
|
3358
|
+
try {
|
|
3359
|
+
seedPath = cg.getNode(id)?.filePath ?? '';
|
|
3360
|
+
}
|
|
3361
|
+
catch { /* */ }
|
|
3362
|
+
try {
|
|
3363
|
+
for (const { node: c } of cg.getCallers(id).slice(0, callersOnly ? 8 : 10)) {
|
|
3364
|
+
if (!nearSeed(c.filePath) && seedPath && !pathAffinity(seedPath, c.filePath))
|
|
3365
|
+
continue;
|
|
3366
|
+
if (!isTestPath(c.filePath) || (0, query_utils_1.queryAsTestOnlyInterpretation)(query)) {
|
|
3367
|
+
neighborIds.add(c.id);
|
|
3368
|
+
// List callers in the trail, but don't pull their whole files into Source
|
|
3369
|
+
// for bare-name / caller-bridge shapes (that ballooned search("SortWidgets")).
|
|
3370
|
+
if (!callersOnly)
|
|
3371
|
+
addNode(c);
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
if (!callersOnly) {
|
|
3375
|
+
for (const { node: c } of cg.getCallees(id).slice(0, 10)) {
|
|
3376
|
+
if (!nearSeed(c.filePath) && seedPath && !pathAffinity(seedPath, c.filePath))
|
|
3377
|
+
continue;
|
|
3378
|
+
if (!isTestPath(c.filePath) || (0, query_utils_1.queryAsTestOnlyInterpretation)(query)) {
|
|
3379
|
+
neighborIds.add(c.id);
|
|
3380
|
+
addNode(c);
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
catch {
|
|
3386
|
+
// skip edge trail for this seed
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
const lines = [
|
|
3390
|
+
`**Exploration: ${query}**`,
|
|
3391
|
+
'',
|
|
3392
|
+
`Local-symbol focus: **${seedIds.size}** seed(s)` +
|
|
3393
|
+
(neighborIds.size > 0 ? `, **${neighborIds.size}** caller/callee neighbor(s)` : '') +
|
|
3394
|
+
' — compact explore (definition + edge trail, no related-file dump).',
|
|
3395
|
+
'',
|
|
3396
|
+
];
|
|
3397
|
+
// Blast radius for entry seeds (locations only) — same signal full explore
|
|
3398
|
+
// always-on section gives, so bare-name edits know what to update/verify.
|
|
3399
|
+
{
|
|
3400
|
+
const nodes = new Map();
|
|
3401
|
+
for (const id of seedIds) {
|
|
3402
|
+
try {
|
|
3403
|
+
const n = cg.getNode(id);
|
|
3404
|
+
if (n)
|
|
3405
|
+
nodes.set(id, n);
|
|
3406
|
+
}
|
|
3407
|
+
catch { /* skip */ }
|
|
3408
|
+
}
|
|
3409
|
+
const blast = this.buildBlastRadiusSection(cg, {
|
|
3410
|
+
nodes,
|
|
3411
|
+
edges: [],
|
|
3412
|
+
roots: [...seedIds],
|
|
3413
|
+
});
|
|
3414
|
+
if (blast)
|
|
3415
|
+
lines.push(blast);
|
|
3416
|
+
}
|
|
3417
|
+
// Always list a short edge trail — property→UI and button→action questions
|
|
3418
|
+
// need callers/callees; agents otherwise re-grep the same names.
|
|
3419
|
+
{
|
|
3420
|
+
const trail = ['**Call / use trail**', ''];
|
|
3421
|
+
let trailBullets = 0;
|
|
3422
|
+
const trailIds = (callersOnly ? expandIds : [...seedIds]).slice(0, 6);
|
|
3423
|
+
for (const id of trailIds) {
|
|
3424
|
+
let seedNode;
|
|
3425
|
+
try {
|
|
3426
|
+
seedNode = cg.getNode(id);
|
|
3427
|
+
}
|
|
3428
|
+
catch {
|
|
3429
|
+
continue;
|
|
3430
|
+
}
|
|
3431
|
+
if (!seedNode)
|
|
3432
|
+
continue;
|
|
3433
|
+
let callers = [];
|
|
3434
|
+
let callees = [];
|
|
3435
|
+
try {
|
|
3436
|
+
callers = cg.getCallers(id).slice(0, 8);
|
|
3437
|
+
}
|
|
3438
|
+
catch { /* */ }
|
|
3439
|
+
if (!callersOnly) {
|
|
3440
|
+
try {
|
|
3441
|
+
callees = cg.getCallees(id).slice(0, 8);
|
|
3442
|
+
}
|
|
3443
|
+
catch { /* */ }
|
|
3444
|
+
}
|
|
3445
|
+
callers = callers.filter((c) => nearSeed(c.node.filePath) || pathAffinity(seedNode.filePath, c.node.filePath));
|
|
3446
|
+
callees = callees.filter((c) => nearSeed(c.node.filePath) || pathAffinity(seedNode.filePath, c.node.filePath));
|
|
3447
|
+
if (callers.length === 0 && callees.length === 0)
|
|
3448
|
+
continue;
|
|
3449
|
+
trail.push(`- \`${seedNode.name}\` (${seedNode.kind}) — ${seedNode.filePath}:${seedNode.startLine}`);
|
|
3450
|
+
for (const c of callers) {
|
|
3451
|
+
const loc = c.node.startLine ? `:${c.node.startLine}` : '';
|
|
3452
|
+
trail.push(` ← used by \`${c.node.name}\` (${c.node.kind}) — ${c.node.filePath}${loc}`);
|
|
3453
|
+
trailBullets++;
|
|
3454
|
+
}
|
|
3455
|
+
for (const c of callees) {
|
|
3456
|
+
const loc = c.node.startLine ? `:${c.node.startLine}` : '';
|
|
3457
|
+
trail.push(` → calls \`${c.node.name}\` (${c.node.kind}) — ${c.node.filePath}${loc}`);
|
|
3458
|
+
trailBullets++;
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
if (trailBullets > 0) {
|
|
3462
|
+
trail.push('');
|
|
3463
|
+
trail.push('> Prefer answering from this trail + Source below. Do not grep/search the same symbols again.');
|
|
3464
|
+
trail.push('');
|
|
3465
|
+
lines.push(...trail);
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
// When the graph has no call edges (common for static Type.member reads),
|
|
3469
|
+
// fall back to a bounded indexed-file text scan for `name(` usages so the
|
|
3470
|
+
// agent does not pay a Grep round-trip for the same anchors.
|
|
3471
|
+
const textUsageHits = [];
|
|
3472
|
+
// Also scan when the agent asks upstream/registration — callee neighbors
|
|
3473
|
+
// alone do not show Export / OH_NativeXComponent wiring sites.
|
|
3474
|
+
const needUsageScan = neighborIds.size === 0
|
|
3475
|
+
|| /上下游|上游|下游|注册|挂到|callback|upstream|downstream/i.test(query)
|
|
3476
|
+
|| names.every((n) => /(?:CB|Callback)$/i.test(n));
|
|
3477
|
+
if (needUsageScan) {
|
|
3478
|
+
// Scan ALL focused anchors (incl. PascalCase callables) — previously only
|
|
3479
|
+
// lowercase names were scanned, so OnSurfaceChangedCB registration sites
|
|
3480
|
+
// never surfaced and agents fell through to callers/grep/read.
|
|
3481
|
+
const typeSeeds = (0, query_utils_1.extractTypeNamesFromQuery)(query);
|
|
3482
|
+
const scanNames = names.slice(0, 3);
|
|
3483
|
+
if (scanNames.length > 0) {
|
|
3484
|
+
try {
|
|
3485
|
+
const defLineKeys = new Set();
|
|
3486
|
+
for (const nodes of fileNodes.values()) {
|
|
3487
|
+
for (const n of nodes) {
|
|
3488
|
+
if (scanNames.includes(n.name))
|
|
3489
|
+
defLineKeys.add(`${n.filePath}:${n.startLine}`);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
const defDirs = new Set([...fileNodes.keys()].map((fp) => fp.split('/').slice(0, 4).join('/')));
|
|
3493
|
+
const files = cg.getFiles()
|
|
3494
|
+
.map((f) => f.path)
|
|
3495
|
+
.filter((fp) => {
|
|
3496
|
+
if (isTestPath(fp))
|
|
3497
|
+
return false;
|
|
3498
|
+
if (defDirs.size === 0)
|
|
3499
|
+
return true;
|
|
3500
|
+
return [...defDirs].some((d) => fp === d || fp.startsWith(`${d}/`));
|
|
3501
|
+
})
|
|
3502
|
+
.slice(0, 120);
|
|
3503
|
+
for (const fp of files) {
|
|
3504
|
+
if (textUsageHits.length >= 12)
|
|
3505
|
+
break;
|
|
3506
|
+
const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
3507
|
+
if (!absPath || !(0, fs_1.existsSync)(absPath))
|
|
3508
|
+
continue;
|
|
3509
|
+
let content;
|
|
3510
|
+
try {
|
|
3511
|
+
content = (0, fs_1.readFileSync)(absPath, 'utf-8');
|
|
3512
|
+
}
|
|
3513
|
+
catch {
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3516
|
+
// Prefer files that also mention a co-named type when both present
|
|
3517
|
+
// (skip for single-anchor upstream/registration scans).
|
|
3518
|
+
if (typeSeeds.length > 1
|
|
3519
|
+
&& names.length > 1
|
|
3520
|
+
&& !typeSeeds.some((t) => content.includes(t))) {
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
const fileLines = content.split('\n');
|
|
3524
|
+
for (const sym of scanNames) {
|
|
3525
|
+
const escaped = sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3526
|
+
// Match call OR bare identifier use (callback registration tables).
|
|
3527
|
+
const re = new RegExp(`\\b${escaped}\\b`);
|
|
3528
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
3529
|
+
const lineText = fileLines[i] ?? '';
|
|
3530
|
+
if (!re.test(lineText))
|
|
3531
|
+
continue;
|
|
3532
|
+
if (defLineKeys.has(`${fp}:${i + 1}`))
|
|
3533
|
+
continue;
|
|
3534
|
+
textUsageHits.push({ filePath: fp, line: i + 1, text: lineText.trim(), symbol: sym });
|
|
3535
|
+
if (!fileNodes.has(fp))
|
|
3536
|
+
fileNodes.set(fp, []);
|
|
3537
|
+
if (textUsageHits.length >= 12)
|
|
3538
|
+
break;
|
|
3539
|
+
}
|
|
3540
|
+
if (textUsageHits.length >= 12)
|
|
3541
|
+
break;
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
catch {
|
|
3546
|
+
// text scan is best-effort
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
if (textUsageHits.length > 0) {
|
|
3550
|
+
lines.push('**Text usage sites** (no call-edge indexed for these names — scanned in-repo)', '');
|
|
3551
|
+
for (const h of textUsageHits.slice(0, 10)) {
|
|
3552
|
+
lines.push(`- \`${h.symbol}\` — ${h.filePath}:${h.line} \`${h.text.slice(0, 120)}\``);
|
|
3553
|
+
}
|
|
3554
|
+
lines.push('');
|
|
3555
|
+
lines.push('> These lines are the in-repo use sites. Answer from them + Source; do not grep the same names again.');
|
|
3556
|
+
lines.push('');
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
// Also surface #include / import lines that make a co-named type visible
|
|
3560
|
+
// near the primary symbol's use sites (IntGrid ↔ SortWidgets shapes).
|
|
3561
|
+
if (names.length >= 2
|
|
3562
|
+
&& /include|import|visible|定义可见|可见|头文件|definition/i.test(query)) {
|
|
3563
|
+
const includeHits = [];
|
|
3564
|
+
const primaryFiles = new Set([
|
|
3565
|
+
...fileNodes.keys(),
|
|
3566
|
+
...textUsageHits.map((h) => h.filePath),
|
|
3567
|
+
]);
|
|
3568
|
+
// Files that call / define earlier anchors.
|
|
3569
|
+
for (const id of seedIds) {
|
|
3570
|
+
try {
|
|
3571
|
+
for (const { node: c } of cg.getCallers(id).slice(0, 8)) {
|
|
3572
|
+
primaryFiles.add(c.filePath);
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
catch { /* */ }
|
|
3576
|
+
}
|
|
3577
|
+
const secondary = names.slice(1);
|
|
3578
|
+
for (const fp of [...primaryFiles].slice(0, 12)) {
|
|
3579
|
+
const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
3580
|
+
if (!absPath || !(0, fs_1.existsSync)(absPath))
|
|
3581
|
+
continue;
|
|
3582
|
+
let content;
|
|
3583
|
+
try {
|
|
3584
|
+
content = (0, fs_1.readFileSync)(absPath, 'utf-8');
|
|
3585
|
+
}
|
|
3586
|
+
catch {
|
|
3587
|
+
continue;
|
|
3588
|
+
}
|
|
3589
|
+
const fileLines = content.split('\n');
|
|
3590
|
+
for (let i = 0; i < Math.min(fileLines.length, 120); i++) {
|
|
3591
|
+
const lineText = fileLines[i] ?? '';
|
|
3592
|
+
if (!/^\s*(?:#\s*include|import\s)/.test(lineText))
|
|
3593
|
+
continue;
|
|
3594
|
+
if (!secondary.some((s) => lineText.includes(s)))
|
|
3595
|
+
continue;
|
|
3596
|
+
includeHits.push(`- \`${fp}:${i + 1}\` \`${lineText.trim().slice(0, 140)}\``);
|
|
3597
|
+
if (includeHits.length >= 8)
|
|
3598
|
+
break;
|
|
3599
|
+
}
|
|
3600
|
+
if (includeHits.length >= 8)
|
|
3601
|
+
break;
|
|
3602
|
+
}
|
|
3603
|
+
if (includeHits.length > 0) {
|
|
3604
|
+
lines.push('**Include / import visibility** (how co-named types become visible at use sites)', '');
|
|
3605
|
+
lines.push(...includeHits);
|
|
3606
|
+
lines.push('');
|
|
3607
|
+
lines.push('> Answer the visibility question from these include/import lines — do not re-read headers.');
|
|
3608
|
+
lines.push('');
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
const callerSection = (0, query_utils_1.shouldBuildCallerInventory)(query)
|
|
3612
|
+
? this.buildCallerListingSection(cg, query)
|
|
3613
|
+
: '';
|
|
3614
|
+
if (callerSection)
|
|
3615
|
+
lines.push(callerSection);
|
|
3616
|
+
// Prefer files whose nodes exactly match query names; then neighbor / usage files.
|
|
3617
|
+
// Primary named symbol (first anchor) always ranks first — secondary types like
|
|
3618
|
+
// IntGrid used to drown SortWidgets' defining .cpp via higher exact-count on the header.
|
|
3619
|
+
const nameSet = new Set(names);
|
|
3620
|
+
const usageFiles = new Set(textUsageHits.map((h) => h.filePath));
|
|
3621
|
+
const maxFiles = ((0, query_utils_1.queryAsNamedComponentAction)(query) || (0, query_utils_1.queryHasNamedMemberFocus)(query) || usageFiles.size > 0 || bridge)
|
|
3622
|
+
? 4
|
|
3623
|
+
: 2;
|
|
3624
|
+
const ranked = [...fileNodes.entries()]
|
|
3625
|
+
.map(([fp, nodes]) => {
|
|
3626
|
+
const exact = nodes.filter((n) => nameSet.has(n.name)).length;
|
|
3627
|
+
const primaryHit = nodes.some((n) => n.name === primaryName) ? 100 : 0;
|
|
3628
|
+
const neighborHit = nodes.filter((n) => neighborIds.has(n.id)).length;
|
|
3629
|
+
const usageHit = usageFiles.has(fp) ? 15 : 0;
|
|
3630
|
+
return {
|
|
3631
|
+
fp,
|
|
3632
|
+
nodes,
|
|
3633
|
+
exact,
|
|
3634
|
+
score: primaryHit + exact * 20 + neighborHit * 5 + usageHit + nodes.length,
|
|
3635
|
+
};
|
|
3636
|
+
})
|
|
3637
|
+
.sort((a, b) => b.score - a.score || a.fp.localeCompare(b.fp))
|
|
3638
|
+
.slice(0, maxFiles);
|
|
3639
|
+
lines.push('**Source Code**', '');
|
|
3640
|
+
lines.push('> Line-numbered source — treat as already Read. Answer from it; do not re-explore/search/grep the same symbols.');
|
|
3641
|
+
lines.push('');
|
|
3642
|
+
let totalChars = lines.join('\n').length;
|
|
3643
|
+
const maxTotal = 9000;
|
|
3644
|
+
const maxPerFile = 4000;
|
|
3645
|
+
let rendered = 0;
|
|
3646
|
+
for (const { fp, nodes } of ranked) {
|
|
3647
|
+
if (rendered >= maxFiles || totalChars > maxTotal)
|
|
3648
|
+
break;
|
|
3649
|
+
const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
3650
|
+
if (!absPath || !(0, fs_1.existsSync)(absPath))
|
|
3651
|
+
continue;
|
|
3652
|
+
let fileContent;
|
|
3653
|
+
try {
|
|
3654
|
+
fileContent = (0, fs_1.readFileSync)(absPath, 'utf-8');
|
|
3655
|
+
}
|
|
3656
|
+
catch {
|
|
3657
|
+
continue;
|
|
3658
|
+
}
|
|
3659
|
+
const fileLines = fileContent.split('\n');
|
|
3660
|
+
const focusNodes = nodes
|
|
3661
|
+
.filter((n) => nameSet.has(n.name) || STRUCTURE_KINDS.has(n.kind))
|
|
3662
|
+
.sort((a, b) => a.startLine - b.startLine);
|
|
3663
|
+
// Prefer contiguous windows around named symbols; usage-only files
|
|
3664
|
+
// window around the first text-hit line.
|
|
3665
|
+
let start = 1;
|
|
3666
|
+
let end = Math.min(fileLines.length, 120);
|
|
3667
|
+
const usageLine = textUsageHits.find((h) => h.filePath === fp)?.line;
|
|
3668
|
+
if (focusNodes.length > 0) {
|
|
3669
|
+
const named = focusNodes.filter((n) => nameSet.has(n.name));
|
|
3670
|
+
const use = named.length > 0 ? named : focusNodes.slice(0, 3);
|
|
3671
|
+
start = Math.max(1, Math.min(...use.map((n) => n.startLine)) - 2);
|
|
3672
|
+
end = Math.min(fileLines.length, Math.max(...use.map((n) => n.endLine)) + 2);
|
|
3673
|
+
// Keep window bounded
|
|
3674
|
+
if ((end - start + 1) * 40 > maxPerFile) {
|
|
3675
|
+
end = Math.min(fileLines.length, start + Math.floor(maxPerFile / 40) - 1);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
else if (usageLine) {
|
|
3679
|
+
start = Math.max(1, usageLine - 15);
|
|
3680
|
+
end = Math.min(fileLines.length, usageLine + 25);
|
|
3681
|
+
}
|
|
3682
|
+
const header = fileSectionHeader(fp, focusNodes.map((n) => `${n.name}(${n.kind})`).slice(0, 6).join(', '));
|
|
3683
|
+
const withLineNumbers = exploreLineNumbersEnabled();
|
|
3684
|
+
const bodyLines = [header, '```' + (nodes[0]?.language || ''), ''];
|
|
3685
|
+
for (let i = start; i <= end; i++) {
|
|
3686
|
+
const code = fileLines[i - 1] ?? '';
|
|
3687
|
+
bodyLines.push(withLineNumbers ? `${i}\t${code}` : code);
|
|
3688
|
+
}
|
|
3689
|
+
bodyLines.push('```', '');
|
|
3690
|
+
let chunk = bodyLines.join('\n');
|
|
3691
|
+
if (chunk.length > maxPerFile) {
|
|
3692
|
+
chunk = `${chunk.slice(0, maxPerFile)}\n... (trimmed)\n\`\`\`\n`;
|
|
3693
|
+
}
|
|
3694
|
+
if (totalChars + chunk.length > maxTotal && rendered > 0)
|
|
3695
|
+
break;
|
|
3696
|
+
lines.push(chunk);
|
|
3697
|
+
totalChars += chunk.length;
|
|
3698
|
+
rendered++;
|
|
3699
|
+
}
|
|
3700
|
+
if (rendered === 0)
|
|
3701
|
+
return null;
|
|
3702
|
+
lines.push('---');
|
|
3703
|
+
lines.push('> **Compact local explore complete** — answer from the definition(s) above. ' +
|
|
3704
|
+
'Do not call `homegraph_search` or another full explore for the same symbol.');
|
|
3705
|
+
lines.push('');
|
|
3706
|
+
return this.textResult(lines.join('\n'));
|
|
3707
|
+
}
|
|
3708
|
+
/**
|
|
3709
|
+
* @kit module capability survey — repo import/usage only (SDK defs are not indexed).
|
|
3710
|
+
*/
|
|
3711
|
+
buildKitModuleUsageSection(cg, query, projectRoot) {
|
|
3712
|
+
const kitTerms = (0, query_utils_1.extractKitModuleNamesFromQuery)(query);
|
|
3713
|
+
const submodules = (0, query_utils_1.extractKitSubmoduleNamesFromQuery)(query);
|
|
3714
|
+
if (kitTerms.length === 0)
|
|
3715
|
+
return { section: '', symbolCount: 0 };
|
|
3716
|
+
const kitSearchTerms = (0, query_utils_1.extractImportSearchTerms)(query);
|
|
3717
|
+
const seen = new Set();
|
|
3718
|
+
const imports = [];
|
|
3719
|
+
const parseImportedSymbols = (lineText) => {
|
|
3720
|
+
const syms = new Set();
|
|
3721
|
+
const brace = lineText.match(/import\s*\{([^}]+)\}/);
|
|
3722
|
+
if (brace?.[1]) {
|
|
3723
|
+
for (const part of brace[1].split(',')) {
|
|
3724
|
+
const tok = part.trim().split(/\s+as\s+/i)[0]?.trim();
|
|
3725
|
+
if (tok)
|
|
3726
|
+
syms.add(tok);
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
const def = lineText.match(/import\s+(\w+)\s+from\b/);
|
|
3730
|
+
if (def?.[1])
|
|
3731
|
+
syms.add(def[1]);
|
|
3732
|
+
const ns = lineText.match(/import\s+\*\s+as\s+(\w+)/);
|
|
3733
|
+
if (ns?.[1])
|
|
3734
|
+
syms.add(ns[1]);
|
|
3735
|
+
return [...syms];
|
|
3736
|
+
};
|
|
3737
|
+
const matchesSubmodule = (lineText, symbols) => {
|
|
3738
|
+
if (submodules.length === 0)
|
|
3739
|
+
return true;
|
|
3740
|
+
const lc = lineText.toLowerCase();
|
|
3741
|
+
return submodules.some((sm) => {
|
|
3742
|
+
const s = sm.toLowerCase();
|
|
3743
|
+
return symbols.some((sym) => sym.toLowerCase() === s || sym.toLowerCase().startsWith(`${s}.`))
|
|
3744
|
+
|| lc.includes(`.${s}`) || lc.includes(`'${s}'`) || lc.includes(`"${s}"`);
|
|
3745
|
+
});
|
|
3746
|
+
};
|
|
3747
|
+
const resolveImportLine = (node) => (0, query_utils_1.resolveImportLineFromNode)(node, projectRoot);
|
|
3748
|
+
for (const term of kitSearchTerms) {
|
|
3749
|
+
const termLc = term.toLowerCase().replace(/^@kit\./, '');
|
|
3750
|
+
let hits = [];
|
|
3751
|
+
try {
|
|
3752
|
+
hits = cg.searchNodes(term, { kinds: ['import'], limit: 80 });
|
|
3753
|
+
}
|
|
3754
|
+
catch {
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
for (const r of hits) {
|
|
3758
|
+
const lineText = resolveImportLine(r.node);
|
|
3759
|
+
if (!lineText.toLowerCase().includes(termLc) && !lineText.toLowerCase().includes('@kit.'))
|
|
3760
|
+
continue;
|
|
3761
|
+
const symbols = parseImportedSymbols(lineText);
|
|
3762
|
+
if (!matchesSubmodule(lineText, symbols))
|
|
3763
|
+
continue;
|
|
3764
|
+
const key = `${r.node.filePath}:${r.node.startLine}`;
|
|
3765
|
+
if (seen.has(key))
|
|
3766
|
+
continue;
|
|
3767
|
+
seen.add(key);
|
|
3768
|
+
imports.push({
|
|
3769
|
+
file: r.node.filePath,
|
|
3770
|
+
line: r.node.startLine,
|
|
3771
|
+
lineText,
|
|
3772
|
+
symbols,
|
|
3773
|
+
});
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
if (imports.length === 0) {
|
|
3777
|
+
const kitLabel = kitTerms.map((k) => `@kit.${k}`).join(', ');
|
|
3778
|
+
const sub = submodules.length > 0 ? ` (${submodules.join(', ')} submodule)` : '';
|
|
3779
|
+
return {
|
|
3780
|
+
section: [
|
|
3781
|
+
'**Kit module usage (this repo)**',
|
|
3782
|
+
'',
|
|
3783
|
+
`No imports from ${kitLabel}${sub} were found in the indexed codebase.`,
|
|
3784
|
+
'',
|
|
3785
|
+
`> @kit module API definitions live in the HarmonyOS SDK, not this repository. Answer from repo imports/usages only.`,
|
|
3786
|
+
'',
|
|
3787
|
+
].join('\n'),
|
|
3788
|
+
symbolCount: 0,
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
const symbolFiles = new Map();
|
|
3792
|
+
for (const imp of imports) {
|
|
3793
|
+
for (const sym of imp.symbols) {
|
|
3794
|
+
const set = symbolFiles.get(sym) ?? new Set();
|
|
3795
|
+
set.add(imp.file);
|
|
3796
|
+
symbolFiles.set(sym, set);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
const lines = [
|
|
3800
|
+
'**Kit module usage (this repo)**',
|
|
3801
|
+
'',
|
|
3802
|
+
`> @kit module API definitions live in the HarmonyOS SDK, not this repository. Below: how **this project** imports and uses them.`,
|
|
3803
|
+
'',
|
|
3804
|
+
`Imports from ${kitTerms.map((k) => `\`@kit.${k}\``).join(', ')} (${imports.length} site(s)):`,
|
|
3805
|
+
'',
|
|
3806
|
+
];
|
|
3807
|
+
for (const imp of imports.slice(0, 35)) {
|
|
3808
|
+
const symStr = imp.symbols.length > 0 ? imp.symbols.join(', ') : '(namespace)';
|
|
3809
|
+
lines.push(`- \`${imp.file}:${imp.line}\` — \`${imp.lineText}\` (${symStr})`);
|
|
3810
|
+
}
|
|
3811
|
+
if (imports.length > 35)
|
|
3812
|
+
lines.push(`- … and ${imports.length - 35} more import site(s)`);
|
|
3813
|
+
if (symbolFiles.size > 0) {
|
|
3814
|
+
lines.push('');
|
|
3815
|
+
lines.push('**Symbols imported (unique):**');
|
|
3816
|
+
for (const [sym, files] of [...symbolFiles.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(0, 40)) {
|
|
3817
|
+
const fileList = [...files].slice(0, 5).map((f) => `\`${f}\``).join(', ');
|
|
3818
|
+
const more = files.size > 5 ? ` +${files.size - 5} files` : '';
|
|
3819
|
+
lines.push(`- \`${sym}\` — imported in ${fileList}${more}`);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
lines.push('');
|
|
3823
|
+
lines.push('> Kit usage survey complete — answer from this section; do not search external SDK docs or grep the repo again.');
|
|
3824
|
+
lines.push('');
|
|
3825
|
+
return { section: lines.join('\n'), symbolCount: symbolFiles.size };
|
|
3826
|
+
}
|
|
3827
|
+
/** Disambiguate homonymous types (Configuration, Rectangle) when several defs exist. */
|
|
3828
|
+
buildHomonymDefinitionsSection(cg, query) {
|
|
3829
|
+
const typeNames = (0, query_utils_1.extractTypeNamesFromQuery)(query);
|
|
3830
|
+
const queryFiles = (0, query_utils_1.extractFileBasenamesFromQuery)(query);
|
|
3831
|
+
const lines = [];
|
|
3832
|
+
for (const name of typeNames.slice(0, 4)) {
|
|
3833
|
+
const defs = cg.getNodesByName(name).filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'interface' || n.kind === 'type_alias')
|
|
3834
|
+
&& !(0, query_utils_1.isTestFile)(n.filePath));
|
|
3835
|
+
if (defs.length <= 1)
|
|
3836
|
+
continue;
|
|
3837
|
+
lines.push(`**${name}** — ${defs.length} distinct definitions (pin with file basename in a follow-up explore):`);
|
|
3838
|
+
for (const d of defs.slice(0, 8)) {
|
|
3839
|
+
const pin = (0, query_utils_1.fileMatchesQueryBasename)(d.filePath, queryFiles) ? ' ← matches query file' : '';
|
|
3840
|
+
lines.push(`- \`${d.qualifiedName || d.name}\` (\`${d.filePath}:${d.startLine}\`)${pin}`);
|
|
3841
|
+
}
|
|
3842
|
+
if (defs.length > 8)
|
|
3843
|
+
lines.push(`- … and ${defs.length - 8} more`);
|
|
3844
|
+
lines.push('');
|
|
3845
|
+
}
|
|
3846
|
+
if (lines.length === 0)
|
|
3847
|
+
return '';
|
|
3848
|
+
return lines.join('\n');
|
|
3849
|
+
}
|
|
3850
|
+
/** Direct subclasses of a named type — paths only. */
|
|
3851
|
+
buildInheritanceSurveySection(cg, query) {
|
|
3852
|
+
if (!(0, query_utils_1.shouldBuildInheritanceSurvey)(query))
|
|
3853
|
+
return '';
|
|
3854
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
3855
|
+
const lines = ['**Inheritance survey**', ''];
|
|
3856
|
+
let listed = 0;
|
|
3857
|
+
for (const typeName of (0, query_utils_1.extractTypeNamesFromQuery)(query).slice(0, 2)) {
|
|
3858
|
+
const bases = cg.getNodesByName(typeName).filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'interface') && !(0, query_utils_1.isTestFile)(n.filePath));
|
|
3859
|
+
for (const base of bases.slice(0, 2)) {
|
|
3860
|
+
const subs = [];
|
|
3861
|
+
for (const e of cg.getIncomingEdges(base.id)) {
|
|
3862
|
+
if (e.kind !== 'extends' && e.kind !== 'implements')
|
|
3863
|
+
continue;
|
|
3864
|
+
try {
|
|
3865
|
+
const child = cg.getNode(e.source);
|
|
3866
|
+
if (child && !(0, query_utils_1.isTestFile)(child.filePath))
|
|
3867
|
+
subs.push(child);
|
|
3868
|
+
}
|
|
3869
|
+
catch { /* skip */ }
|
|
3870
|
+
}
|
|
3871
|
+
if (subs.length === 0)
|
|
3872
|
+
continue;
|
|
3873
|
+
lines.push(`### Subtypes of \`${base.name}\` (\`${rel(base.filePath)}\`)`);
|
|
3874
|
+
for (const s of subs.slice(0, 30)) {
|
|
3875
|
+
lines.push(`- \`${s.name}\` (\`${rel(s.filePath)}:${s.startLine}\`)`);
|
|
3876
|
+
listed++;
|
|
3877
|
+
}
|
|
3878
|
+
if (subs.length > 30)
|
|
3879
|
+
lines.push(`- … and ${subs.length - 30} more`);
|
|
3880
|
+
lines.push('');
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
if (listed === 0)
|
|
3884
|
+
return '';
|
|
3885
|
+
lines.push('> Inheritance survey complete — answer from this list; no read/grep needed for the subtype set.');
|
|
3886
|
+
lines.push('');
|
|
3887
|
+
return lines.join('\n');
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* Files/lines using a member or literal pattern (.drawModifier, .width('100%')).
|
|
3891
|
+
*/
|
|
3892
|
+
buildMemberSurveySection(cg, query, projectRoot) {
|
|
3893
|
+
const members = (0, query_utils_1.extractMemberAccessFromQuery)(query);
|
|
3894
|
+
const patterns = [];
|
|
3895
|
+
for (const ma of members) {
|
|
3896
|
+
if (ma.dotted.startsWith('.'))
|
|
3897
|
+
patterns.push(ma.dotted);
|
|
3898
|
+
else
|
|
3899
|
+
patterns.push(ma.dotted);
|
|
3900
|
+
}
|
|
3901
|
+
// Literal chains in quotes: .width('100%')
|
|
3902
|
+
for (const m of query.matchAll(/(\.[a-zA-Z_][\w]*\s*\([^)]*\))/g)) {
|
|
3903
|
+
patterns.push(m[1].replace(/\s+/g, ''));
|
|
3904
|
+
}
|
|
3905
|
+
if (patterns.length === 0)
|
|
3906
|
+
return '';
|
|
3907
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
3908
|
+
const hits = new Map();
|
|
3909
|
+
const addHit = (file, line) => {
|
|
3910
|
+
const arr = hits.get(file) ?? [];
|
|
3911
|
+
if (!arr.includes(line))
|
|
3912
|
+
arr.push(line);
|
|
3913
|
+
hits.set(file, arr);
|
|
3914
|
+
};
|
|
3915
|
+
for (const pat of patterns) {
|
|
3916
|
+
const bare = pat.replace(/^\./, '');
|
|
3917
|
+
let nodes = [];
|
|
3918
|
+
try {
|
|
3919
|
+
nodes = cg.searchNodes(bare, { limit: 30 });
|
|
3920
|
+
}
|
|
3921
|
+
catch { /* skip */ }
|
|
3922
|
+
for (const r of nodes) {
|
|
3923
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
3924
|
+
continue;
|
|
3925
|
+
addHit(rel(r.node.filePath), r.node.startLine);
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
// Scan top FTS files for literal pattern in source
|
|
3929
|
+
for (const pat of patterns.slice(0, 3)) {
|
|
3930
|
+
const literal = pat.startsWith('.') ? pat : `.${pat}`;
|
|
3931
|
+
const re = new RegExp(literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
3932
|
+
let files = [];
|
|
3933
|
+
try {
|
|
3934
|
+
files = cg.searchNodes(patterns[0].replace(/^\./, ''), { limit: 25 });
|
|
3935
|
+
}
|
|
3936
|
+
catch {
|
|
3937
|
+
continue;
|
|
3938
|
+
}
|
|
3939
|
+
for (const r of files) {
|
|
3940
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
3941
|
+
continue;
|
|
3942
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
3943
|
+
if (!abs)
|
|
3944
|
+
continue;
|
|
3945
|
+
let content;
|
|
3946
|
+
try {
|
|
3947
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
3948
|
+
}
|
|
3949
|
+
catch {
|
|
3950
|
+
continue;
|
|
3951
|
+
}
|
|
3952
|
+
const fileLines = content.split('\n');
|
|
3953
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
3954
|
+
if (re.test(fileLines[i] ?? ''))
|
|
3955
|
+
addHit(rel(r.node.filePath), i + 1);
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
if (hits.size === 0)
|
|
3960
|
+
return '';
|
|
3961
|
+
const lines = ['**Member / pattern usage**', ''];
|
|
3962
|
+
let fileCount = 0;
|
|
3963
|
+
for (const [fp, lineNos] of [...hits.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
3964
|
+
if (fileCount >= 40)
|
|
3965
|
+
break;
|
|
3966
|
+
const sorted = lineNos.sort((a, b) => a - b).slice(0, 6);
|
|
3967
|
+
const lineStr = sorted.join(', ');
|
|
3968
|
+
const more = lineNos.length > 6 ? ` +${lineNos.length - 6} lines` : '';
|
|
3969
|
+
lines.push(`- \`${fp}\` — lines ${lineStr}${more}`);
|
|
3970
|
+
fileCount++;
|
|
3971
|
+
}
|
|
3972
|
+
if (hits.size > 40)
|
|
3973
|
+
lines.push(`- … and ${hits.size - 40} more file(s)`);
|
|
3974
|
+
lines.push('');
|
|
3975
|
+
lines.push('> Pattern survey complete — answer from this list; run another explore naming missing symbols if needed.');
|
|
3976
|
+
lines.push('');
|
|
3977
|
+
return lines.join('\n');
|
|
3978
|
+
}
|
|
3979
|
+
/**
|
|
3980
|
+
* API call-site survey — where a named API/symbol (statfs, napi_*) appears in repo source.
|
|
3981
|
+
*/
|
|
3982
|
+
buildApiUsageSection(cg, query, projectRoot) {
|
|
3983
|
+
// Include PascalCase SDK modules (Telephony) — deps-only missed them.
|
|
3984
|
+
const symbols = (0, query_utils_1.extractApiUsageTokens)(query).slice(0, 6);
|
|
3985
|
+
if (symbols.length === 0)
|
|
3986
|
+
return { section: '', fileCount: 0 };
|
|
3987
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
3988
|
+
const hits = new Map();
|
|
3989
|
+
const addHit = (file, line) => {
|
|
3990
|
+
if ((0, query_utils_1.isTestFile)(file))
|
|
3991
|
+
return;
|
|
3992
|
+
const fp = rel(file);
|
|
3993
|
+
const arr = hits.get(fp) ?? [];
|
|
3994
|
+
if (!arr.includes(line))
|
|
3995
|
+
arr.push(line);
|
|
3996
|
+
hits.set(fp, arr);
|
|
3997
|
+
};
|
|
3998
|
+
for (const sym of symbols.slice(0, 4)) {
|
|
3999
|
+
const symLc = sym.toLowerCase();
|
|
4000
|
+
let nodes = [];
|
|
4001
|
+
try {
|
|
4002
|
+
nodes = cg.searchNodes(sym, { limit: 40 });
|
|
4003
|
+
}
|
|
4004
|
+
catch { /* skip */ }
|
|
4005
|
+
for (const r of nodes) {
|
|
4006
|
+
const n = r.node;
|
|
4007
|
+
if (n.name.toLowerCase() === symLc || (n.signature || '').toLowerCase().includes(symLc)) {
|
|
4008
|
+
addHit(n.filePath, n.startLine);
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
const wordRe = new RegExp(`\\b${sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
|
|
4012
|
+
let scanHits = [];
|
|
4013
|
+
try {
|
|
4014
|
+
scanHits = cg.searchNodes(sym, { limit: 30 });
|
|
4015
|
+
}
|
|
4016
|
+
catch {
|
|
4017
|
+
continue;
|
|
4018
|
+
}
|
|
4019
|
+
const seenFiles = new Set();
|
|
4020
|
+
for (const r of scanHits) {
|
|
4021
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
2283
4022
|
continue;
|
|
2284
|
-
|
|
4023
|
+
const fp = rel(r.node.filePath);
|
|
4024
|
+
if (seenFiles.has(fp))
|
|
4025
|
+
continue;
|
|
4026
|
+
seenFiles.add(fp);
|
|
4027
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
4028
|
+
if (!abs)
|
|
4029
|
+
continue;
|
|
4030
|
+
let content;
|
|
2285
4031
|
try {
|
|
2286
|
-
|
|
4032
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
2287
4033
|
}
|
|
2288
|
-
catch {
|
|
2289
|
-
if (impl < MIN_IMPL)
|
|
4034
|
+
catch {
|
|
2290
4035
|
continue;
|
|
2291
|
-
|
|
2292
|
-
|
|
4036
|
+
}
|
|
4037
|
+
const fileLines = content.split('\n');
|
|
4038
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
4039
|
+
if (wordRe.test(fileLines[i] ?? ''))
|
|
4040
|
+
addHit(fp, i + 1);
|
|
4041
|
+
}
|
|
2293
4042
|
}
|
|
2294
|
-
if (!best || seenSuper.has(best.node.id))
|
|
2295
|
-
continue;
|
|
2296
|
-
seenSuper.add(best.node.id);
|
|
2297
|
-
const namedNames = new Set([...named.values()].map((n) => n.name));
|
|
2298
|
-
const eg = best.targets.slice(0, 4).map((m) => {
|
|
2299
|
-
const cont = containerOf(m);
|
|
2300
|
-
const disp = cont ? `${cont.name}.${m.name}` : (m.qualifiedName || m.name);
|
|
2301
|
-
const mark = cont && namedNames.has(cont.name) ? ' ← you named this' : '';
|
|
2302
|
-
return `\`${disp}\` (${rel(m.filePath)}:${m.startLine})${mark}`;
|
|
2303
|
-
});
|
|
2304
|
-
const more = best.impl > eg.length ? ` +${best.impl - eg.length} more` : '';
|
|
2305
|
-
notes.push(`- \`${token}\` → runtime dispatch to **${best.impl}** types implementing \`${best.node.name}\` — the static path ends here, the target is chosen at runtime. e.g. ${eg.join(', ')}${more}`);
|
|
2306
4043
|
}
|
|
2307
|
-
if (
|
|
2308
|
-
return '';
|
|
2309
|
-
|
|
2310
|
-
'**
|
|
2311
|
-
'',
|
|
2312
|
-
...notes,
|
|
4044
|
+
if (hits.size === 0)
|
|
4045
|
+
return { section: '', fileCount: 0 };
|
|
4046
|
+
const lines = [
|
|
4047
|
+
'**API usage sites**',
|
|
2313
4048
|
'',
|
|
2314
|
-
|
|
4049
|
+
`> Files referencing \`${symbols.join('`, `')}\` in this repo (${hits.size} file(s)). Answer from this list; source bodies omitted.`,
|
|
2315
4050
|
'',
|
|
2316
|
-
]
|
|
4051
|
+
];
|
|
4052
|
+
let shown = 0;
|
|
4053
|
+
for (const [fp, lineNos] of [...hits.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
4054
|
+
if (shown >= 45)
|
|
4055
|
+
break;
|
|
4056
|
+
const sortedLines = [...lineNos].sort((a, b) => a - b).slice(0, 6);
|
|
4057
|
+
const lineStr = sortedLines.map((l) => `L${l}`).join(', ');
|
|
4058
|
+
const more = lineNos.length > sortedLines.length ? ` +${lineNos.length - sortedLines.length} more` : '';
|
|
4059
|
+
lines.push(`- \`${fp}\` (${lineStr}${more})`);
|
|
4060
|
+
shown++;
|
|
4061
|
+
}
|
|
4062
|
+
if (hits.size > shown)
|
|
4063
|
+
lines.push(`- … and ${hits.size - shown} more file(s)`);
|
|
4064
|
+
lines.push('');
|
|
4065
|
+
lines.push(`> API usage survey complete — **${hits.size}** file(s) listed.`);
|
|
4066
|
+
lines.push('');
|
|
4067
|
+
return { section: lines.join('\n'), fileCount: hits.size };
|
|
2317
4068
|
}
|
|
2318
4069
|
/**
|
|
2319
|
-
*
|
|
2320
|
-
*
|
|
2321
|
-
* `onSave`/`handleSave`; `CreateCmd` → `CreateCmdHandler`), then FTS, with a
|
|
2322
|
-
* normalized-containment post-filter (FTS camel-splitting is fuzzier than a
|
|
2323
|
-
* candidate list should be). Symbols the agent already named sort first and
|
|
2324
|
-
* are marked — that's the "you were right, here's the wiring" case.
|
|
4070
|
+
* Domain file inventory — related files, usage existence, or concept comparison.
|
|
4071
|
+
* Paths + top symbols only; source bodies omitted when this section renders.
|
|
2325
4072
|
*/
|
|
2326
|
-
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
const
|
|
2333
|
-
|
|
2334
|
-
if (!n || n.id === selfId || !CALLABLE.has(n.kind) || cands.has(n.id))
|
|
2335
|
-
return;
|
|
2336
|
-
const nameNorm = norm(n.name || '');
|
|
2337
|
-
if (nameNorm.length < 3)
|
|
2338
|
-
return;
|
|
2339
|
-
if (!nameNorm.includes(keyNorm) && !keyNorm.includes(nameNorm))
|
|
4073
|
+
buildDomainFileSurveySection(cg, query) {
|
|
4074
|
+
const terms = (0, query_utils_1.extractDomainSearchTerms)(query);
|
|
4075
|
+
if (terms.length === 0)
|
|
4076
|
+
return { section: '', fileCount: 0 };
|
|
4077
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
4078
|
+
const fileScores = new Map();
|
|
4079
|
+
const addFile = (filePath, score, symbol) => {
|
|
4080
|
+
if ((0, query_utils_1.isTestFile)(filePath))
|
|
2340
4081
|
return;
|
|
2341
|
-
|
|
4082
|
+
const fp = rel(filePath);
|
|
4083
|
+
const existing = fileScores.get(fp) ?? { score: 0, symbols: [] };
|
|
4084
|
+
existing.score += score;
|
|
4085
|
+
if (symbol && existing.symbols.length < 5 && !existing.symbols.includes(symbol)) {
|
|
4086
|
+
existing.symbols.push(symbol);
|
|
4087
|
+
}
|
|
4088
|
+
fileScores.set(fp, existing);
|
|
2342
4089
|
};
|
|
2343
|
-
const
|
|
2344
|
-
|
|
2345
|
-
? [`${key}Handler`, key]
|
|
2346
|
-
: [key, `on${cap}`, `handle${cap}`, `${key}Handler`, `handle_${key}`];
|
|
2347
|
-
for (const p of probes) {
|
|
4090
|
+
for (const term of terms) {
|
|
4091
|
+
let hits = [];
|
|
2348
4092
|
try {
|
|
2349
|
-
|
|
2350
|
-
|
|
4093
|
+
hits = cg.searchNodes(term, { limit: 45 });
|
|
4094
|
+
}
|
|
4095
|
+
catch {
|
|
4096
|
+
continue;
|
|
4097
|
+
}
|
|
4098
|
+
for (const r of hits) {
|
|
4099
|
+
addFile(r.node.filePath, (r.score ?? 1) + 2, r.node.name);
|
|
2351
4100
|
}
|
|
2352
|
-
catch { /* exact probe miss is fine */ }
|
|
2353
4101
|
}
|
|
2354
|
-
let raw = 0;
|
|
2355
4102
|
try {
|
|
2356
|
-
const
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
4103
|
+
for (const f of cg.getFiles()) {
|
|
4104
|
+
const fpLc = f.path.toLowerCase();
|
|
4105
|
+
for (const term of terms) {
|
|
4106
|
+
if (term.length >= 3 && fpLc.includes(term.toLowerCase())) {
|
|
4107
|
+
addFile(f.path, 6);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
2360
4111
|
}
|
|
2361
|
-
catch { /*
|
|
2362
|
-
if (
|
|
2363
|
-
return
|
|
4112
|
+
catch { /* getFiles unavailable */ }
|
|
4113
|
+
if (fileScores.size === 0)
|
|
4114
|
+
return { section: '', fileCount: 0 };
|
|
4115
|
+
const sorted = [...fileScores.entries()]
|
|
4116
|
+
.sort((a, b) => b[1].score - a[1].score)
|
|
4117
|
+
.slice(0, 50);
|
|
4118
|
+
const lines = [
|
|
4119
|
+
'**Related files (domain survey)**',
|
|
4120
|
+
'',
|
|
4121
|
+
`> File paths matching the query domain (${terms.map((t) => `\`${t}\``).join(', ')}). Answer from this inventory; source bodies omitted.`,
|
|
4122
|
+
'',
|
|
4123
|
+
];
|
|
4124
|
+
for (const [fp, info] of sorted.slice(0, 40)) {
|
|
4125
|
+
const symStr = info.symbols.length > 0 ? ` — ${info.symbols.join(', ')}` : '';
|
|
4126
|
+
lines.push(`- \`${fp}\`${symStr}`);
|
|
2364
4127
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
4128
|
+
if (sorted.length > 40)
|
|
4129
|
+
lines.push(`- … and ${sorted.length - 40} more file(s)`);
|
|
4130
|
+
lines.push('');
|
|
4131
|
+
lines.push(`> Domain file survey complete — **${sorted.length}** file(s) listed. ` +
|
|
4132
|
+
'Exhaustive related-file inventory; no glob/search needed.');
|
|
4133
|
+
lines.push('');
|
|
4134
|
+
return { section: lines.join('\n'), fileCount: sorted.length };
|
|
4135
|
+
}
|
|
4136
|
+
/**
|
|
4137
|
+
* Render a small config/manifest file named in the query (build-profile.json5, …).
|
|
4138
|
+
*/
|
|
4139
|
+
buildConfigFileSection(cg, query, projectRoot) {
|
|
4140
|
+
const basenames = (0, query_utils_1.extractFileBasenamesFromQuery)(query);
|
|
4141
|
+
if (basenames.length === 0)
|
|
4142
|
+
return '';
|
|
4143
|
+
const CONFIG_EXT = /\.(?:json5?|ya?ml|toml|xml|ini|properties)$/i;
|
|
4144
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
4145
|
+
const lines = ['**Config / manifest**', ''];
|
|
4146
|
+
let any = false;
|
|
4147
|
+
for (const name of basenames.slice(0, 3)) {
|
|
4148
|
+
let files = [];
|
|
4149
|
+
try {
|
|
4150
|
+
files = cg.searchNodes(name, { kinds: ['file'], limit: 15 });
|
|
4151
|
+
}
|
|
4152
|
+
catch {
|
|
4153
|
+
continue;
|
|
4154
|
+
}
|
|
4155
|
+
for (const r of files) {
|
|
4156
|
+
const base = rel(r.node.filePath).split('/').pop() ?? '';
|
|
4157
|
+
if (!base.toLowerCase().includes(name.toLowerCase()))
|
|
4158
|
+
continue;
|
|
4159
|
+
if (!CONFIG_EXT.test(base))
|
|
4160
|
+
continue;
|
|
4161
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
4162
|
+
if (!abs)
|
|
4163
|
+
continue;
|
|
4164
|
+
let content;
|
|
2381
4165
|
try {
|
|
2382
|
-
|
|
2383
|
-
const method = cg.getOutgoingEdges(n.id)
|
|
2384
|
-
.filter((e) => e.kind === 'contains')
|
|
2385
|
-
.map((e) => { try {
|
|
2386
|
-
return cg.getNode(e.target);
|
|
2387
|
-
}
|
|
2388
|
-
catch {
|
|
2389
|
-
return null;
|
|
2390
|
-
} })
|
|
2391
|
-
.find((c) => !!c && c.kind === 'method' && HANDLER_METHODS.test(c.name));
|
|
2392
|
-
if (method) {
|
|
2393
|
-
display = `${n.name}.${method.name}`;
|
|
2394
|
-
at = `${method.filePath}:${method.startLine}`;
|
|
2395
|
-
}
|
|
4166
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
2396
4167
|
}
|
|
2397
|
-
catch {
|
|
4168
|
+
catch {
|
|
4169
|
+
continue;
|
|
4170
|
+
}
|
|
4171
|
+
if (content.length > 12000)
|
|
4172
|
+
continue;
|
|
4173
|
+
any = true;
|
|
4174
|
+
lines.push(`**\`${rel(r.node.filePath)}\`**`);
|
|
4175
|
+
lines.push('');
|
|
4176
|
+
lines.push('```json5');
|
|
4177
|
+
lines.push(content.replace(/\n+$/, ''));
|
|
4178
|
+
lines.push('```');
|
|
4179
|
+
lines.push('');
|
|
2398
4180
|
}
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
4181
|
+
}
|
|
4182
|
+
if (!any)
|
|
4183
|
+
return '';
|
|
4184
|
+
lines.push('> Config content above is verbatim — answer from it; no broad grep needed.');
|
|
4185
|
+
lines.push('');
|
|
4186
|
+
return lines.join('\n');
|
|
2402
4187
|
}
|
|
2403
4188
|
/**
|
|
2404
4189
|
* Compact "blast radius" for the entry symbols of an explore result: who
|
|
@@ -2553,12 +4338,22 @@ class ToolHandler {
|
|
|
2553
4338
|
* tax on small projects while earning its keep on large ones.
|
|
2554
4339
|
*/
|
|
2555
4340
|
async handleExplore(args) {
|
|
2556
|
-
const
|
|
2557
|
-
if (typeof
|
|
2558
|
-
return
|
|
4341
|
+
const rawQuery = this.validateString(args.query, 'query');
|
|
4342
|
+
if (typeof rawQuery !== 'string')
|
|
4343
|
+
return rawQuery;
|
|
4344
|
+
// One normalization point so the flow-builder, relevance search, and
|
|
4345
|
+
// ranking all see the same canonical spelling (Erlang `mod:fn/arity`).
|
|
4346
|
+
const query = normalizeQuerySpelling(rawQuery);
|
|
2559
4347
|
const cg = this.getHomeGraph(args.projectPath);
|
|
2560
4348
|
const projectRoot = cg.getProjectRoot();
|
|
2561
|
-
|
|
4349
|
+
const compactLocal = this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
4350
|
+
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
4351
|
+
if (compactLocal)
|
|
4352
|
+
return compactLocal;
|
|
4353
|
+
const lightMechanism = this.tryLightMechanismExplore(cg, query, projectRoot);
|
|
4354
|
+
if (lightMechanism)
|
|
4355
|
+
return lightMechanism;
|
|
4356
|
+
// Resolve adaptive output budget from project size.
|
|
2562
4357
|
// largest-tier defaults if stats aren't available, which preserves
|
|
2563
4358
|
// pre-#185 behavior for callers that hit the rare stats failure.
|
|
2564
4359
|
let budget;
|
|
@@ -2568,20 +4363,72 @@ class ToolHandler {
|
|
|
2568
4363
|
catch {
|
|
2569
4364
|
budget = getExploreOutputBudget(Infinity);
|
|
2570
4365
|
}
|
|
2571
|
-
|
|
4366
|
+
budget = tightenExploreBudgetForQuery(budget, query);
|
|
4367
|
+
const explicitMaxFiles = typeof args.maxFiles === 'number' && !Number.isNaN(args.maxFiles);
|
|
4368
|
+
let maxFiles = (0, utils_1.clamp)(args.maxFiles || budget.defaultMaxFiles, 1, 20);
|
|
4369
|
+
const queryFileBasenames = (0, query_utils_1.extractFileBasenamesFromQuery)(query);
|
|
4370
|
+
const interpretationQuery = (0, query_utils_1.queryAsInterpretationSurvey)(query);
|
|
4371
|
+
const testOnlyInterpretation = (0, query_utils_1.queryAsTestOnlyInterpretation)(query);
|
|
4372
|
+
const crossModuleFlow = (0, query_utils_1.queryAsCrossModuleFlowSurvey)(query);
|
|
2572
4373
|
// Step 1: Find relevant context with generous parameters.
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
const
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
minScore: 0.2,
|
|
2581
|
-
});
|
|
4374
|
+
const contextOpts = interpretationQuery && queryFileBasenames.length === 1
|
|
4375
|
+
? { searchLimit: 6, traversalDepth: 2, maxNodes: 60, minScore: 0.25 }
|
|
4376
|
+
: { searchLimit: 8, traversalDepth: 3, maxNodes: 200, minScore: 0.2 };
|
|
4377
|
+
const contextQuery = interpretationQuery && queryFileBasenames.length === 1
|
|
4378
|
+
? `${queryFileBasenames[0]} ${query}`
|
|
4379
|
+
: query;
|
|
4380
|
+
const subgraph = await cg.findRelevantContext(contextQuery, contextOpts);
|
|
2582
4381
|
if (subgraph.nodes.size === 0) {
|
|
2583
4382
|
return this.textResult(`No relevant code found for "${query}"`);
|
|
2584
4383
|
}
|
|
4384
|
+
// Seed import nodes for @kit.* / *Kit names (and named symbols like taskpool).
|
|
4385
|
+
const importTerms = (0, query_utils_1.extractImportSearchTerms)(query);
|
|
4386
|
+
const depSymbols = (0, query_utils_1.extractDependencySymbolsFromQuery)(query);
|
|
4387
|
+
const seedImport = (r, lineText) => {
|
|
4388
|
+
const lineLc = lineText.toLowerCase();
|
|
4389
|
+
if (importTerms.length > 0) {
|
|
4390
|
+
const kitNames = (0, query_utils_1.extractKitModuleNamesFromQuery)(query);
|
|
4391
|
+
if (kitNames.length > 0 && !kitNames.some((k) => lineLc.includes(`@kit.${k.toLowerCase()}`))) {
|
|
4392
|
+
return;
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
if (depSymbols.length > 0 && !depSymbols.some((s) => lineLc.includes(s.toLowerCase()))) {
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
4398
|
+
if (!subgraph.nodes.has(r.node.id)) {
|
|
4399
|
+
subgraph.nodes.set(r.node.id, r.node);
|
|
4400
|
+
subgraph.roots.push(r.node.id);
|
|
4401
|
+
}
|
|
4402
|
+
};
|
|
4403
|
+
for (const sym of depSymbols) {
|
|
4404
|
+
let hits = [];
|
|
4405
|
+
try {
|
|
4406
|
+
hits = cg.searchNodes(sym, { kinds: ['import'], limit: 40 });
|
|
4407
|
+
}
|
|
4408
|
+
catch {
|
|
4409
|
+
continue;
|
|
4410
|
+
}
|
|
4411
|
+
for (const r of hits) {
|
|
4412
|
+
const sig = (r.node.signature || r.node.name || '').trim();
|
|
4413
|
+
seedImport(r, sig);
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
for (const term of importTerms) {
|
|
4417
|
+
const termLc = term.toLowerCase().replace(/^@kit\./, '');
|
|
4418
|
+
let hits = [];
|
|
4419
|
+
try {
|
|
4420
|
+
hits = cg.searchNodes(term, { kinds: ['import'], limit: 12 });
|
|
4421
|
+
}
|
|
4422
|
+
catch {
|
|
4423
|
+
continue;
|
|
4424
|
+
}
|
|
4425
|
+
for (const r of hits) {
|
|
4426
|
+
const sig = (r.node.signature || r.node.name || '').trim();
|
|
4427
|
+
if (!sig.toLowerCase().includes(termLc))
|
|
4428
|
+
continue;
|
|
4429
|
+
seedImport(r, sig);
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
2585
4432
|
// Graph-aware glue: findRelevantContext builds the subgraph from name/text
|
|
2586
4433
|
// search, so a method that BRIDGES named symbols — e.g. App.tsx's
|
|
2587
4434
|
// triggerRender, which calls the named triggerUpdate — is never a search hit
|
|
@@ -2639,6 +4486,13 @@ class ToolHandler {
|
|
|
2639
4486
|
{
|
|
2640
4487
|
const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte|astro)$/i;
|
|
2641
4488
|
const CALLABLE = new Set(['method', 'function', 'component', 'constructor']);
|
|
4489
|
+
// Named types (BridgeInterceptor, ResponseFormatter) must also seed — the
|
|
4490
|
+
// adaptive sibling-skeleton path needs those files in the subgraph even
|
|
4491
|
+
// when they hold no callable the agent also named.
|
|
4492
|
+
const SEED_KINDS = new Set([
|
|
4493
|
+
...CALLABLE,
|
|
4494
|
+
'class', 'struct', 'interface', 'trait', 'protocol', 'constant', 'variable',
|
|
4495
|
+
]);
|
|
2642
4496
|
const isTestPath = (p) => /(^|\/)(tests?|specs?|__tests__|testdata|mocks?|fixtures?)\//i.test(p) || /\.(test|spec)\.[a-z]+$/i.test(p);
|
|
2643
4497
|
const bodyLines = (n) => Math.max(0, (n.endLine ?? n.startLine) - n.startLine);
|
|
2644
4498
|
const callerCount = (n) => { try {
|
|
@@ -2647,9 +4501,28 @@ class ToolHandler {
|
|
|
2647
4501
|
catch {
|
|
2648
4502
|
return 0;
|
|
2649
4503
|
} };
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
|
|
4504
|
+
const namedParts = [];
|
|
4505
|
+
for (const kit of (0, query_utils_1.extractKitModuleNamesFromQuery)(query))
|
|
4506
|
+
namedParts.push(kit);
|
|
4507
|
+
for (const base of (0, query_utils_1.extractFileBasenamesFromQuery)(query))
|
|
4508
|
+
namedParts.push(base);
|
|
4509
|
+
for (const ma of (0, query_utils_1.extractMemberAccessFromQuery)(query)) {
|
|
4510
|
+
namedParts.push(ma.member);
|
|
4511
|
+
if (ma.receiver)
|
|
4512
|
+
namedParts.push(ma.receiver);
|
|
4513
|
+
}
|
|
4514
|
+
for (const m of query.matchAll(/\b([A-Za-z_][\w]*)(::)([A-Za-z_][\w]*)\b/g)) {
|
|
4515
|
+
if (m[1])
|
|
4516
|
+
namedParts.push(m[1]);
|
|
4517
|
+
if (m[3])
|
|
4518
|
+
namedParts.push(m[3]);
|
|
4519
|
+
}
|
|
4520
|
+
const tokens = [...new Set([
|
|
4521
|
+
...namedParts,
|
|
4522
|
+
...query.split(/[\s,()[\]]+/)
|
|
4523
|
+
.map((t) => t.replace(FILE_EXT, '').trim())
|
|
4524
|
+
.filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)),
|
|
4525
|
+
])].slice(0, 16);
|
|
2653
4526
|
// PascalCase tokens in the query are type/file disambiguators — when the
|
|
2654
4527
|
// agent writes "DataRequest task validate", the `task`/`validate` it wants
|
|
2655
4528
|
// are DataRequest's, NOT the same-named overloads in Validation.swift /
|
|
@@ -2673,8 +4546,15 @@ class ToolHandler {
|
|
|
2673
4546
|
const isQual = /[.\/]|::/.test(t);
|
|
2674
4547
|
const raw = isQual ? this.findAllSymbols(cg, t).nodes : cg.getNodesByName(t);
|
|
2675
4548
|
const cands = raw
|
|
2676
|
-
.filter((n) =>
|
|
2677
|
-
.sort((a, b) =>
|
|
4549
|
+
.filter((n) => SEED_KINDS.has(n.kind) && !isTestPath(n.filePath))
|
|
4550
|
+
.sort((a, b) => {
|
|
4551
|
+
// Prefer callables over types when both share a name, then body size.
|
|
4552
|
+
const ac = CALLABLE.has(a.kind) ? 1 : 0;
|
|
4553
|
+
const bc = CALLABLE.has(b.kind) ? 1 : 0;
|
|
4554
|
+
if (ac !== bc)
|
|
4555
|
+
return bc - ac;
|
|
4556
|
+
return (bodyLines(b) > 1 ? 1 : 0) - (bodyLines(a) > 1 ? 1 : 0) || bodyLines(b) - bodyLines(a);
|
|
4557
|
+
});
|
|
2678
4558
|
// A specific name (<=3 defs) injects all its defs. An overloaded name
|
|
2679
4559
|
// (`validate` = 10, `request` = 44) would flood the subgraph, so inject
|
|
2680
4560
|
// only: the overloads whose file/class the query ALSO names (the agent
|
|
@@ -2713,6 +4593,68 @@ class ToolHandler {
|
|
|
2713
4593
|
for (const n of tierPicks)
|
|
2714
4594
|
tierSeedIds.add(n.id);
|
|
2715
4595
|
}
|
|
4596
|
+
// Mechanism survey: seed query-shaped entry symbols first, then domain search.
|
|
4597
|
+
if ((0, query_utils_1.queryAsMechanismSurvey)(query)) {
|
|
4598
|
+
const STRUCTURE_KINDS = new Set(['class', 'struct', 'interface', 'component']);
|
|
4599
|
+
const implEntryNames = [];
|
|
4600
|
+
for (const seed of (0, query_utils_1.extractMechanismEntrySeeds)(query)) {
|
|
4601
|
+
if (seed.startsWith('@')) {
|
|
4602
|
+
let hits = [];
|
|
4603
|
+
try {
|
|
4604
|
+
hits = cg.searchNodes(seed, { kinds: ['import'], limit: 20 });
|
|
4605
|
+
}
|
|
4606
|
+
catch {
|
|
4607
|
+
continue;
|
|
4608
|
+
}
|
|
4609
|
+
for (const r of hits) {
|
|
4610
|
+
if (isTestPath(r.node.filePath))
|
|
4611
|
+
continue;
|
|
4612
|
+
if (!subgraph.nodes.has(r.node.id))
|
|
4613
|
+
subgraph.nodes.set(r.node.id, r.node);
|
|
4614
|
+
namedSeedIds.add(r.node.id);
|
|
4615
|
+
tierSeedIds.add(r.node.id);
|
|
4616
|
+
}
|
|
4617
|
+
continue;
|
|
4618
|
+
}
|
|
4619
|
+
const nodes = cg.getNodesByName(seed).filter((n) => STRUCTURE_KINDS.has(n.kind) && !isTestPath(n.filePath));
|
|
4620
|
+
for (const n of nodes.slice(0, 3)) {
|
|
4621
|
+
if (!subgraph.nodes.has(n.id))
|
|
4622
|
+
subgraph.nodes.set(n.id, n);
|
|
4623
|
+
namedSeedIds.add(n.id);
|
|
4624
|
+
tierSeedIds.add(n.id);
|
|
4625
|
+
if (!implEntryNames.includes(n.name))
|
|
4626
|
+
implEntryNames.push(n.name);
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
if (implEntryNames.length < 2) {
|
|
4630
|
+
const domainTerms = (0, query_utils_1.extractDomainSearchTerms)(query);
|
|
4631
|
+
for (const term of domainTerms.slice(0, 5)) {
|
|
4632
|
+
let hits = [];
|
|
4633
|
+
try {
|
|
4634
|
+
hits = cg.searchNodes(term, { kinds: ['class', 'struct', 'interface'], limit: 30 });
|
|
4635
|
+
}
|
|
4636
|
+
catch {
|
|
4637
|
+
continue;
|
|
4638
|
+
}
|
|
4639
|
+
for (const r of hits) {
|
|
4640
|
+
if (isTestPath(r.node.filePath))
|
|
4641
|
+
continue;
|
|
4642
|
+
if (!(0, query_utils_1.isImplementationEntrySymbol)(r.node.name, domainTerms))
|
|
4643
|
+
continue;
|
|
4644
|
+
if (!subgraph.nodes.has(r.node.id))
|
|
4645
|
+
subgraph.nodes.set(r.node.id, r.node);
|
|
4646
|
+
namedSeedIds.add(r.node.id);
|
|
4647
|
+
tierSeedIds.add(r.node.id);
|
|
4648
|
+
if (!implEntryNames.includes(r.node.name))
|
|
4649
|
+
implEntryNames.push(r.node.name);
|
|
4650
|
+
if (implEntryNames.length >= 6)
|
|
4651
|
+
break;
|
|
4652
|
+
}
|
|
4653
|
+
if (implEntryNames.length >= 6)
|
|
4654
|
+
break;
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
2716
4658
|
}
|
|
2717
4659
|
// Step 2: Group nodes by file, score by relevance
|
|
2718
4660
|
const fileGroups = new Map();
|
|
@@ -2797,6 +4739,14 @@ class ToolHandler {
|
|
|
2797
4739
|
}
|
|
2798
4740
|
fileGroups.set(node.filePath, group);
|
|
2799
4741
|
}
|
|
4742
|
+
if (testOnlyInterpretation) {
|
|
4743
|
+
for (const [, group] of fileGroups) {
|
|
4744
|
+
const fp = group.nodes[0]?.filePath ?? '';
|
|
4745
|
+
if (fp && !(0, query_utils_1.isTestFile)(fp) && !(0, query_utils_1.fileMatchesQueryBasename)(fp, queryFileBasenames)) {
|
|
4746
|
+
group.score = Math.max(0, group.score - 50);
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
2800
4750
|
// Only include files that have entry points or nodes directly connected to entry points
|
|
2801
4751
|
let relevantFiles = [...fileGroups.entries()].filter(([, group]) => group.score >= 3);
|
|
2802
4752
|
// Extract query terms for relevance checking
|
|
@@ -2974,9 +4924,16 @@ class ToolHandler {
|
|
|
2974
4924
|
const isCorroborated = (fp) => !MULTITERM_OFF &&
|
|
2975
4925
|
(fileTermHits.get(fp) ?? 0) >= 2 &&
|
|
2976
4926
|
(entryFiles.has(fp) || centralFiles.has(fp));
|
|
4927
|
+
const queryMemberAccesses = (0, query_utils_1.extractMemberAccessFromQuery)(query);
|
|
2977
4928
|
const sortedFiles = relevantFiles.sort((a, b) => {
|
|
2978
4929
|
const aPath = a[0].toLowerCase();
|
|
2979
4930
|
const bPath = b[0].toLowerCase();
|
|
4931
|
+
// Query-named file (LocationController.ets in the question) before partial
|
|
4932
|
+
// substring matches (control.ets matching "Controller" inside LocationController).
|
|
4933
|
+
const aExactBase = (0, query_utils_1.fileMatchesQueryBasename)(a[0], queryFileBasenames) ? 1 : 0;
|
|
4934
|
+
const bExactBase = (0, query_utils_1.fileMatchesQueryBasename)(b[0], queryFileBasenames) ? 1 : 0;
|
|
4935
|
+
if (aExactBase !== bExactBase)
|
|
4936
|
+
return bExactBase - aExactBase;
|
|
2980
4937
|
// Agent-named files first (it asked for a symbol defined here by name).
|
|
2981
4938
|
const aNamed = namedSeedFiles.has(a[0]) ? 1 : 0;
|
|
2982
4939
|
const bNamed = namedSeedFiles.has(b[0]) ? 1 : 0;
|
|
@@ -3029,16 +4986,154 @@ class ToolHandler {
|
|
|
3029
4986
|
'',
|
|
3030
4987
|
];
|
|
3031
4988
|
const summaryLineIdx = 2;
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
4989
|
+
if (testOnlyInterpretation) {
|
|
4990
|
+
lines.push('> **Test-file scope only** — answer from the named `.test.ets` file below; ' +
|
|
4991
|
+
'production handlers are out of scope unless explicitly referenced in the test.');
|
|
4992
|
+
lines.push('');
|
|
4993
|
+
}
|
|
4994
|
+
const importResult = this.buildImportSitesSection(cg, query, projectRoot);
|
|
4995
|
+
if (importResult.section)
|
|
4996
|
+
lines.push(importResult.section);
|
|
4997
|
+
const homonymSection = this.buildHomonymDefinitionsSection(cg, query);
|
|
4998
|
+
if (homonymSection)
|
|
4999
|
+
lines.push(homonymSection);
|
|
5000
|
+
const kitUsageResult = (0, query_utils_1.shouldBuildKitModuleUsageSurvey)(query)
|
|
5001
|
+
? this.buildKitModuleUsageSection(cg, query, projectRoot)
|
|
5002
|
+
: { section: '', symbolCount: 0 };
|
|
5003
|
+
if (kitUsageResult.section)
|
|
5004
|
+
lines.push(kitUsageResult.section);
|
|
5005
|
+
const domainFileResult = (0, query_utils_1.shouldBuildDomainFileSurvey)(query)
|
|
5006
|
+
? this.buildDomainFileSurveySection(cg, query)
|
|
5007
|
+
: { section: '', fileCount: 0 };
|
|
5008
|
+
if (domainFileResult.section)
|
|
5009
|
+
lines.push(domainFileResult.section);
|
|
5010
|
+
const apiUsageResult = (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
|
|
5011
|
+
? this.buildApiUsageSection(cg, query, projectRoot)
|
|
5012
|
+
: { section: '', fileCount: 0 };
|
|
5013
|
+
if (apiUsageResult.section)
|
|
5014
|
+
lines.push(apiUsageResult.section);
|
|
5015
|
+
const dataSourceResult = (0, query_utils_1.queryAsDataSourceSurvey)(query)
|
|
5016
|
+
? this.buildDataSourceSection(cg, query)
|
|
5017
|
+
: { section: '', edgeCount: 0 };
|
|
5018
|
+
if (dataSourceResult.section)
|
|
5019
|
+
lines.push(dataSourceResult.section);
|
|
5020
|
+
const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
|
|
5021
|
+
const multiAnchor = (0, query_utils_1.queryNamesMultipleExploreAnchors)(query) || crossModuleFlow;
|
|
5022
|
+
const mechanismSurvey = (0, query_utils_1.queryAsMechanismSurvey)(query);
|
|
5023
|
+
// Flow path — computed before omit-source so graph connectivity drives the decision,
|
|
5024
|
+
// not question-text keyword matching. Mechanism/cross-module surveys augment the
|
|
5025
|
+
// query with seeded entry symbol names so buildFlowFromNamedSymbols can connect them.
|
|
5026
|
+
let flowQuery = query;
|
|
5027
|
+
if (crossModuleFlow) {
|
|
5028
|
+
flowQuery = `${query} ${(0, query_utils_1.extractTypeNamesFromQuery)(query).join(' ')}`;
|
|
5029
|
+
}
|
|
5030
|
+
else if (mechanismSurvey) {
|
|
5031
|
+
const seeds = (0, query_utils_1.extractMechanismEntrySeeds)(query);
|
|
5032
|
+
if (seeds.length >= 2) {
|
|
5033
|
+
flowQuery = `${query} ${seeds.join(' ')}`;
|
|
5034
|
+
}
|
|
5035
|
+
else {
|
|
5036
|
+
const entryNames = [...subgraph.nodes.values()]
|
|
5037
|
+
.filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'interface')
|
|
5038
|
+
&& (0, query_utils_1.isImplementationEntrySymbol)(n.name, (0, query_utils_1.extractDomainSearchTerms)(query)))
|
|
5039
|
+
.map((n) => n.name)
|
|
5040
|
+
.slice(0, 6);
|
|
5041
|
+
if (entryNames.length >= 2)
|
|
5042
|
+
flowQuery = `${query} ${entryNames.join(' ')}`;
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
const flow = this.buildFlowFromNamedSymbols(cg, flowQuery);
|
|
5046
|
+
const hasFlowPath = flow.pathNodeIds.size > 0;
|
|
5047
|
+
budget = tightenExploreBudgetForQuery(budget, query, { hasFlowPath });
|
|
5048
|
+
// Honor an explicit maxFiles from the caller — budget.defaultMaxFiles is only
|
|
5049
|
+
// a default when the agent didn't ask for more (adaptive sibling tests pass 12).
|
|
5050
|
+
if (!explicitMaxFiles) {
|
|
5051
|
+
maxFiles = Math.min(maxFiles, (0, utils_1.clamp)(budget.defaultMaxFiles, 1, 20));
|
|
5052
|
+
}
|
|
5053
|
+
const localDetail = (0, query_utils_1.queryAsLocalSymbolDetail)(query);
|
|
5054
|
+
const inheritanceSection = !hasFlowPath && !multiAnchor
|
|
5055
|
+
? this.buildInheritanceSurveySection(cg, query) : '';
|
|
5056
|
+
const callerSection = !hasFlowPath && !multiAnchor && (0, query_utils_1.shouldBuildCallerInventory)(query)
|
|
5057
|
+
? this.buildCallerListingSection(cg, query) : '';
|
|
5058
|
+
const memberSection = !hasFlowPath && !multiAnchor && (0, query_utils_1.shouldBuildMemberSurvey)(query)
|
|
5059
|
+
? this.buildMemberSurveySection(cg, query, projectRoot) : '';
|
|
5060
|
+
const configSection = (0, query_utils_1.shouldBuildConfigSection)(query)
|
|
5061
|
+
? this.buildConfigFileSection(cg, query, projectRoot) : '';
|
|
5062
|
+
if (inheritanceSection)
|
|
5063
|
+
lines.push(inheritanceSection);
|
|
5064
|
+
if (callerSection)
|
|
5065
|
+
lines.push(callerSection);
|
|
5066
|
+
if (memberSection)
|
|
5067
|
+
lines.push(memberSection);
|
|
5068
|
+
if (configSection)
|
|
5069
|
+
lines.push(configSection);
|
|
5070
|
+
const finishCompact = (summary) => {
|
|
5071
|
+
lines[summaryLineIdx] = summary;
|
|
5072
|
+
return this.textResult(lines.join('\n'));
|
|
5073
|
+
};
|
|
5074
|
+
const memberFileCount = memberSection
|
|
5075
|
+
? memberSection.split('\n').filter((l) => l.startsWith('- ')).length
|
|
5076
|
+
: 0;
|
|
5077
|
+
const callerBulletCount = callerSection
|
|
5078
|
+
? callerSection.split('\n').filter((l) => l.startsWith('- ') && l.includes(' ← ')).length
|
|
5079
|
+
: 0;
|
|
5080
|
+
const inheritanceListed = inheritanceSection
|
|
5081
|
+
? inheritanceSection.split('\n').filter((l) => l.startsWith('- `')).length > 0
|
|
5082
|
+
: false;
|
|
5083
|
+
const omitSource = (0, query_utils_1.shouldOmitSourceBodies)({
|
|
5084
|
+
importSiteCount: importResult.siteCount,
|
|
5085
|
+
hasFilteredImports: importInventoryFilter && importResult.siteCount > 0,
|
|
5086
|
+
callerBulletCount,
|
|
5087
|
+
memberFileCount,
|
|
5088
|
+
apiUsageFileCount: apiUsageResult.fileCount,
|
|
5089
|
+
configRendered: !!configSection,
|
|
5090
|
+
kitModuleSurveyRendered: !!kitUsageResult.section,
|
|
5091
|
+
inheritanceListed,
|
|
5092
|
+
domainFileCount: domainFileResult.fileCount,
|
|
5093
|
+
dataSourceEdgeCount: dataSourceResult.edgeCount,
|
|
5094
|
+
}, hasFlowPath, multiAnchor);
|
|
5095
|
+
if (omitSource) {
|
|
5096
|
+
if (configSection) {
|
|
5097
|
+
return finishCompact('Config/manifest content above — answer from it directly.');
|
|
5098
|
+
}
|
|
5099
|
+
if (kitUsageResult.section) {
|
|
5100
|
+
return finishCompact(`Kit module usage survey — **${kitUsageResult.symbolCount}** imported symbol(s) across the repo. SDK definitions are not in this project; answer from the usage list above.`);
|
|
5101
|
+
}
|
|
5102
|
+
if (domainFileResult.section && domainFileResult.fileCount > 0) {
|
|
5103
|
+
return finishCompact(`Domain file survey — **${domainFileResult.fileCount}** related file(s) listed above. Source bodies omitted; answer from the inventory.`);
|
|
5104
|
+
}
|
|
5105
|
+
if (apiUsageResult.section && apiUsageResult.fileCount > 0) {
|
|
5106
|
+
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s) referencing the queried symbol(s). Source bodies omitted; answer from the list above.`);
|
|
5107
|
+
}
|
|
5108
|
+
if (dataSourceResult.section && dataSourceResult.edgeCount > 0) {
|
|
5109
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s) listed above. Source bodies omitted; answer from the inventory.`);
|
|
5110
|
+
}
|
|
5111
|
+
if (inheritanceListed) {
|
|
5112
|
+
return finishCompact('Inheritance survey above lists all direct subtypes found — source bodies omitted.');
|
|
5113
|
+
}
|
|
5114
|
+
if (importResult.compactListing) {
|
|
5115
|
+
return finishCompact(`Listed **${importResult.siteCount}** import site(s) for the queried symbol(s). Source bodies omitted — answer from the dependency list above.`);
|
|
5116
|
+
}
|
|
5117
|
+
if (callerBulletCount >= 1) {
|
|
5118
|
+
return finishCompact(`Caller inventory above lists **${callerBulletCount}** method(s) with external callers — source bodies omitted.`);
|
|
5119
|
+
}
|
|
5120
|
+
if (memberSection && memberFileCount >= 2) {
|
|
5121
|
+
return finishCompact(`Member/pattern usage in **${memberFileCount}** file(s) — source bodies omitted; answer from the inventory above.`);
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
// Blast radius only for structural flow / mechanism answers — skip on local-detail
|
|
5125
|
+
// and no-flow dumps (it's expensive and rarely changes the answer there).
|
|
5126
|
+
if (!localDetail && (hasFlowPath || mechanismSurvey || crossModuleFlow)) {
|
|
5127
|
+
const blastRadius = this.buildBlastRadiusSection(cg, subgraph);
|
|
5128
|
+
if (blastRadius)
|
|
5129
|
+
lines.push(blastRadius);
|
|
5130
|
+
}
|
|
5131
|
+
// Relationship map — show how symbols connect (skip when no flow path: saves
|
|
5132
|
+
// tokens on survey/dependency/how-to questions that don't need call graphs).
|
|
3039
5133
|
const significantEdges = subgraph.edges.filter(e => e.kind !== 'contains' // skip contains — it's implied by file grouping
|
|
3040
5134
|
);
|
|
3041
|
-
if (budget.includeRelationships &&
|
|
5135
|
+
if (budget.includeRelationships && hasFlowPath && !importResult.compactListing
|
|
5136
|
+
&& significantEdges.length > 0) {
|
|
3042
5137
|
lines.push('**Relationships**');
|
|
3043
5138
|
lines.push('');
|
|
3044
5139
|
// Group edges by kind for readability
|
|
@@ -3066,10 +5161,7 @@ class ToolHandler {
|
|
|
3066
5161
|
}
|
|
3067
5162
|
}
|
|
3068
5163
|
// Step 4: Read contiguous file sections
|
|
3069
|
-
//
|
|
3070
|
-
// and to gate adaptive source sizing: files on the spine get full source,
|
|
3071
|
-
// off-spine peers skeletonize.
|
|
3072
|
-
const flow = this.buildFlowFromNamedSymbols(cg, query);
|
|
5164
|
+
// (flow already computed above for relationship gating and adaptive sizing)
|
|
3073
5165
|
// Polymorphic-sibling detector for adaptive sizing. A class that implements/
|
|
3074
5166
|
// extends a supertype shared by >= MIN_SIBLINGS classes is one of many
|
|
3075
5167
|
// INTERCHANGEABLE implementations (OkHttp's 14 `: Interceptor` classes —
|
|
@@ -3126,7 +5218,9 @@ class ToolHandler {
|
|
|
3126
5218
|
};
|
|
3127
5219
|
lines.push('**Source Code**');
|
|
3128
5220
|
lines.push('');
|
|
3129
|
-
lines.push('>
|
|
5221
|
+
lines.push('> Line-numbered source — treat as already Read. Answer from Flow + Source below when you can; ' +
|
|
5222
|
+
'do not re-read/grep these files, and do not fan out `homegraph_node` / search for symbols already shown. ' +
|
|
5223
|
+
'Another `homegraph_explore` with tighter names only if a needed symbol is missing.');
|
|
3130
5224
|
lines.push('');
|
|
3131
5225
|
let totalChars = lines.join('\n').length;
|
|
3132
5226
|
let filesIncluded = 0;
|
|
@@ -3134,8 +5228,51 @@ class ToolHandler {
|
|
|
3134
5228
|
// (#1046) — it must reflect what we show, not the raw candidate gather.
|
|
3135
5229
|
const renderedFilePaths = [];
|
|
3136
5230
|
let anyFileTrimmed = false;
|
|
3137
|
-
|
|
3138
|
-
|
|
5231
|
+
const limitSingleFile = (0, query_utils_1.shouldLimitToQueryNamedFile)(query, hasFlowPath, multiAnchor)
|
|
5232
|
+
|| (interpretationQuery && queryFileBasenames.length === 1);
|
|
5233
|
+
let filesToRender = sortedFiles;
|
|
5234
|
+
if (limitSingleFile) {
|
|
5235
|
+
const anchored = sortedFiles.filter(([fp]) => (0, query_utils_1.fileMatchesQueryBasename)(fp, queryFileBasenames));
|
|
5236
|
+
if (anchored.length > 0)
|
|
5237
|
+
filesToRender = anchored.slice(0, 1);
|
|
5238
|
+
}
|
|
5239
|
+
else if ((0, query_utils_1.shouldFocusOnNamedTypeFile)(query, hasFlowPath, multiAnchor)) {
|
|
5240
|
+
const typeName = (0, query_utils_1.extractTypeNamesFromQuery)(query)[0];
|
|
5241
|
+
const anchored = sortedFiles.filter(([, group]) => group.nodes.some((n) => n.name === typeName && (n.kind === 'class' || n.kind === 'interface' || n.kind === 'struct')));
|
|
5242
|
+
if (anchored.length > 0)
|
|
5243
|
+
filesToRender = anchored.slice(0, 1);
|
|
5244
|
+
}
|
|
5245
|
+
else if ((0, query_utils_1.shouldFocusOnQueryNamedDefs)(query, hasFlowPath, multiAnchor)) {
|
|
5246
|
+
const nameSet = new Set([
|
|
5247
|
+
...(0, query_utils_1.extractTypeNamesFromQuery)(query),
|
|
5248
|
+
...(0, query_utils_1.extractDependencySymbolsFromQuery)(query),
|
|
5249
|
+
...(0, query_utils_1.extractMemberAccessFromQuery)(query).map((m) => m.member),
|
|
5250
|
+
]);
|
|
5251
|
+
const anchored = sortedFiles.filter(([, group]) => group.nodes.some((n) => nameSet.has(n.name)));
|
|
5252
|
+
if (anchored.length > 0) {
|
|
5253
|
+
filesToRender = anchored.slice(0, localDetail ? 2 : 3);
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
else if (crossModuleFlow && hasFlowPath) {
|
|
5257
|
+
const onSpine = sortedFiles.filter(([, group]) => group.nodes.some((n) => flow.pathNodeIds.has(n.id) || flow.uniqueNamedNodeIds.has(n.id)));
|
|
5258
|
+
if (onSpine.length > 0)
|
|
5259
|
+
filesToRender = onSpine;
|
|
5260
|
+
}
|
|
5261
|
+
const sourceFileCap = limitSingleFile && filesToRender.length === 1
|
|
5262
|
+
? 1
|
|
5263
|
+
: localDetail
|
|
5264
|
+
? Math.min(maxFiles, 2)
|
|
5265
|
+
: crossModuleFlow && hasFlowPath
|
|
5266
|
+
? Math.min(maxFiles, 5)
|
|
5267
|
+
: mechanismSurvey && hasFlowPath
|
|
5268
|
+
? Math.min(maxFiles, 4)
|
|
5269
|
+
: mechanismSurvey
|
|
5270
|
+
? Math.min(maxFiles, 3)
|
|
5271
|
+
: !hasFlowPath
|
|
5272
|
+
? Math.min(maxFiles, 3)
|
|
5273
|
+
: maxFiles;
|
|
5274
|
+
for (const [filePath, group] of filesToRender) {
|
|
5275
|
+
if (filesIncluded >= sourceFileCap)
|
|
3139
5276
|
break;
|
|
3140
5277
|
// A file DEFINES a named/spine symbol (the answer) vs merely references the
|
|
3141
5278
|
// flow. Past 90% budget, stop pulling INCIDENTAL files — but keep scanning
|
|
@@ -3431,6 +5568,39 @@ class ToolHandler {
|
|
|
3431
5568
|
ranges.push({ start: edge.line, end: edge.line, name: targetName, kind: edge.kind, importance: 2, spine: false });
|
|
3432
5569
|
}
|
|
3433
5570
|
}
|
|
5571
|
+
// Query member-access anchors: pin lines the question names (locationManager.on,
|
|
5572
|
+
// .drawModifier) so per-file budget gaps don't hide the exact call site.
|
|
5573
|
+
if (queryMemberAccesses.length > 0) {
|
|
5574
|
+
const anchorLines = new Set();
|
|
5575
|
+
for (const ma of queryMemberAccesses) {
|
|
5576
|
+
const patterns = [];
|
|
5577
|
+
if (ma.receiver) {
|
|
5578
|
+
const recv = ma.receiver.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
5579
|
+
const mem = ma.member.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
5580
|
+
patterns.push(new RegExp(`${recv}\\.${mem}\\s*\\(`));
|
|
5581
|
+
patterns.push(new RegExp(`${recv}\\.${mem}\\b`));
|
|
5582
|
+
}
|
|
5583
|
+
else if (ma.dotted.startsWith('.')) {
|
|
5584
|
+
const lit = ma.dotted.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
5585
|
+
patterns.push(new RegExp(`${lit}\\b`));
|
|
5586
|
+
}
|
|
5587
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
5588
|
+
const line = fileLines[i] ?? '';
|
|
5589
|
+
if (patterns.some((p) => p.test(line)))
|
|
5590
|
+
anchorLines.add(i + 1);
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
for (const lineNo of anchorLines) {
|
|
5594
|
+
ranges.push({
|
|
5595
|
+
start: lineNo,
|
|
5596
|
+
end: lineNo,
|
|
5597
|
+
name: 'query-anchor',
|
|
5598
|
+
kind: 'anchor',
|
|
5599
|
+
importance: 11,
|
|
5600
|
+
spine: false,
|
|
5601
|
+
});
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
3434
5604
|
ranges.sort((a, b) => a.start - b.start);
|
|
3435
5605
|
if (ranges.length === 0)
|
|
3436
5606
|
continue;
|
|
@@ -3713,11 +5883,17 @@ class ToolHandler {
|
|
|
3713
5883
|
const lastSection = cut.lastIndexOf('\n' + FILE_SECTION_PREFIX);
|
|
3714
5884
|
const boundary = lastSection > hardCeiling * 0.5 ? lastSection : cut.lastIndexOf('\n');
|
|
3715
5885
|
const safe = boundary > 0 ? cut.slice(0, boundary) : cut;
|
|
3716
|
-
finalText = safe + '\n\n... (output truncated to budget; the source above is complete and verbatim — treat it as already Read. For
|
|
5886
|
+
finalText = safe + '\n\n... (output truncated to budget; the source above is complete and verbatim — treat it as already Read. For uncovered files/symbols, run another homegraph_explore with their exact names — not grep/read/node for symbols already shown.)';
|
|
3717
5887
|
}
|
|
3718
5888
|
else {
|
|
3719
5889
|
finalText = output;
|
|
3720
5890
|
}
|
|
5891
|
+
if (!finalText.includes('**Partial result**') && !finalText.includes('> **Explore complete**')) {
|
|
5892
|
+
finalText +=
|
|
5893
|
+
'\n\n> **Explore complete — ANSWER NOW.** Flow + Source above are authoritative for this query. ' +
|
|
5894
|
+
'Do **not** grep/read/`homegraph_node`/`homegraph_search` for the same symbols or files. ' +
|
|
5895
|
+
'Only call one tighter `homegraph_explore` if a named symbol essential to the answer is missing.';
|
|
5896
|
+
}
|
|
3721
5897
|
// Curated header (#1046): substitute the sentinel with the count of files
|
|
3722
5898
|
// whose source SURVIVES in the final text — not `subgraph`/`fileGroups` (the
|
|
3723
5899
|
// raw gather a broad query inflates) and not `filesIncluded` (which can
|
|
@@ -3807,12 +5983,12 @@ class ToolHandler {
|
|
|
3807
5983
|
const list = matches.map((n) => `- \`${n.name}\` (${n.kind}) — ${n.filePath}:${n.startLine}`);
|
|
3808
5984
|
return this.textResult(this.truncateOutput([header, '', 'Re-query with `includeCode: true` to get every body in one call — no need to pick one first.', '', ...list].join('\n')));
|
|
3809
5985
|
}
|
|
3810
|
-
const BODY_BUDGET =
|
|
5986
|
+
const BODY_BUDGET = 10_000; // leaves room under MAX_OUTPUT_LENGTH for the header + list
|
|
3811
5987
|
// The CHAR budget is the real limiter — keep the count cap high so a set of
|
|
3812
5988
|
// SHORT overloads (Alamofire's 10 `validate` variants, each a few lines) all
|
|
3813
5989
|
// render in full rather than relegating the one the agent wanted to a
|
|
3814
5990
|
// bodiless list. Only a set of many LARGE bodies hits the char budget first.
|
|
3815
|
-
const HARD_CAP =
|
|
5991
|
+
const HARD_CAP = 12;
|
|
3816
5992
|
const rendered = [];
|
|
3817
5993
|
const listed = [];
|
|
3818
5994
|
let used = 0;
|
|
@@ -3948,12 +6124,12 @@ class ToolHandler {
|
|
|
3948
6124
|
const fileLines = content.split('\n');
|
|
3949
6125
|
const total = fileLines.length;
|
|
3950
6126
|
// Read-parity windowing: `offset`/`limit` mean exactly what they do on Read
|
|
3951
|
-
// (1-based start line; max line count). Default
|
|
3952
|
-
//
|
|
3953
|
-
//
|
|
3954
|
-
//
|
|
3955
|
-
const CHAR_BUDGET =
|
|
3956
|
-
const DEFAULT_LIMIT =
|
|
6127
|
+
// (1-based start line; max line count). Default window is intentionally
|
|
6128
|
+
// smaller than explore's flow budget — dumping a whole multi-kLOC file from
|
|
6129
|
+
// `homegraph_node` teaches the agent to over-drill and burns tokens.
|
|
6130
|
+
// Overflow is stated explicitly (pass offset/limit or name a symbol).
|
|
6131
|
+
const CHAR_BUDGET = 12_000;
|
|
6132
|
+
const DEFAULT_LIMIT = 400;
|
|
3957
6133
|
const offset = Math.max(1, opts.offset ?? 1);
|
|
3958
6134
|
if (offset > total) {
|
|
3959
6135
|
return this.textResult(`**${filePath}** has ${total} line${total === 1 ? '' : 's'} — offset ${offset} is past the end. ${depSummary}`);
|
|
@@ -3976,8 +6152,9 @@ class ToolHandler {
|
|
|
3976
6152
|
const complete = offset === 1 && shownEnd >= total;
|
|
3977
6153
|
const out = [header, '', ...numbered];
|
|
3978
6154
|
if (!complete) {
|
|
3979
|
-
out.push('', `(lines ${offset}–${shownEnd} of ${total} — pass \`offset\`/\`limit\` for another range, or \`homegraph_node
|
|
6155
|
+
out.push('', `(lines ${offset}–${shownEnd} of ${total} — pass \`offset\`/\`limit\` for another range, or \`homegraph_node\` with a symbol name for one body in full)`);
|
|
3980
6156
|
}
|
|
6157
|
+
out.push('', '> Treat this source as already Read. Prefer answering now; do not grep/read the same file or fan out more `homegraph_node` calls for symbols listed above.');
|
|
3981
6158
|
// Self-bounded to CHAR_BUDGET — do NOT route through truncateOutput (15k).
|
|
3982
6159
|
return this.textResult(out.join('\n'));
|
|
3983
6160
|
}
|
|
@@ -4744,25 +6921,47 @@ class ToolHandler {
|
|
|
4744
6921
|
if (tail && tail !== symbol)
|
|
4745
6922
|
results = cg.searchNodes(tail, { limit: 50 });
|
|
4746
6923
|
}
|
|
4747
|
-
|
|
6924
|
+
const exactMatches = results.filter(r => this.matchesSymbol(r.node, symbol));
|
|
6925
|
+
// Prefer exact name/qualified matches. Never fall back to the top FTS
|
|
6926
|
+
// hit — that resolved unrelated symbols (e.g. OnSurfaceChangedCB → a
|
|
6927
|
+
// function whose body merely mentions "surface") and poisoned callees.
|
|
6928
|
+
let pool = exactMatches.map((r) => r.node);
|
|
6929
|
+
if (pool.length === 0) {
|
|
6930
|
+
try {
|
|
6931
|
+
pool = cg.getNodesByName(symbol).filter((n) => this.matchesSymbol(n, symbol));
|
|
6932
|
+
}
|
|
6933
|
+
catch {
|
|
6934
|
+
pool = [];
|
|
6935
|
+
}
|
|
6936
|
+
}
|
|
6937
|
+
if (pool.length === 0 && /[.\/]|::/.test(symbol)) {
|
|
6938
|
+
const tail = lastQualifierPart(symbol);
|
|
6939
|
+
if (tail && tail !== symbol) {
|
|
6940
|
+
try {
|
|
6941
|
+
pool = cg.getNodesByName(tail).filter((n) => this.matchesSymbol(n, symbol) || this.matchesSymbol(n, tail));
|
|
6942
|
+
}
|
|
6943
|
+
catch {
|
|
6944
|
+
pool = [];
|
|
6945
|
+
}
|
|
6946
|
+
}
|
|
6947
|
+
}
|
|
6948
|
+
if (pool.length === 0) {
|
|
4748
6949
|
return { nodes: [], note: '' };
|
|
4749
6950
|
}
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
const node = exactMatches[0]?.node ?? results[0].node;
|
|
4753
|
-
return { nodes: [node], note: '' };
|
|
6951
|
+
if (pool.length === 1) {
|
|
6952
|
+
return { nodes: pool, note: '' };
|
|
4754
6953
|
}
|
|
4755
6954
|
// Same generated-file down-rank as findSymbol — keeps callers/callees
|
|
4756
6955
|
// /impact aggregation aligned (a query against "Send" returns the
|
|
4757
6956
|
// hand-written implementations before the protobuf scaffold).
|
|
4758
|
-
const ranked = [...
|
|
4759
|
-
const aGen = (0, generated_detection_1.isGeneratedFile)(a.
|
|
4760
|
-
const bGen = (0, generated_detection_1.isGeneratedFile)(b.
|
|
6957
|
+
const ranked = [...pool].sort((a, b) => {
|
|
6958
|
+
const aGen = (0, generated_detection_1.isGeneratedFile)(a.filePath) ? 1 : 0;
|
|
6959
|
+
const bGen = (0, generated_detection_1.isGeneratedFile)(b.filePath) ? 1 : 0;
|
|
4761
6960
|
return aGen - bGen;
|
|
4762
6961
|
});
|
|
4763
|
-
const locations = ranked.map(
|
|
6962
|
+
const locations = ranked.map(n => `${n.kind} at ${n.filePath}:${n.startLine}`);
|
|
4764
6963
|
const note = `\n\n> **Note:** Aggregated results across ${ranked.length} symbols named "${symbol}": ${locations.join(', ')}`;
|
|
4765
|
-
return { nodes: ranked
|
|
6964
|
+
return { nodes: ranked, note };
|
|
4766
6965
|
}
|
|
4767
6966
|
/**
|
|
4768
6967
|
* Truncate output if it exceeds the maximum length
|