homegraph 1.1.3 → 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/README.md +43 -18
- 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 +709 -168
- 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 +84 -5
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +217 -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 +58 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +131 -2
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +473 -9
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +52 -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 +36 -0
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +274 -0
- 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/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/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/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 +55 -2
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +923 -381
- 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 +98 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +563 -1
- 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/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/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 +152 -0
- package/dist/extraction/parse-pool.d.ts.map +1 -0
- package/dist/extraction/parse-pool.js +387 -0
- package/dist/extraction/parse-pool.js.map +1 -0
- 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 +20 -1
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +59 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +952 -37
- 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 +77 -18
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +85 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +426 -9
- 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-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 +73 -3
- package/dist/mcp/daemon.d.ts.map +1 -1
- package/dist/mcp/daemon.js +258 -40
- 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 +16 -0
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +100 -2
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +66 -49
- 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/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/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 +56 -0
- package/dist/mcp/query-cache.d.ts.map +1 -0
- package/dist/mcp/query-cache.js +261 -0
- package/dist/mcp/query-cache.js.map +1 -0
- package/dist/mcp/query-pool.d.ts +128 -0
- package/dist/mcp/query-pool.d.ts.map +1 -0
- package/dist/mcp/query-pool.js +374 -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 +4 -29
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +39 -86
- 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 +179 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +3027 -192
- 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 +58 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +141 -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/resolution/c-fnptr-synthesizer.d.ts +2 -29
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.js +866 -148
- 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 +1232 -183
- 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/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 +65 -3
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +477 -59
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/name-matcher.d.ts +22 -3
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +657 -39
- 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/spec/config.d.ts +7 -11
- package/dist/spec/config.d.ts.map +1 -1
- package/dist/spec/config.js +77 -48
- package/dist/spec/config.js.map +1 -1
- package/dist/spec/db/fragment-node.d.ts +1 -0
- package/dist/spec/db/fragment-node.d.ts.map +1 -1
- package/dist/spec/db/fragment-node.js +8 -0
- package/dist/spec/db/fragment-node.js.map +1 -1
- package/dist/spec/db/fts.d.ts +14 -0
- package/dist/spec/db/fts.d.ts.map +1 -1
- package/dist/spec/db/fts.js +43 -4
- package/dist/spec/db/fts.js.map +1 -1
- package/dist/spec/db/schema.d.ts +6 -3
- package/dist/spec/db/schema.d.ts.map +1 -1
- package/dist/spec/db/schema.js +36 -4
- package/dist/spec/db/schema.js.map +1 -1
- package/dist/spec/db/schema.sql +11 -1
- package/dist/spec/evolve/logic-checker.d.ts +2 -2
- package/dist/spec/evolve/logic-checker.d.ts.map +1 -1
- package/dist/spec/evolve/logic-checker.js +3 -27
- package/dist/spec/evolve/logic-checker.js.map +1 -1
- package/dist/spec/evolve/pipeline.d.ts +21 -19
- package/dist/spec/evolve/pipeline.d.ts.map +1 -1
- package/dist/spec/evolve/pipeline.js +284 -31
- package/dist/spec/evolve/pipeline.js.map +1 -1
- package/dist/spec/evolve/spec-rewriter.d.ts +2 -2
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
- package/dist/spec/evolve/spec-rewriter.js +7 -31
- package/dist/spec/evolve/spec-rewriter.js.map +1 -1
- package/dist/spec/graph/queries.d.ts +97 -0
- package/dist/spec/graph/queries.d.ts.map +1 -1
- package/dist/spec/graph/queries.js +225 -0
- package/dist/spec/graph/queries.js.map +1 -1
- 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/git-scanner.d.ts +12 -0
- package/dist/spec/mining/git-scanner.d.ts.map +1 -1
- package/dist/spec/mining/git-scanner.js +41 -0
- package/dist/spec/mining/git-scanner.js.map +1 -1
- package/dist/spec/mining/pipeline.d.ts.map +1 -1
- package/dist/spec/mining/pipeline.js +14 -1
- package/dist/spec/mining/pipeline.js.map +1 -1
- package/dist/spec/utils.d.ts +2 -1
- package/dist/spec/utils.d.ts.map +1 -1
- package/dist/spec/utils.js +3 -1
- package/dist/spec/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 +9 -0
- package/dist/sync/worktree.d.ts.map +1 -1
- package/dist/sync/worktree.js +45 -0
- package/dist/sync/worktree.js.map +1 -1
- package/dist/types.d.ts +25 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.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.d.ts +32 -0
- package/dist/upgrade/index.d.ts.map +1 -1
- package/dist/upgrade/index.js +163 -15
- 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 +4 -4
- 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-all.sh +6 -0
- package/scripts/build-bundle.sh +5 -0
- package/scripts/exp_boundary_eval/README.md +247 -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/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/npm-shim.js +32 -3
- package/scripts/ohos-sdk-publish.mjs +133 -0
- 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
|
|
@@ -24,12 +28,15 @@ const query_utils_1 = require("../search/query-utils");
|
|
|
24
28
|
const fs_1 = require("fs");
|
|
25
29
|
const utils_1 = require("../utils");
|
|
26
30
|
const generated_detection_1 = require("../extraction/generated-detection");
|
|
31
|
+
const arkts_1 = require("../extraction/languages/arkts");
|
|
27
32
|
const dynamic_boundaries_1 = require("./dynamic-boundaries");
|
|
33
|
+
const query_cache_1 = require("./query-cache");
|
|
28
34
|
/** ViewTree structural `references` vias — not UI event bindings. */
|
|
29
35
|
const VIEWTREE_STRUCTURE_VIAS = new Set([
|
|
30
36
|
'child-component',
|
|
31
37
|
'state-binding',
|
|
32
|
-
'
|
|
38
|
+
'Prop',
|
|
39
|
+
'Link',
|
|
33
40
|
'builder',
|
|
34
41
|
'builder-param',
|
|
35
42
|
]);
|
|
@@ -93,6 +100,25 @@ function lastQualifierPart(symbol) {
|
|
|
93
100
|
const parts = symbol.split(/::|[./]/).filter((p) => p.length > 0);
|
|
94
101
|
return parts[parts.length - 1] ?? symbol;
|
|
95
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
|
+
}
|
|
96
122
|
/**
|
|
97
123
|
* Calculate the recommended number of homegraph_explore calls based on project size.
|
|
98
124
|
* Larger codebases need more exploration calls to cover their surface area,
|
|
@@ -208,6 +234,46 @@ function getExploreOutputBudget(fileCount) {
|
|
|
208
234
|
excludeLowValueFiles: false,
|
|
209
235
|
};
|
|
210
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
|
+
}
|
|
211
277
|
/**
|
|
212
278
|
* Whether `homegraph_explore` should prefix source lines with their line
|
|
213
279
|
* numbers (cat -n style: `<num>\t<code>`).
|
|
@@ -275,6 +341,29 @@ function numberSourceLines(slice, firstLineNumber) {
|
|
|
275
341
|
}
|
|
276
342
|
return out.join('\n');
|
|
277
343
|
}
|
|
344
|
+
/** Primary signature line (first line when overloads are stored newline-separated). */
|
|
345
|
+
function primarySignatureLine(signature) {
|
|
346
|
+
return signature.split('\n')[0]?.trim() ?? signature.trim();
|
|
347
|
+
}
|
|
348
|
+
/** Render a stored signature (single line or newline-separated overloads) for MCP output. */
|
|
349
|
+
function formatNodeSignatureBlock(signature) {
|
|
350
|
+
const lines = signature.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
351
|
+
if (lines.length <= 1) {
|
|
352
|
+
return lines.length ? [`**Signature:** \`${lines[0]}\``] : [];
|
|
353
|
+
}
|
|
354
|
+
return [
|
|
355
|
+
'**Signature:**',
|
|
356
|
+
`- \`${lines[0]}\` (primary)`,
|
|
357
|
+
...lines.slice(1).map((l) => `- \`${l}\` (overload)`),
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
function formatInlineSignature(signature) {
|
|
361
|
+
const primary = primarySignatureLine(signature);
|
|
362
|
+
const overloadCount = signature.split('\n').filter((l) => l.trim()).length - 1;
|
|
363
|
+
if (overloadCount <= 0)
|
|
364
|
+
return primary;
|
|
365
|
+
return `${primary} (+${overloadCount} overload${overloadCount === 1 ? '' : 's'})`;
|
|
366
|
+
}
|
|
278
367
|
/**
|
|
279
368
|
* Unique line-prefix for a per-file source section in homegraph_explore output.
|
|
280
369
|
* Issue #778: tool results dropped ATX headings (`####`, `##`, `###`) for bold
|
|
@@ -286,6 +375,12 @@ function numberSourceLines(slice, firstLineNumber) {
|
|
|
286
375
|
* (`reasoning/reasoner.ts`) both key off to cut on whole file sections.
|
|
287
376
|
*/
|
|
288
377
|
const FILE_SECTION_PREFIX = '**`';
|
|
378
|
+
// Placeholder for codegraph_explore's "Found N symbols across M files." line.
|
|
379
|
+
// The honest N/M can only be known after the final truncation drops trailing
|
|
380
|
+
// sections (#1046), so the header is emitted as this sentinel and substituted
|
|
381
|
+
// at the very end. This bracketed token never occurs in rendered source or a
|
|
382
|
+
// file path, so the final string-replace can't collide.
|
|
383
|
+
const SUMMARY_SENTINEL = '[[codegraph-explore-summary]]';
|
|
289
384
|
function fileSectionHeader(filePath, suffix) {
|
|
290
385
|
return suffix
|
|
291
386
|
? `${FILE_SECTION_PREFIX}${filePath}\`** — ${suffix}`
|
|
@@ -297,18 +392,26 @@ function fileSectionHeader(filePath, suffix) {
|
|
|
297
392
|
* The agent uses this to fall back to Read for those specific files
|
|
298
393
|
* without waiting for the debounced sync (issue #403).
|
|
299
394
|
*/
|
|
300
|
-
function formatStaleBanner(stale) {
|
|
395
|
+
function formatStaleBanner(stale, opts) {
|
|
301
396
|
const now = Date.now();
|
|
302
397
|
const lines = stale.map((p) => {
|
|
303
398
|
const ageMs = Math.max(0, now - p.lastSeenMs);
|
|
304
399
|
const label = p.indexing ? 'indexing in progress' : 'pending sync';
|
|
305
400
|
return ` - ${p.path} (edited ${ageMs}ms ago, ${label})`;
|
|
306
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.';
|
|
307
410
|
return ('⚠️ Some files referenced below were edited since the last index sync — ' +
|
|
308
411
|
'their homegraph entries may be stale:\n' +
|
|
309
412
|
lines.join('\n') +
|
|
310
|
-
'\
|
|
311
|
-
|
|
413
|
+
'\n' +
|
|
414
|
+
guidance);
|
|
312
415
|
}
|
|
313
416
|
/**
|
|
314
417
|
* Compact footer listing pending files that are NOT referenced in this
|
|
@@ -348,6 +451,23 @@ const projectPathProperty = {
|
|
|
348
451
|
type: 'string',
|
|
349
452
|
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).',
|
|
350
453
|
};
|
|
454
|
+
/**
|
|
455
|
+
* EVERY homegraph tool is query-only: it reads the pre-built index and never
|
|
456
|
+
* mutates the workspace (indexing is the user's explicit CLI call, never the
|
|
457
|
+
* agent's). Advertising this read-only contract lets clients that gate on it run
|
|
458
|
+
* the tools where a possibly-mutating tool would be blocked — most concretely,
|
|
459
|
+
* Cursor's Ask mode, which rejects any MCP tool lacking `readOnlyHint: true`
|
|
460
|
+
* (issue #1018). `idempotentHint`: a repeated query has no additional effect.
|
|
461
|
+
* `openWorldHint: false`: the domain is the closed local index, not an open
|
|
462
|
+
* external world. Shared so the contract is declared once; a hypothetical
|
|
463
|
+
* mutating tool would simply not reference it.
|
|
464
|
+
*/
|
|
465
|
+
const READ_ONLY_ANNOTATIONS = {
|
|
466
|
+
readOnlyHint: true,
|
|
467
|
+
destructiveHint: false,
|
|
468
|
+
idempotentHint: true,
|
|
469
|
+
openWorldHint: false,
|
|
470
|
+
};
|
|
351
471
|
/**
|
|
352
472
|
* All HomeGraph MCP tools
|
|
353
473
|
*
|
|
@@ -360,7 +480,7 @@ const projectPathProperty = {
|
|
|
360
480
|
exports.tools = [
|
|
361
481
|
{
|
|
362
482
|
name: 'homegraph_search',
|
|
363
|
-
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.',
|
|
364
484
|
inputSchema: {
|
|
365
485
|
type: 'object',
|
|
366
486
|
properties: {
|
|
@@ -382,10 +502,11 @@ exports.tools = [
|
|
|
382
502
|
},
|
|
383
503
|
required: ['query'],
|
|
384
504
|
},
|
|
505
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
385
506
|
},
|
|
386
507
|
{
|
|
387
508
|
name: 'homegraph_callers',
|
|
388
|
-
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.',
|
|
389
510
|
inputSchema: {
|
|
390
511
|
type: 'object',
|
|
391
512
|
properties: {
|
|
@@ -406,10 +527,11 @@ exports.tools = [
|
|
|
406
527
|
},
|
|
407
528
|
required: ['symbol'],
|
|
408
529
|
},
|
|
530
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
409
531
|
},
|
|
410
532
|
{
|
|
411
533
|
name: 'homegraph_callees',
|
|
412
|
-
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.',
|
|
413
535
|
inputSchema: {
|
|
414
536
|
type: 'object',
|
|
415
537
|
properties: {
|
|
@@ -430,10 +552,11 @@ exports.tools = [
|
|
|
430
552
|
},
|
|
431
553
|
required: ['symbol'],
|
|
432
554
|
},
|
|
555
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
433
556
|
},
|
|
434
557
|
{
|
|
435
558
|
name: 'homegraph_impact',
|
|
436
|
-
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.',
|
|
437
560
|
inputSchema: {
|
|
438
561
|
type: 'object',
|
|
439
562
|
properties: {
|
|
@@ -454,10 +577,11 @@ exports.tools = [
|
|
|
454
577
|
},
|
|
455
578
|
required: ['symbol'],
|
|
456
579
|
},
|
|
580
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
457
581
|
},
|
|
458
582
|
{
|
|
459
583
|
name: 'homegraph_node',
|
|
460
|
-
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.',
|
|
461
585
|
inputSchema: {
|
|
462
586
|
type: 'object',
|
|
463
587
|
properties: {
|
|
@@ -495,16 +619,17 @@ exports.tools = [
|
|
|
495
619
|
},
|
|
496
620
|
required: [],
|
|
497
621
|
},
|
|
622
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
498
623
|
},
|
|
499
624
|
{
|
|
500
625
|
name: 'homegraph_explore',
|
|
501
|
-
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.',
|
|
502
627
|
inputSchema: {
|
|
503
628
|
type: 'object',
|
|
504
629
|
properties: {
|
|
505
630
|
query: {
|
|
506
631
|
type: 'string',
|
|
507
|
-
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.',
|
|
508
633
|
},
|
|
509
634
|
maxFiles: {
|
|
510
635
|
type: 'number',
|
|
@@ -515,6 +640,7 @@ exports.tools = [
|
|
|
515
640
|
},
|
|
516
641
|
required: ['query'],
|
|
517
642
|
},
|
|
643
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
518
644
|
},
|
|
519
645
|
{
|
|
520
646
|
name: 'homegraph_status',
|
|
@@ -525,10 +651,11 @@ exports.tools = [
|
|
|
525
651
|
projectPath: projectPathProperty,
|
|
526
652
|
},
|
|
527
653
|
},
|
|
654
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
528
655
|
},
|
|
529
656
|
{
|
|
530
657
|
name: 'homegraph_files',
|
|
531
|
-
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.',
|
|
532
659
|
inputSchema: {
|
|
533
660
|
type: 'object',
|
|
534
661
|
properties: {
|
|
@@ -558,6 +685,7 @@ exports.tools = [
|
|
|
558
685
|
projectPath: projectPathProperty,
|
|
559
686
|
},
|
|
560
687
|
},
|
|
688
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
561
689
|
},
|
|
562
690
|
{
|
|
563
691
|
name: 'homegraph_spec_match',
|
|
@@ -592,8 +720,108 @@ exports.tools = [
|
|
|
592
720
|
},
|
|
593
721
|
required: ['query'],
|
|
594
722
|
},
|
|
723
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
name: 'homegraph_spec_find',
|
|
727
|
+
description: 'Find which specs are related to the given file path by matching against code-fragment file paths ' +
|
|
728
|
+
'in the Commit4Spec knowledge graph. Traverses code_fragment_nodes → commit_fragment_relations ' +
|
|
729
|
+
'→ spec_commit_relations → spec_nodes. Useful for answering "which specs does this file affect?" ' +
|
|
730
|
+
'The database defaults to .homegraph/commit4spec/commit4spec.db under the repo path.',
|
|
731
|
+
inputSchema: {
|
|
732
|
+
type: 'object',
|
|
733
|
+
properties: {
|
|
734
|
+
filePath: {
|
|
735
|
+
type: 'string',
|
|
736
|
+
description: 'File path to look up (substring LIKE match). E.g. "src/auth.ts" or "src/auth".',
|
|
737
|
+
},
|
|
738
|
+
repoPath: {
|
|
739
|
+
type: 'string',
|
|
740
|
+
description: 'Path to the repository root. Defaults to the current working directory.',
|
|
741
|
+
},
|
|
742
|
+
dbPath: {
|
|
743
|
+
type: 'string',
|
|
744
|
+
description: 'Explicit path to the Commit4Spec database. Overrides repoPath-based resolution.',
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
required: ['filePath'],
|
|
748
|
+
},
|
|
749
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: 'homegraph_spec_trace',
|
|
753
|
+
description: 'Trace a code symbol (function, method, class) back to its associated design Specs in the Commit4Spec ' +
|
|
754
|
+
'knowledge graph. Resolves the symbol via the HomeGraph code index, then matches against code-fragment ' +
|
|
755
|
+
'records in the Spec database using five-dimensional scoring: file-path match, code-diff content search ' +
|
|
756
|
+
'(FTS5), Spec title/subtitle name match, Spec recency, and line-range overlap. ' +
|
|
757
|
+
'Returns ranked Specs with score breakdowns even when exact line overlap is absent — code drifts over ' +
|
|
758
|
+
'time, so recency and content matching compensate. ' +
|
|
759
|
+
'The Spec DB defaults to .homegraph/commit4spec/commit4spec.db under the repo path.',
|
|
760
|
+
inputSchema: {
|
|
761
|
+
type: 'object',
|
|
762
|
+
properties: {
|
|
763
|
+
symbol: {
|
|
764
|
+
type: 'string',
|
|
765
|
+
description: 'Symbol name (bare or qualified). E.g. "authenticate", "AuthService.login", "auth::validate".',
|
|
766
|
+
},
|
|
767
|
+
file: {
|
|
768
|
+
type: 'string',
|
|
769
|
+
description: 'File path for disambiguation when multiple symbols share the same name (optional).',
|
|
770
|
+
},
|
|
771
|
+
line: {
|
|
772
|
+
type: 'number',
|
|
773
|
+
description: 'Line number for disambiguation (optional).',
|
|
774
|
+
},
|
|
775
|
+
repoPath: {
|
|
776
|
+
type: 'string',
|
|
777
|
+
description: 'Path to the repository root. Defaults to the current working directory.',
|
|
778
|
+
},
|
|
779
|
+
topK: {
|
|
780
|
+
type: 'number',
|
|
781
|
+
description: 'Maximum number of matching Specs to return (default: 10).',
|
|
782
|
+
default: 10,
|
|
783
|
+
},
|
|
784
|
+
dbPath: {
|
|
785
|
+
type: 'string',
|
|
786
|
+
description: 'Explicit path to the Commit4Spec database. Overrides repoPath-based resolution.',
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
required: ['symbol'],
|
|
790
|
+
},
|
|
791
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
595
792
|
},
|
|
596
793
|
];
|
|
794
|
+
/**
|
|
795
|
+
* Return `defs` with `projectPath` marked `required` in each tool's inputSchema.
|
|
796
|
+
*
|
|
797
|
+
* Used for the NO-DEFAULT-PROJECT tool surface (issue #993): when the MCP server
|
|
798
|
+
* has no default project to fall back to — a gateway server started outside any
|
|
799
|
+
* repo, or a monorepo root whose `.codegraph/` indexes live only in sub-projects
|
|
800
|
+
* — every call MUST carry an explicit `projectPath`, so the schema should say so.
|
|
801
|
+
* A `required` field is a HIGH-salience channel (MCP clients surface and often
|
|
802
|
+
* validate it), unlike the instructions text the reporter found too weak to stop
|
|
803
|
+
* the agent omitting the param. When a default project IS open, callers leave
|
|
804
|
+
* projectPath optional and never call this.
|
|
805
|
+
*
|
|
806
|
+
* Pure: clones each tool's schema rather than mutating the shared module-level
|
|
807
|
+
* `tools` array (reused by every session and the static surface). A tool that
|
|
808
|
+
* doesn't expose projectPath, or already requires it, is returned untouched;
|
|
809
|
+
* explore's `['query']` becomes `['query', 'projectPath']`, and a tool with no
|
|
810
|
+
* `required` list (status/files) gains `['projectPath']`.
|
|
811
|
+
*/
|
|
812
|
+
function withRequiredProjectPath(defs) {
|
|
813
|
+
return defs.map((tool) => {
|
|
814
|
+
if (!tool.inputSchema.properties.projectPath)
|
|
815
|
+
return tool;
|
|
816
|
+
const required = tool.inputSchema.required ?? [];
|
|
817
|
+
if (required.includes('projectPath'))
|
|
818
|
+
return tool;
|
|
819
|
+
return {
|
|
820
|
+
...tool,
|
|
821
|
+
inputSchema: { ...tool.inputSchema, required: [...required, 'projectPath'] },
|
|
822
|
+
};
|
|
823
|
+
});
|
|
824
|
+
}
|
|
597
825
|
/**
|
|
598
826
|
* Allowlist-filtered tool definitions WITHOUT an engine — the static surface the
|
|
599
827
|
* proxy answers `tools/list` with before any project is open. Mirrors
|
|
@@ -637,9 +865,23 @@ class ToolHandler {
|
|
|
637
865
|
// huge repo can't hang the first call (#905); cleared on first await so
|
|
638
866
|
// subsequent calls don't pay any cost.
|
|
639
867
|
catchUpGate = null;
|
|
868
|
+
// Optional worker-thread pool for off-loop read-tool dispatch (daemon mode).
|
|
869
|
+
// When set + healthy, the heavy read tools run on a worker so the daemon's
|
|
870
|
+
// main loop stays free for the MCP transport under concurrent load. Null in
|
|
871
|
+
// direct/in-process mode (one client, no concurrency to parallelize).
|
|
872
|
+
queryPool = null;
|
|
640
873
|
constructor(cg) {
|
|
641
874
|
this.cg = cg;
|
|
642
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Engine-only: attach (or detach with null) the worker-thread query pool. The
|
|
878
|
+
* shared daemon sets this once its default project is open; the workers each
|
|
879
|
+
* hold their own WAL read connection and run {@link executeReadTool}. A
|
|
880
|
+
* worker's own ToolHandler never has a pool, so there is no nested off-loading.
|
|
881
|
+
*/
|
|
882
|
+
setQueryPool(pool) {
|
|
883
|
+
this.queryPool = pool;
|
|
884
|
+
}
|
|
643
885
|
/**
|
|
644
886
|
* Update the default HomeGraph instance (e.g. after lazy initialization)
|
|
645
887
|
*/
|
|
@@ -743,8 +985,18 @@ class ToolHandler {
|
|
|
743
985
|
let visible = allow
|
|
744
986
|
? exports.tools.filter(t => allow.has(t.name.replace(/^homegraph_/, '')))
|
|
745
987
|
: exports.tools;
|
|
988
|
+
// No default project loaded → no-root-index case (#993): a gateway server
|
|
989
|
+
// started outside any repo, or a monorepo root whose indexes live in
|
|
990
|
+
// sub-projects. With nothing to fall back to, EVERY call needs an explicit
|
|
991
|
+
// projectPath, so mark it required in the schema — a high-salience nudge the
|
|
992
|
+
// agent acts on, where SERVER_INSTRUCTIONS_NO_ROOT_INDEX's prose alone
|
|
993
|
+
// wasn't enough (the reporter had to add an AGENTS.md note). `this.cg` is
|
|
994
|
+
// settled by `retryInitIfNeeded()` before `handleToolsList` calls us, so a
|
|
995
|
+
// null here means "genuinely no default", not a startup race. When a default
|
|
996
|
+
// IS open we leave projectPath optional (below): a bare call falls back to
|
|
997
|
+
// it, exactly as in the common single-project launch.
|
|
746
998
|
if (!this.cg)
|
|
747
|
-
return visible;
|
|
999
|
+
return withRequiredProjectPath(visible);
|
|
748
1000
|
try {
|
|
749
1001
|
const stats = this.cg.getStats();
|
|
750
1002
|
const budget = getExploreBudget(stats.fileCount);
|
|
@@ -883,6 +1135,7 @@ class ToolHandler {
|
|
|
883
1135
|
freshen(cg) {
|
|
884
1136
|
try {
|
|
885
1137
|
if (cg.reopenIfReplaced()) {
|
|
1138
|
+
(0, query_cache_1.getMcpQueryCacheIndex)(cg.getProjectRoot()).reset();
|
|
886
1139
|
process.stderr.write('[HomeGraph MCP] The index was replaced on disk (e.g. a git worktree ' +
|
|
887
1140
|
'recreated at the same path); reopened the live database in place.\n');
|
|
888
1141
|
}
|
|
@@ -1089,7 +1342,18 @@ class ToolHandler {
|
|
|
1089
1342
|
}
|
|
1090
1343
|
let banner = '';
|
|
1091
1344
|
if (inResponse.length > 0) {
|
|
1092
|
-
|
|
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
|
+
});
|
|
1093
1357
|
}
|
|
1094
1358
|
let footer = '';
|
|
1095
1359
|
if (elsewhere.length > 0) {
|
|
@@ -1118,6 +1382,11 @@ class ToolHandler {
|
|
|
1118
1382
|
this.catchUpGate = null;
|
|
1119
1383
|
await this.awaitCatchUpGate(gate);
|
|
1120
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
|
+
}
|
|
1121
1390
|
// Honor the optional tool allowlist (HOMEGRAPH_MCP_TOOLS): a trimmed
|
|
1122
1391
|
// surface rejects ablated tools defensively even if a client cached them.
|
|
1123
1392
|
if (!this.isToolAllowed(toolName)) {
|
|
@@ -1143,48 +1412,83 @@ class ToolHandler {
|
|
|
1143
1412
|
if (typeof check === 'object' && check !== undefined)
|
|
1144
1413
|
return check;
|
|
1145
1414
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1415
|
+
const projectPath = args.projectPath;
|
|
1416
|
+
const cacheEnabled = (0, query_cache_1.isMcpQueryCacheEnabled)() && (0, query_cache_1.isCacheableMcpTool)(toolName);
|
|
1417
|
+
let cacheKey;
|
|
1418
|
+
let cacheQueries;
|
|
1419
|
+
let cacheIndex;
|
|
1420
|
+
if (cacheEnabled) {
|
|
1421
|
+
try {
|
|
1422
|
+
const cacheCg = this.getHomeGraph(projectPath);
|
|
1423
|
+
cacheQueries = cacheCg.getQueryBuilder();
|
|
1424
|
+
cacheIndex = (0, query_cache_1.getMcpQueryCacheIndex)(cacheCg.getProjectRoot());
|
|
1425
|
+
cacheIndex.ensureValid(cacheQueries, () => cacheCg.getLastIndexedAt());
|
|
1426
|
+
let fileCount;
|
|
1427
|
+
try {
|
|
1428
|
+
fileCount = cacheCg.getStats().fileCount;
|
|
1429
|
+
}
|
|
1430
|
+
catch {
|
|
1431
|
+
fileCount = undefined;
|
|
1432
|
+
}
|
|
1433
|
+
cacheKey = (0, query_cache_1.buildMcpQueryCacheKey)(toolName, args, fileCount);
|
|
1434
|
+
const cached = cacheIndex.getEntry(cacheQueries, cacheKey);
|
|
1435
|
+
if (cached) {
|
|
1436
|
+
const withWorktree = this.withWorktreeNotice(cached, projectPath);
|
|
1437
|
+
return this.withStalenessNotice(withWorktree, projectPath);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
catch {
|
|
1441
|
+
// No indexed project — fall through; handler returns guidance.
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
// homegraph_status reports watcher state (pending files, degraded mode,
|
|
1445
|
+
// worktree warning) and embeds its own sections — it must run on the MAIN
|
|
1446
|
+
// thread against the watched default instance, so it is NEVER off-loaded to
|
|
1447
|
+
// a worker (whose read connection has no watcher). It also skips the
|
|
1448
|
+
// auto-banner wrapper to avoid duplicating its own pending-files section.
|
|
1449
|
+
if (toolName === 'homegraph_status') {
|
|
1450
|
+
return await this.handleStatus(args);
|
|
1451
|
+
}
|
|
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);
|
|
1185
1489
|
}
|
|
1186
|
-
const withWorktree = this.withWorktreeNotice(result,
|
|
1187
|
-
return this.withStalenessNotice(withWorktree,
|
|
1490
|
+
const withWorktree = this.withWorktreeNotice(result, projectPath);
|
|
1491
|
+
return this.withStalenessNotice(withWorktree, projectPath);
|
|
1188
1492
|
}
|
|
1189
1493
|
catch (err) {
|
|
1190
1494
|
// Expected condition, not a malfunction: answer as a SUCCESS so the
|
|
@@ -1203,20 +1507,151 @@ class ToolHandler {
|
|
|
1203
1507
|
}
|
|
1204
1508
|
}
|
|
1205
1509
|
/**
|
|
1206
|
-
*
|
|
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
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Run a single read tool to completion and return its raw {@link ToolResult},
|
|
1569
|
+
* classifying expected failures the same way {@link execute}'s catch does so
|
|
1570
|
+
* the SHAPE is identical whether dispatch runs in-process or on a worker:
|
|
1571
|
+
* NotIndexed → success-shaped guidance, PathRefusal → clean error, anything
|
|
1572
|
+
* else → internal-error-with-retry. Never throws.
|
|
1573
|
+
*
|
|
1574
|
+
* This is the worker thread's entry point (see {@link ./query-worker}) and the
|
|
1575
|
+
* in-process fallback for {@link execute}. It deliberately does NOT run the
|
|
1576
|
+
* catch-up gate or the staleness/worktree notices — those need the daemon's
|
|
1577
|
+
* watched main instance and stay on the main thread. Cross-cutting allowlist +
|
|
1578
|
+
* path validation already ran in {@link execute} before routing here.
|
|
1579
|
+
*/
|
|
1580
|
+
async executeReadTool(toolName, args) {
|
|
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;
|
|
1588
|
+
return await this.dispatchTool(toolName, args);
|
|
1589
|
+
}
|
|
1590
|
+
catch (err) {
|
|
1591
|
+
if (err instanceof NotIndexedError) {
|
|
1592
|
+
return this.textResult(err.message);
|
|
1593
|
+
}
|
|
1594
|
+
if (err instanceof PathRefusalError) {
|
|
1595
|
+
return this.errorResult(err.message);
|
|
1596
|
+
}
|
|
1597
|
+
return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
1598
|
+
'This is an internal homegraph error — retry the call once; if it persists, ' +
|
|
1599
|
+
'continue without homegraph for this task.');
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Pure dispatch over the read tools — the switch, with no gate, no notices, no
|
|
1604
|
+
* allowlist/validation (the caller owns those). `homegraph_status` is handled
|
|
1605
|
+
* on the main thread in {@link execute} and never reaches here. May throw
|
|
1606
|
+
* NotIndexed/PathRefusal, which {@link executeReadTool} classifies.
|
|
1207
1607
|
*/
|
|
1608
|
+
async dispatchTool(toolName, args) {
|
|
1609
|
+
switch (toolName) {
|
|
1610
|
+
case 'homegraph_search': return await this.handleSearch(args);
|
|
1611
|
+
case 'homegraph_callers': return await this.handleCallers(args);
|
|
1612
|
+
case 'homegraph_callees': return await this.handleCallees(args);
|
|
1613
|
+
case 'homegraph_impact': return await this.handleImpact(args);
|
|
1614
|
+
case 'homegraph_explore': return await this.handleExplore(args);
|
|
1615
|
+
case 'homegraph_node': return await this.handleNode(args);
|
|
1616
|
+
case 'homegraph_files': return await this.handleFiles(args);
|
|
1617
|
+
case 'homegraph_spec_match': return await this.handleSpecMatch(args);
|
|
1618
|
+
case 'homegraph_spec_find': return await this.handleSpecFind(args);
|
|
1619
|
+
case 'homegraph_spec_trace': return await this.handleSpecTrace(args);
|
|
1620
|
+
default: return this.errorResult(`Unknown tool: ${toolName}`);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1208
1623
|
async handleSearch(args) {
|
|
1209
1624
|
const query = this.validateString(args.query, 'query');
|
|
1210
1625
|
if (typeof query !== 'string')
|
|
1211
1626
|
return query;
|
|
1212
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
|
+
}
|
|
1213
1648
|
const rawKind = args.kind;
|
|
1214
1649
|
// The schema enum says 'type' (what agents naturally reach for); the
|
|
1215
1650
|
// NodeKind is 'type_alias'. Without the mapping, kind: "type" silently
|
|
1216
1651
|
// matched nothing — a filter value we advertise must work.
|
|
1217
1652
|
const kind = rawKind === 'type' ? 'type_alias' : rawKind;
|
|
1218
|
-
const rawLimit = Number(args.limit)
|
|
1219
|
-
const limit = (0, utils_1.clamp)(rawLimit, 1, 100);
|
|
1653
|
+
const rawLimit = Number(args.limit);
|
|
1654
|
+
const limit = (0, utils_1.clamp)(isNaN(rawLimit) ? 10 : rawLimit, 1, 100);
|
|
1220
1655
|
const results = cg.searchNodes(query, {
|
|
1221
1656
|
limit,
|
|
1222
1657
|
kinds: kind ? [kind] : undefined,
|
|
@@ -1233,7 +1668,9 @@ class ToolHandler {
|
|
|
1233
1668
|
return aGen - bGen;
|
|
1234
1669
|
});
|
|
1235
1670
|
const formatted = this.formatSearchResults(ranked);
|
|
1236
|
-
|
|
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));
|
|
1237
1674
|
}
|
|
1238
1675
|
/**
|
|
1239
1676
|
* Group symbol matches into DISTINCT DEFINITIONS — one group per
|
|
@@ -1295,6 +1732,8 @@ class ToolHandler {
|
|
|
1295
1732
|
const callers = [];
|
|
1296
1733
|
const labels = new Map();
|
|
1297
1734
|
for (const node of defNodes) {
|
|
1735
|
+
if ((0, memory_budget_1.isOverRssBudget)())
|
|
1736
|
+
break;
|
|
1298
1737
|
for (const c of cg.getCallers(node.id)) {
|
|
1299
1738
|
if (!seen.has(c.node.id)) {
|
|
1300
1739
|
seen.add(c.node.id);
|
|
@@ -1303,6 +1742,8 @@ class ToolHandler {
|
|
|
1303
1742
|
if (label)
|
|
1304
1743
|
labels.set(c.node.id, label);
|
|
1305
1744
|
}
|
|
1745
|
+
if (callers.length >= limit)
|
|
1746
|
+
return { callers, labels };
|
|
1306
1747
|
}
|
|
1307
1748
|
}
|
|
1308
1749
|
return { callers, labels };
|
|
@@ -1316,7 +1757,9 @@ class ToolHandler {
|
|
|
1316
1757
|
// A successful `file` narrowing makes the multi-symbol aggregation note
|
|
1317
1758
|
// stale — suppress it.
|
|
1318
1759
|
const note = fileFilter && !filteredOut ? '' : allMatches.note;
|
|
1319
|
-
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;
|
|
1320
1763
|
return this.textResult(this.truncateOutput(formatted));
|
|
1321
1764
|
}
|
|
1322
1765
|
// Multiple DISTINCT definitions (#764): one section per definition so an
|
|
@@ -1338,7 +1781,7 @@ class ToolHandler {
|
|
|
1338
1781
|
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
|
|
1339
1782
|
}
|
|
1340
1783
|
}
|
|
1341
|
-
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.'));
|
|
1342
1785
|
}
|
|
1343
1786
|
/**
|
|
1344
1787
|
* Handle homegraph_callees
|
|
@@ -1362,6 +1805,12 @@ class ToolHandler {
|
|
|
1362
1805
|
const seen = new Set();
|
|
1363
1806
|
const callees = [];
|
|
1364
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
|
+
});
|
|
1365
1814
|
for (const node of defNodes) {
|
|
1366
1815
|
for (const c of cg.getCallees(node.id)) {
|
|
1367
1816
|
if (!seen.has(c.node.id)) {
|
|
@@ -1373,7 +1822,10 @@ class ToolHandler {
|
|
|
1373
1822
|
}
|
|
1374
1823
|
}
|
|
1375
1824
|
}
|
|
1376
|
-
|
|
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 };
|
|
1377
1829
|
};
|
|
1378
1830
|
if (groups.length === 1) {
|
|
1379
1831
|
const { callees, labels } = collect(groups[0]);
|
|
@@ -1526,6 +1978,20 @@ class ToolHandler {
|
|
|
1526
1978
|
}
|
|
1527
1979
|
if (m?.synthesizedBy === 'viewtree') {
|
|
1528
1980
|
const via = typeof m.via === 'string' ? m.via : '';
|
|
1981
|
+
if (via === 'Prop') {
|
|
1982
|
+
return {
|
|
1983
|
+
label: `@Prop one-way state transfer (parent → child)`,
|
|
1984
|
+
compact: `state: @Prop one-way${at}`,
|
|
1985
|
+
registeredAt,
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
if (via === 'Link') {
|
|
1989
|
+
return {
|
|
1990
|
+
label: `@Link two-way state transfer (parent ↔ child)`,
|
|
1991
|
+
compact: `state: @Link two-way${at}`,
|
|
1992
|
+
registeredAt,
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1529
1995
|
if (via && !VIEWTREE_STRUCTURE_VIAS.has(via)) {
|
|
1530
1996
|
return {
|
|
1531
1997
|
label: `ArkUI event \`.${via}\` — bound handler (dynamic dispatch)`,
|
|
@@ -2146,76 +2612,1650 @@ class ToolHandler {
|
|
|
2146
2612
|
return `candidates for key \`${key}\`: ${list.join(', ')}`;
|
|
2147
2613
|
}
|
|
2148
2614
|
/**
|
|
2149
|
-
*
|
|
2150
|
-
*
|
|
2151
|
-
* no source, so the agent knows what to update / re-verify before editing
|
|
2152
|
-
* without reaching for a separate impact call. Always-on, but skips symbols
|
|
2153
|
-
* that have no dependents (nothing to warn about), and returns '' when none
|
|
2154
|
-
* qualify so a leaf-only exploration stays clean.
|
|
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.
|
|
2155
2617
|
*/
|
|
2156
|
-
|
|
2157
|
-
const
|
|
2158
|
-
const
|
|
2159
|
-
const
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
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 = [];
|
|
2173
2652
|
try {
|
|
2174
|
-
|
|
2653
|
+
hits = cg.searchNodes(sym, { kinds: ['import'], limit: importLimit });
|
|
2175
2654
|
}
|
|
2176
|
-
catch {
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
for (const
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
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);
|
|
2183
2683
|
}
|
|
2184
2684
|
}
|
|
2185
|
-
if (uniq.length === 0)
|
|
2186
|
-
continue; // no blast radius → nothing to flag
|
|
2187
|
-
const callerFiles = [...new Set(uniq.map((n) => rel(n.filePath)))];
|
|
2188
|
-
const testFiles = callerFiles.filter((f) => (0, query_utils_1.isTestFile)(f));
|
|
2189
|
-
const nonTest = callerFiles.filter((f) => !(0, query_utils_1.isTestFile)(f));
|
|
2190
|
-
const shown = nonTest.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ');
|
|
2191
|
-
const more = nonTest.length > FILE_CAP ? ` +${nonTest.length - FILE_CAP} more` : '';
|
|
2192
|
-
const where = nonTest.length > 0 ? ` in ${shown}${more}` : '';
|
|
2193
|
-
const tests = testFiles.length > 0
|
|
2194
|
-
? `; tests: ${testFiles.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ')}${testFiles.length > FILE_CAP ? ` +${testFiles.length - FILE_CAP}` : ''}`
|
|
2195
|
-
: '; ⚠️ no covering tests found';
|
|
2196
|
-
entries.push(`- \`${root.name}\` (${rel(root.filePath)}:${root.startLine}) — ${uniq.length} caller${uniq.length === 1 ? '' : 's'}${where}${tests}`);
|
|
2197
2685
|
}
|
|
2198
|
-
if (
|
|
2199
|
-
return '';
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
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 };
|
|
2206
2719
|
}
|
|
2207
2720
|
/**
|
|
2208
|
-
*
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
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))
|
|
4022
|
+
continue;
|
|
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;
|
|
4031
|
+
try {
|
|
4032
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
4033
|
+
}
|
|
4034
|
+
catch {
|
|
4035
|
+
continue;
|
|
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
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
if (hits.size === 0)
|
|
4045
|
+
return { section: '', fileCount: 0 };
|
|
4046
|
+
const lines = [
|
|
4047
|
+
'**API usage sites**',
|
|
4048
|
+
'',
|
|
4049
|
+
`> Files referencing \`${symbols.join('`, `')}\` in this repo (${hits.size} file(s)). Answer from this list; source bodies omitted.`,
|
|
4050
|
+
'',
|
|
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 };
|
|
4068
|
+
}
|
|
4069
|
+
/**
|
|
4070
|
+
* Domain file inventory — related files, usage existence, or concept comparison.
|
|
4071
|
+
* Paths + top symbols only; source bodies omitted when this section renders.
|
|
4072
|
+
*/
|
|
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))
|
|
4081
|
+
return;
|
|
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);
|
|
4089
|
+
};
|
|
4090
|
+
for (const term of terms) {
|
|
4091
|
+
let hits = [];
|
|
4092
|
+
try {
|
|
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);
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
try {
|
|
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
|
+
}
|
|
4111
|
+
}
|
|
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}`);
|
|
4127
|
+
}
|
|
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;
|
|
4165
|
+
try {
|
|
4166
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
4167
|
+
}
|
|
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('');
|
|
4180
|
+
}
|
|
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');
|
|
4187
|
+
}
|
|
4188
|
+
/**
|
|
4189
|
+
* Compact "blast radius" for the entry symbols of an explore result: who
|
|
4190
|
+
* depends on each (callers) and which test files cover it — LOCATIONS ONLY,
|
|
4191
|
+
* no source, so the agent knows what to update / re-verify before editing
|
|
4192
|
+
* without reaching for a separate impact call. Always-on, but skips symbols
|
|
4193
|
+
* that have no dependents (nothing to warn about), and returns '' when none
|
|
4194
|
+
* qualify so a leaf-only exploration stays clean.
|
|
4195
|
+
*/
|
|
4196
|
+
buildBlastRadiusSection(cg, subgraph) {
|
|
4197
|
+
const ROOT_CAP = 5; // only the symbols the query actually targeted
|
|
4198
|
+
const FILE_CAP = 4; // caller files listed per symbol before "+N more"
|
|
4199
|
+
const MEANINGFUL = new Set([
|
|
4200
|
+
'function', 'method', 'class', 'interface', 'struct', 'trait', 'protocol',
|
|
4201
|
+
'enum', 'type_alias', 'component', 'constant', 'variable', 'property', 'field',
|
|
4202
|
+
]);
|
|
4203
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
4204
|
+
const roots = subgraph.roots
|
|
4205
|
+
.map((id) => subgraph.nodes.get(id))
|
|
4206
|
+
.filter((n) => !!n && MEANINGFUL.has(n.kind))
|
|
4207
|
+
.slice(0, ROOT_CAP);
|
|
4208
|
+
if (roots.length === 0)
|
|
4209
|
+
return '';
|
|
4210
|
+
const entries = [];
|
|
4211
|
+
for (const root of roots) {
|
|
4212
|
+
let callers = [];
|
|
4213
|
+
try {
|
|
4214
|
+
callers = cg.getCallers(root.id);
|
|
4215
|
+
}
|
|
4216
|
+
catch { /* skip this root */ }
|
|
4217
|
+
const seen = new Set();
|
|
4218
|
+
const uniq = [];
|
|
4219
|
+
for (const c of callers) {
|
|
4220
|
+
if (c?.node && !seen.has(c.node.id)) {
|
|
4221
|
+
seen.add(c.node.id);
|
|
4222
|
+
uniq.push(c.node);
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
if (uniq.length === 0)
|
|
4226
|
+
continue; // no blast radius → nothing to flag
|
|
4227
|
+
const callerFiles = [...new Set(uniq.map((n) => rel(n.filePath)))];
|
|
4228
|
+
const testFiles = callerFiles.filter((f) => (0, query_utils_1.isTestFile)(f));
|
|
4229
|
+
const nonTest = callerFiles.filter((f) => !(0, query_utils_1.isTestFile)(f));
|
|
4230
|
+
const shown = nonTest.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ');
|
|
4231
|
+
const more = nonTest.length > FILE_CAP ? ` +${nonTest.length - FILE_CAP} more` : '';
|
|
4232
|
+
const where = nonTest.length > 0 ? ` in ${shown}${more}` : '';
|
|
4233
|
+
const tests = testFiles.length > 0
|
|
4234
|
+
? `; tests: ${testFiles.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ')}${testFiles.length > FILE_CAP ? ` +${testFiles.length - FILE_CAP}` : ''}`
|
|
4235
|
+
: '; ⚠️ no covering tests found';
|
|
4236
|
+
entries.push(`- \`${root.name}\` (${rel(root.filePath)}:${root.startLine}) — ${uniq.length} caller${uniq.length === 1 ? '' : 's'}${where}${tests}`);
|
|
4237
|
+
}
|
|
4238
|
+
if (entries.length === 0)
|
|
4239
|
+
return '';
|
|
4240
|
+
return [
|
|
4241
|
+
'**Blast radius — what depends on these (update/verify before editing)**',
|
|
4242
|
+
'',
|
|
4243
|
+
...entries,
|
|
4244
|
+
'',
|
|
4245
|
+
].join('\n');
|
|
4246
|
+
}
|
|
4247
|
+
/**
|
|
4248
|
+
* Graph-connectivity relevance via Random-Walk-with-Restart (personalized
|
|
4249
|
+
* PageRank) from the query's matched SEED nodes over the call/reference graph.
|
|
4250
|
+
*
|
|
4251
|
+
* This is the ranking signal text search (FTS/bm25) CANNOT provide, and it's
|
|
4252
|
+
* homegraph's home turf: relevance by STRUCTURE, not words. A file whose
|
|
4253
|
+
* symbols are call-connected to the matched cluster accrues walk mass and
|
|
4254
|
+
* ranks high; a lone TEXT match — e.g. `LensSwitcher.swift` matched the word
|
|
4255
|
+
* "switch" from `switchOrganization`, but calls none of `setUser`/`fetchUser`
|
|
4256
|
+
* — gets only its own restart probability and ranks ~0. Immune to the
|
|
4257
|
+
* tokenization trap that fools term matching, deterministic, no embeddings.
|
|
4258
|
+
*
|
|
2219
4259
|
* Undirected adjacency (reachability both ways), restart α=0.25 to the seeds,
|
|
2220
4260
|
* power iteration to convergence. Bounded to the already-relevant subgraph, so
|
|
2221
4261
|
* it's a few hundred nodes × ~25 iterations — negligible cost.
|
|
@@ -2298,12 +4338,22 @@ class ToolHandler {
|
|
|
2298
4338
|
* tax on small projects while earning its keep on large ones.
|
|
2299
4339
|
*/
|
|
2300
4340
|
async handleExplore(args) {
|
|
2301
|
-
const
|
|
2302
|
-
if (typeof
|
|
2303
|
-
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);
|
|
2304
4347
|
const cg = this.getHomeGraph(args.projectPath);
|
|
2305
4348
|
const projectRoot = cg.getProjectRoot();
|
|
2306
|
-
|
|
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.
|
|
2307
4357
|
// largest-tier defaults if stats aren't available, which preserves
|
|
2308
4358
|
// pre-#185 behavior for callers that hit the rare stats failure.
|
|
2309
4359
|
let budget;
|
|
@@ -2313,20 +4363,72 @@ class ToolHandler {
|
|
|
2313
4363
|
catch {
|
|
2314
4364
|
budget = getExploreOutputBudget(Infinity);
|
|
2315
4365
|
}
|
|
2316
|
-
|
|
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);
|
|
2317
4373
|
// Step 1: Find relevant context with generous parameters.
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
minScore: 0.2,
|
|
2326
|
-
});
|
|
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);
|
|
2327
4381
|
if (subgraph.nodes.size === 0) {
|
|
2328
4382
|
return this.textResult(`No relevant code found for "${query}"`);
|
|
2329
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
|
+
}
|
|
2330
4432
|
// Graph-aware glue: findRelevantContext builds the subgraph from name/text
|
|
2331
4433
|
// search, so a method that BRIDGES named symbols — e.g. App.tsx's
|
|
2332
4434
|
// triggerRender, which calls the named triggerUpdate — is never a search hit
|
|
@@ -2374,14 +4476,53 @@ class ToolHandler {
|
|
|
2374
4476
|
// trace endpoint picker uses) and inject it as an entry, so every symbol the
|
|
2375
4477
|
// agent explicitly named is in the subgraph and its file is scored.
|
|
2376
4478
|
const namedSeedIds = new Set();
|
|
4479
|
+
// The subset of named seeds that earns the named-FIRST sort tier. We still
|
|
4480
|
+
// SEED every ≤3-def name (so RWR / flow ranking is unchanged), but only the
|
|
4481
|
+
// most-substantive def is tiered — a bare name's unrelated namesakes (Go's
|
|
4482
|
+
// `NewClient` = real client + test fake + xds pool) must not fill the tier
|
|
4483
|
+
// and crowd out the real answer file (grpc's `dialoptions.go`). Corroborated
|
|
4484
|
+
// overloads (the query also named the type) all earn it. (#1064)
|
|
4485
|
+
const tierSeedIds = new Set();
|
|
2377
4486
|
{
|
|
2378
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;
|
|
2379
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
|
+
]);
|
|
2380
4496
|
const isTestPath = (p) => /(^|\/)(tests?|specs?|__tests__|testdata|mocks?|fixtures?)\//i.test(p) || /\.(test|spec)\.[a-z]+$/i.test(p);
|
|
2381
4497
|
const bodyLines = (n) => Math.max(0, (n.endLine ?? n.startLine) - n.startLine);
|
|
2382
|
-
const
|
|
2383
|
-
|
|
2384
|
-
|
|
4498
|
+
const callerCount = (n) => { try {
|
|
4499
|
+
return cg.getCallers(n.id).length;
|
|
4500
|
+
}
|
|
4501
|
+
catch {
|
|
4502
|
+
return 0;
|
|
4503
|
+
} };
|
|
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);
|
|
2385
4526
|
// PascalCase tokens in the query are type/file disambiguators — when the
|
|
2386
4527
|
// agent writes "DataRequest task validate", the `task`/`validate` it wants
|
|
2387
4528
|
// are DataRequest's, NOT the same-named overloads in Validation.swift /
|
|
@@ -2405,8 +4546,15 @@ class ToolHandler {
|
|
|
2405
4546
|
const isQual = /[.\/]|::/.test(t);
|
|
2406
4547
|
const raw = isQual ? this.findAllSymbols(cg, t).nodes : cg.getNodesByName(t);
|
|
2407
4548
|
const cands = raw
|
|
2408
|
-
.filter((n) =>
|
|
2409
|
-
.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
|
+
});
|
|
2410
4558
|
// A specific name (<=3 defs) injects all its defs. An overloaded name
|
|
2411
4559
|
// (`validate` = 10, `request` = 44) would flood the subgraph, so inject
|
|
2412
4560
|
// only: the overloads whose file/class the query ALSO names (the agent
|
|
@@ -2414,12 +4562,23 @@ class ToolHandler {
|
|
|
2414
4562
|
// capped; else fall back to the single most-substantive def. This is the
|
|
2415
4563
|
// explore-side mirror of homegraph_node's overload disambiguation.
|
|
2416
4564
|
let picks;
|
|
4565
|
+
let tierPicks; // subset that earns the named-first tier (#1064)
|
|
2417
4566
|
if (cands.length <= 3) {
|
|
2418
4567
|
picks = cands;
|
|
4568
|
+
// Centrality de-noise: tier the most-substantive def PLUS any co-named
|
|
4569
|
+
// def of comparable centrality (a real overload/wrapper — excalidraw's
|
|
4570
|
+
// `mutateElement` lives in mutateElement.ts, App.tsx AND Scene.ts, all
|
|
4571
|
+
// within ~2x callers). EXCLUDE a vastly-less-central namesake (Go's
|
|
4572
|
+
// `NewClient`: real client 492 callers vs xds-pool 11, test-fake 3 →
|
|
4573
|
+
// ratio <0.025) so it doesn't fill the tier and crowd out the answer.
|
|
4574
|
+
const counts = new Map(cands.map((c) => [c.id, callerCount(c)]));
|
|
4575
|
+
const maxCallers = Math.max(1, ...counts.values());
|
|
4576
|
+
tierPicks = cands.filter((c, i) => i === 0 || (counts.get(c.id) ?? 0) >= maxCallers * 0.25);
|
|
2419
4577
|
}
|
|
2420
4578
|
else {
|
|
2421
4579
|
const ctx = cands.filter(inNamedContext);
|
|
2422
4580
|
picks = ctx.length > 0 ? ctx.slice(0, 4) : cands.slice(0, 1);
|
|
4581
|
+
tierPicks = picks; // corroborated overloads (or the single fallback) all earn it
|
|
2423
4582
|
}
|
|
2424
4583
|
for (const n of picks) {
|
|
2425
4584
|
if (!subgraph.nodes.has(n.id))
|
|
@@ -2431,6 +4590,70 @@ class ToolHandler {
|
|
|
2431
4590
|
// so a named symbol FTS already gathered never sorted to the top.)
|
|
2432
4591
|
namedSeedIds.add(n.id);
|
|
2433
4592
|
}
|
|
4593
|
+
for (const n of tierPicks)
|
|
4594
|
+
tierSeedIds.add(n.id);
|
|
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
|
+
}
|
|
2434
4657
|
}
|
|
2435
4658
|
}
|
|
2436
4659
|
// Step 2: Group nodes by file, score by relevance
|
|
@@ -2444,6 +4667,46 @@ class ToolHandler {
|
|
|
2444
4667
|
if (entryNodeIds.has(edge.target))
|
|
2445
4668
|
connectedToEntry.add(edge.source);
|
|
2446
4669
|
}
|
|
4670
|
+
// CHANGE SURFACE (#1064): a named method's signature types — its parameter
|
|
4671
|
+
// and return types — are part of what you'd edit to "add a parameter to X",
|
|
4672
|
+
// yet they can be lexically dissimilar to the query ("add a parameter to
|
|
4673
|
+
// NewClient" shares no words with `dialoptions.go`, which defines NewClient's
|
|
4674
|
+
// `DialOption`) and sit a hop away. COLLECT them here from each named-seed
|
|
4675
|
+
// callable's outgoing signature edges (full graph — the type is often not in
|
|
4676
|
+
// the subgraph); the decision to surface one is DEFERRED to the buried-rescue
|
|
4677
|
+
// pass below, which fires only when the type's file would otherwise be
|
|
4678
|
+
// dropped — so a well-connected type (excalidraw's element types, Alamofire's
|
|
4679
|
+
// `DataRequest` on a flow query) is left to rank on its own and never
|
|
4680
|
+
// displaces a flow-central file. Bounded: only the few named seeds, only the
|
|
4681
|
+
// types in their signatures.
|
|
4682
|
+
const CALLABLE_KINDS = new Set(['method', 'function', 'component', 'constructor']);
|
|
4683
|
+
const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'type_alias']);
|
|
4684
|
+
const SIG_EDGE = new Set(['references', 'type_of', 'returns']);
|
|
4685
|
+
const changeSurfaceCandidates = [];
|
|
4686
|
+
const seenChangeSurface = new Set();
|
|
4687
|
+
for (const seedId of tierSeedIds) {
|
|
4688
|
+
const seedNode = subgraph.nodes.get(seedId);
|
|
4689
|
+
if (!seedNode || !CALLABLE_KINDS.has(seedNode.kind))
|
|
4690
|
+
continue;
|
|
4691
|
+
let outs = [];
|
|
4692
|
+
try {
|
|
4693
|
+
outs = cg.getOutgoingEdges(seedId);
|
|
4694
|
+
}
|
|
4695
|
+
catch {
|
|
4696
|
+
continue;
|
|
4697
|
+
}
|
|
4698
|
+
for (const e of outs) {
|
|
4699
|
+
if (!SIG_EDGE.has(e.kind))
|
|
4700
|
+
continue;
|
|
4701
|
+
const tgt = cg.getNode(e.target);
|
|
4702
|
+
if (!tgt || !TYPE_KINDS.has(tgt.kind) || namedSeedIds.has(tgt.id))
|
|
4703
|
+
continue;
|
|
4704
|
+
if (seenChangeSurface.has(tgt.id))
|
|
4705
|
+
continue;
|
|
4706
|
+
seenChangeSurface.add(tgt.id);
|
|
4707
|
+
changeSurfaceCandidates.push(tgt);
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
2447
4710
|
for (const node of subgraph.nodes.values()) {
|
|
2448
4711
|
// Skip import/export nodes — they add noise without information
|
|
2449
4712
|
if (node.kind === 'import' || node.kind === 'export')
|
|
@@ -2476,6 +4739,14 @@ class ToolHandler {
|
|
|
2476
4739
|
}
|
|
2477
4740
|
fileGroups.set(node.filePath, group);
|
|
2478
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
|
+
}
|
|
2479
4750
|
// Only include files that have entry points or nodes directly connected to entry points
|
|
2480
4751
|
let relevantFiles = [...fileGroups.entries()].filter(([, group]) => group.score >= 3);
|
|
2481
4752
|
// Extract query terms for relevance checking
|
|
@@ -2563,6 +4834,36 @@ class ToolHandler {
|
|
|
2563
4834
|
if (n)
|
|
2564
4835
|
entryFiles.add(n.filePath);
|
|
2565
4836
|
}
|
|
4837
|
+
// Buried-rescue pass (#1064): surface a named method's signature type ONLY
|
|
4838
|
+
// when its file is genuinely buried — near-zero graph mass AND not lexically
|
|
4839
|
+
// matched. That is the invisible case (grpc's `DialOption` → `dialoptions.go`,
|
|
4840
|
+
// g≈0, 0 term hits): reachable but ranked nowhere, so the agent greps. A
|
|
4841
|
+
// well-connected type file (excalidraw element types, Alamofire `DataRequest`)
|
|
4842
|
+
// is NOT buried and is left alone — rescuing it would displace a flow-central
|
|
4843
|
+
// file (App.tsx, Validation.swift). Buried is judged on the PRE-rescue graph,
|
|
4844
|
+
// so injecting the type below can't make it look connected. A rescued file is
|
|
4845
|
+
// injected (so it renders), force-kept (gate + relevantFiles), and tiered.
|
|
4846
|
+
const changeSurfaceFiles = new Set();
|
|
4847
|
+
for (const t of changeSurfaceCandidates) {
|
|
4848
|
+
const fp = t.filePath;
|
|
4849
|
+
const buried = (fileGraphScore.get(fp) ?? 0) < maxGraph * 0.06
|
|
4850
|
+
&& (fileTermHits.get(fp) ?? 0) < 2;
|
|
4851
|
+
if (!buried)
|
|
4852
|
+
continue;
|
|
4853
|
+
changeSurfaceFiles.add(fp);
|
|
4854
|
+
if (!subgraph.nodes.has(t.id))
|
|
4855
|
+
subgraph.nodes.set(t.id, t);
|
|
4856
|
+
let group = fileGroups.get(fp);
|
|
4857
|
+
if (!group) {
|
|
4858
|
+
group = { nodes: [], score: 0 };
|
|
4859
|
+
fileGroups.set(fp, group);
|
|
4860
|
+
}
|
|
4861
|
+
if (!group.nodes.some((n) => n.id === t.id))
|
|
4862
|
+
group.nodes.push(t);
|
|
4863
|
+
group.score = Math.max(group.score, 45);
|
|
4864
|
+
if (!relevantFiles.some(([f]) => f === fp))
|
|
4865
|
+
relevantFiles.push([fp, group]);
|
|
4866
|
+
}
|
|
2566
4867
|
// Relevance gate (so the generous budget is a CEILING, not a target): keep a
|
|
2567
4868
|
// file only if it is STRUCTURALLY relevant by ANY of:
|
|
2568
4869
|
// - graph score within a fraction of the top (it's on/near the flow), OR
|
|
@@ -2579,6 +4880,7 @@ class ToolHandler {
|
|
|
2579
4880
|
const gated = relevantFiles.filter(([fp]) => (fileGraphScore.get(fp) ?? 0) >= maxGraph * 0.06
|
|
2580
4881
|
|| centralFiles.has(fp)
|
|
2581
4882
|
|| entryFiles.has(fp)
|
|
4883
|
+
|| changeSurfaceFiles.has(fp)
|
|
2582
4884
|
|| (fileTermHits.get(fp) ?? 0) >= 2);
|
|
2583
4885
|
if (gated.length >= 2)
|
|
2584
4886
|
relevantFiles = gated;
|
|
@@ -2593,11 +4895,16 @@ class ToolHandler {
|
|
|
2593
4895
|
// in other files (`Validation.swift`), falls outside the budget, and the
|
|
2594
4896
|
// agent Reads it. The named file is the answer — rank it at the top.
|
|
2595
4897
|
const namedSeedFiles = new Set();
|
|
2596
|
-
for (const id of
|
|
4898
|
+
for (const id of tierSeedIds) {
|
|
2597
4899
|
const n = subgraph.nodes.get(id);
|
|
2598
4900
|
if (n)
|
|
2599
4901
|
namedSeedFiles.add(n.filePath);
|
|
2600
4902
|
}
|
|
4903
|
+
// A rescued change-surface file (only the genuinely-buried ones — see the
|
|
4904
|
+
// buried-rescue pass) is the lexically-dissimilar answer; give it the named
|
|
4905
|
+
// tier so it isn't buried under files that merely share surface words (#1064).
|
|
4906
|
+
for (const fp of changeSurfaceFiles)
|
|
4907
|
+
namedSeedFiles.add(fp);
|
|
2601
4908
|
// Multi-term corroboration tier: a file that is BOTH (a) an entry/central file
|
|
2602
4909
|
// (a search root, named seed, or graph-central hub — i.e. structurally part of
|
|
2603
4910
|
// the answer) AND (b) matched by ≥2 DISTINCT query terms must not be buried by
|
|
@@ -2617,9 +4924,16 @@ class ToolHandler {
|
|
|
2617
4924
|
const isCorroborated = (fp) => !MULTITERM_OFF &&
|
|
2618
4925
|
(fileTermHits.get(fp) ?? 0) >= 2 &&
|
|
2619
4926
|
(entryFiles.has(fp) || centralFiles.has(fp));
|
|
4927
|
+
const queryMemberAccesses = (0, query_utils_1.extractMemberAccessFromQuery)(query);
|
|
2620
4928
|
const sortedFiles = relevantFiles.sort((a, b) => {
|
|
2621
4929
|
const aPath = a[0].toLowerCase();
|
|
2622
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;
|
|
2623
4937
|
// Agent-named files first (it asked for a symbol defined here by name).
|
|
2624
4938
|
const aNamed = namedSeedFiles.has(a[0]) ? 1 : 0;
|
|
2625
4939
|
const bNamed = namedSeedFiles.has(b[0]) ? 1 : 0;
|
|
@@ -2662,19 +4976,164 @@ class ToolHandler {
|
|
|
2662
4976
|
const lines = [
|
|
2663
4977
|
`**Exploration: ${query}**`,
|
|
2664
4978
|
'',
|
|
2665
|
-
|
|
4979
|
+
// Curated summary — filled in after the source loop (see below). We do NOT
|
|
4980
|
+
// report `subgraph.nodes.size` / `fileGroups.size` here: that's the raw
|
|
4981
|
+
// candidate gather, which a broad natural-language query inflates wildly
|
|
4982
|
+
// (260 symbols / 124 files on a 636-file repo) even though only a handful
|
|
4983
|
+
// render. Reporting the pool read as "260 results to wade through" when the
|
|
4984
|
+
// real, correctly-ranked answer is the few files below (#1046).
|
|
4985
|
+
'',
|
|
2666
4986
|
'',
|
|
2667
4987
|
];
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
4988
|
+
const summaryLineIdx = 2;
|
|
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).
|
|
2675
5133
|
const significantEdges = subgraph.edges.filter(e => e.kind !== 'contains' // skip contains — it's implied by file grouping
|
|
2676
5134
|
);
|
|
2677
|
-
if (budget.includeRelationships &&
|
|
5135
|
+
if (budget.includeRelationships && hasFlowPath && !importResult.compactListing
|
|
5136
|
+
&& significantEdges.length > 0) {
|
|
2678
5137
|
lines.push('**Relationships**');
|
|
2679
5138
|
lines.push('');
|
|
2680
5139
|
// Group edges by kind for readability
|
|
@@ -2702,10 +5161,7 @@ class ToolHandler {
|
|
|
2702
5161
|
}
|
|
2703
5162
|
}
|
|
2704
5163
|
// Step 4: Read contiguous file sections
|
|
2705
|
-
//
|
|
2706
|
-
// and to gate adaptive source sizing: files on the spine get full source,
|
|
2707
|
-
// off-spine peers skeletonize.
|
|
2708
|
-
const flow = this.buildFlowFromNamedSymbols(cg, query);
|
|
5164
|
+
// (flow already computed above for relationship gating and adaptive sizing)
|
|
2709
5165
|
// Polymorphic-sibling detector for adaptive sizing. A class that implements/
|
|
2710
5166
|
// extends a supertype shared by >= MIN_SIBLINGS classes is one of many
|
|
2711
5167
|
// INTERCHANGEABLE implementations (OkHttp's 14 `: Interceptor` classes —
|
|
@@ -2762,13 +5218,61 @@ class ToolHandler {
|
|
|
2762
5218
|
};
|
|
2763
5219
|
lines.push('**Source Code**');
|
|
2764
5220
|
lines.push('');
|
|
2765
|
-
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.');
|
|
2766
5224
|
lines.push('');
|
|
2767
5225
|
let totalChars = lines.join('\n').length;
|
|
2768
5226
|
let filesIncluded = 0;
|
|
5227
|
+
// Paths we actually render source for below. Drives the curated header count
|
|
5228
|
+
// (#1046) — it must reflect what we show, not the raw candidate gather.
|
|
5229
|
+
const renderedFilePaths = [];
|
|
2769
5230
|
let anyFileTrimmed = false;
|
|
2770
|
-
|
|
2771
|
-
|
|
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)
|
|
2772
5276
|
break;
|
|
2773
5277
|
// A file DEFINES a named/spine symbol (the answer) vs merely references the
|
|
2774
5278
|
// flow. Past 90% budget, stop pulling INCIDENTAL files — but keep scanning
|
|
@@ -2779,6 +5283,24 @@ class ToolHandler {
|
|
|
2779
5283
|
const fileNecessary = group.nodes.some(n => entryNodeIds.has(n.id) || flow.pathNodeIds.has(n.id) || flow.uniqueNamedNodeIds.has(n.id));
|
|
2780
5284
|
if (!fileNecessary && totalChars > budget.maxOutputChars * 0.9)
|
|
2781
5285
|
continue;
|
|
5286
|
+
if ((0, arkts_1.isOhosApiFilePath)(filePath)) {
|
|
5287
|
+
const rel = filePath.slice(arkts_1.OHOS_API_FILE_PREFIX.length);
|
|
5288
|
+
const syms = group.nodes
|
|
5289
|
+
.filter((n) => n.kind !== 'import' && n.kind !== 'export')
|
|
5290
|
+
.sort((a, b) => a.startLine - b.startLine);
|
|
5291
|
+
if (syms.length === 0)
|
|
5292
|
+
continue;
|
|
5293
|
+
lines.push(fileSectionHeader(rel, 'HarmonyOS SDK API (prebuilt db)'));
|
|
5294
|
+
lines.push('');
|
|
5295
|
+
for (const n of syms) {
|
|
5296
|
+
const sig = n.signature || n.docstring || `${n.kind} ${n.qualifiedName || n.name}`;
|
|
5297
|
+
lines.push(`\`${sig}\``);
|
|
5298
|
+
}
|
|
5299
|
+
lines.push('');
|
|
5300
|
+
totalChars = lines.join('\n').length;
|
|
5301
|
+
filesIncluded++;
|
|
5302
|
+
continue;
|
|
5303
|
+
}
|
|
2782
5304
|
const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, filePath);
|
|
2783
5305
|
if (!absPath || !(0, fs_1.existsSync)(absPath))
|
|
2784
5306
|
continue;
|
|
@@ -2917,6 +5439,7 @@ class ToolHandler {
|
|
|
2917
5439
|
: 'skeleton (signatures only — homegraph_explore a name for its full body; do NOT Read)';
|
|
2918
5440
|
lines.push(fileSectionHeader(filePath, `${names} · ${tag}`), '', '```' + lang, skel.join('\n'), '```', '');
|
|
2919
5441
|
totalChars += skel.join('\n').length + 120;
|
|
5442
|
+
renderedFilePaths.push(filePath);
|
|
2920
5443
|
filesIncluded++;
|
|
2921
5444
|
continue;
|
|
2922
5445
|
}
|
|
@@ -2963,6 +5486,7 @@ class ToolHandler {
|
|
|
2963
5486
|
}
|
|
2964
5487
|
lines.push(wholeHeader, '', '```' + lang, wholeSection, '```', '');
|
|
2965
5488
|
totalChars += wholeSection.length + 200;
|
|
5489
|
+
renderedFilePaths.push(filePath);
|
|
2966
5490
|
filesIncluded++;
|
|
2967
5491
|
continue;
|
|
2968
5492
|
}
|
|
@@ -3044,6 +5568,39 @@ class ToolHandler {
|
|
|
3044
5568
|
ranges.push({ start: edge.line, end: edge.line, name: targetName, kind: edge.kind, importance: 2, spine: false });
|
|
3045
5569
|
}
|
|
3046
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
|
+
}
|
|
3047
5604
|
ranges.sort((a, b) => a.start - b.start);
|
|
3048
5605
|
if (ranges.length === 0)
|
|
3049
5606
|
continue;
|
|
@@ -3251,8 +5808,14 @@ class ToolHandler {
|
|
|
3251
5808
|
lines.push('```');
|
|
3252
5809
|
lines.push('');
|
|
3253
5810
|
totalChars += fileSection.length + 200;
|
|
5811
|
+
renderedFilePaths.push(filePath);
|
|
3254
5812
|
filesIncluded++;
|
|
3255
5813
|
}
|
|
5814
|
+
// The curated header count is computed from the files that SURVIVE the final
|
|
5815
|
+
// truncation (see end of method) — `filesIncluded` can over-count when the
|
|
5816
|
+
// hard ceiling drops trailing sections — so leave a sentinel here and fill it
|
|
5817
|
+
// in once the output is final.
|
|
5818
|
+
lines[summaryLineIdx] = SUMMARY_SENTINEL;
|
|
3256
5819
|
// Add remaining files as references (from both relevant and peripheral files).
|
|
3257
5820
|
// Small projects (per budget) skip this — the relevant story already fits
|
|
3258
5821
|
// in the source section, and a trailing pointer list is pure overhead.
|
|
@@ -3309,6 +5872,7 @@ class ToolHandler {
|
|
|
3309
5872
|
// externalize territory.
|
|
3310
5873
|
const output = flow.text + lines.join('\n');
|
|
3311
5874
|
const hardCeiling = Math.min(Math.round(budget.maxOutputChars * 1.5), 25000);
|
|
5875
|
+
let finalText;
|
|
3312
5876
|
if (output.length > hardCeiling) {
|
|
3313
5877
|
// Cut at a FILE-SECTION boundary (the last ``**` `` file header before the
|
|
3314
5878
|
// ceiling) so we drop whole trailing file-sections rather than slicing
|
|
@@ -3319,9 +5883,36 @@ class ToolHandler {
|
|
|
3319
5883
|
const lastSection = cut.lastIndexOf('\n' + FILE_SECTION_PREFIX);
|
|
3320
5884
|
const boundary = lastSection > hardCeiling * 0.5 ? lastSection : cut.lastIndexOf('\n');
|
|
3321
5885
|
const safe = boundary > 0 ? cut.slice(0, boundary) : cut;
|
|
3322
|
-
|
|
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.)';
|
|
3323
5887
|
}
|
|
3324
|
-
|
|
5888
|
+
else {
|
|
5889
|
+
finalText = output;
|
|
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
|
+
}
|
|
5897
|
+
// Curated header (#1046): substitute the sentinel with the count of files
|
|
5898
|
+
// whose source SURVIVES in the final text — not `subgraph`/`fileGroups` (the
|
|
5899
|
+
// raw gather a broad query inflates) and not `filesIncluded` (which can
|
|
5900
|
+
// over-count when the ceiling above drops trailing sections). A file counts
|
|
5901
|
+
// only if its section header is still present; its relevant (non-import)
|
|
5902
|
+
// symbols are summed for N. Files we couldn't fit are still named under "Not
|
|
5903
|
+
// shown above" + the budget note, so nothing is silently dropped.
|
|
5904
|
+
const survivors = renderedFilePaths.filter((fp) => finalText.includes(`${FILE_SECTION_PREFIX}${fp}\``));
|
|
5905
|
+
const shownSymbols = survivors.reduce((sum, fp) => {
|
|
5906
|
+
const g = fileGroups.get(fp);
|
|
5907
|
+
if (!g)
|
|
5908
|
+
return sum;
|
|
5909
|
+
return sum + new Set(g.nodes.filter((n) => n.kind !== 'import' && n.kind !== 'export').map((n) => n.id)).size;
|
|
5910
|
+
}, 0);
|
|
5911
|
+
const summaryLine = survivors.length > 0
|
|
5912
|
+
? `Found ${shownSymbols} symbol${shownSymbols === 1 ? '' : 's'} across ${survivors.length} file${survivors.length === 1 ? '' : 's'}.`
|
|
5913
|
+
: `Found ${subgraph.nodes.size} symbol${subgraph.nodes.size === 1 ? '' : 's'} across ${fileGroups.size} file${fileGroups.size === 1 ? '' : 's'}.`;
|
|
5914
|
+
finalText = finalText.replace(SUMMARY_SENTINEL, summaryLine);
|
|
5915
|
+
return this.textResult(finalText);
|
|
3325
5916
|
}
|
|
3326
5917
|
/**
|
|
3327
5918
|
* Handle homegraph_node
|
|
@@ -3392,12 +5983,12 @@ class ToolHandler {
|
|
|
3392
5983
|
const list = matches.map((n) => `- \`${n.name}\` (${n.kind}) — ${n.filePath}:${n.startLine}`);
|
|
3393
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')));
|
|
3394
5985
|
}
|
|
3395
|
-
const BODY_BUDGET =
|
|
5986
|
+
const BODY_BUDGET = 10_000; // leaves room under MAX_OUTPUT_LENGTH for the header + list
|
|
3396
5987
|
// The CHAR budget is the real limiter — keep the count cap high so a set of
|
|
3397
5988
|
// SHORT overloads (Alamofire's 10 `validate` variants, each a few lines) all
|
|
3398
5989
|
// render in full rather than relegating the one the agent wanted to a
|
|
3399
5990
|
// bodiless list. Only a set of many LARGE bodies hits the char budget first.
|
|
3400
|
-
const HARD_CAP =
|
|
5991
|
+
const HARD_CAP = 12;
|
|
3401
5992
|
const rendered = [];
|
|
3402
5993
|
const listed = [];
|
|
3403
5994
|
let used = 0;
|
|
@@ -3483,7 +6074,7 @@ class ToolHandler {
|
|
|
3483
6074
|
const symbolMap = (heading, limit = 200) => {
|
|
3484
6075
|
const lines = [heading];
|
|
3485
6076
|
for (const n of nodes.slice(0, limit)) {
|
|
3486
|
-
const sig = n.signature ? ` ${n.signature
|
|
6077
|
+
const sig = n.signature ? ` ${formatInlineSignature(n.signature)}` : '';
|
|
3487
6078
|
lines.push(`- \`${n.name}\` (${n.kind})${sig} — :${n.startLine}`);
|
|
3488
6079
|
}
|
|
3489
6080
|
if (nodes.length > limit)
|
|
@@ -3533,12 +6124,12 @@ class ToolHandler {
|
|
|
3533
6124
|
const fileLines = content.split('\n');
|
|
3534
6125
|
const total = fileLines.length;
|
|
3535
6126
|
// Read-parity windowing: `offset`/`limit` mean exactly what they do on Read
|
|
3536
|
-
// (1-based start line; max line count). Default
|
|
3537
|
-
//
|
|
3538
|
-
//
|
|
3539
|
-
//
|
|
3540
|
-
const CHAR_BUDGET =
|
|
3541
|
-
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;
|
|
3542
6133
|
const offset = Math.max(1, opts.offset ?? 1);
|
|
3543
6134
|
if (offset > total) {
|
|
3544
6135
|
return this.textResult(`**${filePath}** has ${total} line${total === 1 ? '' : 's'} — offset ${offset} is past the end. ${depSummary}`);
|
|
@@ -3561,8 +6152,9 @@ class ToolHandler {
|
|
|
3561
6152
|
const complete = offset === 1 && shownEnd >= total;
|
|
3562
6153
|
const out = [header, '', ...numbered];
|
|
3563
6154
|
if (!complete) {
|
|
3564
|
-
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)`);
|
|
3565
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.');
|
|
3566
6158
|
// Self-bounded to CHAR_BUDGET — do NOT route through truncateOutput (15k).
|
|
3567
6159
|
return this.textResult(out.join('\n'));
|
|
3568
6160
|
}
|
|
@@ -3886,12 +6478,12 @@ class ToolHandler {
|
|
|
3886
6478
|
return query;
|
|
3887
6479
|
const repoPath = args.repoPath;
|
|
3888
6480
|
const explicitDbPath = args.dbPath;
|
|
3889
|
-
const
|
|
6481
|
+
const topKRaw = Number(args.topK);
|
|
6482
|
+
const topK = Math.max(1, Math.min(isNaN(topKRaw) ? 5 : topKRaw, 50));
|
|
3890
6483
|
const includeFragments = args.includeFragments !== false;
|
|
3891
6484
|
// Lazily require spec modules so the MCP startup path stays lean.
|
|
3892
6485
|
const { resolveDbPath } = require('../spec/utils');
|
|
3893
6486
|
const { createDatabase } = require('../db/sqlite-adapter');
|
|
3894
|
-
const { initSpecSchema } = require('../spec/db/schema');
|
|
3895
6487
|
const { searchAndGetContext } = require('../spec/graph/queries');
|
|
3896
6488
|
const { truncateCodeDiff, truncateSubtitles, computeBudgetProfile, } = require('../spec/utils');
|
|
3897
6489
|
// Resolve the database path.
|
|
@@ -3906,8 +6498,6 @@ class ToolHandler {
|
|
|
3906
6498
|
`${err instanceof Error ? err.message : String(err)}`);
|
|
3907
6499
|
}
|
|
3908
6500
|
try {
|
|
3909
|
-
// Ensure the schema exists (idempotent).
|
|
3910
|
-
initSpecSchema(db);
|
|
3911
6501
|
// Search and traverse.
|
|
3912
6502
|
const contexts = searchAndGetContext(db, query, topK, includeFragments);
|
|
3913
6503
|
if (contexts.length === 0) {
|
|
@@ -3975,15 +6565,238 @@ class ToolHandler {
|
|
|
3975
6565
|
return this.textResult(`(Fragments elided — output exceeded ${MAX_OUTPUT_LENGTH} chars)\n\n` + slim);
|
|
3976
6566
|
}
|
|
3977
6567
|
}
|
|
3978
|
-
// Fallback:
|
|
3979
|
-
|
|
3980
|
-
|
|
6568
|
+
// Fallback: truncate at a newline to avoid broken JSON.
|
|
6569
|
+
return this.textResult(this.truncateOutput(json));
|
|
6570
|
+
}
|
|
6571
|
+
finally {
|
|
6572
|
+
db.close();
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
// =========================================================================
|
|
6576
|
+
// handleSpecFind — file-path based spec lookup
|
|
6577
|
+
// =========================================================================
|
|
6578
|
+
/**
|
|
6579
|
+
* Find which specs are related to the given file path by matching against
|
|
6580
|
+
* code-fragment file paths in the Commit4Spec knowledge graph.
|
|
6581
|
+
*
|
|
6582
|
+
* Traverses: filePath → code_fragment_nodes → commit_fragment_relations
|
|
6583
|
+
* → spec_commit_relations → spec_nodes.
|
|
6584
|
+
*/
|
|
6585
|
+
async handleSpecFind(args) {
|
|
6586
|
+
const filePath = this.validateString(args.filePath, 'filePath');
|
|
6587
|
+
if (typeof filePath !== 'string')
|
|
6588
|
+
return filePath;
|
|
6589
|
+
const repoPath = args.repoPath;
|
|
6590
|
+
const explicitDbPath = args.dbPath;
|
|
6591
|
+
// Lazily require spec modules so the MCP startup path stays lean.
|
|
6592
|
+
const { resolveDbPath } = require('../spec/utils');
|
|
6593
|
+
const { createDatabase } = require('../db/sqlite-adapter');
|
|
6594
|
+
const { findSpecsByFilePath } = require('../spec/graph/queries');
|
|
6595
|
+
// Resolve the database path.
|
|
6596
|
+
const dbPath = resolveDbPath(repoPath || process.cwd(), explicitDbPath);
|
|
6597
|
+
// Open the database.
|
|
6598
|
+
let db;
|
|
6599
|
+
try {
|
|
6600
|
+
db = createDatabase(dbPath).db;
|
|
6601
|
+
}
|
|
6602
|
+
catch (err) {
|
|
6603
|
+
return this.errorResult(`Failed to open Commit4Spec database at "${dbPath}": ` +
|
|
6604
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
6605
|
+
}
|
|
6606
|
+
try {
|
|
6607
|
+
const result = findSpecsByFilePath(db, filePath);
|
|
6608
|
+
const response = {
|
|
6609
|
+
filePath,
|
|
6610
|
+
matched_count: result.matched_count,
|
|
6611
|
+
truncated: result.truncated,
|
|
6612
|
+
results: result.results,
|
|
6613
|
+
};
|
|
6614
|
+
const json = JSON.stringify(response, null, 2);
|
|
6615
|
+
if (json.length <= MAX_OUTPUT_LENGTH) {
|
|
6616
|
+
return this.textResult(json);
|
|
6617
|
+
}
|
|
6618
|
+
// Truncate: reduce results
|
|
6619
|
+
const slim = {
|
|
6620
|
+
...response,
|
|
6621
|
+
results: response.results.slice(0, Math.max(1, Math.floor(response.results.length / 2))),
|
|
6622
|
+
};
|
|
6623
|
+
const slimJson = JSON.stringify(slim, null, 2);
|
|
6624
|
+
if (slimJson.length <= MAX_OUTPUT_LENGTH) {
|
|
6625
|
+
return this.textResult(`(Results trimmed to fit output limit)\n\n${slimJson}`);
|
|
6626
|
+
}
|
|
6627
|
+
return this.textResult(this.truncateOutput(json));
|
|
3981
6628
|
}
|
|
3982
6629
|
finally {
|
|
3983
6630
|
db.close();
|
|
3984
6631
|
}
|
|
3985
6632
|
}
|
|
3986
6633
|
// =========================================================================
|
|
6634
|
+
// handleSpecTrace — code symbol → Spec reverse trace
|
|
6635
|
+
// =========================================================================
|
|
6636
|
+
/**
|
|
6637
|
+
* Trace a code symbol back to its associated Specs.
|
|
6638
|
+
*
|
|
6639
|
+
* Uses the HomeGraph code index to resolve the symbol to AST-level node(s),
|
|
6640
|
+
* then queries the Commit4Spec knowledge graph for associated Specs via
|
|
6641
|
+
* five-dimensional scoring (file-path, content FTS5, name FTS5, recency,
|
|
6642
|
+
* line overlap).
|
|
6643
|
+
*
|
|
6644
|
+
* This bridges the two databases: homegraph.db (code entities) →
|
|
6645
|
+
* commit4spec.db (Spec knowledge graph).
|
|
6646
|
+
*/
|
|
6647
|
+
async handleSpecTrace(args) {
|
|
6648
|
+
const symbol = this.validateString(args.symbol, 'symbol');
|
|
6649
|
+
if (typeof symbol !== 'string')
|
|
6650
|
+
return symbol;
|
|
6651
|
+
const fileRaw = this.validateOptionalPath(args.file, 'file');
|
|
6652
|
+
if (typeof fileRaw === 'object')
|
|
6653
|
+
return fileRaw;
|
|
6654
|
+
const file = fileRaw;
|
|
6655
|
+
const line = typeof args.line === 'number' ? args.line : undefined;
|
|
6656
|
+
const repoPath = args.repoPath;
|
|
6657
|
+
const explicitDbPath = args.dbPath;
|
|
6658
|
+
const topKRaw = Number(args.topK);
|
|
6659
|
+
const topK = Math.max(1, Math.min(isNaN(topKRaw) ? 10 : topKRaw, 50));
|
|
6660
|
+
// Lazily require all needed modules
|
|
6661
|
+
const HomeGraph = loadHomeGraph();
|
|
6662
|
+
const { resolveDbPath } = require('../spec/utils');
|
|
6663
|
+
const { createDatabase } = require('../db/sqlite-adapter');
|
|
6664
|
+
const { findSpecsByCodeSymbol } = require('../spec/graph/queries');
|
|
6665
|
+
const { initSpecSchema, runSpecMigrations, getCurrentSpecVersion, CURRENT_SPEC_SCHEMA_VERSION } = require('../spec/db/schema');
|
|
6666
|
+
// Resolve project path for the code graph
|
|
6667
|
+
const projectPath = repoPath || process.cwd();
|
|
6668
|
+
// Open the HomeGraph code index
|
|
6669
|
+
let cg;
|
|
6670
|
+
try {
|
|
6671
|
+
cg = await HomeGraph.open(projectPath);
|
|
6672
|
+
}
|
|
6673
|
+
catch (err) {
|
|
6674
|
+
return this.errorResult(`Failed to open HomeGraph code index at "${projectPath}": ` +
|
|
6675
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
6676
|
+
}
|
|
6677
|
+
try {
|
|
6678
|
+
// Step 1: Resolve symbol to nodes via findSymbolMatches
|
|
6679
|
+
let nodes = this.findSymbolMatches(cg, symbol);
|
|
6680
|
+
if (nodes.length === 0) {
|
|
6681
|
+
await cg.close();
|
|
6682
|
+
return this.textResult(JSON.stringify({
|
|
6683
|
+
symbol,
|
|
6684
|
+
error: `No code entities found for symbol "${symbol}".`,
|
|
6685
|
+
matches: [],
|
|
6686
|
+
}, null, 2));
|
|
6687
|
+
}
|
|
6688
|
+
// Disambiguate by file/line if provided
|
|
6689
|
+
if (file) {
|
|
6690
|
+
// Use endsWith for precise file path matching
|
|
6691
|
+
nodes = nodes.filter((n) => n.filePath.endsWith(file));
|
|
6692
|
+
}
|
|
6693
|
+
if (line !== undefined && nodes.length > 1) {
|
|
6694
|
+
const closest = nodes.reduce((best, n) => {
|
|
6695
|
+
const bestDist = Math.abs(best.startLine - line) + Math.abs(best.endLine - line);
|
|
6696
|
+
const curDist = Math.abs(n.startLine - line) + Math.abs(n.endLine - line);
|
|
6697
|
+
return curDist < bestDist ? n : best;
|
|
6698
|
+
});
|
|
6699
|
+
nodes = [closest];
|
|
6700
|
+
}
|
|
6701
|
+
// Take the best disambiguated node
|
|
6702
|
+
const node = nodes[0];
|
|
6703
|
+
if (!node) {
|
|
6704
|
+
await cg.close();
|
|
6705
|
+
return this.textResult(JSON.stringify({
|
|
6706
|
+
symbol,
|
|
6707
|
+
error: `Could not resolve symbol "${symbol}" to a specific code entity.`,
|
|
6708
|
+
matches: [],
|
|
6709
|
+
}, null, 2));
|
|
6710
|
+
}
|
|
6711
|
+
// Step 2: Resolve the Spec database path
|
|
6712
|
+
const dbPath = resolveDbPath(repoPath || process.cwd(), explicitDbPath);
|
|
6713
|
+
// Step 3: Open the Spec database
|
|
6714
|
+
let db;
|
|
6715
|
+
try {
|
|
6716
|
+
db = createDatabase(dbPath).db;
|
|
6717
|
+
}
|
|
6718
|
+
catch (err) {
|
|
6719
|
+
await cg.close();
|
|
6720
|
+
return this.errorResult(`Failed to open Commit4Spec database at "${dbPath}": ` +
|
|
6721
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
6722
|
+
}
|
|
6723
|
+
try {
|
|
6724
|
+
// Ensure schema is up to date
|
|
6725
|
+
initSpecSchema(db);
|
|
6726
|
+
const currentVersion = getCurrentSpecVersion(db);
|
|
6727
|
+
if (currentVersion < CURRENT_SPEC_SCHEMA_VERSION) {
|
|
6728
|
+
runSpecMigrations(db, currentVersion);
|
|
6729
|
+
}
|
|
6730
|
+
// Step 4: Query Specs for the code entity
|
|
6731
|
+
const result = findSpecsByCodeSymbol(db, {
|
|
6732
|
+
name: node.name,
|
|
6733
|
+
qualifiedName: node.qualifiedName,
|
|
6734
|
+
kind: node.kind,
|
|
6735
|
+
filePath: node.filePath,
|
|
6736
|
+
startLine: node.startLine,
|
|
6737
|
+
endLine: node.endLine,
|
|
6738
|
+
}, topK);
|
|
6739
|
+
// Step 5: Serialize the response
|
|
6740
|
+
const response = {
|
|
6741
|
+
symbol,
|
|
6742
|
+
entity: {
|
|
6743
|
+
name: result.entity.name,
|
|
6744
|
+
qualifiedName: result.entity.qualifiedName,
|
|
6745
|
+
kind: result.entity.kind,
|
|
6746
|
+
filePath: result.entity.filePath,
|
|
6747
|
+
startLine: result.entity.startLine,
|
|
6748
|
+
endLine: result.entity.endLine,
|
|
6749
|
+
},
|
|
6750
|
+
matched_count: result.matches.length,
|
|
6751
|
+
total_candidates: result.totalCandidates,
|
|
6752
|
+
matches: result.matches.map((m) => ({
|
|
6753
|
+
spec_id: m.spec.id,
|
|
6754
|
+
title: m.spec.title,
|
|
6755
|
+
status: m.spec.status,
|
|
6756
|
+
version: m.spec.version,
|
|
6757
|
+
file_path: m.spec.filePath,
|
|
6758
|
+
score: Math.round(m.score * 1000) / 1000,
|
|
6759
|
+
score_detail: {
|
|
6760
|
+
file_path: Math.round(m.scoreDetail.filePathScore * 1000) / 1000,
|
|
6761
|
+
content: Math.round(m.scoreDetail.contentScore * 1000) / 1000,
|
|
6762
|
+
name: Math.round(m.scoreDetail.nameScore * 1000) / 1000,
|
|
6763
|
+
recency: Math.round(m.scoreDetail.recencyScore * 1000) / 1000,
|
|
6764
|
+
line_overlap: Math.round(m.scoreDetail.overlapScore * 1000) / 1000,
|
|
6765
|
+
},
|
|
6766
|
+
fragment_count: m.fragmentCount,
|
|
6767
|
+
commit_count: m.commitCount,
|
|
6768
|
+
})),
|
|
6769
|
+
};
|
|
6770
|
+
const json = JSON.stringify(response, null, 2);
|
|
6771
|
+
if (json.length <= MAX_OUTPUT_LENGTH) {
|
|
6772
|
+
return this.textResult(json);
|
|
6773
|
+
}
|
|
6774
|
+
// Truncate: reduce matches
|
|
6775
|
+
const slim = {
|
|
6776
|
+
...response,
|
|
6777
|
+
matches: response.matches.slice(0, Math.max(1, Math.floor(topK / 2))),
|
|
6778
|
+
};
|
|
6779
|
+
const slimJson = JSON.stringify(slim, null, 2);
|
|
6780
|
+
if (slimJson.length <= MAX_OUTPUT_LENGTH) {
|
|
6781
|
+
return this.textResult(`(Results trimmed to fit output limit)\n\n${slimJson}`);
|
|
6782
|
+
}
|
|
6783
|
+
return this.textResult(this.truncateOutput(json));
|
|
6784
|
+
}
|
|
6785
|
+
finally {
|
|
6786
|
+
try {
|
|
6787
|
+
db.close();
|
|
6788
|
+
}
|
|
6789
|
+
catch { /* best effort */ }
|
|
6790
|
+
}
|
|
6791
|
+
}
|
|
6792
|
+
finally {
|
|
6793
|
+
try {
|
|
6794
|
+
await cg.close();
|
|
6795
|
+
}
|
|
6796
|
+
catch { /* best effort */ }
|
|
6797
|
+
}
|
|
6798
|
+
}
|
|
6799
|
+
// =========================================================================
|
|
3987
6800
|
// Symbol resolution helpers
|
|
3988
6801
|
// =========================================================================
|
|
3989
6802
|
/**
|
|
@@ -4108,25 +6921,47 @@ class ToolHandler {
|
|
|
4108
6921
|
if (tail && tail !== symbol)
|
|
4109
6922
|
results = cg.searchNodes(tail, { limit: 50 });
|
|
4110
6923
|
}
|
|
4111
|
-
|
|
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) {
|
|
4112
6949
|
return { nodes: [], note: '' };
|
|
4113
6950
|
}
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
const node = exactMatches[0]?.node ?? results[0].node;
|
|
4117
|
-
return { nodes: [node], note: '' };
|
|
6951
|
+
if (pool.length === 1) {
|
|
6952
|
+
return { nodes: pool, note: '' };
|
|
4118
6953
|
}
|
|
4119
6954
|
// Same generated-file down-rank as findSymbol — keeps callers/callees
|
|
4120
6955
|
// /impact aggregation aligned (a query against "Send" returns the
|
|
4121
6956
|
// hand-written implementations before the protobuf scaffold).
|
|
4122
|
-
const ranked = [...
|
|
4123
|
-
const aGen = (0, generated_detection_1.isGeneratedFile)(a.
|
|
4124
|
-
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;
|
|
4125
6960
|
return aGen - bGen;
|
|
4126
6961
|
});
|
|
4127
|
-
const locations = ranked.map(
|
|
6962
|
+
const locations = ranked.map(n => `${n.kind} at ${n.filePath}:${n.startLine}`);
|
|
4128
6963
|
const note = `\n\n> **Note:** Aggregated results across ${ranked.length} symbols named "${symbol}": ${locations.join(', ')}`;
|
|
4129
|
-
return { nodes: ranked
|
|
6964
|
+
return { nodes: ranked, note };
|
|
4130
6965
|
}
|
|
4131
6966
|
/**
|
|
4132
6967
|
* Truncate output if it exceeds the maximum length
|
|
@@ -4151,7 +6986,7 @@ class ToolHandler {
|
|
|
4151
6986
|
lines.push(`**${node.name}** (${node.kind})`);
|
|
4152
6987
|
lines.push(`${node.filePath}${location}`);
|
|
4153
6988
|
if (node.signature)
|
|
4154
|
-
lines.push(`\`${node.signature}\``);
|
|
6989
|
+
lines.push(`\`${formatInlineSignature(node.signature)}\``);
|
|
4155
6990
|
lines.push('');
|
|
4156
6991
|
}
|
|
4157
6992
|
return lines.join('\n');
|
|
@@ -4225,7 +7060,7 @@ class ToolHandler {
|
|
|
4225
7060
|
const lines = [`**Members (${children.length}):**`, ''];
|
|
4226
7061
|
for (const c of children) {
|
|
4227
7062
|
const loc = c.startLine ? `:${c.startLine}` : '';
|
|
4228
|
-
const sig = c.signature ? ` — \`${c.signature}\`` : '';
|
|
7063
|
+
const sig = c.signature ? ` — \`${formatInlineSignature(c.signature)}\`` : '';
|
|
4229
7064
|
lines.push(`- ${c.name} (${c.kind})${loc}${sig}`);
|
|
4230
7065
|
}
|
|
4231
7066
|
return lines.join('\n');
|
|
@@ -4238,7 +7073,7 @@ class ToolHandler {
|
|
|
4238
7073
|
`**Location:** ${node.filePath}${location}`,
|
|
4239
7074
|
];
|
|
4240
7075
|
if (node.signature) {
|
|
4241
|
-
lines.push(
|
|
7076
|
+
lines.push(...formatNodeSignatureBlock(node.signature));
|
|
4242
7077
|
}
|
|
4243
7078
|
// Only include docstring if it's short and useful
|
|
4244
7079
|
if (node.docstring && node.docstring.length < 200) {
|