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