homegraph 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +268 -243
- package/dist/arkts/ohos-api-index.d.ts +15 -0
- package/dist/arkts/ohos-api-index.d.ts.map +1 -0
- package/dist/arkts/ohos-api-index.js +190 -0
- package/dist/arkts/ohos-api-index.js.map +1 -0
- package/dist/arkts/ohos-sdk-input.d.ts +36 -0
- package/dist/arkts/ohos-sdk-input.d.ts.map +1 -0
- package/dist/arkts/ohos-sdk-input.js +214 -0
- package/dist/arkts/ohos-sdk-input.js.map +1 -0
- package/dist/bin/command-supervision.d.ts +12 -0
- package/dist/bin/command-supervision.d.ts.map +1 -0
- package/dist/bin/command-supervision.js +86 -0
- package/dist/bin/command-supervision.js.map +1 -0
- package/dist/bin/homegraph.d.ts +1 -1
- package/dist/bin/homegraph.js +971 -71
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/db/index.d.ts +20 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +39 -0
- 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 +58 -19
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +14 -0
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +314 -143
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +172 -152
- package/dist/directory.d.ts +32 -0
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +88 -5
- package/dist/directory.js.map +1 -1
- package/dist/extraction/arkts-batch-worker.d.ts +2 -0
- package/dist/extraction/arkts-batch-worker.d.ts.map +1 -0
- package/dist/extraction/arkts-batch-worker.js +28 -0
- package/dist/extraction/arkts-batch-worker.js.map +1 -0
- package/dist/extraction/context.d.ts +11 -0
- package/dist/extraction/context.d.ts.map +1 -1
- package/dist/extraction/context.js +20 -0
- package/dist/extraction/context.js.map +1 -1
- package/dist/extraction/index.d.ts +16 -2
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +619 -380
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/arkts-state-decorators.d.ts +13 -0
- package/dist/extraction/languages/arkts-state-decorators.d.ts.map +1 -0
- package/dist/extraction/languages/arkts-state-decorators.js +26 -0
- package/dist/extraction/languages/arkts-state-decorators.js.map +1 -0
- package/dist/extraction/languages/arkts-viewtree.d.ts +4 -2
- package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
- package/dist/extraction/languages/arkts-viewtree.js +21 -6
- package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
- package/dist/extraction/languages/arkts.d.ts +98 -2
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +1290 -61
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/extraction/languages/c-cpp.d.ts +56 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +198 -1
- package/dist/extraction/languages/c-cpp.js.map +1 -1
- package/dist/extraction/languages/ohos-api-consumer.d.ts +34 -0
- package/dist/extraction/languages/ohos-api-consumer.d.ts.map +1 -0
- package/dist/extraction/languages/ohos-api-consumer.js +283 -0
- package/dist/extraction/languages/ohos-api-consumer.js.map +1 -0
- package/dist/extraction/parse-pool.d.ts +126 -0
- package/dist/extraction/parse-pool.d.ts.map +1 -0
- package/dist/extraction/parse-pool.js +319 -0
- package/dist/extraction/parse-pool.js.map +1 -0
- package/dist/extraction/tree-sitter-types.d.ts +17 -0
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +21 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +198 -27
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/dist/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +76 -17
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -4
- package/dist/index.js.map +1 -1
- package/dist/installer/instructions-template.js +9 -9
- package/dist/installer/targets/shared.d.ts +5 -6
- package/dist/installer/targets/shared.d.ts.map +1 -1
- package/dist/installer/targets/shared.js +5 -6
- package/dist/installer/targets/shared.js.map +1 -1
- package/dist/mcp/daemon-paths.d.ts +30 -3
- package/dist/mcp/daemon-paths.d.ts.map +1 -1
- package/dist/mcp/daemon-paths.js +50 -10
- package/dist/mcp/daemon-paths.js.map +1 -1
- package/dist/mcp/daemon-registry.d.ts.map +1 -1
- package/dist/mcp/daemon-registry.js +7 -3
- package/dist/mcp/daemon-registry.js.map +1 -1
- package/dist/mcp/daemon.d.ts +48 -0
- package/dist/mcp/daemon.d.ts.map +1 -1
- package/dist/mcp/daemon.js +196 -32
- package/dist/mcp/daemon.js.map +1 -1
- package/dist/mcp/engine.d.ts +17 -0
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +73 -1
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +25 -43
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/liveness-watchdog.js +16 -16
- package/dist/mcp/ppid-watchdog.d.ts +18 -0
- package/dist/mcp/ppid-watchdog.d.ts.map +1 -1
- package/dist/mcp/ppid-watchdog.js +37 -0
- package/dist/mcp/ppid-watchdog.js.map +1 -1
- package/dist/mcp/query-cache.d.ts +25 -0
- package/dist/mcp/query-cache.d.ts.map +1 -0
- package/dist/mcp/query-cache.js +191 -0
- package/dist/mcp/query-cache.js.map +1 -0
- package/dist/mcp/query-pool.d.ts +94 -0
- package/dist/mcp/query-pool.d.ts.map +1 -0
- package/dist/mcp/query-pool.js +297 -0
- package/dist/mcp/query-pool.js.map +1 -0
- package/dist/mcp/query-worker.d.ts +24 -0
- package/dist/mcp/query-worker.d.ts.map +1 -0
- package/dist/mcp/query-worker.js +87 -0
- package/dist/mcp/query-worker.js.map +1 -0
- package/dist/mcp/server-instructions.d.ts +5 -7
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +72 -74
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/tools.d.ts +94 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +877 -71
- package/dist/mcp/tools.js.map +1 -1
- package/dist/project-config.d.ts +20 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +42 -2
- package/dist/project-config.js.map +1 -1
- package/dist/reasoning/login.js +1 -1
- package/dist/reasoning/login.js.map +1 -1
- package/dist/reasoning/reasoner.js +32 -32
- package/dist/resolution/c-fnptr-synthesizer.d.ts +0 -28
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.js +765 -79
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts +1 -1
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +72 -11
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- package/dist/resolution/cooperative-yield.d.ts +32 -0
- package/dist/resolution/cooperative-yield.d.ts.map +1 -0
- package/dist/resolution/cooperative-yield.js +42 -0
- package/dist/resolution/cooperative-yield.js.map +1 -0
- package/dist/resolution/index.d.ts +11 -2
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +72 -4
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/name-matcher.d.ts +22 -0
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +317 -20
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/spec/config.d.ts +39 -0
- package/dist/spec/config.d.ts.map +1 -0
- package/dist/spec/config.js +304 -0
- package/dist/spec/config.js.map +1 -0
- package/dist/spec/db/commit-node.d.ts +23 -0
- package/dist/spec/db/commit-node.d.ts.map +1 -0
- package/dist/spec/db/commit-node.js +62 -0
- package/dist/spec/db/commit-node.js.map +1 -0
- package/dist/spec/db/fragment-node.d.ts +24 -0
- package/dist/spec/db/fragment-node.d.ts.map +1 -0
- package/dist/spec/db/fragment-node.js +128 -0
- package/dist/spec/db/fragment-node.js.map +1 -0
- package/dist/spec/db/fts.d.ts +74 -0
- package/dist/spec/db/fts.d.ts.map +1 -0
- package/dist/spec/db/fts.js +324 -0
- package/dist/spec/db/fts.js.map +1 -0
- package/dist/spec/db/index.d.ts +13 -0
- package/dist/spec/db/index.d.ts.map +1 -0
- package/dist/spec/db/index.js +50 -0
- package/dist/spec/db/index.js.map +1 -0
- package/dist/spec/db/relations.d.ts +55 -0
- package/dist/spec/db/relations.d.ts.map +1 -0
- package/dist/spec/db/relations.js +158 -0
- package/dist/spec/db/relations.js.map +1 -0
- package/dist/spec/db/schema.d.ts +33 -0
- package/dist/spec/db/schema.d.ts.map +1 -0
- package/dist/spec/db/schema.js +119 -0
- package/dist/spec/db/schema.js.map +1 -0
- package/dist/spec/db/schema.sql +117 -0
- package/dist/spec/db/spec-node.d.ts +41 -0
- package/dist/spec/db/spec-node.d.ts.map +1 -0
- package/dist/spec/db/spec-node.js +114 -0
- package/dist/spec/db/spec-node.js.map +1 -0
- package/dist/spec/evolve/impact-locator.d.ts +13 -0
- package/dist/spec/evolve/impact-locator.d.ts.map +1 -0
- package/dist/spec/evolve/impact-locator.js +25 -0
- package/dist/spec/evolve/impact-locator.js.map +1 -0
- package/dist/spec/evolve/llm-client.d.ts +50 -0
- package/dist/spec/evolve/llm-client.d.ts.map +1 -0
- package/dist/spec/evolve/llm-client.js +176 -0
- package/dist/spec/evolve/llm-client.js.map +1 -0
- package/dist/spec/evolve/logic-checker.d.ts +12 -0
- package/dist/spec/evolve/logic-checker.d.ts.map +1 -0
- package/dist/spec/evolve/logic-checker.js +24 -0
- package/dist/spec/evolve/logic-checker.js.map +1 -0
- package/dist/spec/evolve/pipeline.d.ts +42 -0
- package/dist/spec/evolve/pipeline.d.ts.map +1 -0
- package/dist/spec/evolve/pipeline.js +567 -0
- package/dist/spec/evolve/pipeline.js.map +1 -0
- package/dist/spec/evolve/spec-rewriter.d.ts +42 -0
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -0
- package/dist/spec/evolve/spec-rewriter.js +230 -0
- package/dist/spec/evolve/spec-rewriter.js.map +1 -0
- package/dist/spec/graph/queries.d.ts +155 -0
- package/dist/spec/graph/queries.d.ts.map +1 -0
- package/dist/spec/graph/queries.js +440 -0
- package/dist/spec/graph/queries.js.map +1 -0
- package/dist/spec/llm/client.d.ts +29 -0
- package/dist/spec/llm/client.d.ts.map +1 -0
- package/dist/spec/llm/client.js +123 -0
- package/dist/spec/llm/client.js.map +1 -0
- package/dist/spec/llm/index.d.ts +3 -0
- package/dist/spec/llm/index.d.ts.map +1 -0
- package/dist/spec/llm/index.js +11 -0
- package/dist/spec/llm/index.js.map +1 -0
- package/dist/spec/llm/prompts.d.ts +13 -0
- package/dist/spec/llm/prompts.d.ts.map +1 -0
- package/dist/spec/llm/prompts.js +75 -0
- package/dist/spec/llm/prompts.js.map +1 -0
- package/dist/spec/mining/diff-parser.d.ts +33 -0
- package/dist/spec/mining/diff-parser.d.ts.map +1 -0
- package/dist/spec/mining/diff-parser.js +166 -0
- package/dist/spec/mining/diff-parser.js.map +1 -0
- package/dist/spec/mining/git-scanner.d.ts +103 -0
- package/dist/spec/mining/git-scanner.d.ts.map +1 -0
- package/dist/spec/mining/git-scanner.js +307 -0
- package/dist/spec/mining/git-scanner.js.map +1 -0
- package/dist/spec/mining/pipeline.d.ts +53 -0
- package/dist/spec/mining/pipeline.d.ts.map +1 -0
- package/dist/spec/mining/pipeline.js +178 -0
- package/dist/spec/mining/pipeline.js.map +1 -0
- package/dist/spec/mining/scope-resolver.d.ts +45 -0
- package/dist/spec/mining/scope-resolver.d.ts.map +1 -0
- package/dist/spec/mining/scope-resolver.js +103 -0
- package/dist/spec/mining/scope-resolver.js.map +1 -0
- package/dist/spec/mining/spec-extractor.d.ts +69 -0
- package/dist/spec/mining/spec-extractor.d.ts.map +1 -0
- package/dist/spec/mining/spec-extractor.js +369 -0
- package/dist/spec/mining/spec-extractor.js.map +1 -0
- package/dist/spec/types.d.ts +149 -0
- package/dist/spec/types.d.ts.map +1 -0
- package/dist/spec/types.js +15 -0
- package/dist/spec/types.js.map +1 -0
- package/dist/spec/utils.d.ts +167 -0
- package/dist/spec/utils.d.ts.map +1 -0
- package/dist/spec/utils.js +463 -0
- package/dist/spec/utils.js.map +1 -0
- package/dist/sync/worktree.d.ts +9 -0
- package/dist/sync/worktree.d.ts.map +1 -1
- package/dist/sync/worktree.js +40 -0
- package/dist/sync/worktree.js.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/shimmer-progress.d.ts +2 -0
- package/dist/ui/shimmer-progress.d.ts.map +1 -1
- package/dist/ui/shimmer-progress.js +19 -2
- package/dist/ui/shimmer-progress.js.map +1 -1
- package/dist/upgrade/index.js +1 -1
- package/dist/upgrade/index.js.map +1 -1
- package/package.json +58 -57
- package/scripts/add-lang/bench.sh +60 -60
- package/scripts/add-lang/check-grammar.mjs +75 -75
- package/scripts/add-lang/dump-ast.mjs +103 -103
- package/scripts/add-lang/verify-extraction.mjs +70 -70
- package/scripts/agent-eval/ab-adoption.sh +91 -91
- package/scripts/agent-eval/ab-hook.sh +86 -86
- package/scripts/agent-eval/ab-impl.sh +78 -78
- package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
- package/scripts/agent-eval/ab-sufficiency.sh +78 -78
- package/scripts/agent-eval/arms-F.sh +21 -21
- package/scripts/agent-eval/arms-matrix.sh +37 -37
- package/scripts/agent-eval/audit.sh +68 -68
- package/scripts/agent-eval/bench-readme.sh +28 -28
- package/scripts/agent-eval/bench-why-repo.sh +22 -22
- package/scripts/agent-eval/block-read-hook.sh +19 -19
- package/scripts/agent-eval/hook-settings.json +15 -15
- package/scripts/agent-eval/itrun.sh +120 -120
- package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
- package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
- package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
- package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
- package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
- package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
- package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
- package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
- package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
- package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
- package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
- package/scripts/agent-eval/offload-eval-setup.sh +24 -24
- package/scripts/agent-eval/offload-eval-styles.sh +71 -71
- package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
- package/scripts/agent-eval/offload-eval.md +76 -76
- package/scripts/agent-eval/parse-arms.mjs +116 -116
- package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
- package/scripts/agent-eval/parse-run.mjs +45 -45
- package/scripts/agent-eval/parse-session.mjs +93 -93
- package/scripts/agent-eval/probe-context.mjs +21 -21
- package/scripts/agent-eval/probe-explore.mjs +40 -40
- package/scripts/agent-eval/probe-node.mjs +20 -20
- package/scripts/agent-eval/probe-sweep.mjs +119 -119
- package/scripts/agent-eval/probe-trace.mjs +20 -20
- package/scripts/agent-eval/redirect-read-hook.sh +38 -38
- package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -0
- package/scripts/agent-eval/repro-daemon-clients.mjs +125 -0
- package/scripts/agent-eval/run-agent.sh +34 -34
- package/scripts/agent-eval/run-all.sh +69 -69
- package/scripts/agent-eval/run-arms.sh +56 -56
- package/scripts/agent-eval/seq-matrix.mjs +137 -137
- package/scripts/build-bundle.sh +118 -118
- package/scripts/exp_boundary_eval/README.md +247 -0
- 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/_test_mcp_chain.py +78 -0
- package/scripts/exp_boundary_eval/_test_stdin.py +8 -0
- package/scripts/exp_boundary_eval/_utils.py +1116 -0
- package/scripts/exp_boundary_eval/analyze.py +1313 -0
- package/scripts/exp_boundary_eval/data/agents.json +109 -0
- package/scripts/exp_boundary_eval/data/experiments.json +140 -0
- package/scripts/exp_boundary_eval/deveco_arm.py +519 -0
- package/scripts/exp_boundary_eval/run_all.py +378 -0
- package/scripts/exp_boundary_eval/run_one.py +165 -0
- package/scripts/exp_boundary_eval/run_session.py +158 -0
- package/scripts/exp_boundary_eval/setup.py +120 -0
- package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -0
- package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -0
- package/scripts/exp_boundary_eval/win_node_launcher.py +24 -0
- package/scripts/extract-release-notes.mjs +130 -130
- package/scripts/local-install.sh +41 -41
- package/scripts/npm-sdk.js +75 -75
- package/scripts/npm-shim.js +268 -246
- package/scripts/ohos-sdk-publish.mjs +133 -0
- package/scripts/pack-npm.sh +119 -119
- package/scripts/prepare-release.mjs +270 -270
- package/scripts/qa_eval/README.md +407 -404
- package/scripts/qa_eval/_test_deveco_probe.py +41 -41
- package/scripts/qa_eval/agent_runner.py +526 -526
- package/scripts/qa_eval/data/.gitignore +4 -4
- package/scripts/qa_eval/data/test-set.jsonl +2 -22
- package/scripts/qa_eval/eval_metrics.py +274 -233
- package/scripts/qa_eval/external_agent.py +976 -671
- package/scripts/qa_eval/llm_config.py +92 -92
- package/scripts/qa_eval/memory_monitor.py +132 -132
- package/scripts/qa_eval/my_answer_accuracy.py +187 -187
- package/scripts/qa_eval/requirements.txt +2 -2
- package/scripts/qa_eval/run_pipeline.py +804 -711
- package/scripts/qa_eval/stats_efficiency.py +279 -279
- package/scripts/qa_eval/stats_scores.py +207 -207
package/dist/mcp/tools.js
CHANGED
|
@@ -24,7 +24,18 @@ const query_utils_1 = require("../search/query-utils");
|
|
|
24
24
|
const fs_1 = require("fs");
|
|
25
25
|
const utils_1 = require("../utils");
|
|
26
26
|
const generated_detection_1 = require("../extraction/generated-detection");
|
|
27
|
+
const arkts_1 = require("../extraction/languages/arkts");
|
|
27
28
|
const dynamic_boundaries_1 = require("./dynamic-boundaries");
|
|
29
|
+
const query_cache_1 = require("./query-cache");
|
|
30
|
+
/** ViewTree structural `references` vias — not UI event bindings. */
|
|
31
|
+
const VIEWTREE_STRUCTURE_VIAS = new Set([
|
|
32
|
+
'child-component',
|
|
33
|
+
'state-binding',
|
|
34
|
+
'Prop',
|
|
35
|
+
'Link',
|
|
36
|
+
'builder',
|
|
37
|
+
'builder-param',
|
|
38
|
+
]);
|
|
28
39
|
/**
|
|
29
40
|
* An expected, recoverable "homegraph can't serve this" condition — most
|
|
30
41
|
* importantly a project with no index. The dispatch catch converts these to
|
|
@@ -267,6 +278,29 @@ function numberSourceLines(slice, firstLineNumber) {
|
|
|
267
278
|
}
|
|
268
279
|
return out.join('\n');
|
|
269
280
|
}
|
|
281
|
+
/** Primary signature line (first line when overloads are stored newline-separated). */
|
|
282
|
+
function primarySignatureLine(signature) {
|
|
283
|
+
return signature.split('\n')[0]?.trim() ?? signature.trim();
|
|
284
|
+
}
|
|
285
|
+
/** Render a stored signature (single line or newline-separated overloads) for MCP output. */
|
|
286
|
+
function formatNodeSignatureBlock(signature) {
|
|
287
|
+
const lines = signature.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
288
|
+
if (lines.length <= 1) {
|
|
289
|
+
return lines.length ? [`**Signature:** \`${lines[0]}\``] : [];
|
|
290
|
+
}
|
|
291
|
+
return [
|
|
292
|
+
'**Signature:**',
|
|
293
|
+
`- \`${lines[0]}\` (primary)`,
|
|
294
|
+
...lines.slice(1).map((l) => `- \`${l}\` (overload)`),
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
function formatInlineSignature(signature) {
|
|
298
|
+
const primary = primarySignatureLine(signature);
|
|
299
|
+
const overloadCount = signature.split('\n').filter((l) => l.trim()).length - 1;
|
|
300
|
+
if (overloadCount <= 0)
|
|
301
|
+
return primary;
|
|
302
|
+
return `${primary} (+${overloadCount} overload${overloadCount === 1 ? '' : 's'})`;
|
|
303
|
+
}
|
|
270
304
|
/**
|
|
271
305
|
* Unique line-prefix for a per-file source section in homegraph_explore output.
|
|
272
306
|
* Issue #778: tool results dropped ATX headings (`####`, `##`, `###`) for bold
|
|
@@ -278,6 +312,12 @@ function numberSourceLines(slice, firstLineNumber) {
|
|
|
278
312
|
* (`reasoning/reasoner.ts`) both key off to cut on whole file sections.
|
|
279
313
|
*/
|
|
280
314
|
const FILE_SECTION_PREFIX = '**`';
|
|
315
|
+
// Placeholder for codegraph_explore's "Found N symbols across M files." line.
|
|
316
|
+
// The honest N/M can only be known after the final truncation drops trailing
|
|
317
|
+
// sections (#1046), so the header is emitted as this sentinel and substituted
|
|
318
|
+
// at the very end. This bracketed token never occurs in rendered source or a
|
|
319
|
+
// file path, so the final string-replace can't collide.
|
|
320
|
+
const SUMMARY_SENTINEL = '[[codegraph-explore-summary]]';
|
|
281
321
|
function fileSectionHeader(filePath, suffix) {
|
|
282
322
|
return suffix
|
|
283
323
|
? `${FILE_SECTION_PREFIX}${filePath}\`** — ${suffix}`
|
|
@@ -340,6 +380,23 @@ const projectPathProperty = {
|
|
|
340
380
|
type: 'string',
|
|
341
381
|
description: 'Absolute path to the project to query (or any directory inside it) — homegraph uses the nearest .homegraph/ index at or above that path. Omit to use this session\'s default project. Pass it to query a second codebase, or when the server root has no index of its own (e.g. a monorepo where only sub-projects are indexed, so there is no default project).',
|
|
342
382
|
};
|
|
383
|
+
/**
|
|
384
|
+
* EVERY homegraph tool is query-only: it reads the pre-built index and never
|
|
385
|
+
* mutates the workspace (indexing is the user's explicit CLI call, never the
|
|
386
|
+
* agent's). Advertising this read-only contract lets clients that gate on it run
|
|
387
|
+
* the tools where a possibly-mutating tool would be blocked — most concretely,
|
|
388
|
+
* Cursor's Ask mode, which rejects any MCP tool lacking `readOnlyHint: true`
|
|
389
|
+
* (issue #1018). `idempotentHint`: a repeated query has no additional effect.
|
|
390
|
+
* `openWorldHint: false`: the domain is the closed local index, not an open
|
|
391
|
+
* external world. Shared so the contract is declared once; a hypothetical
|
|
392
|
+
* mutating tool would simply not reference it.
|
|
393
|
+
*/
|
|
394
|
+
const READ_ONLY_ANNOTATIONS = {
|
|
395
|
+
readOnlyHint: true,
|
|
396
|
+
destructiveHint: false,
|
|
397
|
+
idempotentHint: true,
|
|
398
|
+
openWorldHint: false,
|
|
399
|
+
};
|
|
343
400
|
/**
|
|
344
401
|
* All HomeGraph MCP tools
|
|
345
402
|
*
|
|
@@ -374,6 +431,7 @@ exports.tools = [
|
|
|
374
431
|
},
|
|
375
432
|
required: ['query'],
|
|
376
433
|
},
|
|
434
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
377
435
|
},
|
|
378
436
|
{
|
|
379
437
|
name: 'homegraph_callers',
|
|
@@ -398,6 +456,7 @@ exports.tools = [
|
|
|
398
456
|
},
|
|
399
457
|
required: ['symbol'],
|
|
400
458
|
},
|
|
459
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
401
460
|
},
|
|
402
461
|
{
|
|
403
462
|
name: 'homegraph_callees',
|
|
@@ -422,6 +481,7 @@ exports.tools = [
|
|
|
422
481
|
},
|
|
423
482
|
required: ['symbol'],
|
|
424
483
|
},
|
|
484
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
425
485
|
},
|
|
426
486
|
{
|
|
427
487
|
name: 'homegraph_impact',
|
|
@@ -446,6 +506,7 @@ exports.tools = [
|
|
|
446
506
|
},
|
|
447
507
|
required: ['symbol'],
|
|
448
508
|
},
|
|
509
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
449
510
|
},
|
|
450
511
|
{
|
|
451
512
|
name: 'homegraph_node',
|
|
@@ -487,6 +548,7 @@ exports.tools = [
|
|
|
487
548
|
},
|
|
488
549
|
required: [],
|
|
489
550
|
},
|
|
551
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
490
552
|
},
|
|
491
553
|
{
|
|
492
554
|
name: 'homegraph_explore',
|
|
@@ -507,6 +569,7 @@ exports.tools = [
|
|
|
507
569
|
},
|
|
508
570
|
required: ['query'],
|
|
509
571
|
},
|
|
572
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
510
573
|
},
|
|
511
574
|
{
|
|
512
575
|
name: 'homegraph_status',
|
|
@@ -517,6 +580,7 @@ exports.tools = [
|
|
|
517
580
|
projectPath: projectPathProperty,
|
|
518
581
|
},
|
|
519
582
|
},
|
|
583
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
520
584
|
},
|
|
521
585
|
{
|
|
522
586
|
name: 'homegraph_files',
|
|
@@ -550,8 +614,143 @@ exports.tools = [
|
|
|
550
614
|
projectPath: projectPathProperty,
|
|
551
615
|
},
|
|
552
616
|
},
|
|
617
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: 'homegraph_spec_match',
|
|
621
|
+
description: 'Match a new spec/feature description against the Commit4Spec knowledge graph using FTS5 full-text search. ' +
|
|
622
|
+
'Returns the most similar historical specs with their associated commits and code fragments. ' +
|
|
623
|
+
'The database defaults to .homegraph/commit4spec/commit4spec.db under the repo path.',
|
|
624
|
+
inputSchema: {
|
|
625
|
+
type: 'object',
|
|
626
|
+
properties: {
|
|
627
|
+
query: {
|
|
628
|
+
type: 'string',
|
|
629
|
+
description: 'Spec text (title + description) to match against historical specs.',
|
|
630
|
+
},
|
|
631
|
+
repoPath: {
|
|
632
|
+
type: 'string',
|
|
633
|
+
description: 'Path to the repository root. Defaults to the current working directory.',
|
|
634
|
+
},
|
|
635
|
+
topK: {
|
|
636
|
+
type: 'number',
|
|
637
|
+
description: 'Maximum number of similar specs to return (default: 5).',
|
|
638
|
+
default: 5,
|
|
639
|
+
},
|
|
640
|
+
includeFragments: {
|
|
641
|
+
type: 'boolean',
|
|
642
|
+
description: 'Whether to include full code diffs per commit (default: true).',
|
|
643
|
+
default: true,
|
|
644
|
+
},
|
|
645
|
+
dbPath: {
|
|
646
|
+
type: 'string',
|
|
647
|
+
description: 'Explicit path to the Commit4Spec database. Overrides repoPath-based resolution.',
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
required: ['query'],
|
|
651
|
+
},
|
|
652
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
name: 'homegraph_spec_find',
|
|
656
|
+
description: 'Find which specs are related to the given file path by matching against code-fragment file paths ' +
|
|
657
|
+
'in the Commit4Spec knowledge graph. Traverses code_fragment_nodes → commit_fragment_relations ' +
|
|
658
|
+
'→ spec_commit_relations → spec_nodes. Useful for answering "which specs does this file affect?" ' +
|
|
659
|
+
'The database defaults to .homegraph/commit4spec/commit4spec.db under the repo path.',
|
|
660
|
+
inputSchema: {
|
|
661
|
+
type: 'object',
|
|
662
|
+
properties: {
|
|
663
|
+
filePath: {
|
|
664
|
+
type: 'string',
|
|
665
|
+
description: 'File path to look up (substring LIKE match). E.g. "src/auth.ts" or "src/auth".',
|
|
666
|
+
},
|
|
667
|
+
repoPath: {
|
|
668
|
+
type: 'string',
|
|
669
|
+
description: 'Path to the repository root. Defaults to the current working directory.',
|
|
670
|
+
},
|
|
671
|
+
dbPath: {
|
|
672
|
+
type: 'string',
|
|
673
|
+
description: 'Explicit path to the Commit4Spec database. Overrides repoPath-based resolution.',
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
required: ['filePath'],
|
|
677
|
+
},
|
|
678
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: 'homegraph_spec_trace',
|
|
682
|
+
description: 'Trace a code symbol (function, method, class) back to its associated design Specs in the Commit4Spec ' +
|
|
683
|
+
'knowledge graph. Resolves the symbol via the HomeGraph code index, then matches against code-fragment ' +
|
|
684
|
+
'records in the Spec database using five-dimensional scoring: file-path match, code-diff content search ' +
|
|
685
|
+
'(FTS5), Spec title/subtitle name match, Spec recency, and line-range overlap. ' +
|
|
686
|
+
'Returns ranked Specs with score breakdowns even when exact line overlap is absent — code drifts over ' +
|
|
687
|
+
'time, so recency and content matching compensate. ' +
|
|
688
|
+
'The Spec DB defaults to .homegraph/commit4spec/commit4spec.db under the repo path.',
|
|
689
|
+
inputSchema: {
|
|
690
|
+
type: 'object',
|
|
691
|
+
properties: {
|
|
692
|
+
symbol: {
|
|
693
|
+
type: 'string',
|
|
694
|
+
description: 'Symbol name (bare or qualified). E.g. "authenticate", "AuthService.login", "auth::validate".',
|
|
695
|
+
},
|
|
696
|
+
file: {
|
|
697
|
+
type: 'string',
|
|
698
|
+
description: 'File path for disambiguation when multiple symbols share the same name (optional).',
|
|
699
|
+
},
|
|
700
|
+
line: {
|
|
701
|
+
type: 'number',
|
|
702
|
+
description: 'Line number for disambiguation (optional).',
|
|
703
|
+
},
|
|
704
|
+
repoPath: {
|
|
705
|
+
type: 'string',
|
|
706
|
+
description: 'Path to the repository root. Defaults to the current working directory.',
|
|
707
|
+
},
|
|
708
|
+
topK: {
|
|
709
|
+
type: 'number',
|
|
710
|
+
description: 'Maximum number of matching Specs to return (default: 10).',
|
|
711
|
+
default: 10,
|
|
712
|
+
},
|
|
713
|
+
dbPath: {
|
|
714
|
+
type: 'string',
|
|
715
|
+
description: 'Explicit path to the Commit4Spec database. Overrides repoPath-based resolution.',
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
required: ['symbol'],
|
|
719
|
+
},
|
|
720
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
553
721
|
},
|
|
554
722
|
];
|
|
723
|
+
/**
|
|
724
|
+
* Return `defs` with `projectPath` marked `required` in each tool's inputSchema.
|
|
725
|
+
*
|
|
726
|
+
* Used for the NO-DEFAULT-PROJECT tool surface (issue #993): when the MCP server
|
|
727
|
+
* has no default project to fall back to — a gateway server started outside any
|
|
728
|
+
* repo, or a monorepo root whose `.codegraph/` indexes live only in sub-projects
|
|
729
|
+
* — every call MUST carry an explicit `projectPath`, so the schema should say so.
|
|
730
|
+
* A `required` field is a HIGH-salience channel (MCP clients surface and often
|
|
731
|
+
* validate it), unlike the instructions text the reporter found too weak to stop
|
|
732
|
+
* the agent omitting the param. When a default project IS open, callers leave
|
|
733
|
+
* projectPath optional and never call this.
|
|
734
|
+
*
|
|
735
|
+
* Pure: clones each tool's schema rather than mutating the shared module-level
|
|
736
|
+
* `tools` array (reused by every session and the static surface). A tool that
|
|
737
|
+
* doesn't expose projectPath, or already requires it, is returned untouched;
|
|
738
|
+
* explore's `['query']` becomes `['query', 'projectPath']`, and a tool with no
|
|
739
|
+
* `required` list (status/files) gains `['projectPath']`.
|
|
740
|
+
*/
|
|
741
|
+
function withRequiredProjectPath(defs) {
|
|
742
|
+
return defs.map((tool) => {
|
|
743
|
+
if (!tool.inputSchema.properties.projectPath)
|
|
744
|
+
return tool;
|
|
745
|
+
const required = tool.inputSchema.required ?? [];
|
|
746
|
+
if (required.includes('projectPath'))
|
|
747
|
+
return tool;
|
|
748
|
+
return {
|
|
749
|
+
...tool,
|
|
750
|
+
inputSchema: { ...tool.inputSchema, required: [...required, 'projectPath'] },
|
|
751
|
+
};
|
|
752
|
+
});
|
|
753
|
+
}
|
|
555
754
|
/**
|
|
556
755
|
* Allowlist-filtered tool definitions WITHOUT an engine — the static surface the
|
|
557
756
|
* proxy answers `tools/list` with before any project is open. Mirrors
|
|
@@ -561,23 +760,11 @@ exports.tools = [
|
|
|
561
760
|
function getStaticTools() {
|
|
562
761
|
const raw = process.env.HOMEGRAPH_MCP_TOOLS ?? process.env.HOMEGRAPH_MCP_TOOLS;
|
|
563
762
|
if (!raw || !raw.trim()) {
|
|
564
|
-
return exports.tools
|
|
763
|
+
return exports.tools;
|
|
565
764
|
}
|
|
566
765
|
const allow = new Set(raw.split(',').map(s => s.trim().replace(/^homegraph_/, '').replace(/^homegraph_/, '')).filter(Boolean));
|
|
567
766
|
return allow.size ? exports.tools.filter(t => allow.has(t.name.replace(/^homegraph_/, ''))) : exports.tools;
|
|
568
767
|
}
|
|
569
|
-
/**
|
|
570
|
-
* The MCP tools served by DEFAULT (short names). Pared to ONLY `homegraph_explore`
|
|
571
|
-
* — the single tool that reliably earns its place: one capped call returns the
|
|
572
|
-
* verbatim source of the relevant symbols grouped by file. Every other tool is a
|
|
573
|
-
* narrower slice of what explore already does, and presence itself steers
|
|
574
|
-
* mis-picks, so they are no longer LISTED to agents.
|
|
575
|
-
*
|
|
576
|
-
* The other defined tools (`node`, `search`, `callers`, plus callees/impact/files/
|
|
577
|
-
* status) remain fully functional — handlers stay, the library API and CLI are
|
|
578
|
-
* untouched, and `HOMEGRAPH_MCP_TOOLS=explore,node,...` re-enables any of them.
|
|
579
|
-
*/
|
|
580
|
-
const DEFAULT_MCP_TOOLS = new Set(['explore']);
|
|
581
768
|
/**
|
|
582
769
|
* Tool handler that executes tools against a HomeGraph instance
|
|
583
770
|
*
|
|
@@ -607,9 +794,23 @@ class ToolHandler {
|
|
|
607
794
|
// huge repo can't hang the first call (#905); cleared on first await so
|
|
608
795
|
// subsequent calls don't pay any cost.
|
|
609
796
|
catchUpGate = null;
|
|
797
|
+
// Optional worker-thread pool for off-loop read-tool dispatch (daemon mode).
|
|
798
|
+
// When set + healthy, the heavy read tools run on a worker so the daemon's
|
|
799
|
+
// main loop stays free for the MCP transport under concurrent load. Null in
|
|
800
|
+
// direct/in-process mode (one client, no concurrency to parallelize).
|
|
801
|
+
queryPool = null;
|
|
610
802
|
constructor(cg) {
|
|
611
803
|
this.cg = cg;
|
|
612
804
|
}
|
|
805
|
+
/**
|
|
806
|
+
* Engine-only: attach (or detach with null) the worker-thread query pool. The
|
|
807
|
+
* shared daemon sets this once its default project is open; the workers each
|
|
808
|
+
* hold their own WAL read connection and run {@link executeReadTool}. A
|
|
809
|
+
* worker's own ToolHandler never has a pool, so there is no nested off-loading.
|
|
810
|
+
*/
|
|
811
|
+
setQueryPool(pool) {
|
|
812
|
+
this.queryPool = pool;
|
|
813
|
+
}
|
|
613
814
|
/**
|
|
614
815
|
* Update the default HomeGraph instance (e.g. after lazy initialization)
|
|
615
816
|
*/
|
|
@@ -680,10 +881,11 @@ class ToolHandler {
|
|
|
680
881
|
}
|
|
681
882
|
/**
|
|
682
883
|
* Optional allowlist of exposed tools, parsed from the HOMEGRAPH_MCP_TOOLS
|
|
683
|
-
* env var (comma-separated short names, e.g. "
|
|
684
|
-
* Unset/empty → every tool is exposed.
|
|
685
|
-
*
|
|
686
|
-
*
|
|
884
|
+
* env var (comma-separated short names, e.g. "explore,search,node").
|
|
885
|
+
* Unset/empty → every tool is exposed. Set → only the listed tools are
|
|
886
|
+
* exposed. Lets an operator (or an A/B harness) trim the tool surface
|
|
887
|
+
* without rebuilding the client config; the ablated tool is then truly
|
|
888
|
+
* absent from ListTools rather than merely denied on call.
|
|
687
889
|
* Matching is on the short form, so "node" and "homegraph_node" both work.
|
|
688
890
|
*/
|
|
689
891
|
toolAllowlist() {
|
|
@@ -707,14 +909,23 @@ class ToolHandler {
|
|
|
707
909
|
*/
|
|
708
910
|
getTools() {
|
|
709
911
|
const allow = this.toolAllowlist();
|
|
710
|
-
// No explicit allowlist →
|
|
711
|
-
//
|
|
712
|
-
// default entirely, so any defined tool can be re-enabled.
|
|
912
|
+
// No explicit allowlist → expose every defined tool. An allowlist trims
|
|
913
|
+
// the surface to only the listed short names.
|
|
713
914
|
let visible = allow
|
|
714
915
|
? exports.tools.filter(t => allow.has(t.name.replace(/^homegraph_/, '')))
|
|
715
|
-
: exports.tools
|
|
916
|
+
: exports.tools;
|
|
917
|
+
// No default project loaded → no-root-index case (#993): a gateway server
|
|
918
|
+
// started outside any repo, or a monorepo root whose indexes live in
|
|
919
|
+
// sub-projects. With nothing to fall back to, EVERY call needs an explicit
|
|
920
|
+
// projectPath, so mark it required in the schema — a high-salience nudge the
|
|
921
|
+
// agent acts on, where SERVER_INSTRUCTIONS_NO_ROOT_INDEX's prose alone
|
|
922
|
+
// wasn't enough (the reporter had to add an AGENTS.md note). `this.cg` is
|
|
923
|
+
// settled by `retryInitIfNeeded()` before `handleToolsList` calls us, so a
|
|
924
|
+
// null here means "genuinely no default", not a startup race. When a default
|
|
925
|
+
// IS open we leave projectPath optional (below): a bare call falls back to
|
|
926
|
+
// it, exactly as in the common single-project launch.
|
|
716
927
|
if (!this.cg)
|
|
717
|
-
return visible;
|
|
928
|
+
return withRequiredProjectPath(visible);
|
|
718
929
|
try {
|
|
719
930
|
const stats = this.cg.getStats();
|
|
720
931
|
const budget = getExploreBudget(stats.fileCount);
|
|
@@ -1113,45 +1324,58 @@ class ToolHandler {
|
|
|
1113
1324
|
if (typeof check === 'object' && check !== undefined)
|
|
1114
1325
|
return check;
|
|
1115
1326
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1327
|
+
const projectPath = args.projectPath;
|
|
1328
|
+
const cacheEnabled = (0, query_cache_1.isMcpQueryCacheEnabled)() && (0, query_cache_1.isCacheableMcpTool)(toolName);
|
|
1329
|
+
let cacheKey;
|
|
1330
|
+
let cacheQueries;
|
|
1331
|
+
if (cacheEnabled) {
|
|
1332
|
+
try {
|
|
1333
|
+
const cacheCg = this.getHomeGraph(projectPath);
|
|
1334
|
+
cacheQueries = cacheCg.getQueryBuilder();
|
|
1335
|
+
(0, query_cache_1.ensureMcpQueryCacheValid)(cacheQueries, () => cacheCg.getLastIndexedAt());
|
|
1336
|
+
let fileCount;
|
|
1337
|
+
try {
|
|
1338
|
+
fileCount = cacheCg.getStats().fileCount;
|
|
1339
|
+
}
|
|
1340
|
+
catch {
|
|
1341
|
+
fileCount = undefined;
|
|
1342
|
+
}
|
|
1343
|
+
cacheKey = (0, query_cache_1.buildMcpQueryCacheKey)(toolName, args, fileCount);
|
|
1344
|
+
const cached = (0, query_cache_1.getMcpQueryCacheEntry)(cacheQueries, cacheKey);
|
|
1345
|
+
if (cached) {
|
|
1346
|
+
const withWorktree = this.withWorktreeNotice(cached, projectPath);
|
|
1347
|
+
return this.withStalenessNotice(withWorktree, projectPath);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
catch {
|
|
1351
|
+
// No indexed project — fall through; handler returns guidance.
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
// homegraph_status reports watcher state (pending files, degraded mode,
|
|
1355
|
+
// worktree warning) and embeds its own sections — it must run on the MAIN
|
|
1356
|
+
// thread against the watched default instance, so it is NEVER off-loaded to
|
|
1357
|
+
// a worker (whose read connection has no watcher). It also skips the
|
|
1358
|
+
// auto-banner wrapper to avoid duplicating its own pending-files section.
|
|
1359
|
+
if (toolName === 'homegraph_status') {
|
|
1360
|
+
return await this.handleStatus(args);
|
|
1361
|
+
}
|
|
1362
|
+
// Read tools: off-load the CPU-heavy dispatch to the worker pool when one
|
|
1363
|
+
// is attached and healthy (daemon mode), so the daemon's single event loop
|
|
1364
|
+
// stays free for the MCP transport under concurrent load — otherwise N
|
|
1365
|
+
// concurrent explores serialize AND starve the transport until the whole
|
|
1366
|
+
// batch drains (clients then time out). With no pool (direct mode) or a
|
|
1367
|
+
// degraded one, dispatch runs in-process exactly as before. Either way the
|
|
1368
|
+
// result flows through the cross-cutting notices — worktree-index mismatch
|
|
1369
|
+
// (#155) and per-file staleness (#403) — which need the watched MAIN
|
|
1370
|
+
// instance and so are always applied here, never in the worker.
|
|
1371
|
+
const result = (this.queryPool && this.queryPool.healthy)
|
|
1372
|
+
? await this.queryPool.run(toolName, args)
|
|
1373
|
+
: await this.executeReadTool(toolName, args);
|
|
1374
|
+
if (cacheEnabled && cacheKey && cacheQueries && !result.isError) {
|
|
1375
|
+
(0, query_cache_1.setMcpQueryCacheEntry)(cacheQueries, cacheKey, toolName, result);
|
|
1152
1376
|
}
|
|
1153
|
-
const withWorktree = this.withWorktreeNotice(result,
|
|
1154
|
-
return this.withStalenessNotice(withWorktree,
|
|
1377
|
+
const withWorktree = this.withWorktreeNotice(result, projectPath);
|
|
1378
|
+
return this.withStalenessNotice(withWorktree, projectPath);
|
|
1155
1379
|
}
|
|
1156
1380
|
catch (err) {
|
|
1157
1381
|
// Expected condition, not a malfunction: answer as a SUCCESS so the
|
|
@@ -1169,6 +1393,56 @@ class ToolHandler {
|
|
|
1169
1393
|
'continue without homegraph for this task.');
|
|
1170
1394
|
}
|
|
1171
1395
|
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Run a single read tool to completion and return its raw {@link ToolResult},
|
|
1398
|
+
* classifying expected failures the same way {@link execute}'s catch does so
|
|
1399
|
+
* the SHAPE is identical whether dispatch runs in-process or on a worker:
|
|
1400
|
+
* NotIndexed → success-shaped guidance, PathRefusal → clean error, anything
|
|
1401
|
+
* else → internal-error-with-retry. Never throws.
|
|
1402
|
+
*
|
|
1403
|
+
* This is the worker thread's entry point (see {@link ./query-worker}) and the
|
|
1404
|
+
* in-process fallback for {@link execute}. It deliberately does NOT run the
|
|
1405
|
+
* catch-up gate or the staleness/worktree notices — those need the daemon's
|
|
1406
|
+
* watched main instance and stay on the main thread. Cross-cutting allowlist +
|
|
1407
|
+
* path validation already ran in {@link execute} before routing here.
|
|
1408
|
+
*/
|
|
1409
|
+
async executeReadTool(toolName, args) {
|
|
1410
|
+
try {
|
|
1411
|
+
return await this.dispatchTool(toolName, args);
|
|
1412
|
+
}
|
|
1413
|
+
catch (err) {
|
|
1414
|
+
if (err instanceof NotIndexedError) {
|
|
1415
|
+
return this.textResult(err.message);
|
|
1416
|
+
}
|
|
1417
|
+
if (err instanceof PathRefusalError) {
|
|
1418
|
+
return this.errorResult(err.message);
|
|
1419
|
+
}
|
|
1420
|
+
return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
1421
|
+
'This is an internal homegraph error — retry the call once; if it persists, ' +
|
|
1422
|
+
'continue without homegraph for this task.');
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Pure dispatch over the read tools — the switch, with no gate, no notices, no
|
|
1427
|
+
* allowlist/validation (the caller owns those). `homegraph_status` is handled
|
|
1428
|
+
* on the main thread in {@link execute} and never reaches here. May throw
|
|
1429
|
+
* NotIndexed/PathRefusal, which {@link executeReadTool} classifies.
|
|
1430
|
+
*/
|
|
1431
|
+
async dispatchTool(toolName, args) {
|
|
1432
|
+
switch (toolName) {
|
|
1433
|
+
case 'homegraph_search': return await this.handleSearch(args);
|
|
1434
|
+
case 'homegraph_callers': return await this.handleCallers(args);
|
|
1435
|
+
case 'homegraph_callees': return await this.handleCallees(args);
|
|
1436
|
+
case 'homegraph_impact': return await this.handleImpact(args);
|
|
1437
|
+
case 'homegraph_explore': return await this.handleExplore(args);
|
|
1438
|
+
case 'homegraph_node': return await this.handleNode(args);
|
|
1439
|
+
case 'homegraph_files': return await this.handleFiles(args);
|
|
1440
|
+
case 'homegraph_spec_match': return await this.handleSpecMatch(args);
|
|
1441
|
+
case 'homegraph_spec_find': return await this.handleSpecFind(args);
|
|
1442
|
+
case 'homegraph_spec_trace': return await this.handleSpecTrace(args);
|
|
1443
|
+
default: return this.errorResult(`Unknown tool: ${toolName}`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1172
1446
|
/**
|
|
1173
1447
|
* Handle homegraph_search
|
|
1174
1448
|
*/
|
|
@@ -1182,8 +1456,8 @@ class ToolHandler {
|
|
|
1182
1456
|
// NodeKind is 'type_alias'. Without the mapping, kind: "type" silently
|
|
1183
1457
|
// matched nothing — a filter value we advertise must work.
|
|
1184
1458
|
const kind = rawKind === 'type' ? 'type_alias' : rawKind;
|
|
1185
|
-
const rawLimit = Number(args.limit)
|
|
1186
|
-
const limit = (0, utils_1.clamp)(rawLimit, 1, 100);
|
|
1459
|
+
const rawLimit = Number(args.limit);
|
|
1460
|
+
const limit = (0, utils_1.clamp)(isNaN(rawLimit) ? 10 : rawLimit, 1, 100);
|
|
1187
1461
|
const results = cg.searchNodes(query, {
|
|
1188
1462
|
limit,
|
|
1189
1463
|
kinds: kind ? [kind] : undefined,
|
|
@@ -1427,6 +1701,18 @@ class ToolHandler {
|
|
|
1427
1701
|
}
|
|
1428
1702
|
return this.textResult(this.truncateOutput(sections.join('\n') + filterNote));
|
|
1429
1703
|
}
|
|
1704
|
+
/** Whether a graph edge may be traversed by homegraph_explore's main Flow BFS. */
|
|
1705
|
+
isExploreFlowEdge(edge) {
|
|
1706
|
+
if (edge.kind === 'calls')
|
|
1707
|
+
return true;
|
|
1708
|
+
if (edge.kind !== 'references' || edge.provenance !== 'heuristic')
|
|
1709
|
+
return false;
|
|
1710
|
+
const m = edge.metadata;
|
|
1711
|
+
if (m?.synthesizedBy !== 'viewtree')
|
|
1712
|
+
return false;
|
|
1713
|
+
const via = typeof m.via === 'string' ? m.via : '';
|
|
1714
|
+
return via.length > 0 && !VIEWTREE_STRUCTURE_VIAS.has(via);
|
|
1715
|
+
}
|
|
1430
1716
|
/**
|
|
1431
1717
|
* Describe a synthesized (dynamic-dispatch) edge for human output: how the
|
|
1432
1718
|
* callback was wired up — the bridge static parsing can't see. Returns null
|
|
@@ -1479,6 +1765,30 @@ class ToolHandler {
|
|
|
1479
1765
|
registeredAt,
|
|
1480
1766
|
};
|
|
1481
1767
|
}
|
|
1768
|
+
if (m?.synthesizedBy === 'viewtree') {
|
|
1769
|
+
const via = typeof m.via === 'string' ? m.via : '';
|
|
1770
|
+
if (via === 'Prop') {
|
|
1771
|
+
return {
|
|
1772
|
+
label: `@Prop one-way state transfer (parent → child)`,
|
|
1773
|
+
compact: `state: @Prop one-way${at}`,
|
|
1774
|
+
registeredAt,
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
if (via === 'Link') {
|
|
1778
|
+
return {
|
|
1779
|
+
label: `@Link two-way state transfer (parent ↔ child)`,
|
|
1780
|
+
compact: `state: @Link two-way${at}`,
|
|
1781
|
+
registeredAt,
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
if (via && !VIEWTREE_STRUCTURE_VIAS.has(via)) {
|
|
1785
|
+
return {
|
|
1786
|
+
label: `ArkUI event \`.${via}\` — bound handler (dynamic dispatch)`,
|
|
1787
|
+
compact: `dynamic: ArkUI .${via}${at}`,
|
|
1788
|
+
registeredAt,
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1482
1792
|
if (m?.synthesizedBy === 'interface-impl') {
|
|
1483
1793
|
return {
|
|
1484
1794
|
label: `interface/abstract dispatch — runs the implementation override (dynamic dispatch)`,
|
|
@@ -1687,7 +1997,7 @@ class ToolHandler {
|
|
|
1687
1997
|
if (depth >= MAX_HOPS - 1)
|
|
1688
1998
|
continue;
|
|
1689
1999
|
for (const c of cg.getCallees(id)) {
|
|
1690
|
-
if (c.edge
|
|
2000
|
+
if (!this.isExploreFlowEdge(c.edge) || parent.has(c.node.id))
|
|
1691
2001
|
continue;
|
|
1692
2002
|
const newStreak = named.has(c.node.id) ? 0 : streak + 1;
|
|
1693
2003
|
if (newStreak > MAX_BRIDGE)
|
|
@@ -2319,11 +2629,24 @@ class ToolHandler {
|
|
|
2319
2629
|
// trace endpoint picker uses) and inject it as an entry, so every symbol the
|
|
2320
2630
|
// agent explicitly named is in the subgraph and its file is scored.
|
|
2321
2631
|
const namedSeedIds = new Set();
|
|
2632
|
+
// The subset of named seeds that earns the named-FIRST sort tier. We still
|
|
2633
|
+
// SEED every ≤3-def name (so RWR / flow ranking is unchanged), but only the
|
|
2634
|
+
// most-substantive def is tiered — a bare name's unrelated namesakes (Go's
|
|
2635
|
+
// `NewClient` = real client + test fake + xds pool) must not fill the tier
|
|
2636
|
+
// and crowd out the real answer file (grpc's `dialoptions.go`). Corroborated
|
|
2637
|
+
// overloads (the query also named the type) all earn it. (#1064)
|
|
2638
|
+
const tierSeedIds = new Set();
|
|
2322
2639
|
{
|
|
2323
2640
|
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;
|
|
2324
2641
|
const CALLABLE = new Set(['method', 'function', 'component', 'constructor']);
|
|
2325
2642
|
const isTestPath = (p) => /(^|\/)(tests?|specs?|__tests__|testdata|mocks?|fixtures?)\//i.test(p) || /\.(test|spec)\.[a-z]+$/i.test(p);
|
|
2326
2643
|
const bodyLines = (n) => Math.max(0, (n.endLine ?? n.startLine) - n.startLine);
|
|
2644
|
+
const callerCount = (n) => { try {
|
|
2645
|
+
return cg.getCallers(n.id).length;
|
|
2646
|
+
}
|
|
2647
|
+
catch {
|
|
2648
|
+
return 0;
|
|
2649
|
+
} };
|
|
2327
2650
|
const tokens = [...new Set(query.split(/[\s,()[\]]+/)
|
|
2328
2651
|
.map((t) => t.replace(FILE_EXT, '').trim())
|
|
2329
2652
|
.filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)))].slice(0, 16);
|
|
@@ -2359,12 +2682,23 @@ class ToolHandler {
|
|
|
2359
2682
|
// capped; else fall back to the single most-substantive def. This is the
|
|
2360
2683
|
// explore-side mirror of homegraph_node's overload disambiguation.
|
|
2361
2684
|
let picks;
|
|
2685
|
+
let tierPicks; // subset that earns the named-first tier (#1064)
|
|
2362
2686
|
if (cands.length <= 3) {
|
|
2363
2687
|
picks = cands;
|
|
2688
|
+
// Centrality de-noise: tier the most-substantive def PLUS any co-named
|
|
2689
|
+
// def of comparable centrality (a real overload/wrapper — excalidraw's
|
|
2690
|
+
// `mutateElement` lives in mutateElement.ts, App.tsx AND Scene.ts, all
|
|
2691
|
+
// within ~2x callers). EXCLUDE a vastly-less-central namesake (Go's
|
|
2692
|
+
// `NewClient`: real client 492 callers vs xds-pool 11, test-fake 3 →
|
|
2693
|
+
// ratio <0.025) so it doesn't fill the tier and crowd out the answer.
|
|
2694
|
+
const counts = new Map(cands.map((c) => [c.id, callerCount(c)]));
|
|
2695
|
+
const maxCallers = Math.max(1, ...counts.values());
|
|
2696
|
+
tierPicks = cands.filter((c, i) => i === 0 || (counts.get(c.id) ?? 0) >= maxCallers * 0.25);
|
|
2364
2697
|
}
|
|
2365
2698
|
else {
|
|
2366
2699
|
const ctx = cands.filter(inNamedContext);
|
|
2367
2700
|
picks = ctx.length > 0 ? ctx.slice(0, 4) : cands.slice(0, 1);
|
|
2701
|
+
tierPicks = picks; // corroborated overloads (or the single fallback) all earn it
|
|
2368
2702
|
}
|
|
2369
2703
|
for (const n of picks) {
|
|
2370
2704
|
if (!subgraph.nodes.has(n.id))
|
|
@@ -2376,6 +2710,8 @@ class ToolHandler {
|
|
|
2376
2710
|
// so a named symbol FTS already gathered never sorted to the top.)
|
|
2377
2711
|
namedSeedIds.add(n.id);
|
|
2378
2712
|
}
|
|
2713
|
+
for (const n of tierPicks)
|
|
2714
|
+
tierSeedIds.add(n.id);
|
|
2379
2715
|
}
|
|
2380
2716
|
}
|
|
2381
2717
|
// Step 2: Group nodes by file, score by relevance
|
|
@@ -2389,6 +2725,46 @@ class ToolHandler {
|
|
|
2389
2725
|
if (entryNodeIds.has(edge.target))
|
|
2390
2726
|
connectedToEntry.add(edge.source);
|
|
2391
2727
|
}
|
|
2728
|
+
// CHANGE SURFACE (#1064): a named method's signature types — its parameter
|
|
2729
|
+
// and return types — are part of what you'd edit to "add a parameter to X",
|
|
2730
|
+
// yet they can be lexically dissimilar to the query ("add a parameter to
|
|
2731
|
+
// NewClient" shares no words with `dialoptions.go`, which defines NewClient's
|
|
2732
|
+
// `DialOption`) and sit a hop away. COLLECT them here from each named-seed
|
|
2733
|
+
// callable's outgoing signature edges (full graph — the type is often not in
|
|
2734
|
+
// the subgraph); the decision to surface one is DEFERRED to the buried-rescue
|
|
2735
|
+
// pass below, which fires only when the type's file would otherwise be
|
|
2736
|
+
// dropped — so a well-connected type (excalidraw's element types, Alamofire's
|
|
2737
|
+
// `DataRequest` on a flow query) is left to rank on its own and never
|
|
2738
|
+
// displaces a flow-central file. Bounded: only the few named seeds, only the
|
|
2739
|
+
// types in their signatures.
|
|
2740
|
+
const CALLABLE_KINDS = new Set(['method', 'function', 'component', 'constructor']);
|
|
2741
|
+
const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'type_alias']);
|
|
2742
|
+
const SIG_EDGE = new Set(['references', 'type_of', 'returns']);
|
|
2743
|
+
const changeSurfaceCandidates = [];
|
|
2744
|
+
const seenChangeSurface = new Set();
|
|
2745
|
+
for (const seedId of tierSeedIds) {
|
|
2746
|
+
const seedNode = subgraph.nodes.get(seedId);
|
|
2747
|
+
if (!seedNode || !CALLABLE_KINDS.has(seedNode.kind))
|
|
2748
|
+
continue;
|
|
2749
|
+
let outs = [];
|
|
2750
|
+
try {
|
|
2751
|
+
outs = cg.getOutgoingEdges(seedId);
|
|
2752
|
+
}
|
|
2753
|
+
catch {
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
for (const e of outs) {
|
|
2757
|
+
if (!SIG_EDGE.has(e.kind))
|
|
2758
|
+
continue;
|
|
2759
|
+
const tgt = cg.getNode(e.target);
|
|
2760
|
+
if (!tgt || !TYPE_KINDS.has(tgt.kind) || namedSeedIds.has(tgt.id))
|
|
2761
|
+
continue;
|
|
2762
|
+
if (seenChangeSurface.has(tgt.id))
|
|
2763
|
+
continue;
|
|
2764
|
+
seenChangeSurface.add(tgt.id);
|
|
2765
|
+
changeSurfaceCandidates.push(tgt);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2392
2768
|
for (const node of subgraph.nodes.values()) {
|
|
2393
2769
|
// Skip import/export nodes — they add noise without information
|
|
2394
2770
|
if (node.kind === 'import' || node.kind === 'export')
|
|
@@ -2508,6 +2884,36 @@ class ToolHandler {
|
|
|
2508
2884
|
if (n)
|
|
2509
2885
|
entryFiles.add(n.filePath);
|
|
2510
2886
|
}
|
|
2887
|
+
// Buried-rescue pass (#1064): surface a named method's signature type ONLY
|
|
2888
|
+
// when its file is genuinely buried — near-zero graph mass AND not lexically
|
|
2889
|
+
// matched. That is the invisible case (grpc's `DialOption` → `dialoptions.go`,
|
|
2890
|
+
// g≈0, 0 term hits): reachable but ranked nowhere, so the agent greps. A
|
|
2891
|
+
// well-connected type file (excalidraw element types, Alamofire `DataRequest`)
|
|
2892
|
+
// is NOT buried and is left alone — rescuing it would displace a flow-central
|
|
2893
|
+
// file (App.tsx, Validation.swift). Buried is judged on the PRE-rescue graph,
|
|
2894
|
+
// so injecting the type below can't make it look connected. A rescued file is
|
|
2895
|
+
// injected (so it renders), force-kept (gate + relevantFiles), and tiered.
|
|
2896
|
+
const changeSurfaceFiles = new Set();
|
|
2897
|
+
for (const t of changeSurfaceCandidates) {
|
|
2898
|
+
const fp = t.filePath;
|
|
2899
|
+
const buried = (fileGraphScore.get(fp) ?? 0) < maxGraph * 0.06
|
|
2900
|
+
&& (fileTermHits.get(fp) ?? 0) < 2;
|
|
2901
|
+
if (!buried)
|
|
2902
|
+
continue;
|
|
2903
|
+
changeSurfaceFiles.add(fp);
|
|
2904
|
+
if (!subgraph.nodes.has(t.id))
|
|
2905
|
+
subgraph.nodes.set(t.id, t);
|
|
2906
|
+
let group = fileGroups.get(fp);
|
|
2907
|
+
if (!group) {
|
|
2908
|
+
group = { nodes: [], score: 0 };
|
|
2909
|
+
fileGroups.set(fp, group);
|
|
2910
|
+
}
|
|
2911
|
+
if (!group.nodes.some((n) => n.id === t.id))
|
|
2912
|
+
group.nodes.push(t);
|
|
2913
|
+
group.score = Math.max(group.score, 45);
|
|
2914
|
+
if (!relevantFiles.some(([f]) => f === fp))
|
|
2915
|
+
relevantFiles.push([fp, group]);
|
|
2916
|
+
}
|
|
2511
2917
|
// Relevance gate (so the generous budget is a CEILING, not a target): keep a
|
|
2512
2918
|
// file only if it is STRUCTURALLY relevant by ANY of:
|
|
2513
2919
|
// - graph score within a fraction of the top (it's on/near the flow), OR
|
|
@@ -2524,6 +2930,7 @@ class ToolHandler {
|
|
|
2524
2930
|
const gated = relevantFiles.filter(([fp]) => (fileGraphScore.get(fp) ?? 0) >= maxGraph * 0.06
|
|
2525
2931
|
|| centralFiles.has(fp)
|
|
2526
2932
|
|| entryFiles.has(fp)
|
|
2933
|
+
|| changeSurfaceFiles.has(fp)
|
|
2527
2934
|
|| (fileTermHits.get(fp) ?? 0) >= 2);
|
|
2528
2935
|
if (gated.length >= 2)
|
|
2529
2936
|
relevantFiles = gated;
|
|
@@ -2538,11 +2945,16 @@ class ToolHandler {
|
|
|
2538
2945
|
// in other files (`Validation.swift`), falls outside the budget, and the
|
|
2539
2946
|
// agent Reads it. The named file is the answer — rank it at the top.
|
|
2540
2947
|
const namedSeedFiles = new Set();
|
|
2541
|
-
for (const id of
|
|
2948
|
+
for (const id of tierSeedIds) {
|
|
2542
2949
|
const n = subgraph.nodes.get(id);
|
|
2543
2950
|
if (n)
|
|
2544
2951
|
namedSeedFiles.add(n.filePath);
|
|
2545
2952
|
}
|
|
2953
|
+
// A rescued change-surface file (only the genuinely-buried ones — see the
|
|
2954
|
+
// buried-rescue pass) is the lexically-dissimilar answer; give it the named
|
|
2955
|
+
// tier so it isn't buried under files that merely share surface words (#1064).
|
|
2956
|
+
for (const fp of changeSurfaceFiles)
|
|
2957
|
+
namedSeedFiles.add(fp);
|
|
2546
2958
|
// Multi-term corroboration tier: a file that is BOTH (a) an entry/central file
|
|
2547
2959
|
// (a search root, named seed, or graph-central hub — i.e. structurally part of
|
|
2548
2960
|
// the answer) AND (b) matched by ≥2 DISTINCT query terms must not be buried by
|
|
@@ -2607,9 +3019,16 @@ class ToolHandler {
|
|
|
2607
3019
|
const lines = [
|
|
2608
3020
|
`**Exploration: ${query}**`,
|
|
2609
3021
|
'',
|
|
2610
|
-
|
|
3022
|
+
// Curated summary — filled in after the source loop (see below). We do NOT
|
|
3023
|
+
// report `subgraph.nodes.size` / `fileGroups.size` here: that's the raw
|
|
3024
|
+
// candidate gather, which a broad natural-language query inflates wildly
|
|
3025
|
+
// (260 symbols / 124 files on a 636-file repo) even though only a handful
|
|
3026
|
+
// render. Reporting the pool read as "260 results to wade through" when the
|
|
3027
|
+
// real, correctly-ranked answer is the few files below (#1046).
|
|
3028
|
+
'',
|
|
2611
3029
|
'',
|
|
2612
3030
|
];
|
|
3031
|
+
const summaryLineIdx = 2;
|
|
2613
3032
|
// Blast radius (always-on, compact): for the entry symbols, who depends on
|
|
2614
3033
|
// them + which tests cover them — locations only, no source — so the agent
|
|
2615
3034
|
// knows what to update/verify before editing without a separate call.
|
|
@@ -2711,6 +3130,9 @@ class ToolHandler {
|
|
|
2711
3130
|
lines.push('');
|
|
2712
3131
|
let totalChars = lines.join('\n').length;
|
|
2713
3132
|
let filesIncluded = 0;
|
|
3133
|
+
// Paths we actually render source for below. Drives the curated header count
|
|
3134
|
+
// (#1046) — it must reflect what we show, not the raw candidate gather.
|
|
3135
|
+
const renderedFilePaths = [];
|
|
2714
3136
|
let anyFileTrimmed = false;
|
|
2715
3137
|
for (const [filePath, group] of sortedFiles) {
|
|
2716
3138
|
if (filesIncluded >= maxFiles)
|
|
@@ -2724,6 +3146,24 @@ class ToolHandler {
|
|
|
2724
3146
|
const fileNecessary = group.nodes.some(n => entryNodeIds.has(n.id) || flow.pathNodeIds.has(n.id) || flow.uniqueNamedNodeIds.has(n.id));
|
|
2725
3147
|
if (!fileNecessary && totalChars > budget.maxOutputChars * 0.9)
|
|
2726
3148
|
continue;
|
|
3149
|
+
if ((0, arkts_1.isOhosApiFilePath)(filePath)) {
|
|
3150
|
+
const rel = filePath.slice(arkts_1.OHOS_API_FILE_PREFIX.length);
|
|
3151
|
+
const syms = group.nodes
|
|
3152
|
+
.filter((n) => n.kind !== 'import' && n.kind !== 'export')
|
|
3153
|
+
.sort((a, b) => a.startLine - b.startLine);
|
|
3154
|
+
if (syms.length === 0)
|
|
3155
|
+
continue;
|
|
3156
|
+
lines.push(fileSectionHeader(rel, 'HarmonyOS SDK API (prebuilt db)'));
|
|
3157
|
+
lines.push('');
|
|
3158
|
+
for (const n of syms) {
|
|
3159
|
+
const sig = n.signature || n.docstring || `${n.kind} ${n.qualifiedName || n.name}`;
|
|
3160
|
+
lines.push(`\`${sig}\``);
|
|
3161
|
+
}
|
|
3162
|
+
lines.push('');
|
|
3163
|
+
totalChars = lines.join('\n').length;
|
|
3164
|
+
filesIncluded++;
|
|
3165
|
+
continue;
|
|
3166
|
+
}
|
|
2727
3167
|
const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, filePath);
|
|
2728
3168
|
if (!absPath || !(0, fs_1.existsSync)(absPath))
|
|
2729
3169
|
continue;
|
|
@@ -2862,6 +3302,7 @@ class ToolHandler {
|
|
|
2862
3302
|
: 'skeleton (signatures only — homegraph_explore a name for its full body; do NOT Read)';
|
|
2863
3303
|
lines.push(fileSectionHeader(filePath, `${names} · ${tag}`), '', '```' + lang, skel.join('\n'), '```', '');
|
|
2864
3304
|
totalChars += skel.join('\n').length + 120;
|
|
3305
|
+
renderedFilePaths.push(filePath);
|
|
2865
3306
|
filesIncluded++;
|
|
2866
3307
|
continue;
|
|
2867
3308
|
}
|
|
@@ -2908,6 +3349,7 @@ class ToolHandler {
|
|
|
2908
3349
|
}
|
|
2909
3350
|
lines.push(wholeHeader, '', '```' + lang, wholeSection, '```', '');
|
|
2910
3351
|
totalChars += wholeSection.length + 200;
|
|
3352
|
+
renderedFilePaths.push(filePath);
|
|
2911
3353
|
filesIncluded++;
|
|
2912
3354
|
continue;
|
|
2913
3355
|
}
|
|
@@ -3196,8 +3638,14 @@ class ToolHandler {
|
|
|
3196
3638
|
lines.push('```');
|
|
3197
3639
|
lines.push('');
|
|
3198
3640
|
totalChars += fileSection.length + 200;
|
|
3641
|
+
renderedFilePaths.push(filePath);
|
|
3199
3642
|
filesIncluded++;
|
|
3200
3643
|
}
|
|
3644
|
+
// The curated header count is computed from the files that SURVIVE the final
|
|
3645
|
+
// truncation (see end of method) — `filesIncluded` can over-count when the
|
|
3646
|
+
// hard ceiling drops trailing sections — so leave a sentinel here and fill it
|
|
3647
|
+
// in once the output is final.
|
|
3648
|
+
lines[summaryLineIdx] = SUMMARY_SENTINEL;
|
|
3201
3649
|
// Add remaining files as references (from both relevant and peripheral files).
|
|
3202
3650
|
// Small projects (per budget) skip this — the relevant story already fits
|
|
3203
3651
|
// in the source section, and a trailing pointer list is pure overhead.
|
|
@@ -3254,6 +3702,7 @@ class ToolHandler {
|
|
|
3254
3702
|
// externalize territory.
|
|
3255
3703
|
const output = flow.text + lines.join('\n');
|
|
3256
3704
|
const hardCeiling = Math.min(Math.round(budget.maxOutputChars * 1.5), 25000);
|
|
3705
|
+
let finalText;
|
|
3257
3706
|
if (output.length > hardCeiling) {
|
|
3258
3707
|
// Cut at a FILE-SECTION boundary (the last ``**` `` file header before the
|
|
3259
3708
|
// ceiling) so we drop whole trailing file-sections rather than slicing
|
|
@@ -3264,9 +3713,30 @@ class ToolHandler {
|
|
|
3264
3713
|
const lastSection = cut.lastIndexOf('\n' + FILE_SECTION_PREFIX);
|
|
3265
3714
|
const boundary = lastSection > hardCeiling * 0.5 ? lastSection : cut.lastIndexOf('\n');
|
|
3266
3715
|
const safe = boundary > 0 ? cut.slice(0, boundary) : cut;
|
|
3267
|
-
|
|
3716
|
+
finalText = safe + '\n\n... (output truncated to budget; the source above is complete and verbatim — treat it as already Read. For any area not covered, run another homegraph_explore with the specific names — do NOT Read these files.)';
|
|
3268
3717
|
}
|
|
3269
|
-
|
|
3718
|
+
else {
|
|
3719
|
+
finalText = output;
|
|
3720
|
+
}
|
|
3721
|
+
// Curated header (#1046): substitute the sentinel with the count of files
|
|
3722
|
+
// whose source SURVIVES in the final text — not `subgraph`/`fileGroups` (the
|
|
3723
|
+
// raw gather a broad query inflates) and not `filesIncluded` (which can
|
|
3724
|
+
// over-count when the ceiling above drops trailing sections). A file counts
|
|
3725
|
+
// only if its section header is still present; its relevant (non-import)
|
|
3726
|
+
// symbols are summed for N. Files we couldn't fit are still named under "Not
|
|
3727
|
+
// shown above" + the budget note, so nothing is silently dropped.
|
|
3728
|
+
const survivors = renderedFilePaths.filter((fp) => finalText.includes(`${FILE_SECTION_PREFIX}${fp}\``));
|
|
3729
|
+
const shownSymbols = survivors.reduce((sum, fp) => {
|
|
3730
|
+
const g = fileGroups.get(fp);
|
|
3731
|
+
if (!g)
|
|
3732
|
+
return sum;
|
|
3733
|
+
return sum + new Set(g.nodes.filter((n) => n.kind !== 'import' && n.kind !== 'export').map((n) => n.id)).size;
|
|
3734
|
+
}, 0);
|
|
3735
|
+
const summaryLine = survivors.length > 0
|
|
3736
|
+
? `Found ${shownSymbols} symbol${shownSymbols === 1 ? '' : 's'} across ${survivors.length} file${survivors.length === 1 ? '' : 's'}.`
|
|
3737
|
+
: `Found ${subgraph.nodes.size} symbol${subgraph.nodes.size === 1 ? '' : 's'} across ${fileGroups.size} file${fileGroups.size === 1 ? '' : 's'}.`;
|
|
3738
|
+
finalText = finalText.replace(SUMMARY_SENTINEL, summaryLine);
|
|
3739
|
+
return this.textResult(finalText);
|
|
3270
3740
|
}
|
|
3271
3741
|
/**
|
|
3272
3742
|
* Handle homegraph_node
|
|
@@ -3428,7 +3898,7 @@ class ToolHandler {
|
|
|
3428
3898
|
const symbolMap = (heading, limit = 200) => {
|
|
3429
3899
|
const lines = [heading];
|
|
3430
3900
|
for (const n of nodes.slice(0, limit)) {
|
|
3431
|
-
const sig = n.signature ? ` ${n.signature
|
|
3901
|
+
const sig = n.signature ? ` ${formatInlineSignature(n.signature)}` : '';
|
|
3432
3902
|
lines.push(`- \`${n.name}\` (${n.kind})${sig} — :${n.startLine}`);
|
|
3433
3903
|
}
|
|
3434
3904
|
if (nodes.length > limit)
|
|
@@ -3814,6 +4284,342 @@ class ToolHandler {
|
|
|
3814
4284
|
return lines.join('\n');
|
|
3815
4285
|
}
|
|
3816
4286
|
// =========================================================================
|
|
4287
|
+
// handleSpecMatch — Commit4Spec knowledge-graph search
|
|
4288
|
+
// =========================================================================
|
|
4289
|
+
/**
|
|
4290
|
+
* Match a spec/feature description against the Commit4Spec knowledge graph.
|
|
4291
|
+
*
|
|
4292
|
+
* Uses FTS5 full-text search to find the most similar historical specs,
|
|
4293
|
+
* returning each with its linked commits and optional code fragments.
|
|
4294
|
+
* The database lives at `.homegraph/commit4spec/commit4spec.db` by default and is
|
|
4295
|
+
* separate from the HomeGraph code-symbol index — this tool works whether
|
|
4296
|
+
* or not the code index is present.
|
|
4297
|
+
*/
|
|
4298
|
+
async handleSpecMatch(args) {
|
|
4299
|
+
const query = this.validateString(args.query, 'query');
|
|
4300
|
+
if (typeof query !== 'string')
|
|
4301
|
+
return query;
|
|
4302
|
+
const repoPath = args.repoPath;
|
|
4303
|
+
const explicitDbPath = args.dbPath;
|
|
4304
|
+
const topKRaw = Number(args.topK);
|
|
4305
|
+
const topK = Math.max(1, Math.min(isNaN(topKRaw) ? 5 : topKRaw, 50));
|
|
4306
|
+
const includeFragments = args.includeFragments !== false;
|
|
4307
|
+
// Lazily require spec modules so the MCP startup path stays lean.
|
|
4308
|
+
const { resolveDbPath } = require('../spec/utils');
|
|
4309
|
+
const { createDatabase } = require('../db/sqlite-adapter');
|
|
4310
|
+
const { searchAndGetContext } = require('../spec/graph/queries');
|
|
4311
|
+
const { truncateCodeDiff, truncateSubtitles, computeBudgetProfile, } = require('../spec/utils');
|
|
4312
|
+
// Resolve the database path.
|
|
4313
|
+
const dbPath = resolveDbPath(repoPath || process.cwd(), explicitDbPath);
|
|
4314
|
+
// Open the database.
|
|
4315
|
+
let db;
|
|
4316
|
+
try {
|
|
4317
|
+
db = createDatabase(dbPath).db;
|
|
4318
|
+
}
|
|
4319
|
+
catch (err) {
|
|
4320
|
+
return this.errorResult(`Failed to open Commit4Spec database at "${dbPath}": ` +
|
|
4321
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
4322
|
+
}
|
|
4323
|
+
try {
|
|
4324
|
+
// Search and traverse.
|
|
4325
|
+
const contexts = searchAndGetContext(db, query, topK, includeFragments);
|
|
4326
|
+
if (contexts.length === 0) {
|
|
4327
|
+
return this.textResult(JSON.stringify({
|
|
4328
|
+
query: query.slice(0, 200),
|
|
4329
|
+
matched_count: 0,
|
|
4330
|
+
results: [],
|
|
4331
|
+
}, null, 2));
|
|
4332
|
+
}
|
|
4333
|
+
// Build budget profile for truncation.
|
|
4334
|
+
const profile = computeBudgetProfile(contexts.length);
|
|
4335
|
+
// Serialize to JSON (matching Python's spec_contexts_to_results format).
|
|
4336
|
+
const results = contexts.map((ctx) => {
|
|
4337
|
+
const subtitles = profile.tier === 'vlarge'
|
|
4338
|
+
? [] // vlarge disables subtitles entirely
|
|
4339
|
+
: truncateSubtitles(ctx.spec.subtitles, 200, profile.maxContents);
|
|
4340
|
+
const commits = ctx.commits.slice(0, profile.maxContents || 5).map((c) => {
|
|
4341
|
+
const cd = {
|
|
4342
|
+
hash: c.commit.hash,
|
|
4343
|
+
message: c.commit.message,
|
|
4344
|
+
relation_type: c.relationType,
|
|
4345
|
+
};
|
|
4346
|
+
if (includeFragments) {
|
|
4347
|
+
const maxFrags = profile.maxFragments || 3;
|
|
4348
|
+
cd.fragments = c.fragments.slice(0, maxFrags).map((f) => ({
|
|
4349
|
+
file_path: f.filePath,
|
|
4350
|
+
change_type: f.changeType,
|
|
4351
|
+
start_line: f.startLine,
|
|
4352
|
+
end_line: f.endLine,
|
|
4353
|
+
code_diff: truncateCodeDiff(f.codeDiff),
|
|
4354
|
+
}));
|
|
4355
|
+
}
|
|
4356
|
+
return cd;
|
|
4357
|
+
});
|
|
4358
|
+
return {
|
|
4359
|
+
spec_id: ctx.spec.id,
|
|
4360
|
+
title: ctx.spec.title,
|
|
4361
|
+
subtitles,
|
|
4362
|
+
file_path: ctx.spec.filePath,
|
|
4363
|
+
commits,
|
|
4364
|
+
};
|
|
4365
|
+
});
|
|
4366
|
+
const response = {
|
|
4367
|
+
query: query.slice(0, 200),
|
|
4368
|
+
matched_count: results.length,
|
|
4369
|
+
results,
|
|
4370
|
+
};
|
|
4371
|
+
const json = JSON.stringify(response, null, 2);
|
|
4372
|
+
// Hard cap to MAX_OUTPUT_LENGTH to prevent context bloat.
|
|
4373
|
+
if (json.length <= MAX_OUTPUT_LENGTH) {
|
|
4374
|
+
return this.textResult(json);
|
|
4375
|
+
}
|
|
4376
|
+
// When the full payload exceeds the cap, drop fragments first,
|
|
4377
|
+
// then trim commits, then trim the whole thing.
|
|
4378
|
+
if (includeFragments) {
|
|
4379
|
+
const slimResults = results.map((r) => ({
|
|
4380
|
+
...r,
|
|
4381
|
+
commits: r.commits.map((c) => {
|
|
4382
|
+
const { fragments: _, ...rest } = c;
|
|
4383
|
+
return rest;
|
|
4384
|
+
}),
|
|
4385
|
+
}));
|
|
4386
|
+
const slim = JSON.stringify({ ...response, results: slimResults }, null, 2);
|
|
4387
|
+
if (slim.length <= MAX_OUTPUT_LENGTH) {
|
|
4388
|
+
return this.textResult(`(Fragments elided — output exceeded ${MAX_OUTPUT_LENGTH} chars)\n\n` + slim);
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
// Fallback: truncate at a newline to avoid broken JSON.
|
|
4392
|
+
return this.textResult(this.truncateOutput(json));
|
|
4393
|
+
}
|
|
4394
|
+
finally {
|
|
4395
|
+
db.close();
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
// =========================================================================
|
|
4399
|
+
// handleSpecFind — file-path based spec lookup
|
|
4400
|
+
// =========================================================================
|
|
4401
|
+
/**
|
|
4402
|
+
* Find which specs are related to the given file path by matching against
|
|
4403
|
+
* code-fragment file paths in the Commit4Spec knowledge graph.
|
|
4404
|
+
*
|
|
4405
|
+
* Traverses: filePath → code_fragment_nodes → commit_fragment_relations
|
|
4406
|
+
* → spec_commit_relations → spec_nodes.
|
|
4407
|
+
*/
|
|
4408
|
+
async handleSpecFind(args) {
|
|
4409
|
+
const filePath = this.validateString(args.filePath, 'filePath');
|
|
4410
|
+
if (typeof filePath !== 'string')
|
|
4411
|
+
return filePath;
|
|
4412
|
+
const repoPath = args.repoPath;
|
|
4413
|
+
const explicitDbPath = args.dbPath;
|
|
4414
|
+
// Lazily require spec modules so the MCP startup path stays lean.
|
|
4415
|
+
const { resolveDbPath } = require('../spec/utils');
|
|
4416
|
+
const { createDatabase } = require('../db/sqlite-adapter');
|
|
4417
|
+
const { findSpecsByFilePath } = require('../spec/graph/queries');
|
|
4418
|
+
// Resolve the database path.
|
|
4419
|
+
const dbPath = resolveDbPath(repoPath || process.cwd(), explicitDbPath);
|
|
4420
|
+
// Open the database.
|
|
4421
|
+
let db;
|
|
4422
|
+
try {
|
|
4423
|
+
db = createDatabase(dbPath).db;
|
|
4424
|
+
}
|
|
4425
|
+
catch (err) {
|
|
4426
|
+
return this.errorResult(`Failed to open Commit4Spec database at "${dbPath}": ` +
|
|
4427
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
4428
|
+
}
|
|
4429
|
+
try {
|
|
4430
|
+
const result = findSpecsByFilePath(db, filePath);
|
|
4431
|
+
const response = {
|
|
4432
|
+
filePath,
|
|
4433
|
+
matched_count: result.matched_count,
|
|
4434
|
+
truncated: result.truncated,
|
|
4435
|
+
results: result.results,
|
|
4436
|
+
};
|
|
4437
|
+
const json = JSON.stringify(response, null, 2);
|
|
4438
|
+
if (json.length <= MAX_OUTPUT_LENGTH) {
|
|
4439
|
+
return this.textResult(json);
|
|
4440
|
+
}
|
|
4441
|
+
// Truncate: reduce results
|
|
4442
|
+
const slim = {
|
|
4443
|
+
...response,
|
|
4444
|
+
results: response.results.slice(0, Math.max(1, Math.floor(response.results.length / 2))),
|
|
4445
|
+
};
|
|
4446
|
+
const slimJson = JSON.stringify(slim, null, 2);
|
|
4447
|
+
if (slimJson.length <= MAX_OUTPUT_LENGTH) {
|
|
4448
|
+
return this.textResult(`(Results trimmed to fit output limit)\n\n${slimJson}`);
|
|
4449
|
+
}
|
|
4450
|
+
return this.textResult(this.truncateOutput(json));
|
|
4451
|
+
}
|
|
4452
|
+
finally {
|
|
4453
|
+
db.close();
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
// =========================================================================
|
|
4457
|
+
// handleSpecTrace — code symbol → Spec reverse trace
|
|
4458
|
+
// =========================================================================
|
|
4459
|
+
/**
|
|
4460
|
+
* Trace a code symbol back to its associated Specs.
|
|
4461
|
+
*
|
|
4462
|
+
* Uses the HomeGraph code index to resolve the symbol to AST-level node(s),
|
|
4463
|
+
* then queries the Commit4Spec knowledge graph for associated Specs via
|
|
4464
|
+
* five-dimensional scoring (file-path, content FTS5, name FTS5, recency,
|
|
4465
|
+
* line overlap).
|
|
4466
|
+
*
|
|
4467
|
+
* This bridges the two databases: homegraph.db (code entities) →
|
|
4468
|
+
* commit4spec.db (Spec knowledge graph).
|
|
4469
|
+
*/
|
|
4470
|
+
async handleSpecTrace(args) {
|
|
4471
|
+
const symbol = this.validateString(args.symbol, 'symbol');
|
|
4472
|
+
if (typeof symbol !== 'string')
|
|
4473
|
+
return symbol;
|
|
4474
|
+
const fileRaw = this.validateOptionalPath(args.file, 'file');
|
|
4475
|
+
if (typeof fileRaw === 'object')
|
|
4476
|
+
return fileRaw;
|
|
4477
|
+
const file = fileRaw;
|
|
4478
|
+
const line = typeof args.line === 'number' ? args.line : undefined;
|
|
4479
|
+
const repoPath = args.repoPath;
|
|
4480
|
+
const explicitDbPath = args.dbPath;
|
|
4481
|
+
const topKRaw = Number(args.topK);
|
|
4482
|
+
const topK = Math.max(1, Math.min(isNaN(topKRaw) ? 10 : topKRaw, 50));
|
|
4483
|
+
// Lazily require all needed modules
|
|
4484
|
+
const HomeGraph = loadHomeGraph();
|
|
4485
|
+
const { resolveDbPath } = require('../spec/utils');
|
|
4486
|
+
const { createDatabase } = require('../db/sqlite-adapter');
|
|
4487
|
+
const { findSpecsByCodeSymbol } = require('../spec/graph/queries');
|
|
4488
|
+
const { initSpecSchema, runSpecMigrations, getCurrentSpecVersion, CURRENT_SPEC_SCHEMA_VERSION } = require('../spec/db/schema');
|
|
4489
|
+
// Resolve project path for the code graph
|
|
4490
|
+
const projectPath = repoPath || process.cwd();
|
|
4491
|
+
// Open the HomeGraph code index
|
|
4492
|
+
let cg;
|
|
4493
|
+
try {
|
|
4494
|
+
cg = await HomeGraph.open(projectPath);
|
|
4495
|
+
}
|
|
4496
|
+
catch (err) {
|
|
4497
|
+
return this.errorResult(`Failed to open HomeGraph code index at "${projectPath}": ` +
|
|
4498
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
4499
|
+
}
|
|
4500
|
+
try {
|
|
4501
|
+
// Step 1: Resolve symbol to nodes via findSymbolMatches
|
|
4502
|
+
let nodes = this.findSymbolMatches(cg, symbol);
|
|
4503
|
+
if (nodes.length === 0) {
|
|
4504
|
+
await cg.close();
|
|
4505
|
+
return this.textResult(JSON.stringify({
|
|
4506
|
+
symbol,
|
|
4507
|
+
error: `No code entities found for symbol "${symbol}".`,
|
|
4508
|
+
matches: [],
|
|
4509
|
+
}, null, 2));
|
|
4510
|
+
}
|
|
4511
|
+
// Disambiguate by file/line if provided
|
|
4512
|
+
if (file) {
|
|
4513
|
+
// Use endsWith for precise file path matching
|
|
4514
|
+
nodes = nodes.filter((n) => n.filePath.endsWith(file));
|
|
4515
|
+
}
|
|
4516
|
+
if (line !== undefined && nodes.length > 1) {
|
|
4517
|
+
const closest = nodes.reduce((best, n) => {
|
|
4518
|
+
const bestDist = Math.abs(best.startLine - line) + Math.abs(best.endLine - line);
|
|
4519
|
+
const curDist = Math.abs(n.startLine - line) + Math.abs(n.endLine - line);
|
|
4520
|
+
return curDist < bestDist ? n : best;
|
|
4521
|
+
});
|
|
4522
|
+
nodes = [closest];
|
|
4523
|
+
}
|
|
4524
|
+
// Take the best disambiguated node
|
|
4525
|
+
const node = nodes[0];
|
|
4526
|
+
if (!node) {
|
|
4527
|
+
await cg.close();
|
|
4528
|
+
return this.textResult(JSON.stringify({
|
|
4529
|
+
symbol,
|
|
4530
|
+
error: `Could not resolve symbol "${symbol}" to a specific code entity.`,
|
|
4531
|
+
matches: [],
|
|
4532
|
+
}, null, 2));
|
|
4533
|
+
}
|
|
4534
|
+
// Step 2: Resolve the Spec database path
|
|
4535
|
+
const dbPath = resolveDbPath(repoPath || process.cwd(), explicitDbPath);
|
|
4536
|
+
// Step 3: Open the Spec database
|
|
4537
|
+
let db;
|
|
4538
|
+
try {
|
|
4539
|
+
db = createDatabase(dbPath).db;
|
|
4540
|
+
}
|
|
4541
|
+
catch (err) {
|
|
4542
|
+
await cg.close();
|
|
4543
|
+
return this.errorResult(`Failed to open Commit4Spec database at "${dbPath}": ` +
|
|
4544
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
4545
|
+
}
|
|
4546
|
+
try {
|
|
4547
|
+
// Ensure schema is up to date
|
|
4548
|
+
initSpecSchema(db);
|
|
4549
|
+
const currentVersion = getCurrentSpecVersion(db);
|
|
4550
|
+
if (currentVersion < CURRENT_SPEC_SCHEMA_VERSION) {
|
|
4551
|
+
runSpecMigrations(db, currentVersion);
|
|
4552
|
+
}
|
|
4553
|
+
// Step 4: Query Specs for the code entity
|
|
4554
|
+
const result = findSpecsByCodeSymbol(db, {
|
|
4555
|
+
name: node.name,
|
|
4556
|
+
qualifiedName: node.qualifiedName,
|
|
4557
|
+
kind: node.kind,
|
|
4558
|
+
filePath: node.filePath,
|
|
4559
|
+
startLine: node.startLine,
|
|
4560
|
+
endLine: node.endLine,
|
|
4561
|
+
}, topK);
|
|
4562
|
+
// Step 5: Serialize the response
|
|
4563
|
+
const response = {
|
|
4564
|
+
symbol,
|
|
4565
|
+
entity: {
|
|
4566
|
+
name: result.entity.name,
|
|
4567
|
+
qualifiedName: result.entity.qualifiedName,
|
|
4568
|
+
kind: result.entity.kind,
|
|
4569
|
+
filePath: result.entity.filePath,
|
|
4570
|
+
startLine: result.entity.startLine,
|
|
4571
|
+
endLine: result.entity.endLine,
|
|
4572
|
+
},
|
|
4573
|
+
matched_count: result.matches.length,
|
|
4574
|
+
total_candidates: result.totalCandidates,
|
|
4575
|
+
matches: result.matches.map((m) => ({
|
|
4576
|
+
spec_id: m.spec.id,
|
|
4577
|
+
title: m.spec.title,
|
|
4578
|
+
status: m.spec.status,
|
|
4579
|
+
version: m.spec.version,
|
|
4580
|
+
file_path: m.spec.filePath,
|
|
4581
|
+
score: Math.round(m.score * 1000) / 1000,
|
|
4582
|
+
score_detail: {
|
|
4583
|
+
file_path: Math.round(m.scoreDetail.filePathScore * 1000) / 1000,
|
|
4584
|
+
content: Math.round(m.scoreDetail.contentScore * 1000) / 1000,
|
|
4585
|
+
name: Math.round(m.scoreDetail.nameScore * 1000) / 1000,
|
|
4586
|
+
recency: Math.round(m.scoreDetail.recencyScore * 1000) / 1000,
|
|
4587
|
+
line_overlap: Math.round(m.scoreDetail.overlapScore * 1000) / 1000,
|
|
4588
|
+
},
|
|
4589
|
+
fragment_count: m.fragmentCount,
|
|
4590
|
+
commit_count: m.commitCount,
|
|
4591
|
+
})),
|
|
4592
|
+
};
|
|
4593
|
+
const json = JSON.stringify(response, null, 2);
|
|
4594
|
+
if (json.length <= MAX_OUTPUT_LENGTH) {
|
|
4595
|
+
return this.textResult(json);
|
|
4596
|
+
}
|
|
4597
|
+
// Truncate: reduce matches
|
|
4598
|
+
const slim = {
|
|
4599
|
+
...response,
|
|
4600
|
+
matches: response.matches.slice(0, Math.max(1, Math.floor(topK / 2))),
|
|
4601
|
+
};
|
|
4602
|
+
const slimJson = JSON.stringify(slim, null, 2);
|
|
4603
|
+
if (slimJson.length <= MAX_OUTPUT_LENGTH) {
|
|
4604
|
+
return this.textResult(`(Results trimmed to fit output limit)\n\n${slimJson}`);
|
|
4605
|
+
}
|
|
4606
|
+
return this.textResult(this.truncateOutput(json));
|
|
4607
|
+
}
|
|
4608
|
+
finally {
|
|
4609
|
+
try {
|
|
4610
|
+
db.close();
|
|
4611
|
+
}
|
|
4612
|
+
catch { /* best effort */ }
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
finally {
|
|
4616
|
+
try {
|
|
4617
|
+
await cg.close();
|
|
4618
|
+
}
|
|
4619
|
+
catch { /* best effort */ }
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
// =========================================================================
|
|
3817
4623
|
// Symbol resolution helpers
|
|
3818
4624
|
// =========================================================================
|
|
3819
4625
|
/**
|
|
@@ -3981,7 +4787,7 @@ class ToolHandler {
|
|
|
3981
4787
|
lines.push(`**${node.name}** (${node.kind})`);
|
|
3982
4788
|
lines.push(`${node.filePath}${location}`);
|
|
3983
4789
|
if (node.signature)
|
|
3984
|
-
lines.push(`\`${node.signature}\``);
|
|
4790
|
+
lines.push(`\`${formatInlineSignature(node.signature)}\``);
|
|
3985
4791
|
lines.push('');
|
|
3986
4792
|
}
|
|
3987
4793
|
return lines.join('\n');
|
|
@@ -4055,7 +4861,7 @@ class ToolHandler {
|
|
|
4055
4861
|
const lines = [`**Members (${children.length}):**`, ''];
|
|
4056
4862
|
for (const c of children) {
|
|
4057
4863
|
const loc = c.startLine ? `:${c.startLine}` : '';
|
|
4058
|
-
const sig = c.signature ? ` — \`${c.signature}\`` : '';
|
|
4864
|
+
const sig = c.signature ? ` — \`${formatInlineSignature(c.signature)}\`` : '';
|
|
4059
4865
|
lines.push(`- ${c.name} (${c.kind})${loc}${sig}`);
|
|
4060
4866
|
}
|
|
4061
4867
|
return lines.join('\n');
|
|
@@ -4068,7 +4874,7 @@ class ToolHandler {
|
|
|
4068
4874
|
`**Location:** ${node.filePath}${location}`,
|
|
4069
4875
|
];
|
|
4070
4876
|
if (node.signature) {
|
|
4071
|
-
lines.push(
|
|
4877
|
+
lines.push(...formatNodeSignatureBlock(node.signature));
|
|
4072
4878
|
}
|
|
4073
4879
|
// Only include docstring if it's short and useful
|
|
4074
4880
|
if (node.docstring && node.docstring.length < 200) {
|