gitnexus 1.6.8-rc.8 → 1.6.8
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 +3 -3
- package/dist/_shared/graph/types.d.ts +31 -1
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/language-detection.js +11 -1
- package/dist/_shared/language-detection.js.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +6 -0
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +6 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
- package/dist/cli/ai-context.d.ts +31 -1
- package/dist/cli/ai-context.js +18 -16
- package/dist/cli/analyze-config.js +11 -0
- package/dist/cli/analyze.d.ts +14 -0
- package/dist/cli/analyze.js +98 -0
- package/dist/cli/embedding-dims.d.ts +33 -0
- package/dist/cli/embedding-dims.js +39 -0
- package/dist/cli/eval-server.d.ts +8 -0
- package/dist/cli/eval-server.js +242 -2
- package/dist/cli/help-i18n.js +19 -0
- package/dist/cli/i18n/en.d.ts +15 -1
- package/dist/cli/i18n/en.js +15 -1
- package/dist/cli/i18n/resources.d.ts +29 -1
- package/dist/cli/i18n/zh-CN.d.ts +14 -0
- package/dist/cli/i18n/zh-CN.js +15 -1
- package/dist/cli/index.js +89 -1
- package/dist/cli/mcp.d.ts +6 -1
- package/dist/cli/mcp.js +26 -1
- package/dist/cli/optional-grammars.d.ts +6 -8
- package/dist/cli/optional-grammars.js +8 -11
- package/dist/cli/setup.d.ts +3 -1
- package/dist/cli/setup.js +67 -17
- package/dist/cli/skill-gen.js +2 -1
- package/dist/cli/tool.d.ts +19 -1
- package/dist/cli/tool.js +96 -3
- package/dist/core/embeddings/hf-env.d.ts +0 -66
- package/dist/core/embeddings/http-client.d.ts +7 -0
- package/dist/core/embeddings/http-client.js +5 -3
- package/dist/core/embeddings/server-mapping.d.ts +1 -1
- package/dist/core/embeddings/server-mapping.js +7 -3
- package/dist/core/graph/import-cycles.d.ts +10 -0
- package/dist/core/graph/import-cycles.js +103 -0
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +5 -1
- package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
- package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
- package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
- package/dist/core/group/extractors/http-route-extractor.js +22 -4
- package/dist/core/group/extractors/include-extractor.js +22 -13
- package/dist/core/group/sync.d.ts +27 -1
- package/dist/core/group/sync.js +0 -0
- package/dist/core/incremental/subgraph-extract.js +18 -1
- package/dist/core/ingestion/cfg/cfg-builder.d.ts +114 -0
- package/dist/core/ingestion/cfg/cfg-builder.js +196 -0
- package/dist/core/ingestion/cfg/collect.d.ts +51 -0
- package/dist/core/ingestion/cfg/collect.js +77 -0
- package/dist/core/ingestion/cfg/control-dependence.d.ts +58 -0
- package/dist/core/ingestion/cfg/control-dependence.js +182 -0
- package/dist/core/ingestion/cfg/control-flow-context.d.ts +106 -0
- package/dist/core/ingestion/cfg/control-flow-context.js +124 -0
- package/dist/core/ingestion/cfg/emit.d.ts +293 -0
- package/dist/core/ingestion/cfg/emit.js +646 -0
- package/dist/core/ingestion/cfg/post-dominators.d.ts +79 -0
- package/dist/core/ingestion/cfg/post-dominators.js +176 -0
- package/dist/core/ingestion/cfg/reaching-def-reason-codec.d.ts +106 -0
- package/dist/core/ingestion/cfg/reaching-def-reason-codec.js +133 -0
- package/dist/core/ingestion/cfg/reaching-defs-graph.d.ts +18 -0
- package/dist/core/ingestion/cfg/reaching-defs-graph.js +312 -0
- package/dist/core/ingestion/cfg/reaching-defs.d.ts +162 -0
- package/dist/core/ingestion/cfg/reaching-defs.js +785 -0
- package/dist/core/ingestion/cfg/synthetic-escape.d.ts +124 -0
- package/dist/core/ingestion/cfg/synthetic-escape.js +300 -0
- package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
- package/dist/core/ingestion/cfg/traversal-result.js +2 -0
- package/dist/core/ingestion/cfg/types.d.ts +274 -0
- package/dist/core/ingestion/cfg/types.js +13 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp-harvest.d.ts +111 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp-harvest.js +541 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp.d.ts +74 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp.js +584 -0
- package/dist/core/ingestion/cfg/visitors/call-site-harvest.d.ts +195 -0
- package/dist/core/ingestion/cfg/visitors/call-site-harvest.js +350 -0
- package/dist/core/ingestion/cfg/visitors/csharp-harvest.d.ts +124 -0
- package/dist/core/ingestion/cfg/visitors/csharp-harvest.js +589 -0
- package/dist/core/ingestion/cfg/visitors/csharp.d.ts +87 -0
- package/dist/core/ingestion/cfg/visitors/csharp.js +867 -0
- package/dist/core/ingestion/cfg/visitors/dart-harvest.d.ts +278 -0
- package/dist/core/ingestion/cfg/visitors/dart-harvest.js +870 -0
- package/dist/core/ingestion/cfg/visitors/dart.d.ts +119 -0
- package/dist/core/ingestion/cfg/visitors/dart.js +836 -0
- package/dist/core/ingestion/cfg/visitors/go-harvest.d.ts +160 -0
- package/dist/core/ingestion/cfg/visitors/go-harvest.js +621 -0
- package/dist/core/ingestion/cfg/visitors/go.d.ts +108 -0
- package/dist/core/ingestion/cfg/visitors/go.js +638 -0
- package/dist/core/ingestion/cfg/visitors/java-harvest.d.ts +108 -0
- package/dist/core/ingestion/cfg/visitors/java-harvest.js +513 -0
- package/dist/core/ingestion/cfg/visitors/java.d.ts +101 -0
- package/dist/core/ingestion/cfg/visitors/java.js +812 -0
- package/dist/core/ingestion/cfg/visitors/kotlin-harvest.d.ts +253 -0
- package/dist/core/ingestion/cfg/visitors/kotlin-harvest.js +719 -0
- package/dist/core/ingestion/cfg/visitors/kotlin.d.ts +121 -0
- package/dist/core/ingestion/cfg/visitors/kotlin.js +809 -0
- package/dist/core/ingestion/cfg/visitors/php-harvest.d.ts +172 -0
- package/dist/core/ingestion/cfg/visitors/php-harvest.js +626 -0
- package/dist/core/ingestion/cfg/visitors/php.d.ts +96 -0
- package/dist/core/ingestion/cfg/visitors/php.js +721 -0
- package/dist/core/ingestion/cfg/visitors/python-harvest.d.ts +212 -0
- package/dist/core/ingestion/cfg/visitors/python-harvest.js +739 -0
- package/dist/core/ingestion/cfg/visitors/python.d.ts +103 -0
- package/dist/core/ingestion/cfg/visitors/python.js +558 -0
- package/dist/core/ingestion/cfg/visitors/ruby-harvest.d.ts +223 -0
- package/dist/core/ingestion/cfg/visitors/ruby-harvest.js +587 -0
- package/dist/core/ingestion/cfg/visitors/ruby.d.ts +112 -0
- package/dist/core/ingestion/cfg/visitors/ruby.js +756 -0
- package/dist/core/ingestion/cfg/visitors/rust-harvest.d.ts +324 -0
- package/dist/core/ingestion/cfg/visitors/rust-harvest.js +873 -0
- package/dist/core/ingestion/cfg/visitors/rust.d.ts +95 -0
- package/dist/core/ingestion/cfg/visitors/rust.js +558 -0
- package/dist/core/ingestion/cfg/visitors/scope-tree-harvest.d.ts +87 -0
- package/dist/core/ingestion/cfg/visitors/scope-tree-harvest.js +116 -0
- package/dist/core/ingestion/cfg/visitors/swift-harvest.d.ts +255 -0
- package/dist/core/ingestion/cfg/visitors/swift-harvest.js +679 -0
- package/dist/core/ingestion/cfg/visitors/swift.d.ts +116 -0
- package/dist/core/ingestion/cfg/visitors/swift.js +787 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +149 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +1056 -0
- package/dist/core/ingestion/cfg/visitors/typescript.d.ts +66 -0
- package/dist/core/ingestion/cfg/visitors/typescript.js +583 -0
- package/dist/core/ingestion/import-resolvers/utils.js +2 -0
- package/dist/core/ingestion/language-provider.d.ts +23 -2
- package/dist/core/ingestion/languages/c-cpp.js +17 -5
- package/dist/core/ingestion/languages/cobol.js +4 -0
- package/dist/core/ingestion/languages/cpp/arity-metadata.js +41 -2
- package/dist/core/ingestion/languages/cpp/captures.js +50 -1
- package/dist/core/ingestion/languages/cpp/conversion-rank.d.ts +2 -0
- package/dist/core/ingestion/languages/cpp/conversion-rank.js +89 -0
- package/dist/core/ingestion/languages/cpp/header-scan.d.ts +1 -1
- package/dist/core/ingestion/languages/cpp/header-scan.js +2 -2
- package/dist/core/ingestion/languages/cpp/inline-namespaces.js +7 -2
- package/dist/core/ingestion/languages/cpp/member-lookup.js +2 -1
- package/dist/core/ingestion/languages/cpp/query.js +23 -0
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +2 -1
- package/dist/core/ingestion/languages/csharp.js +2 -0
- package/dist/core/ingestion/languages/dart.js +2 -0
- package/dist/core/ingestion/languages/go.js +2 -0
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/languages/kotlin/query.js +3 -2
- package/dist/core/ingestion/languages/kotlin.js +8 -1
- package/dist/core/ingestion/languages/php.js +2 -0
- package/dist/core/ingestion/languages/python.js +2 -0
- package/dist/core/ingestion/languages/ruby.js +2 -0
- package/dist/core/ingestion/languages/rust.js +2 -0
- package/dist/core/ingestion/languages/swift.js +2 -0
- package/dist/core/ingestion/languages/typescript.js +5 -0
- package/dist/core/ingestion/languages/vue.js +6 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
- package/dist/core/ingestion/method-extractors/generic.js +1 -0
- package/dist/core/ingestion/method-types.d.ts +2 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
- package/dist/core/ingestion/model/symbol-table.js +1 -0
- package/dist/core/ingestion/mro-processor.js +109 -0
- package/dist/core/ingestion/parsing-processor.js +44 -0
- package/dist/core/ingestion/pipeline-phases/call-summaries.d.ts +26 -0
- package/dist/core/ingestion/pipeline-phases/call-summaries.js +46 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/index.js +2 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +0 -21
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
- package/dist/core/ingestion/pipeline-phases/routes.d.ts +10 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +30 -1
- package/dist/core/ingestion/pipeline-phases/taint-summaries.d.ts +30 -0
- package/dist/core/ingestion/pipeline-phases/taint-summaries.js +81 -0
- package/dist/core/ingestion/pipeline.d.ts +92 -10
- package/dist/core/ingestion/pipeline.js +11 -2
- package/dist/core/ingestion/scope-extractor.js +12 -2
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +9 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.d.ts +75 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js +67 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +12 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +14 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +6 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +13 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +9 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +28 -2
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +4 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +9 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +6 -1
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +174 -40
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +24 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +276 -190
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +75 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +349 -1
- package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
- package/dist/core/ingestion/taint/call-summary-codec.d.ts +75 -0
- package/dist/core/ingestion/taint/call-summary-codec.js +128 -0
- package/dist/core/ingestion/taint/call-summary-emit.d.ts +43 -0
- package/dist/core/ingestion/taint/call-summary-emit.js +70 -0
- package/dist/core/ingestion/taint/call-summary-harvest.d.ts +77 -0
- package/dist/core/ingestion/taint/call-summary-harvest.js +179 -0
- package/dist/core/ingestion/taint/call-summary-model.d.ts +47 -0
- package/dist/core/ingestion/taint/call-summary-model.js +25 -0
- package/dist/core/ingestion/taint/emit.d.ts +125 -0
- package/dist/core/ingestion/taint/emit.js +208 -0
- package/dist/core/ingestion/taint/interproc-emit.d.ts +50 -0
- package/dist/core/ingestion/taint/interproc-emit.js +88 -0
- package/dist/core/ingestion/taint/interproc-solver.d.ts +110 -0
- package/dist/core/ingestion/taint/interproc-solver.js +312 -0
- package/dist/core/ingestion/taint/match.d.ts +153 -0
- package/dist/core/ingestion/taint/match.js +278 -0
- package/dist/core/ingestion/taint/path-codec.d.ts +134 -0
- package/dist/core/ingestion/taint/path-codec.js +190 -0
- package/dist/core/ingestion/taint/propagate.d.ts +216 -0
- package/dist/core/ingestion/taint/propagate.js +659 -0
- package/dist/core/ingestion/taint/site-safety.d.ts +29 -0
- package/dist/core/ingestion/taint/site-safety.js +98 -0
- package/dist/core/ingestion/taint/source-sink-config.d.ts +107 -23
- package/dist/core/ingestion/taint/source-sink-config.js +35 -12
- package/dist/core/ingestion/taint/source-sink-registry.d.ts +6 -4
- package/dist/core/ingestion/taint/source-sink-registry.js +6 -4
- package/dist/core/ingestion/taint/summary-harvest-driver.d.ts +80 -0
- package/dist/core/ingestion/taint/summary-harvest-driver.js +179 -0
- package/dist/core/ingestion/taint/summary-harvest.d.ts +89 -0
- package/dist/core/ingestion/taint/summary-harvest.js +537 -0
- package/dist/core/ingestion/taint/summary-model.d.ts +200 -0
- package/dist/core/ingestion/taint/summary-model.js +86 -0
- package/dist/core/ingestion/taint/typescript-model.d.ts +38 -0
- package/dist/core/ingestion/taint/typescript-model.js +102 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +21 -0
- package/dist/core/ingestion/utils/env.d.ts +7 -0
- package/dist/core/ingestion/utils/env.js +12 -0
- package/dist/core/ingestion/utils/method-props.js +1 -0
- package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
- package/dist/core/ingestion/workers/clone-safety.js +465 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +23 -0
- package/dist/core/ingestion/workers/parse-worker.js +81 -53
- package/dist/core/ingestion/workers/post-result.d.ts +22 -0
- package/dist/core/ingestion/workers/post-result.js +87 -0
- package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
- package/dist/core/ingestion/workers/result-merge.js +43 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +15 -40
- package/dist/core/ingestion/workers/worker-pool.js +43 -16
- package/dist/core/lbug/csv-generator.d.ts +51 -3
- package/dist/core/lbug/csv-generator.js +369 -255
- package/dist/core/lbug/lbug-adapter.d.ts +58 -18
- package/dist/core/lbug/lbug-adapter.js +346 -84
- package/dist/core/lbug/lbug-config.d.ts +19 -0
- package/dist/core/lbug/lbug-config.js +29 -0
- package/dist/core/lbug/pdg-emit-sink.d.ts +127 -0
- package/dist/core/lbug/pdg-emit-sink.js +358 -0
- package/dist/core/lbug/pool-adapter.d.ts +35 -0
- package/dist/core/lbug/pool-adapter.js +90 -1
- package/dist/core/lbug/rel-pair-routing.d.ts +54 -0
- package/dist/core/lbug/rel-pair-routing.js +140 -0
- package/dist/core/lbug/schema.d.ts +2 -2
- package/dist/core/lbug/schema.js +3 -0
- package/dist/core/logger.d.ts +0 -5
- package/dist/core/run-analyze.d.ts +96 -0
- package/dist/core/run-analyze.js +207 -3
- package/dist/core/tree-sitter/parser-loader.js +5 -4
- package/dist/core/tree-sitter/safe-parse.d.ts +0 -5
- package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
- package/dist/core/tree-sitter/vendored-grammars.js +57 -0
- package/dist/core/wiki/generator.js +3 -1
- package/dist/core/wiki/graph-queries.d.ts +6 -0
- package/dist/core/wiki/graph-queries.js +9 -1
- package/dist/mcp/http-transport.d.ts +134 -0
- package/dist/mcp/http-transport.js +503 -0
- package/dist/mcp/local/local-backend.d.ts +110 -24
- package/dist/mcp/local/local-backend.js +1225 -19
- package/dist/mcp/local/pdg-impact.d.ts +384 -0
- package/dist/mcp/local/pdg-impact.js +1972 -0
- package/dist/mcp/resources.js +13 -1
- package/dist/mcp/tools.d.ts +12 -0
- package/dist/mcp/tools.js +214 -7
- package/dist/server/analyze-launch.d.ts +29 -0
- package/dist/server/analyze-launch.js +137 -0
- package/dist/server/analyze-upload.d.ts +33 -0
- package/dist/server/analyze-upload.js +123 -0
- package/dist/server/analyze-worker-ipc.d.ts +58 -0
- package/dist/server/analyze-worker-ipc.js +16 -0
- package/dist/server/analyze-worker.d.ts +17 -1
- package/dist/server/analyze-worker.js +7 -1
- package/dist/server/api.d.ts +17 -0
- package/dist/server/api.js +132 -168
- package/dist/server/git-clone.d.ts +43 -2
- package/dist/server/git-clone.js +211 -16
- package/dist/server/mcp-http.d.ts +5 -6
- package/dist/server/mcp-http.js +9 -82
- package/dist/server/middleware.d.ts +40 -0
- package/dist/server/middleware.js +92 -0
- package/dist/server/private-ip.d.ts +1 -0
- package/dist/server/private-ip.js +15 -0
- package/dist/server/upload-ingest.d.ts +56 -0
- package/dist/server/upload-ingest.js +276 -0
- package/dist/server/upload-paths.d.ts +37 -0
- package/dist/server/upload-paths.js +57 -0
- package/dist/server/upload-sweep.d.ts +20 -0
- package/dist/server/upload-sweep.js +57 -0
- package/dist/storage/git.js +5 -8
- package/dist/storage/parse-cache.d.ts +22 -1
- package/dist/storage/parse-cache.js +44 -10
- package/dist/storage/repo-manager.d.ts +121 -10
- package/dist/storage/repo-manager.js +43 -5
- package/dist/types/pipeline.d.ts +9 -0
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +110 -10
- package/hooks/claude/gitnexus-hook.cjs +110 -13
- package/hooks/claude/hook-db-lock-probe.cjs +513 -26
- package/package.json +8 -2
- package/scripts/assert-publish-grammar-coverage.cjs +31 -0
- package/scripts/build-tree-sitter-grammars.cjs +16 -10
- package/scripts/cross-platform-tests.ts +1 -0
- package/skills/gitnexus-debugging.md +16 -4
- package/skills/gitnexus-exploring.md +3 -3
- package/skills/gitnexus-guide.md +33 -0
- package/skills/gitnexus-pdg-query.md +89 -0
- package/skills/gitnexus-refactoring.md +1 -1
- package/skills/gitnexus-taint-analysis.md +178 -0
- package/vendor/tree-sitter-c/package.json +1 -1
- package/vendor/tree-sitter-dart/package.json +1 -1
- package/vendor/tree-sitter-kotlin/package.json +1 -1
- package/vendor/tree-sitter-proto/package.json +1 -1
- package/vendor/tree-sitter-swift/package.json +1 -1
- package/web/assets/{agent-CKPMqImC.js → agent-D3Xd7zZE.js} +123 -123
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-DxJw65fT.js +1 -0
- package/web/assets/architectureDiagram-UL44E2DR-ChH9xW-N.js +36 -0
- package/web/assets/{blockDiagram-7IZFK4PR-B4niSLvR.js → blockDiagram-7IZFK4PR-BkWhpzWB.js} +1 -1
- package/web/assets/{c4Diagram-Y2BXMSZH-DIzEXalR.js → c4Diagram-Y2BXMSZH-DtBvpzWD.js} +1 -1
- package/web/assets/{chunk-3SSMPTDK-BzGeeheI.js → chunk-3SSMPTDK-B4_etUhr.js} +2 -2
- package/web/assets/{chunk-5IMINLNL-CnTLsXIV.js → chunk-5IMINLNL-DOdTeQri.js} +1 -1
- package/web/assets/{chunk-5VCL7Z4A-w1BCx98Z.js → chunk-5VCL7Z4A-CQ-2dYWj.js} +1 -1
- package/web/assets/chunk-6764PJDD-DpIS7hxD.js +1 -0
- package/web/assets/{chunk-67TQ5CYL-CChC2cAv.js → chunk-67TQ5CYL-4sNPJU1q.js} +3 -3
- package/web/assets/{chunk-7W6UQGC5-CgFyfZ6k.js → chunk-7W6UQGC5-KyEG0HQg.js} +1 -1
- package/web/assets/{chunk-AZZRMDJM-5E9ARdDQ.js → chunk-AZZRMDJM-y-xJBuMz.js} +1 -1
- package/web/assets/{chunk-INKRHTLW-ELpfuk9h.js → chunk-INKRHTLW-Nu4ri9P-.js} +1 -1
- package/web/assets/{chunk-JQRUD6KW-Dcq_Qnyy.js → chunk-JQRUD6KW-Dbva2Z17.js} +1 -1
- package/web/assets/{chunk-KGFNY3KK-W-VR57xb.js → chunk-KGFNY3KK-JGMJvKCK.js} +1 -1
- package/web/assets/chunk-KGYTTC2M-CWC_c3H9.js +161 -0
- package/web/assets/{chunk-KNLZD3CH-BuZBOcso.js → chunk-KNLZD3CH-BGlP6X9R.js} +1 -1
- package/web/assets/{chunk-KRXBNO2N-DH2NPM7I.js → chunk-KRXBNO2N-BW5o0KUN.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-zoomRtWX.js} +1 -1
- package/web/assets/{chunk-LII3EMHJ-3X33tCpU.js → chunk-LII3EMHJ-D-jm-dLa.js} +1 -1
- package/web/assets/{chunk-QA3QBVWF-D5Ha3GgB.js → chunk-QA3QBVWF-BgRW3SXd.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV-DD0B2N2B.js} +1 -1
- package/web/assets/{chunk-T2UQINTJ-kGYG7ppP.js → chunk-T2UQINTJ-DSBqkzH7.js} +1 -1
- package/web/assets/chunk-T5OCTHI4-DdZvN-9i.js +1 -0
- package/web/assets/{chunk-UY5QBCOK-Du-c56gs.js → chunk-UY5QBCOK-DyF0vyGd.js} +1 -1
- package/web/assets/{chunk-W44A43WB-lrCpuupA.js → chunk-W44A43WB-JqMLIpR0.js} +1 -1
- package/web/assets/{chunk-ZXARS5L4-CSZRiyOf.js → chunk-ZXARS5L4-CxN8oiwI.js} +1 -1
- package/web/assets/classDiagram-KGZ6W3CR-DJm9BgF-.js +1 -0
- package/web/assets/classDiagram-v2-72OJOZXJ-DJm9BgF-.js +1 -0
- package/web/assets/{context-builder-_HS0v2ma.js → context-builder-BHiFUA8O.js} +10 -2
- package/web/assets/{cose-bilkent-UX7MHV2Q-BHv2bbtl.js → cose-bilkent-UX7MHV2Q-D71wNYRJ.js} +1 -1
- package/web/assets/dagre-ND4H6XIP-BIkxmhcu.js +4 -0
- package/web/assets/diagram-3NCE3AQN-BTa6iH0T.js +43 -0
- package/web/assets/diagram-GF46GFSD-DZDV0vBj.js +24 -0
- package/web/assets/{diagram-HNR7UZ2L-fUWreZzx.js → diagram-HNR7UZ2L-DFCfd5LI.js} +2 -2
- package/web/assets/diagram-QXG6HAR7-BzM--Jj0.js +24 -0
- package/web/assets/diagram-WEQXMOUZ-DFFwz0k4.js +10 -0
- package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-CfHMJJqo.js} +1 -1
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-BQm9QzEa.js +1 -0
- package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-Be805QiJ.js} +3 -3
- package/web/assets/{ganttDiagram-JCBTUEKG-BHQssTz6.js → ganttDiagram-JCBTUEKG-D3iL2Aet.js} +1 -1
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-Cr_Bm2Nb.js +1 -0
- package/web/assets/gitGraphDiagram-S2ZK5IYY-CPIDgxGm.js +106 -0
- package/web/assets/index-BvYev1UJ.js +635 -0
- package/web/assets/index-_lgn7hs5.css +2 -0
- package/web/assets/info-J43DQDTF-KCYPFFUO-Cs4p2oyk.js +1 -0
- package/web/assets/infoDiagram-3YFTVSEB-Y7BKSoke.js +2 -0
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-CT4SHFuI.js} +1 -1
- package/web/assets/{journeyDiagram-M6C3CM3L-DNPYCpvq.js → journeyDiagram-M6C3CM3L-CvArszLo.js} +1 -1
- package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-C6yDxr7N.js} +3 -3
- package/web/assets/{katex-K3KEBU37-CrVLz8l7.js → katex-K3KEBU37-CTc5BslQ.js} +1 -1
- package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-BOc2WEne.js} +1 -1
- package/web/assets/packet-YPE3B663-LP52Z2RK-CKCrztD2.js +1 -0
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-BB7pHqoD.js +1 -0
- package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-Cvc0f_Pd.js} +1 -1
- package/web/assets/{quadrantDiagram-VICAPDV7-bAm6Hi_5.js → quadrantDiagram-VICAPDV7-yXSZ2lLz.js} +1 -1
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-DhoTOcuK.js +1 -0
- package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-BRcZvHs4.js} +1 -1
- package/web/assets/{sankeyDiagram-URQDO5SZ-LFJz53u5.js → sankeyDiagram-URQDO5SZ-1qEjlFIB.js} +1 -1
- package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-PcSCuvrB.js} +3 -3
- package/web/assets/stateDiagram-7D4R322I-D6qYSV1E.js +1 -0
- package/web/assets/stateDiagram-v2-36443NZ5-Bg-ImOSX.js +1 -0
- package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-QWkh01xw.js} +1 -1
- package/web/assets/treeView-BLDUP644-QA4HXRO3-ODehyGKL.js +1 -0
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-BfpgC-7o.js +1 -0
- package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-ak4DfwuM.js} +3 -3
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-CNeL6VHE.js +1 -0
- package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-CTzR4Loq.js} +1 -1
- package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-DA1BEaQz.js} +1 -1
- package/web/index.html +13 -15
- package/scripts/materialize-vendor-grammars.cjs +0 -97
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-BETj2x4g.js +0 -1
- package/web/assets/architectureDiagram-UL44E2DR-D1EXI0zA.js +0 -36
- package/web/assets/chunk-2T2R6R2M-n6s9JZqv.js +0 -4
- package/web/assets/chunk-2UTLFMKG-CMBB1TMN.js +0 -1
- package/web/assets/chunk-4R4BOZG6-mKhL59ul.js +0 -159
- package/web/assets/chunk-6764PJDD-ChwMM2z4.js +0 -1
- package/web/assets/chunk-7J6CGLKN-DT-b53FT.js +0 -10
- package/web/assets/chunk-C62D2QBJ-CDAWj26E.js +0 -1
- package/web/assets/chunk-CEXFNPSA-D68Tk6ls.js +0 -1
- package/web/assets/chunk-CilyBKbf.js +0 -1
- package/web/assets/chunk-J5EP6P6S-DxWW0yvu.js +0 -1
- package/web/assets/chunk-KGYTTC2M-C6PHeuay.js +0 -1
- package/web/assets/chunk-RERM46MO-DQNbXtfw.js +0 -1
- package/web/assets/chunk-RKZBBQEN-BmTPLSyv.js +0 -1
- package/web/assets/chunk-RLI5ZMPA-DExu2DOK.js +0 -1
- package/web/assets/chunk-T5OCTHI4--9wWpVws.js +0 -1
- package/web/assets/chunk-UP6H54XL-DsKdC6jC.js +0 -1
- package/web/assets/chunk-UXSXWOXI-DR81EqLr.js +0 -1
- package/web/assets/classDiagram-KGZ6W3CR-Bv93af_b.js +0 -1
- package/web/assets/classDiagram-v2-72OJOZXJ-CLfEqHUa.js +0 -1
- package/web/assets/dagre-ND4H6XIP-DsIr9MOv.js +0 -4
- package/web/assets/diagram-3NCE3AQN-BqAtKUpW.js +0 -43
- package/web/assets/diagram-GF46GFSD-CaBG6n6o.js +0 -24
- package/web/assets/diagram-QXG6HAR7-CZ-O3rcV.js +0 -24
- package/web/assets/diagram-WEQXMOUZ-P4lSL4GH.js +0 -10
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-B3myHUst.js +0 -1
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DF3rtf4A.js +0 -1
- package/web/assets/gitGraphDiagram-S2ZK5IYY-CCwVLT__.js +0 -106
- package/web/assets/index-BKWA-m7o.css +0 -2
- package/web/assets/index-CG6q8eTs.js +0 -626
- package/web/assets/info-J43DQDTF-KCYPFFUO-R4XV_akJ.js +0 -1
- package/web/assets/infoDiagram-3YFTVSEB-DmLICZx1.js +0 -2
- package/web/assets/packet-YPE3B663-LP52Z2RK-CgQPdNZa.js +0 -1
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-BzWOwc_c.js +0 -1
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-B7KG0FN7.js +0 -1
- package/web/assets/stateDiagram-7D4R322I-CB2nABwH.js +0 -1
- package/web/assets/stateDiagram-v2-36443NZ5-COBGd2RL.js +0 -1
- package/web/assets/treeView-BLDUP644-QA4HXRO3-CRSxEwLO.js +0 -1
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-a0PfKoVn.js +0 -1
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-BKQ6e5mC.js +0 -1
- /package/web/assets/{chunk-AQ6EADP3-CCFje6lL.js → chunk-AQ6EADP3-CZhslHi-.js} +0 -0
|
@@ -27,7 +27,41 @@ import { getExactScanLimit, isVectorExtensionSupportedByPlatform, } from '../../
|
|
|
27
27
|
import { PhaseTimer } from '../../core/search/phase-timer.js';
|
|
28
28
|
import { checkStalenessAsync, checkCwdMatch } from '../../core/git-staleness.js';
|
|
29
29
|
import { logger } from '../../core/logger.js';
|
|
30
|
-
import { LIST_REPOS_DEFAULT_LIMIT, LIST_REPOS_MAX_LIMIT } from '../tools.js';
|
|
30
|
+
import { LIST_REPOS_DEFAULT_LIMIT, LIST_REPOS_MAX_LIMIT, EXPLAIN_DEFAULT_LIMIT, EXPLAIN_MAX_LIMIT, PDG_QUERY_DEFAULT_LIMIT, PDG_QUERY_MAX_LIMIT, } from '../tools.js';
|
|
31
|
+
import { findImportCycles } from '../../core/graph/import-cycles.js';
|
|
32
|
+
import { decodeTaintPath } from '../../core/ingestion/taint/path-codec.js';
|
|
33
|
+
import { decodeReachingDefReason } from '../../core/ingestion/cfg/reaching-def-reason-codec.js';
|
|
34
|
+
import { EXTENSIONS } from '../../core/ingestion/import-resolvers/utils.js';
|
|
35
|
+
import { fnLineOf, isPdgDegradedLayerStatus, makePdgImpactErrorResult, makePdgLayerDegradedResult, pdgLayerStatus, pdgStampForMode, runImpactPDG, validateImpactMode, pdgBridgeEvidenceForImpact, betterBridgeEvidence, composeUnifiedPdgImpactResult, splitCalleeIds, } from './pdg-impact.js';
|
|
36
|
+
/** Real source-file extensions (`.ts`, `.py`, …) from the resolver's list,
|
|
37
|
+
* excluding the empty entry and the `/index.*` forms — used to decide whether
|
|
38
|
+
* an `explain` target is a file path vs a (possibly dotted) symbol name. */
|
|
39
|
+
const SOURCE_FILE_EXTENSIONS = EXTENSIONS.filter((e) => e.startsWith('.') && !e.includes('/'));
|
|
40
|
+
/** A target is path-ish if it has a path separator or ends in a known source
|
|
41
|
+
* extension. A bare dotted symbol (`UserController.create`) is NOT path-ish. */
|
|
42
|
+
function looksLikeFilePath(target) {
|
|
43
|
+
if (/[\\/]/.test(target))
|
|
44
|
+
return true;
|
|
45
|
+
const lower = target.toLowerCase();
|
|
46
|
+
return SOURCE_FILE_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a string tool param from its canonical name or legacy alias (#2175).
|
|
50
|
+
* Returns the first NON-BLANK string of [canonical, legacy] — the canonical (new)
|
|
51
|
+
* name is preferred when it carries a real value, otherwise the legacy value is used.
|
|
52
|
+
* A blank/whitespace new value therefore does NOT clobber a valid legacy value (e.g. a
|
|
53
|
+
* gradually-migrating client that always emits the new key, blank when unset). A
|
|
54
|
+
* non-string value (the MCP envelope is not schema-validated, so clients can send any
|
|
55
|
+
* JSON type) and an all-blank input resolve to `undefined`, so the caller returns a
|
|
56
|
+
* friendly required-param error instead of throwing `TypeError` on `.trim()`.
|
|
57
|
+
*/
|
|
58
|
+
function resolveAliasString(canonical, legacy) {
|
|
59
|
+
for (const value of [canonical, legacy]) {
|
|
60
|
+
if (typeof value === 'string' && value.trim())
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
31
65
|
// AI context generation is CLI-only (gitnexus analyze)
|
|
32
66
|
// import { generateAIContextFiles } from '../../cli/ai-context.js';
|
|
33
67
|
/**
|
|
@@ -1015,6 +1049,15 @@ export class LocalBackend {
|
|
|
1015
1049
|
return this.handleGroupTool(method, params || {});
|
|
1016
1050
|
}
|
|
1017
1051
|
const p = params && typeof params === 'object' ? params : {};
|
|
1052
|
+
// #2175: Claude Code drops a tool-call argument named exactly "query", so the
|
|
1053
|
+
// query/cypher tools advertise "search_query"/"statement" while still accepting the
|
|
1054
|
+
// legacy "query" key for backward compat. The alias is resolved with `?? ` (new name
|
|
1055
|
+
// wins) at every consumer site rather than by mutating params here, so precedence is
|
|
1056
|
+
// uniform and there is no hidden mutation: query()/cypher() read it directly, the
|
|
1057
|
+
// legacy "search" alias routes through query(), and the cross-repo group-forward
|
|
1058
|
+
// resolves it self-contained in callToolAtGroupRepo. This is permanent compatibility
|
|
1059
|
+
// — third-party MCP clients may legitimately send "query", so the alias is not slated
|
|
1060
|
+
// for removal even if Claude Code's argument handling later changes.
|
|
1018
1061
|
if ((method === 'impact' || method === 'query' || method === 'context') &&
|
|
1019
1062
|
typeof p.repo === 'string' &&
|
|
1020
1063
|
p.repo.startsWith('@')) {
|
|
@@ -1033,10 +1076,16 @@ export class LocalBackend {
|
|
|
1033
1076
|
}
|
|
1034
1077
|
case 'context':
|
|
1035
1078
|
return this.context(repo, params);
|
|
1079
|
+
case 'explain':
|
|
1080
|
+
return this.explain(repo, params);
|
|
1081
|
+
case 'pdg_query':
|
|
1082
|
+
return this.pdgQuery(repo, params);
|
|
1036
1083
|
case 'impact':
|
|
1037
1084
|
return this.impact(repo, params);
|
|
1038
1085
|
case 'detect_changes':
|
|
1039
1086
|
return this.detectChanges(repo, params);
|
|
1087
|
+
case 'check':
|
|
1088
|
+
return this.check(repo, params);
|
|
1040
1089
|
case 'rename':
|
|
1041
1090
|
return this.rename(repo, params);
|
|
1042
1091
|
// Legacy aliases for backwards compatibility
|
|
@@ -1054,11 +1103,44 @@ export class LocalBackend {
|
|
|
1054
1103
|
return this.toolMap(repo, params);
|
|
1055
1104
|
case 'api_impact':
|
|
1056
1105
|
return this.apiImpact(repo, params);
|
|
1106
|
+
case 'trace':
|
|
1107
|
+
return this.trace(repo, params);
|
|
1057
1108
|
default:
|
|
1058
1109
|
throw new Error(`Unknown tool: ${method}`);
|
|
1059
1110
|
}
|
|
1060
1111
|
}
|
|
1061
1112
|
// ─── Tool Implementations ────────────────────────────────────────
|
|
1113
|
+
/** Check repository graph invariants that are suitable for CI gating. */
|
|
1114
|
+
async check(repo, params) {
|
|
1115
|
+
if (params?.cycles === false) {
|
|
1116
|
+
return { error: 'No checks selected. Set "cycles" to true.' };
|
|
1117
|
+
}
|
|
1118
|
+
await this.ensureInitialized(repo);
|
|
1119
|
+
const rowLimit = 100_001;
|
|
1120
|
+
const rows = await executeParameterized(repo.lbugPath, `MATCH (source:File)-[r:CodeRelation]->(target:File)
|
|
1121
|
+
WHERE r.type = 'IMPORTS'
|
|
1122
|
+
AND (r.reason IS NULL OR (
|
|
1123
|
+
r.reason <> 'swift-scope: implicit module visibility'
|
|
1124
|
+
AND r.reason <> 'markdown-link'
|
|
1125
|
+
))
|
|
1126
|
+
RETURN source.filePath AS source, target.filePath AS target
|
|
1127
|
+
LIMIT ${rowLimit}`, {});
|
|
1128
|
+
if (rows.length === rowLimit) {
|
|
1129
|
+
return {
|
|
1130
|
+
error: `Import graph exceeds the ${rowLimit - 1} edge safety limit.`,
|
|
1131
|
+
truncated: true,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
const cycles = findImportCycles(rows.map((row) => ({
|
|
1135
|
+
source: String(row.source ?? row[0] ?? ''),
|
|
1136
|
+
target: String(row.target ?? row[1] ?? ''),
|
|
1137
|
+
})));
|
|
1138
|
+
return {
|
|
1139
|
+
status: cycles.length === 0 ? 'clean' : 'cycles_found',
|
|
1140
|
+
cycleCount: cycles.length,
|
|
1141
|
+
cycles: cycles.map((files) => ({ files })),
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1062
1144
|
/**
|
|
1063
1145
|
* Query tool — process-grouped search.
|
|
1064
1146
|
*
|
|
@@ -1068,14 +1150,18 @@ export class LocalBackend {
|
|
|
1068
1150
|
* 4. Return: { processes, process_symbols, definitions }
|
|
1069
1151
|
*/
|
|
1070
1152
|
async query(repo, params) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1153
|
+
// #2175: each consumer resolves the search_query/query alias itself (there is no
|
|
1154
|
+
// chokepoint mutation in callTool). This also serves the GroupService port, which
|
|
1155
|
+
// reaches query() carrying only the legacy `query` key.
|
|
1156
|
+
const rawQuery = resolveAliasString(params.search_query, params.query);
|
|
1157
|
+
if (!rawQuery?.trim()) {
|
|
1158
|
+
return { error: 'search_query (or legacy query) parameter is required and cannot be empty.' };
|
|
1073
1159
|
}
|
|
1074
1160
|
await this.ensureInitialized(repo);
|
|
1075
1161
|
const processLimit = params.limit || 5;
|
|
1076
1162
|
const maxSymbolsPerProcess = params.max_symbols || 10;
|
|
1077
1163
|
const includeContent = params.include_content ?? false;
|
|
1078
|
-
const searchQuery =
|
|
1164
|
+
const searchQuery = rawQuery.trim();
|
|
1079
1165
|
// Per-phase timing instrumentation (#553). Records wall time for each
|
|
1080
1166
|
// observable sub-step of the search pipeline so production latency can
|
|
1081
1167
|
// be aggregated offline for Pareto analysis and bottleneck detection.
|
|
@@ -1389,9 +1475,15 @@ export class LocalBackend {
|
|
|
1389
1475
|
WHERE n.id IN $nodeIds
|
|
1390
1476
|
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
1391
1477
|
`, { nodeIds })
|
|
1392
|
-
: await executeParameterized(repo.lbugPath,
|
|
1478
|
+
: await executeParameterized(repo.lbugPath,
|
|
1479
|
+
// Same BasicBlock exclusion as detect_changes (#2082 U7): on a
|
|
1480
|
+
// --pdg index a function-heavy file has far more BasicBlock rows
|
|
1481
|
+
// than symbols, so an unfiltered LIMIT 3 would surface nameless
|
|
1482
|
+
// substrate rows and displace the real symbols.
|
|
1483
|
+
`
|
|
1393
1484
|
MATCH (n)
|
|
1394
1485
|
WHERE n.filePath = $filePath
|
|
1486
|
+
AND NOT n.id STARTS WITH 'BasicBlock:'
|
|
1395
1487
|
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
1396
1488
|
LIMIT 3
|
|
1397
1489
|
`, { filePath: fullPath });
|
|
@@ -1546,7 +1638,10 @@ export class LocalBackend {
|
|
|
1546
1638
|
const repo = await this.resolveRepo(repoName);
|
|
1547
1639
|
return this.cypher(repo, { query, params });
|
|
1548
1640
|
}
|
|
1549
|
-
async cypher(repo,
|
|
1641
|
+
async cypher(repo,
|
|
1642
|
+
// #2175: "statement" is the advertised param; "query" is the legacy alias,
|
|
1643
|
+
// still accepted (and the field the internal executeCypher() passes). New wins.
|
|
1644
|
+
request) {
|
|
1550
1645
|
await this.ensureInitialized(repo);
|
|
1551
1646
|
if (!isLbugReady(repo.lbugPath)) {
|
|
1552
1647
|
return { error: 'LadybugDB not ready. Index may be corrupted.' };
|
|
@@ -1556,8 +1651,14 @@ export class LocalBackend {
|
|
|
1556
1651
|
error: '"params" must be a plain object with scalar values (string/number/boolean/null).',
|
|
1557
1652
|
};
|
|
1558
1653
|
}
|
|
1654
|
+
const cypherText = resolveAliasString(request.statement, request.query) ?? '';
|
|
1655
|
+
if (!cypherText.trim()) {
|
|
1656
|
+
// Mirror query()'s friendly required-param error instead of letting an empty
|
|
1657
|
+
// string fall through to a raw LadybugDB prepare error (#2175 review).
|
|
1658
|
+
return { error: 'statement (or legacy query) parameter is required and cannot be empty.' };
|
|
1659
|
+
}
|
|
1559
1660
|
try {
|
|
1560
|
-
const result = await executeParameterized(repo.lbugPath,
|
|
1661
|
+
const result = await executeParameterized(repo.lbugPath, cypherText, request.params ?? {});
|
|
1561
1662
|
return result;
|
|
1562
1663
|
}
|
|
1563
1664
|
catch (err) {
|
|
@@ -2207,6 +2308,510 @@ export class LocalBackend {
|
|
|
2207
2308
|
})),
|
|
2208
2309
|
};
|
|
2209
2310
|
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Resolve a `target` (file path OR symbol/function name) into a BasicBlock
|
|
2313
|
+
* SOURCE-block anchor, shared by `explain` (TAINTED) and `pdg_query`
|
|
2314
|
+
* (CDG/REACHING_DEF) — both reconstruct the symbol↔block join the same way
|
|
2315
|
+
* (there is no Function→BasicBlock edge). #2188 review: extracted from two
|
|
2316
|
+
* near-identical copies that had DRIFTED — `_explainImpl` used a 0-based,
|
|
2317
|
+
* un-widened span window that dropped a function's final-line block and could
|
|
2318
|
+
* leak a neighbor's line-above block; this single resolver applies the correct
|
|
2319
|
+
* `[symStart+1, symEnd+1]` window (1-based BasicBlock startLine vs 0-based
|
|
2320
|
+
* symbol span) to BOTH callers.
|
|
2321
|
+
*
|
|
2322
|
+
* Returns a BARE `anchorClause` (no leading `AND`) so each caller composes its
|
|
2323
|
+
* own `WHERE`; `early` carries the not-found/ambiguous payload (caller returns
|
|
2324
|
+
* it verbatim). `target` / symbol names flow only through `queryParams` bind
|
|
2325
|
+
* params — never interpolated into Cypher.
|
|
2326
|
+
*/
|
|
2327
|
+
async resolveBlockAnchor(repo, target, toolName) {
|
|
2328
|
+
if (looksLikeFilePath(target)) {
|
|
2329
|
+
return {
|
|
2330
|
+
anchorClause: '(a.id STARTS WITH $idPrefix OR a.filePath = $targetPath OR a.filePath ENDS WITH $targetSuffix)',
|
|
2331
|
+
queryParams: {
|
|
2332
|
+
idPrefix: `BasicBlock:${target}:`,
|
|
2333
|
+
targetPath: target,
|
|
2334
|
+
targetSuffix: `/${target}`,
|
|
2335
|
+
},
|
|
2336
|
+
anchor: { file: target },
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
const outcome = await this.resolveSymbolCandidates(repo, { name: target }, {});
|
|
2340
|
+
if (outcome.kind === 'not_found') {
|
|
2341
|
+
return {
|
|
2342
|
+
anchorClause: '',
|
|
2343
|
+
queryParams: {},
|
|
2344
|
+
anchor: { file: '' },
|
|
2345
|
+
early: { error: `Symbol '${target}' not found` },
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
if (outcome.kind === 'ambiguous') {
|
|
2349
|
+
return {
|
|
2350
|
+
anchorClause: '',
|
|
2351
|
+
queryParams: {},
|
|
2352
|
+
anchor: { file: '' },
|
|
2353
|
+
early: {
|
|
2354
|
+
status: 'ambiguous',
|
|
2355
|
+
message: `Found ${outcome.candidates.length} symbols matching '${target}'. Re-call ${toolName} with the file path, or disambiguate via context() first.`,
|
|
2356
|
+
candidates: outcome.candidates.map((c) => ({
|
|
2357
|
+
uid: c.id,
|
|
2358
|
+
name: c.name,
|
|
2359
|
+
kind: c.type,
|
|
2360
|
+
filePath: c.filePath,
|
|
2361
|
+
line: c.startLine,
|
|
2362
|
+
score: Number(c.score.toFixed(2)),
|
|
2363
|
+
})),
|
|
2364
|
+
},
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
const sym = outcome.symbol;
|
|
2368
|
+
const idPrefix = `BasicBlock:${sym.filePath}:`;
|
|
2369
|
+
if (typeof sym.startLine === 'number' &&
|
|
2370
|
+
typeof sym.endLine === 'number' &&
|
|
2371
|
+
sym.endLine >= sym.startLine) {
|
|
2372
|
+
// BasicBlock startLine is 1-based; the symbol span is 0-based. Shift BOTH
|
|
2373
|
+
// bounds +1 so the window is the function's true block span: the lower +1
|
|
2374
|
+
// excludes a neighbor's block on the line directly above, the upper +1
|
|
2375
|
+
// keeps a guard/def/use on the final line (#2188 review).
|
|
2376
|
+
return {
|
|
2377
|
+
anchorClause: 'a.id STARTS WITH $idPrefix AND a.startLine >= $symStart AND a.startLine <= $symEnd',
|
|
2378
|
+
queryParams: { idPrefix, symStart: sym.startLine + 1, symEnd: sym.endLine + 1 },
|
|
2379
|
+
anchor: {
|
|
2380
|
+
file: sym.filePath,
|
|
2381
|
+
symbol: sym.name,
|
|
2382
|
+
startLine: sym.startLine,
|
|
2383
|
+
endLine: sym.endLine,
|
|
2384
|
+
},
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
// No usable span — degrade to the file-level filter (documented).
|
|
2388
|
+
return {
|
|
2389
|
+
anchorClause: 'a.id STARTS WITH $idPrefix',
|
|
2390
|
+
queryParams: { idPrefix },
|
|
2391
|
+
anchor: { file: sym.filePath, symbol: sym.name },
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Explain tool (#2083 M3 U6) — persisted taint-finding explanation.
|
|
2396
|
+
* WAL-aware wrapper mirroring `context`.
|
|
2397
|
+
*/
|
|
2398
|
+
async explain(repo, params) {
|
|
2399
|
+
try {
|
|
2400
|
+
return await this._explainImpl(repo, params);
|
|
2401
|
+
}
|
|
2402
|
+
catch (err) {
|
|
2403
|
+
const msg = (err instanceof Error ? err.message : String(err)) || 'Explain query failed';
|
|
2404
|
+
if (isWalCorruptionError(err)) {
|
|
2405
|
+
return {
|
|
2406
|
+
error: msg,
|
|
2407
|
+
recoverySuggestion: WAL_RECOVERY_SUGGESTION,
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
throw err;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Taint findings are persisted as `TAINTED` rows in CodeRelation whose
|
|
2415
|
+
* endpoints are BOTH BasicBlock nodes — the label anchor restricts every
|
|
2416
|
+
* query here to the BasicBlock→BasicBlock partition of the rel table
|
|
2417
|
+
* (which holds only the sparse, per-function-capped pdg layers), never a
|
|
2418
|
+
* global symbol-space scan (the S1 verdict; LadybugDB has no rel-property
|
|
2419
|
+
* index, so the label anchor IS the bound).
|
|
2420
|
+
*
|
|
2421
|
+
* Anchoring granularity:
|
|
2422
|
+
* - file target → BasicBlock id prefix (`BasicBlock:<filePath>:` — the
|
|
2423
|
+
* shared `basicBlockId` template) with an exact-or-suffix path match so
|
|
2424
|
+
* `vuln.ts` finds `src/vuln.ts`.
|
|
2425
|
+
* - symbol target → resolved via `resolveSymbolCandidates` (the context()
|
|
2426
|
+
* path: ambiguous ⇒ ranked candidates, unknown ⇒ not-found), then the
|
|
2427
|
+
* file id-prefix PLUS source-block startLine within the symbol's
|
|
2428
|
+
* [startLine, endLine] span. Findings are intra-procedural, so filtering
|
|
2429
|
+
* the SOURCE endpoint is sufficient — both endpoints share the function.
|
|
2430
|
+
* Symbols without a line span degrade to the file-level filter.
|
|
2431
|
+
*
|
|
2432
|
+
* The per-finding `sinkKind` and hop path decode from the persisted
|
|
2433
|
+
* `reason` via the SHARED `taint/path-codec.ts` (the U4 write path encodes
|
|
2434
|
+
* with the same module — `;<kind>` header + ordered `variable:line` hops).
|
|
2435
|
+
*/
|
|
2436
|
+
async _explainImpl(repo, params) {
|
|
2437
|
+
await this.ensureInitialized(repo);
|
|
2438
|
+
const rawLimit = params.limit ?? EXPLAIN_DEFAULT_LIMIT;
|
|
2439
|
+
if (!Number.isInteger(rawLimit) || rawLimit < 1 || rawLimit > EXPLAIN_MAX_LIMIT) {
|
|
2440
|
+
return {
|
|
2441
|
+
error: `Invalid "limit": expected an integer in [1, ${EXPLAIN_MAX_LIMIT}], got ${JSON.stringify(params.limit)}.`,
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
const limit = rawLimit;
|
|
2445
|
+
const NO_TAINT_NOTE = 'no taint layer — run gitnexus analyze --pdg to record taint findings for this repo';
|
|
2446
|
+
// Cheap meta probe: the TAINT layer exists iff the pdg stamp carries a
|
|
2447
|
+
// `taintModelVersion` (the field M3 added). An M1/M2-era `--pdg` index has
|
|
2448
|
+
// `meta.pdg` defined but no taintModelVersion — BasicBlock/REACHING_DEF
|
|
2449
|
+
// exist, zero TAINTED rows do — so it must surface the no-taint-layer hint,
|
|
2450
|
+
// not the generic "analyzed, nothing found" note. An unreadable meta (e.g.
|
|
2451
|
+
// a seeded test DB) falls through to the row-existence probe below.
|
|
2452
|
+
let pdgStamped;
|
|
2453
|
+
try {
|
|
2454
|
+
const meta = await loadMeta(path.dirname(repo.lbugPath));
|
|
2455
|
+
if (meta)
|
|
2456
|
+
pdgStamped = meta.pdg?.taintModelVersion !== undefined;
|
|
2457
|
+
}
|
|
2458
|
+
catch {
|
|
2459
|
+
/* meta unreadable — decide from the DB below */
|
|
2460
|
+
}
|
|
2461
|
+
if (pdgStamped === false) {
|
|
2462
|
+
return { findings: [], totalFindings: 0, note: NO_TAINT_NOTE };
|
|
2463
|
+
}
|
|
2464
|
+
// Resolve the optional anchor into a WHERE clause on the SOURCE block.
|
|
2465
|
+
const target = typeof params.target === 'string' ? params.target.trim() : '';
|
|
2466
|
+
let anchorClause = '';
|
|
2467
|
+
let queryParams = {};
|
|
2468
|
+
let anchor;
|
|
2469
|
+
// Bounded by construction: the BasicBlock→BasicBlock partition holds only
|
|
2470
|
+
// the sparse pdg layers, TAINTED rows are per-function-capped at analyze
|
|
2471
|
+
// time, and the page is LIMIT-bounded (the limit is a validated integer —
|
|
2472
|
+
// interpolated because LadybugDB does not parameterize LIMIT).
|
|
2473
|
+
const runAnchoredQuery = async () => {
|
|
2474
|
+
const matchClause = `
|
|
2475
|
+
MATCH (a:BasicBlock)-[r:CodeRelation]->(b:BasicBlock)
|
|
2476
|
+
WHERE r.type = 'TAINTED'${anchorClause ? ` AND ${anchorClause}` : ''}`;
|
|
2477
|
+
const [qRows, countRows] = await Promise.all([
|
|
2478
|
+
executeParameterized(repo.lbugPath, `${matchClause}
|
|
2479
|
+
RETURN a.id AS sourceBlockId, a.filePath AS file, a.startLine AS sourceStart,
|
|
2480
|
+
b.startLine AS sinkStart, r.reason AS reason, b.id AS sinkBlockId
|
|
2481
|
+
ORDER BY sourceBlockId, sinkBlockId, reason
|
|
2482
|
+
LIMIT ${limit}`, queryParams),
|
|
2483
|
+
executeParameterized(repo.lbugPath, `${matchClause}
|
|
2484
|
+
RETURN COUNT(*) AS total`, queryParams),
|
|
2485
|
+
]);
|
|
2486
|
+
return {
|
|
2487
|
+
rows: qRows,
|
|
2488
|
+
totalFindings: Number(countRows[0]?.total ?? countRows[0]?.[0] ?? 0),
|
|
2489
|
+
};
|
|
2490
|
+
};
|
|
2491
|
+
if (target) {
|
|
2492
|
+
// Shared symbol↔block anchor resolver (#2188): file id-prefix OR symbol
|
|
2493
|
+
// span, with the corrected [symStart+1, symEnd+1] window. A bare/dotted
|
|
2494
|
+
// symbol name resolves as a symbol rather than silently file-anchoring.
|
|
2495
|
+
const resolved = await this.resolveBlockAnchor(repo, target, 'explain');
|
|
2496
|
+
if (resolved.early)
|
|
2497
|
+
return resolved.early;
|
|
2498
|
+
anchorClause = resolved.anchorClause;
|
|
2499
|
+
queryParams = resolved.queryParams;
|
|
2500
|
+
anchor = resolved.anchor;
|
|
2501
|
+
}
|
|
2502
|
+
const { rows, totalFindings } = await runAnchoredQuery();
|
|
2503
|
+
// M4 (#2084 U7): cross-function findings ride TAINT_PATH edges (Function/
|
|
2504
|
+
// Method → Function/Method), separate from the intra-procedural TAINTED
|
|
2505
|
+
// BasicBlock rows above. Enumerate them too so `explain` is the discovery
|
|
2506
|
+
// surface for interprocedural flows (TAINT_PATH stays out of
|
|
2507
|
+
// VALID_RELATION_TYPES + the web schema, like TAINTED). File-anchored:
|
|
2508
|
+
// filter on the source function's file; symbol-anchored: either endpoint
|
|
2509
|
+
// matches the symbol name; anchorless: all (bounded by LIMIT). Computed
|
|
2510
|
+
// BEFORE the no-taint early returns — a repo with ONLY cross-function
|
|
2511
|
+
// findings (no intra-procedural TAINTED rows) must not look empty.
|
|
2512
|
+
const runInterprocQuery = async () => {
|
|
2513
|
+
const where = [`r.type = 'TAINT_PATH'`];
|
|
2514
|
+
const p = {};
|
|
2515
|
+
if (anchor?.symbol) {
|
|
2516
|
+
where.push('(a.name = $ipSym OR b.name = $ipSym)');
|
|
2517
|
+
p.ipSym = anchor.symbol;
|
|
2518
|
+
}
|
|
2519
|
+
else if (anchor?.file) {
|
|
2520
|
+
// Match EITHER endpoint's file — a cross-function flow anchored on the
|
|
2521
|
+
// SINK's file (b) is as relevant as one anchored on the source's (a).
|
|
2522
|
+
where.push('(a.filePath = $ipFile OR a.filePath ENDS WITH $ipSuffix OR ' +
|
|
2523
|
+
'b.filePath = $ipFile OR b.filePath ENDS WITH $ipSuffix)');
|
|
2524
|
+
p.ipFile = anchor.file;
|
|
2525
|
+
p.ipSuffix = `/${anchor.file}`;
|
|
2526
|
+
}
|
|
2527
|
+
const matchClause = `MATCH (a)-[r:CodeRelation]->(b)\n WHERE ${where.join(' AND ')}`;
|
|
2528
|
+
// Page query + a separate COUNT (#2084 review P2-4): the page is
|
|
2529
|
+
// LIMIT-capped, so its row count cannot stand in for the true total —
|
|
2530
|
+
// run a COUNT with the same WHERE (no LIMIT) like the intra layer does.
|
|
2531
|
+
const [ipRows, ipCountRows] = await Promise.all([
|
|
2532
|
+
executeParameterized(repo.lbugPath, `${matchClause}
|
|
2533
|
+
RETURN a.filePath AS file, a.name AS sourceFn, a.startLine AS sourceLine,
|
|
2534
|
+
b.name AS sinkFn, b.startLine AS sinkLine, r.reason AS reason
|
|
2535
|
+
ORDER BY sourceFn, sinkFn, reason
|
|
2536
|
+
LIMIT ${limit}`, p),
|
|
2537
|
+
executeParameterized(repo.lbugPath, `${matchClause}\n RETURN COUNT(*) AS total`, p),
|
|
2538
|
+
]);
|
|
2539
|
+
const total = Number(ipCountRows[0]?.total ?? ipCountRows[0]?.[0] ?? 0);
|
|
2540
|
+
const findings = ipRows.map((r) => {
|
|
2541
|
+
const decoded = decodeTaintPath(r.reason ?? r[5]);
|
|
2542
|
+
const hops = decoded.ok
|
|
2543
|
+
? decoded.hops.map((h) => ({ function: h.variable, line: h.line }))
|
|
2544
|
+
: [];
|
|
2545
|
+
return {
|
|
2546
|
+
interprocedural: true,
|
|
2547
|
+
file: String(r.file ?? r[0] ?? ''),
|
|
2548
|
+
sinkKind: decoded.ok ? (decoded.kind ?? 'unknown') : 'unknown',
|
|
2549
|
+
source: { function: String(r.sourceFn ?? r[1] ?? ''), line: r.sourceLine ?? r[2] },
|
|
2550
|
+
sink: { function: String(r.sinkFn ?? r[3] ?? ''), line: r.sinkLine ?? r[4] },
|
|
2551
|
+
hops,
|
|
2552
|
+
...(decoded.ok && decoded.truncated ? { pathIncomplete: true } : {}),
|
|
2553
|
+
};
|
|
2554
|
+
});
|
|
2555
|
+
return { findings, total };
|
|
2556
|
+
};
|
|
2557
|
+
const { findings: interprocFindings, total: interprocTotal } = await runInterprocQuery();
|
|
2558
|
+
if (totalFindings === 0 &&
|
|
2559
|
+
interprocFindings.length === 0 &&
|
|
2560
|
+
pdgStamped === undefined &&
|
|
2561
|
+
!target) {
|
|
2562
|
+
// Meta was unreadable and the repo-wide enumerate (both layers) found
|
|
2563
|
+
// nothing — the counts above WERE the existence probe; surface the hint.
|
|
2564
|
+
return { findings: [], totalFindings: 0, note: NO_TAINT_NOTE };
|
|
2565
|
+
}
|
|
2566
|
+
if (totalFindings === 0 &&
|
|
2567
|
+
interprocFindings.length === 0 &&
|
|
2568
|
+
pdgStamped === undefined &&
|
|
2569
|
+
target) {
|
|
2570
|
+
// Anchored miss with unreadable meta: one extra bounded probe decides
|
|
2571
|
+
// "no findings for this anchor" vs "no taint layer at all". Probe BOTH
|
|
2572
|
+
// intra (TAINTED) and inter (TAINT_PATH) existence.
|
|
2573
|
+
const probe = await executeParameterized(repo.lbugPath, `MATCH (a:BasicBlock)-[r:CodeRelation]->(b:BasicBlock) WHERE r.type = 'TAINTED' RETURN r.reason AS reason LIMIT 1`, {});
|
|
2574
|
+
const ipProbe = probe.length === 0
|
|
2575
|
+
? await executeParameterized(repo.lbugPath, `MATCH (a)-[r:CodeRelation]->(b) WHERE r.type = 'TAINT_PATH' RETURN r.reason AS reason LIMIT 1`, {})
|
|
2576
|
+
: [];
|
|
2577
|
+
if (probe.length === 0 && ipProbe.length === 0) {
|
|
2578
|
+
return { findings: [], totalFindings: 0, note: NO_TAINT_NOTE };
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
const findings = rows.map((r) => {
|
|
2582
|
+
const sourceBlockId = String(r.sourceBlockId ?? r[0] ?? '');
|
|
2583
|
+
const file = String(r.file ?? r[1] ?? '');
|
|
2584
|
+
const sourceStart = (r.sourceStart ?? r[2]);
|
|
2585
|
+
const sinkStart = (r.sinkStart ?? r[3]);
|
|
2586
|
+
const reason = r.reason ?? r[4];
|
|
2587
|
+
// basicBlockId = `BasicBlock:<filePath>:<fnLine>:<fnCol>:<blockIdx>` —
|
|
2588
|
+
// split from the RIGHT (the filePath may itself contain ':').
|
|
2589
|
+
const idParts = sourceBlockId.split(':');
|
|
2590
|
+
const fnLine = Number(idParts[idParts.length - 3]);
|
|
2591
|
+
const decoded = decodeTaintPath(reason);
|
|
2592
|
+
if (!decoded.ok) {
|
|
2593
|
+
// Unreadable reason (foreign/corrupt row): surface the finding's
|
|
2594
|
+
// existence with its block anchors, never throw.
|
|
2595
|
+
return {
|
|
2596
|
+
file,
|
|
2597
|
+
...(Number.isInteger(fnLine) ? { functionLine: fnLine } : {}),
|
|
2598
|
+
sinkKind: 'unknown',
|
|
2599
|
+
source: { line: sourceStart },
|
|
2600
|
+
sink: { line: sinkStart },
|
|
2601
|
+
hops: [],
|
|
2602
|
+
pathIncomplete: true,
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
const hops = decoded.hops.map((h) => ({
|
|
2606
|
+
variable: h.variable,
|
|
2607
|
+
line: h.line,
|
|
2608
|
+
...(h.viaCall ? { viaCall: true } : {}),
|
|
2609
|
+
}));
|
|
2610
|
+
const first = hops[0];
|
|
2611
|
+
const last = hops[hops.length - 1];
|
|
2612
|
+
return {
|
|
2613
|
+
file,
|
|
2614
|
+
...(Number.isInteger(fnLine) ? { functionLine: fnLine } : {}),
|
|
2615
|
+
sinkKind: decoded.kind ?? 'unknown',
|
|
2616
|
+
source: first ? { variable: first.variable, line: first.line } : { line: sourceStart },
|
|
2617
|
+
sink: { line: last?.line ?? sinkStart },
|
|
2618
|
+
hops,
|
|
2619
|
+
...(decoded.truncated ? { pathIncomplete: true } : {}),
|
|
2620
|
+
};
|
|
2621
|
+
});
|
|
2622
|
+
// Combine both layers and re-apply the page LIMIT to the union — each
|
|
2623
|
+
// layer was queried with its own LIMIT, so the union can hold up to 2×;
|
|
2624
|
+
// cap it so `findings.length` honours the caller's `limit`. `truncated`
|
|
2625
|
+
// reflects EITHER layer overflowing OR the union being trimmed here, and
|
|
2626
|
+
// `totalFindings` counts both layers' matched rows (the intra COUNT plus
|
|
2627
|
+
// the interproc rows returned — interproc has no separate COUNT, so a
|
|
2628
|
+
// capped interproc layer is reflected via `truncated`, never undercounted
|
|
2629
|
+
// into a false "complete" signal). Review: code-review #2/#4 (explain
|
|
2630
|
+
// accounting + sink-file anchoring) — both layers now accounted.
|
|
2631
|
+
const combined = [...findings, ...interprocFindings];
|
|
2632
|
+
const pageFindings = combined.length > limit ? combined.slice(0, limit) : combined;
|
|
2633
|
+
// Truncated iff EITHER layer overflowed its own LIMIT (strict `>` — exactly
|
|
2634
|
+
// `limit` rows is not truncated), OR the combined union was trimmed to the
|
|
2635
|
+
// page (#2084 review P2-4). `totalFindings` uses the interproc COUNT, not
|
|
2636
|
+
// the capped slice length, so it never undercounts.
|
|
2637
|
+
const truncated = totalFindings > findings.length ||
|
|
2638
|
+
interprocTotal > interprocFindings.length ||
|
|
2639
|
+
combined.length > pageFindings.length;
|
|
2640
|
+
return {
|
|
2641
|
+
...(anchor ? { anchor } : {}),
|
|
2642
|
+
findings: pageFindings,
|
|
2643
|
+
totalFindings: totalFindings + interprocTotal,
|
|
2644
|
+
...(truncated ? { truncated: true } : {}),
|
|
2645
|
+
note: 'Intra-procedural (TAINTED, statement hops) AND cross-function (TAINT_PATH, function hops, `interprocedural: true`) flows are modeled. Closure/callback, property/field, and implicit flows are NOT modeled; absence of a finding is not proof of safety. Cross-function findings are context-insensitive and may over-attribute among same-named callees. SANITIZES (kill) edges are queryable via cypher.',
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
async pdgQuery(repo, params) {
|
|
2649
|
+
try {
|
|
2650
|
+
return await this._pdgQueryImpl(repo, params);
|
|
2651
|
+
}
|
|
2652
|
+
catch (err) {
|
|
2653
|
+
const msg = (err instanceof Error ? err.message : String(err)) || 'pdg_query failed';
|
|
2654
|
+
if (isWalCorruptionError(err)) {
|
|
2655
|
+
return { error: msg, recoverySuggestion: WAL_RECOVERY_SUGGESTION };
|
|
2656
|
+
}
|
|
2657
|
+
throw err;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Query the persisted PDG (#2086 M6) — the control/data-dependence analog of
|
|
2662
|
+
* `explain`. `controls` reads CDG ("under what condition does X run?", branch
|
|
2663
|
+
* sense 'T'|'F' in `reason`); `flows` reads REACHING_DEF (def→use, variable
|
|
2664
|
+
* name in `reason`). Intra-procedural, basic-block granular.
|
|
2665
|
+
*
|
|
2666
|
+
* Bounded by construction: the BasicBlock→BasicBlock partition holds only the
|
|
2667
|
+
* sparse, per-function-capped pdg layers, the query is anchored to one file/
|
|
2668
|
+
* symbol, and the page is LIMIT-bounded (validated integer, interpolated
|
|
2669
|
+
* because LadybugDB does not parameterize LIMIT). LadybugDB has no rel-
|
|
2670
|
+
* property index, so the anchor IS the bound — there is no anchorless mode.
|
|
2671
|
+
*
|
|
2672
|
+
* Symbol↔block join: there is no Function→BasicBlock edge; the SOURCE block
|
|
2673
|
+
* (`a` — controller for CDG, def for REACHING_DEF) is filtered by the
|
|
2674
|
+
* BasicBlock id-prefix (`basicBlockId` template) plus its `startLine` within
|
|
2675
|
+
* the symbol's span. BasicBlock `startLine` is 1-based while symbol-node
|
|
2676
|
+
* `startLine`/`endLine` are 0-based, so BOTH bounds are shifted +1
|
|
2677
|
+
* (`[symStart+1, symEnd+1]`) onto the block basis: the upper +1 keeps a
|
|
2678
|
+
* guard/def/use on the function's final line, and the lower +1 excludes an
|
|
2679
|
+
* adjacent function's block on the line directly above (#2188 review). Both
|
|
2680
|
+
* endpoints share the function (intra-procedural), so filtering the source
|
|
2681
|
+
* endpoint suffices.
|
|
2682
|
+
*/
|
|
2683
|
+
async _pdgQueryImpl(repo, params = {}) {
|
|
2684
|
+
await this.ensureInitialized(repo);
|
|
2685
|
+
// Mode validation — the JSON-schema enum is advisory for MCP clients, so
|
|
2686
|
+
// the backend enforces it (an unhandled mode would otherwise fall through).
|
|
2687
|
+
const mode = params.mode;
|
|
2688
|
+
if (mode !== 'controls' && mode !== 'flows') {
|
|
2689
|
+
return {
|
|
2690
|
+
error: `Invalid "mode": expected "controls" or "flows", got ${JSON.stringify(params.mode)}.`,
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
const rawLimit = params.limit ?? PDG_QUERY_DEFAULT_LIMIT;
|
|
2694
|
+
if (!Number.isInteger(rawLimit) || rawLimit < 1 || rawLimit > PDG_QUERY_MAX_LIMIT) {
|
|
2695
|
+
return {
|
|
2696
|
+
error: `Invalid "limit": expected an integer in [1, ${PDG_QUERY_MAX_LIMIT}], got ${JSON.stringify(params.limit)}.`,
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
const limit = rawLimit;
|
|
2700
|
+
// PDG queries are always anchored (no rel-property index ⇒ an unanchored
|
|
2701
|
+
// basic-block path scan is unbounded). `target` is required.
|
|
2702
|
+
const target = typeof params.target === 'string' ? params.target.trim() : '';
|
|
2703
|
+
if (!target) {
|
|
2704
|
+
return {
|
|
2705
|
+
error: 'pdg_query requires a "target" (a file path or symbol/function name) — PDG queries are always anchored.',
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
const edgeType = mode === 'controls' ? 'CDG' : 'REACHING_DEF';
|
|
2709
|
+
// Definitive: the meta stamp says this layer was never recorded.
|
|
2710
|
+
const NO_PDG_NOTE = `no PDG layer — run gitnexus analyze --pdg to record ${edgeType} edges for this repo`;
|
|
2711
|
+
// Inconclusive: meta is unreadable AND a global probe found zero rows of this
|
|
2712
|
+
// edge type — but a genuinely edge-free layer (all-linear functions) looks
|
|
2713
|
+
// identical to a missing one, so don't assert absence (#2188 review).
|
|
2714
|
+
const PDG_LAYER_UNKNOWN_NOTE = `no ${edgeType} edges found for this target; PDG layer status unknown — was this repo indexed with gitnexus analyze --pdg?`;
|
|
2715
|
+
// Cheap meta probe: the layer exists iff the pdg stamp carries the
|
|
2716
|
+
// mode-relevant cap (maxCdgEdgesPerFunction for CDG, maxReachingDef…
|
|
2717
|
+
// for REACHING_DEF). Absent ⇒ the no-layer hint without a DB scan.
|
|
2718
|
+
// `pdgStampForMode` is the shared meta read (the both-caps `pdgLayerStatus`
|
|
2719
|
+
// helper consumes the same underlying read for impact); here we project it
|
|
2720
|
+
// down to this one mode's cap, preserving the tri-state `boolean | undefined`
|
|
2721
|
+
// contract byte-for-byte: `false` ⇒ definitive no-layer (short-circuit
|
|
2722
|
+
// below), `true` ⇒ proceed, `undefined` ⇒ meta unreadable, defer to the
|
|
2723
|
+
// post-anchored-query probe (Feasibility Issue 4).
|
|
2724
|
+
const pdgStamped = await pdgStampForMode(repo.lbugPath, mode);
|
|
2725
|
+
if (pdgStamped === false) {
|
|
2726
|
+
return { mode, results: [], total: 0, note: NO_PDG_NOTE };
|
|
2727
|
+
}
|
|
2728
|
+
// Resolve the anchor on the SOURCE block via the shared resolver also used
|
|
2729
|
+
// by explain (#2188): file id-prefix OR symbol span on the corrected
|
|
2730
|
+
// [symStart+1, symEnd+1] window. `target` is required, so the early cases
|
|
2731
|
+
// (not-found/ambiguous) return here and `anchor`/`anchorClause` are always
|
|
2732
|
+
// set below (anchor stays non-optional — no `| undefined` — #2188 CodeQL).
|
|
2733
|
+
const resolved = await this.resolveBlockAnchor(repo, target, 'pdg_query');
|
|
2734
|
+
if (resolved.early)
|
|
2735
|
+
return resolved.early;
|
|
2736
|
+
const { anchorClause, anchor } = resolved;
|
|
2737
|
+
const queryParams = resolved.queryParams;
|
|
2738
|
+
// Optional variable filter (flows mode) — REACHING_DEF stores the variable
|
|
2739
|
+
// name in `reason`. FU-B-2 prefixes the name with a `<name>|1:<def>:<use>`
|
|
2740
|
+
// annotation (name FIRST), so match BOTH a legacy bare-name reason (`=`) AND
|
|
2741
|
+
// an annotated one (`STARTS WITH <name>|`). Source identifiers never contain
|
|
2742
|
+
// `|`, so the `<name>|` prefix is exact (it cannot collide with a longer name
|
|
2743
|
+
// — `ab|…` is not a prefix of `abc|…`).
|
|
2744
|
+
let reasonClause = '';
|
|
2745
|
+
if (mode === 'flows' && typeof params.variable === 'string' && params.variable.trim()) {
|
|
2746
|
+
reasonClause = ' AND (r.reason = $variable OR r.reason STARTS WITH $variablePrefix)';
|
|
2747
|
+
const variable = params.variable.trim();
|
|
2748
|
+
queryParams.variable = variable;
|
|
2749
|
+
queryParams.variablePrefix = `${variable}|`;
|
|
2750
|
+
}
|
|
2751
|
+
// edgeType is a hardcoded per-mode literal (never user input); `target` /
|
|
2752
|
+
// `variable` flow only through bind params (no Cypher interpolation).
|
|
2753
|
+
const matchClause = `
|
|
2754
|
+
MATCH (a:BasicBlock)-[r:CodeRelation]->(b:BasicBlock)
|
|
2755
|
+
WHERE r.type = '${edgeType}' AND ${anchorClause}${reasonClause}`;
|
|
2756
|
+
const [rows, countRows] = await Promise.all([
|
|
2757
|
+
executeParameterized(repo.lbugPath, `${matchClause}
|
|
2758
|
+
RETURN a.id AS srcId, a.startLine AS srcLine, b.startLine AS dstLine, b.text AS dstText, r.reason AS reason
|
|
2759
|
+
ORDER BY srcId, dstLine, reason
|
|
2760
|
+
LIMIT ${limit}`, queryParams),
|
|
2761
|
+
executeParameterized(repo.lbugPath, `${matchClause}\n RETURN COUNT(*) AS total`, queryParams),
|
|
2762
|
+
]);
|
|
2763
|
+
const total = Number(countRows[0]?.total ?? countRows[0]?.[0] ?? 0);
|
|
2764
|
+
// Unreadable meta + anchored miss: one bounded probe distinguishes "no rows
|
|
2765
|
+
// for this anchor" from "no rows of this edge type at all". With meta
|
|
2766
|
+
// unreadable we cannot tell a missing layer from an edge-free one, so the
|
|
2767
|
+
// note is the inconclusive "status unknown" form, not the definitive
|
|
2768
|
+
// NO_PDG_NOTE (which is reserved for the meta-stamped absence above).
|
|
2769
|
+
if (total === 0 && pdgStamped === undefined) {
|
|
2770
|
+
const probe = await executeParameterized(repo.lbugPath, `MATCH (:BasicBlock)-[r:CodeRelation]->(:BasicBlock) WHERE r.type = '${edgeType}' RETURN r.reason AS reason LIMIT 1`, {});
|
|
2771
|
+
if (probe.length === 0)
|
|
2772
|
+
return { mode, results: [], total: 0, note: PDG_LAYER_UNKNOWN_NOTE };
|
|
2773
|
+
}
|
|
2774
|
+
// basicBlockId = `BasicBlock:<filePath>:<fnLine>:<fnCol>:<blockIdx>` — split
|
|
2775
|
+
// from the RIGHT (filePath may contain ':'). Shared module-scope `fnLineOf`.
|
|
2776
|
+
const results = mode === 'controls'
|
|
2777
|
+
? rows.map((r) => {
|
|
2778
|
+
const fnLine = fnLineOf(String(r.srcId ?? r[0] ?? ''));
|
|
2779
|
+
const dstText = String(r.dstText ?? r[3] ?? '');
|
|
2780
|
+
// A CDG edge into an early-exit block is a guard clause (subsumes
|
|
2781
|
+
// #559): the controller predicate gates the dependent via `label`.
|
|
2782
|
+
const isGuardExit = /^\s*(return|throw|continue|break)\b/.test(dstText);
|
|
2783
|
+
return {
|
|
2784
|
+
...(Number.isInteger(fnLine) ? { functionLine: fnLine } : {}),
|
|
2785
|
+
controller: { line: (r.srcLine ?? r[1]) },
|
|
2786
|
+
dependent: { line: (r.dstLine ?? r[2]), text: dstText },
|
|
2787
|
+
label: String(r.reason ?? r[4] ?? ''),
|
|
2788
|
+
...(isGuardExit ? { guard: true } : {}),
|
|
2789
|
+
};
|
|
2790
|
+
})
|
|
2791
|
+
: rows.map((r) => {
|
|
2792
|
+
const fnLine = fnLineOf(String(r.srcId ?? r[0] ?? ''));
|
|
2793
|
+
// FU-B-2: REACHING_DEF `reason` is `<name>` (legacy) or
|
|
2794
|
+
// `<name>|1:<defLine>:<useLine>` — decode to surface the bare
|
|
2795
|
+
// variable name, not the encoded annotation.
|
|
2796
|
+
const variable = decodeReachingDefReason(r.reason ?? r[4] ?? '').name;
|
|
2797
|
+
return {
|
|
2798
|
+
...(Number.isInteger(fnLine) ? { functionLine: fnLine } : {}),
|
|
2799
|
+
variable,
|
|
2800
|
+
def: { line: (r.srcLine ?? r[1]) },
|
|
2801
|
+
use: {
|
|
2802
|
+
line: (r.dstLine ?? r[2]),
|
|
2803
|
+
text: String(r.dstText ?? r[3] ?? ''),
|
|
2804
|
+
},
|
|
2805
|
+
};
|
|
2806
|
+
});
|
|
2807
|
+
return {
|
|
2808
|
+
mode,
|
|
2809
|
+
anchor,
|
|
2810
|
+
results,
|
|
2811
|
+
total,
|
|
2812
|
+
...(total > results.length ? { truncated: true } : {}),
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2210
2815
|
/**
|
|
2211
2816
|
* Legacy explore — kept for backwards compatibility with resources.ts.
|
|
2212
2817
|
* Routes cluster/process types to direct graph queries.
|
|
@@ -2402,8 +3007,20 @@ export class LocalBackend {
|
|
|
2402
3007
|
queryParams[`hunkStart${i}`] = hunk.startLine;
|
|
2403
3008
|
queryParams[`hunkEnd${i}`] = hunk.endLine;
|
|
2404
3009
|
});
|
|
3010
|
+
// Exclude BasicBlock rows by id prefix: on a --pdg index every edited
|
|
3011
|
+
// function otherwise contributes N nameless BasicBlock pseudo-"symbols"
|
|
3012
|
+
// (they carry filePath/start/end but no name), inflating changed_count
|
|
3013
|
+
// and risk level with rows no consumer can act on (#2082 U7). Blocks
|
|
3014
|
+
// are implementation substrate, not symbols — the owning Function row
|
|
3015
|
+
// already represents the change. The id prefix (`BasicBlock:<file>:…`,
|
|
3016
|
+
// cfg/emit.ts basicBlockId) beats a label predicate (`labels(n)[0]` is
|
|
3017
|
+
// known to come back empty for several node types — see
|
|
3018
|
+
// enrichCandidateLabels) AND beats `n.name IS NOT NULL` (which would
|
|
3019
|
+
// also drop legitimate symbols whose name loaded as NULL, e.g.
|
|
3020
|
+
// quoted-empty CSV fields for anonymous constructs).
|
|
2405
3021
|
const symbolQuery = `
|
|
2406
3022
|
MATCH (n) WHERE n.filePath ENDS WITH $filePath
|
|
3023
|
+
AND NOT n.id STARTS WITH 'BasicBlock:'
|
|
2407
3024
|
AND n.startLine IS NOT NULL AND n.endLine IS NOT NULL
|
|
2408
3025
|
AND (${overlapConditions})
|
|
2409
3026
|
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type,
|
|
@@ -2648,26 +3265,317 @@ export class LocalBackend {
|
|
|
2648
3265
|
applied: !dry_run,
|
|
2649
3266
|
};
|
|
2650
3267
|
}
|
|
3268
|
+
async trace(repo, params) {
|
|
3269
|
+
try {
|
|
3270
|
+
return await this._traceImpl(repo, params);
|
|
3271
|
+
}
|
|
3272
|
+
catch (err) {
|
|
3273
|
+
return {
|
|
3274
|
+
status: 'error',
|
|
3275
|
+
error: (err instanceof Error ? err.message : String(err)) || 'Trace analysis failed',
|
|
3276
|
+
from: { name: params.from },
|
|
3277
|
+
to: { name: params.to },
|
|
3278
|
+
suggestion: 'The graph query failed — try gitnexus context <symbol> to see connections, ' +
|
|
3279
|
+
'or check if an interface bridges them.',
|
|
3280
|
+
...(isWalCorruptionError(err) ? { recoverySuggestion: WAL_RECOVERY_SUGGESTION } : {}),
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
async _traceImpl(repo, params) {
|
|
3285
|
+
await this.ensureInitialized(repo);
|
|
3286
|
+
// resolveSymbolCandidates feeds `from`/`to` into string operations
|
|
3287
|
+
// (e.g. name.includes), so a non-string param would surface a low-level
|
|
3288
|
+
// "x.includes is not a function". Reject it with a clear message instead.
|
|
3289
|
+
const isStringOrAbsent = (v) => v === undefined || typeof v === 'string';
|
|
3290
|
+
if (!isStringOrAbsent(params.from) ||
|
|
3291
|
+
!isStringOrAbsent(params.to) ||
|
|
3292
|
+
!isStringOrAbsent(params.from_uid) ||
|
|
3293
|
+
!isStringOrAbsent(params.to_uid)) {
|
|
3294
|
+
return {
|
|
3295
|
+
status: 'error',
|
|
3296
|
+
error: "'from', 'to', and their *_uid variants must be strings.",
|
|
3297
|
+
suggestion: 'Pass symbol names or UIDs as strings, e.g. trace from="A" to="B".',
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
const fromOutcome = await this.resolveSymbolCandidates(repo, { uid: params.from_uid, name: params.from }, { file_path: params.from_file });
|
|
3301
|
+
if (fromOutcome.kind === 'not_found') {
|
|
3302
|
+
return {
|
|
3303
|
+
status: 'not_found',
|
|
3304
|
+
error: `Source symbol '${params.from_uid ?? params.from}' not found.`,
|
|
3305
|
+
suggestion: 'Check the symbol name or use --from-uid for zero-ambiguity.',
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
if (fromOutcome.kind === 'ambiguous') {
|
|
3309
|
+
return {
|
|
3310
|
+
status: 'ambiguous',
|
|
3311
|
+
role: 'from',
|
|
3312
|
+
message: `Found ${fromOutcome.candidates.length} symbols matching '${params.from}'. Disambiguate with --from-uid.`,
|
|
3313
|
+
candidates: fromOutcome.candidates,
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
const toOutcome = await this.resolveSymbolCandidates(repo, { uid: params.to_uid, name: params.to }, { file_path: params.to_file });
|
|
3317
|
+
if (toOutcome.kind === 'not_found') {
|
|
3318
|
+
return {
|
|
3319
|
+
status: 'not_found',
|
|
3320
|
+
error: `Target symbol '${params.to_uid ?? params.to}' not found.`,
|
|
3321
|
+
suggestion: 'Check the symbol name or use --to-uid for zero-ambiguity.',
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
3324
|
+
if (toOutcome.kind === 'ambiguous') {
|
|
3325
|
+
return {
|
|
3326
|
+
status: 'ambiguous',
|
|
3327
|
+
role: 'to',
|
|
3328
|
+
message: `Found ${toOutcome.candidates.length} symbols matching '${params.to}'. Disambiguate with --to-uid.`,
|
|
3329
|
+
candidates: toOutcome.candidates,
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
const fromSym = fromOutcome.symbol;
|
|
3333
|
+
const toSym = toOutcome.symbol;
|
|
3334
|
+
if (fromSym.id === toSym.id) {
|
|
3335
|
+
return {
|
|
3336
|
+
status: 'ok',
|
|
3337
|
+
from: { name: fromSym.name, filePath: fromSym.filePath, startLine: fromSym.startLine },
|
|
3338
|
+
to: { name: toSym.name, filePath: toSym.filePath, startLine: toSym.startLine },
|
|
3339
|
+
hopCount: 0,
|
|
3340
|
+
hops: [{ name: fromSym.name, filePath: fromSym.filePath, startLine: fromSym.startLine }],
|
|
3341
|
+
edges: [],
|
|
3342
|
+
};
|
|
3343
|
+
}
|
|
3344
|
+
// Sanitize maxDepth at the real boundary: the MCP inputSchema's
|
|
3345
|
+
// minimum/maximum is advisory only (callTool is reachable directly), so a
|
|
3346
|
+
// caller can pass 0, a negative, NaN, or a non-integer. `??` does NOT
|
|
3347
|
+
// recover 0/NaN, and Math.min has no lower bound — left unguarded, any of
|
|
3348
|
+
// those makes the BFS loop run zero iterations and return a false no_path.
|
|
3349
|
+
const DEFAULT_TRACE_DEPTH = 10;
|
|
3350
|
+
const MAX_TRACE_DEPTH = 30;
|
|
3351
|
+
const requestedDepth = Number.isInteger(params.maxDepth) && params.maxDepth > 0
|
|
3352
|
+
? params.maxDepth
|
|
3353
|
+
: DEFAULT_TRACE_DEPTH;
|
|
3354
|
+
const maxDepth = Math.min(requestedDepth, MAX_TRACE_DEPTH);
|
|
3355
|
+
const includeTests = params.includeTests ?? false;
|
|
3356
|
+
// Traversal vocabulary: CALLS for actual calls, HAS_METHOD so a class-rooted
|
|
3357
|
+
// trace can descend into its methods. Not "calls only" — per-hop edge type is
|
|
3358
|
+
// surfaced in edges[] so containment hops stay distinguishable.
|
|
3359
|
+
const TRAVERSAL_EDGE_TYPES = ['CALLS', 'HAS_METHOD'];
|
|
3360
|
+
// Bound the traversal so a high-fanout hub (a logger/util reached by many
|
|
3361
|
+
// symbols) can't materialize an unbounded frontier. Per-level rows are
|
|
3362
|
+
// capped and the total visited set is capped; either cap sets `truncated`
|
|
3363
|
+
// so a resulting no_path is never reported as if the graph was exhausted.
|
|
3364
|
+
const PER_NODE_FANOUT_CAP = 200;
|
|
3365
|
+
const ABS_ROW_CAP = 5000;
|
|
3366
|
+
const MAX_VISITED = 50000;
|
|
3367
|
+
let truncated = false;
|
|
3368
|
+
const visited = new Set([fromSym.id]);
|
|
3369
|
+
let frontier = [fromSym.id];
|
|
3370
|
+
const parent = new Map();
|
|
3371
|
+
let found = false;
|
|
3372
|
+
// The last node discovered at the deepest reached level — surfaced as
|
|
3373
|
+
// `furthest` in the no_path response to hint where the chain breaks.
|
|
3374
|
+
let lastReached = null;
|
|
3375
|
+
let reachedDepth = 0;
|
|
3376
|
+
for (let depth = 1; depth <= maxDepth && frontier.length > 0 && !found; depth++) {
|
|
3377
|
+
const nextFrontier = [];
|
|
3378
|
+
// LadybugDB/Kuzu does not support a parameterized LIMIT, so the cap is
|
|
3379
|
+
// interpolated (it is a derived integer, not user input).
|
|
3380
|
+
const rowCap = Math.min(frontier.length * PER_NODE_FANOUT_CAP, ABS_ROW_CAP);
|
|
3381
|
+
const rows = await executeParameterized(repo.lbugPath, `MATCH (n)-[r:CodeRelation]->(m)
|
|
3382
|
+
WHERE n.id IN $frontierIds AND r.type IN $edgeTypes
|
|
3383
|
+
RETURN n.id AS sourceId, m.id AS id, m.name AS name, labels(m)[0] AS type,
|
|
3384
|
+
m.filePath AS filePath, m.startLine AS startLine,
|
|
3385
|
+
r.type AS edgeType, r.confidence AS confidence
|
|
3386
|
+
LIMIT ${rowCap}`, { frontierIds: frontier, edgeTypes: TRAVERSAL_EDGE_TYPES });
|
|
3387
|
+
// A clipped level may have dropped a node that lies on the only shortest
|
|
3388
|
+
// path, so any subsequent no_path is not authoritative.
|
|
3389
|
+
if (rows.length >= rowCap)
|
|
3390
|
+
truncated = true;
|
|
3391
|
+
for (const row of rows) {
|
|
3392
|
+
// Decode once. The `?? row[N]` fallback handles LadybugDB tuple-mode
|
|
3393
|
+
// returns; the positional indices mirror the RETURN column order above.
|
|
3394
|
+
const nodeId = (row.id ?? row[1]);
|
|
3395
|
+
const sourceId = (row.sourceId ?? row[0]);
|
|
3396
|
+
const name = (row.name ?? row[2]);
|
|
3397
|
+
const filePath = (row.filePath ?? row[4]);
|
|
3398
|
+
const startLine = (row.startLine ?? row[5]);
|
|
3399
|
+
const edgeType = (row.edgeType ?? row[6]);
|
|
3400
|
+
const storedConfidence = row.confidence ?? row[7];
|
|
3401
|
+
const confidence = typeof storedConfidence === 'number' && storedConfidence > 0
|
|
3402
|
+
? storedConfidence
|
|
3403
|
+
: confidenceForRelType(edgeType);
|
|
3404
|
+
// Match the explicitly-requested target before the test-file filter.
|
|
3405
|
+
// resolveSymbolCandidates does not exclude test-file symbols, so a
|
|
3406
|
+
// target (or a required hop) that lives in a test file would otherwise
|
|
3407
|
+
// be dropped by the includeTests guard below and produce a false
|
|
3408
|
+
// no_path even when a direct edge exists.
|
|
3409
|
+
if (nodeId === toSym.id) {
|
|
3410
|
+
parent.set(nodeId, { from: sourceId, name, filePath, startLine, edgeType, confidence });
|
|
3411
|
+
found = true;
|
|
3412
|
+
break;
|
|
3413
|
+
}
|
|
3414
|
+
// Skip non-target nodes that live in test files unless includeTests.
|
|
3415
|
+
if (!includeTests && isTestFilePath(filePath))
|
|
3416
|
+
continue;
|
|
3417
|
+
if (!visited.has(nodeId)) {
|
|
3418
|
+
visited.add(nodeId);
|
|
3419
|
+
parent.set(nodeId, { from: sourceId, name, filePath, startLine, edgeType, confidence });
|
|
3420
|
+
nextFrontier.push(nodeId);
|
|
3421
|
+
lastReached = { name, filePath, startLine };
|
|
3422
|
+
reachedDepth = depth;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
frontier = nextFrontier;
|
|
3426
|
+
if (visited.size >= MAX_VISITED) {
|
|
3427
|
+
truncated = true;
|
|
3428
|
+
break;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
if (found) {
|
|
3432
|
+
const path = [];
|
|
3433
|
+
const edges = [];
|
|
3434
|
+
let current = toSym.id;
|
|
3435
|
+
while (current !== fromSym.id) {
|
|
3436
|
+
const info = parent.get(current);
|
|
3437
|
+
path.unshift({ name: info.name, filePath: info.filePath, startLine: info.startLine });
|
|
3438
|
+
edges.unshift({ relType: info.edgeType, confidence: info.confidence });
|
|
3439
|
+
current = info.from;
|
|
3440
|
+
}
|
|
3441
|
+
path.unshift({
|
|
3442
|
+
name: fromSym.name,
|
|
3443
|
+
filePath: fromSym.filePath,
|
|
3444
|
+
startLine: fromSym.startLine,
|
|
3445
|
+
});
|
|
3446
|
+
return {
|
|
3447
|
+
status: 'ok',
|
|
3448
|
+
from: { name: fromSym.name, filePath: fromSym.filePath, startLine: fromSym.startLine },
|
|
3449
|
+
to: { name: toSym.name, filePath: toSym.filePath, startLine: toSym.startLine },
|
|
3450
|
+
hopCount: edges.length,
|
|
3451
|
+
hops: path,
|
|
3452
|
+
edges,
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
return {
|
|
3456
|
+
status: 'no_path',
|
|
3457
|
+
from: { name: fromSym.name, filePath: fromSym.filePath, startLine: fromSym.startLine },
|
|
3458
|
+
to: { name: toSym.name, filePath: toSym.filePath, startLine: toSym.startLine },
|
|
3459
|
+
furthest: lastReached ? { ...lastReached, depth: reachedDepth } : null,
|
|
3460
|
+
...(truncated ? { truncated: true } : {}),
|
|
3461
|
+
suggestion: truncated
|
|
3462
|
+
? 'Search was truncated at a traversal cap before exhausting the graph — a path ' +
|
|
3463
|
+
'may still exist. Narrow the search (a lower --depth, or trace from a more ' +
|
|
3464
|
+
'specific symbol), or use gitnexus context <symbol> to inspect connections.'
|
|
3465
|
+
: 'No directed path found. The call chain likely breaks at dynamic dispatch, ' +
|
|
3466
|
+
'reflection, or an external API boundary. Try gitnexus context <symbol> to see ' +
|
|
3467
|
+
"both symbols' connections, or check if an interface/abstraction bridges them.",
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
2651
3470
|
async impact(repo, params) {
|
|
2652
3471
|
try {
|
|
2653
3472
|
return await this._impactImpl(repo, params);
|
|
2654
3473
|
}
|
|
2655
3474
|
catch (err) {
|
|
2656
3475
|
// Return structured error instead of crashing (#321)
|
|
3476
|
+
const message = (err instanceof Error ? err.message : String(err)) || 'Impact analysis failed';
|
|
3477
|
+
const suggestion = 'The graph query failed — try gitnexus context <symbol> as a fallback';
|
|
3478
|
+
const recoverySuggestion = isWalCorruptionError(err) ? WAL_RECOVERY_SUGGESTION : undefined;
|
|
3479
|
+
if (params.mode === 'pdg') {
|
|
3480
|
+
// Symbol resolution never reached the catch with a resolved symbol (the
|
|
3481
|
+
// throw can originate before/within resolution), so the envelope carries
|
|
3482
|
+
// the partial-but-typed target — typed as PdgImpactTarget so the partial
|
|
3483
|
+
// is type-checked, not an inline literal in a Promise<any> hole.
|
|
3484
|
+
const target = { name: params.target };
|
|
3485
|
+
const pdgErr = makePdgImpactErrorResult({
|
|
3486
|
+
mode: 'pdg',
|
|
3487
|
+
error: message,
|
|
3488
|
+
target,
|
|
3489
|
+
direction: params.direction,
|
|
3490
|
+
suggestion,
|
|
3491
|
+
recoverySuggestion,
|
|
3492
|
+
});
|
|
3493
|
+
return pdgErr;
|
|
3494
|
+
}
|
|
2657
3495
|
return {
|
|
2658
|
-
error:
|
|
3496
|
+
error: message,
|
|
2659
3497
|
target: { name: params.target },
|
|
2660
3498
|
direction: params.direction,
|
|
2661
3499
|
impactedCount: 0,
|
|
2662
3500
|
risk: 'UNKNOWN',
|
|
2663
|
-
suggestion
|
|
2664
|
-
...(
|
|
3501
|
+
suggestion,
|
|
3502
|
+
...(recoverySuggestion ? { recoverySuggestion } : {}),
|
|
2665
3503
|
};
|
|
2666
3504
|
}
|
|
2667
3505
|
}
|
|
2668
3506
|
async _impactImpl(repo, params) {
|
|
2669
3507
|
await this.ensureInitialized(repo);
|
|
2670
3508
|
const { target, direction } = params;
|
|
3509
|
+
// ── Dispatch order (KTD5) ──────────────────────────────────────────
|
|
3510
|
+
// (1) Validate `mode`. Absent/'callgraph' → unchanged path; 'pdg' → the
|
|
3511
|
+
// intra-procedural PDG engine; anything else → hard error.
|
|
3512
|
+
// This MUST come before resolveSymbolCandidates so the ambiguous branch can
|
|
3513
|
+
// fork on the validated mode and never run the callgraph fan-out under pdg.
|
|
3514
|
+
const modeResult = validateImpactMode(params.mode);
|
|
3515
|
+
if ('error' in modeResult) {
|
|
3516
|
+
return {
|
|
3517
|
+
error: modeResult.error,
|
|
3518
|
+
target: { name: target },
|
|
3519
|
+
direction,
|
|
3520
|
+
impactedCount: 0,
|
|
3521
|
+
risk: 'UNKNOWN',
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
const mode = modeResult.mode;
|
|
3525
|
+
// `line` is a PDG-only statement anchor. Reject it on the callgraph path
|
|
3526
|
+
// rather than silently ignore (the symbol→symbol BFS has no statement notion).
|
|
3527
|
+
if (params.line !== undefined && mode !== 'pdg') {
|
|
3528
|
+
return {
|
|
3529
|
+
error: `Parameter 'line' is only supported with mode:'pdg' (it anchors the dependence slice on a statement). Remove it or set mode:'pdg'.`,
|
|
3530
|
+
target: { name: params.target },
|
|
3531
|
+
direction: params.direction,
|
|
3532
|
+
impactedCount: 0,
|
|
3533
|
+
risk: 'UNKNOWN',
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
// A provided `line` must be a positive integer.
|
|
3537
|
+
if (params.line !== undefined &&
|
|
3538
|
+
(!Number.isInteger(params.line) || params.line < 1)) {
|
|
3539
|
+
// Line param fails validation before target resolution → partial-but-typed
|
|
3540
|
+
// target on the pdg path (typed PdgImpactTarget, not an inline literal).
|
|
3541
|
+
const badLineTarget = { name: params.target };
|
|
3542
|
+
return mode === 'pdg'
|
|
3543
|
+
? makePdgImpactErrorResult({
|
|
3544
|
+
mode: 'pdg',
|
|
3545
|
+
error: `Parameter 'line' must be a positive integer (1-based source line), got ${JSON.stringify(params.line)}.`,
|
|
3546
|
+
target: badLineTarget,
|
|
3547
|
+
direction: params.direction,
|
|
3548
|
+
})
|
|
3549
|
+
: {
|
|
3550
|
+
error: `Parameter 'line' must be a positive integer (1-based source line), got ${JSON.stringify(params.line)}.`,
|
|
3551
|
+
target: { name: params.target },
|
|
3552
|
+
direction: params.direction,
|
|
3553
|
+
impactedCount: 0,
|
|
3554
|
+
risk: 'UNKNOWN',
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
if (mode === 'pdg') {
|
|
3558
|
+
// PDG mode is now unified inside a single repo: it combines the local
|
|
3559
|
+
// CDG/RD statement slice with the same inter-symbol reach used for the
|
|
3560
|
+
// option-driven comparison path. Cross-repo fan-out remains a callgraph
|
|
3561
|
+
// feature, so crossDepth is still a loud error rather than a silent ignore.
|
|
3562
|
+
const incompatible = [];
|
|
3563
|
+
if (params.crossDepth !== undefined)
|
|
3564
|
+
incompatible.push('crossDepth');
|
|
3565
|
+
if (incompatible.length > 0) {
|
|
3566
|
+
// crossDepth is rejected before target resolution → partial-but-typed
|
|
3567
|
+
// target (typed PdgImpactTarget).
|
|
3568
|
+
const crossDepthTarget = { name: target };
|
|
3569
|
+
const pdgErr = makePdgImpactErrorResult({
|
|
3570
|
+
mode: 'pdg',
|
|
3571
|
+
error: `Parameter(s) ${incompatible.join(', ')} are not supported with mode:'pdg' ` +
|
|
3572
|
+
`(single-repo PDG impact). Remove them or use mode:'callgraph' for cross-repo fan-out.`,
|
|
3573
|
+
target: crossDepthTarget,
|
|
3574
|
+
direction,
|
|
3575
|
+
});
|
|
3576
|
+
return pdgErr;
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
2671
3579
|
const maxDepth = params.maxDepth || 3;
|
|
2672
3580
|
// Map legacy relation type names before filtering (backward compat for OVERRIDES → METHOD_OVERRIDES)
|
|
2673
3581
|
const mappedRelTypes = params.relationTypes?.flatMap((t) => t === 'OVERRIDES' ? ['OVERRIDES', 'METHOD_OVERRIDES'] : [t]);
|
|
@@ -2710,15 +3618,64 @@ export class LocalBackend {
|
|
|
2710
3618
|
const outcome = await this.resolveSymbolCandidates(repo, { uid: params.target_uid, name: target }, { file_path: params.file_path, kind: params.kind });
|
|
2711
3619
|
if (outcome.kind === 'not_found') {
|
|
2712
3620
|
const missing = params.target_uid ?? target;
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
3621
|
+
// not_found = no resolved symbol, so the envelope keeps the partial-but-
|
|
3622
|
+
// typed target (typed PdgImpactTarget — there is no id/type/filePath yet).
|
|
3623
|
+
const notFoundTarget = { name: target };
|
|
3624
|
+
return mode === 'pdg'
|
|
3625
|
+
? makePdgImpactErrorResult({
|
|
3626
|
+
mode: 'pdg',
|
|
3627
|
+
error: `Target '${missing}' not found`,
|
|
3628
|
+
target: notFoundTarget,
|
|
3629
|
+
direction,
|
|
3630
|
+
})
|
|
3631
|
+
: {
|
|
3632
|
+
error: `Target '${missing}' not found`,
|
|
3633
|
+
target: { name: target },
|
|
3634
|
+
direction,
|
|
3635
|
+
impactedCount: 0,
|
|
3636
|
+
risk: 'UNKNOWN',
|
|
3637
|
+
};
|
|
2720
3638
|
}
|
|
2721
3639
|
if (outcome.kind === 'ambiguous') {
|
|
3640
|
+
// Shared truncation cap for the ambiguous candidate list — both the pdg
|
|
3641
|
+
// branch (shows candidates) and the callgraph branch (probes candidates)
|
|
3642
|
+
// bound to this many.
|
|
3643
|
+
const AMBIGUOUS_MAX_CANDIDATES = 6;
|
|
3644
|
+
// KTD5 ambiguous trap — under mode:'pdg' we MUST NOT fall into the
|
|
3645
|
+
// callgraph fan-out below: it runs `_runImpactBFS` per candidate, which
|
|
3646
|
+
// would silently execute the call-graph engine under a `pdg` call (the
|
|
3647
|
+
// exact silent fallback KTD5 forbids). For U1 the pdg ambiguous path
|
|
3648
|
+
// returns the candidate list WITHOUT any callgraph probe; the full pdg
|
|
3649
|
+
// ambiguous handling (per-candidate PDG summaries / ranking) lands in U4.
|
|
3650
|
+
if (mode === 'pdg') {
|
|
3651
|
+
const truncated = outcome.candidates.length > AMBIGUOUS_MAX_CANDIDATES;
|
|
3652
|
+
const shown = outcome.candidates.slice(0, AMBIGUOUS_MAX_CANDIDATES);
|
|
3653
|
+
return {
|
|
3654
|
+
status: 'ambiguous',
|
|
3655
|
+
mode,
|
|
3656
|
+
message: `Found ${outcome.candidates.length} symbols matching '${target}'` +
|
|
3657
|
+
(truncated ? ` (showing ${shown.length} of ${outcome.candidates.length})` : '') +
|
|
3658
|
+
`. Disambiguate with target_uid (or file_path/kind) for a single ` +
|
|
3659
|
+
`authoritative PDG result.`,
|
|
3660
|
+
target: { name: target },
|
|
3661
|
+
direction,
|
|
3662
|
+
totalCandidates: outcome.candidates.length,
|
|
3663
|
+
// No single resolved symbol → impactedCount stays 0 / risk UNKNOWN
|
|
3664
|
+
// (UNKNOWN must never read as "safe to refactor"). No callgraph
|
|
3665
|
+
// fan-out runs, so there is no per-candidate blast radius here yet.
|
|
3666
|
+
impactedCount: 0,
|
|
3667
|
+
risk: 'UNKNOWN',
|
|
3668
|
+
...(truncated && { candidatesTruncated: true }),
|
|
3669
|
+
candidates: shown.map((c) => ({
|
|
3670
|
+
uid: c.id,
|
|
3671
|
+
name: c.name,
|
|
3672
|
+
kind: c.type,
|
|
3673
|
+
filePath: c.filePath,
|
|
3674
|
+
line: c.startLine,
|
|
3675
|
+
score: Number(c.score.toFixed(2)),
|
|
3676
|
+
})),
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
2722
3679
|
// #2129 — a bare name that collides with several symbols must NOT report a
|
|
2723
3680
|
// bare `impactedCount: 0`. The real blast radius lives under whichever
|
|
2724
3681
|
// candidate the caller meant; a flat zero here is precisely the silent
|
|
@@ -2728,7 +3685,6 @@ export class LocalBackend {
|
|
|
2728
3685
|
// summary-only BFS per candidate so each one's true count + risk is
|
|
2729
3686
|
// visible, and surface the maximum at the top level so the headline can
|
|
2730
3687
|
// never read as "safe to refactor". Candidates arrive sorted by score.
|
|
2731
|
-
const AMBIGUOUS_MAX_CANDIDATES = 6;
|
|
2732
3688
|
const probed = outcome.candidates.slice(0, AMBIGUOUS_MAX_CANDIDATES);
|
|
2733
3689
|
// `partialProbe` is intentionally a SECOND incompleteness flag, distinct
|
|
2734
3690
|
// from the traversal-interrupted `partial` flag used elsewhere: it means
|
|
@@ -2822,13 +3778,127 @@ export class LocalBackend {
|
|
|
2822
3778
|
id: outcome.symbol.id,
|
|
2823
3779
|
name: outcome.symbol.name,
|
|
2824
3780
|
filePath: outcome.symbol.filePath,
|
|
3781
|
+
// Carry the resolved span so the PDG seed anchors on THIS symbol directly,
|
|
3782
|
+
// without re-resolving its (possibly ambiguous) name (FIX 1).
|
|
3783
|
+
startLine: outcome.symbol.startLine,
|
|
3784
|
+
endLine: outcome.symbol.endLine,
|
|
2825
3785
|
};
|
|
2826
3786
|
const symType = outcome.resolvedLabel || outcome.symbol.type || '';
|
|
3787
|
+
// (2) PDG-layer presence probe (U2, KTD7) — the four-state degradation
|
|
3788
|
+
// contract after target resolution, before traversal. A repo never analyzed
|
|
3789
|
+
// with `--pdg` (no-layer), one with only a partial layer (sub-layer-missing
|
|
3790
|
+
// — impact needs BOTH CDG and REACHING_DEF), or one whose meta is unreadable
|
|
3791
|
+
// (unknown) each returns a distinct guidance note here rather than a
|
|
3792
|
+
// confusing empty blast radius. Resolve first so target-known degraded
|
|
3793
|
+
// responses keep the same id/type/filePath envelope as successful PDG
|
|
3794
|
+
// responses; only `ready` falls through to traversal.
|
|
3795
|
+
// Hoisted so the (4) traversal branch can read `layer.hasCallSummary` (FU-C):
|
|
3796
|
+
// the same single meta-stamp probe serves both the degradation gate and the
|
|
3797
|
+
// ascent-availability note — no second probe.
|
|
3798
|
+
let layer;
|
|
3799
|
+
if (mode === 'pdg') {
|
|
3800
|
+
layer = await pdgLayerStatus({
|
|
3801
|
+
lbugPath: repo.lbugPath,
|
|
3802
|
+
executeParameterized,
|
|
3803
|
+
});
|
|
3804
|
+
if (isPdgDegradedLayerStatus(layer)) {
|
|
3805
|
+
// Degradation occurs AFTER target resolution → thread the FULL typed
|
|
3806
|
+
// envelope ({ id, name, type, filePath }) so degraded responses keep the
|
|
3807
|
+
// same target shape as a successful PDG result (typed PdgImpactTarget).
|
|
3808
|
+
const degradedTarget = {
|
|
3809
|
+
id: sym.id,
|
|
3810
|
+
name: sym.name,
|
|
3811
|
+
type: symType || 'Function',
|
|
3812
|
+
filePath: sym.filePath,
|
|
3813
|
+
};
|
|
3814
|
+
return makePdgLayerDegradedResult({
|
|
3815
|
+
mode,
|
|
3816
|
+
layer,
|
|
3817
|
+
target: degradedTarget,
|
|
3818
|
+
direction,
|
|
3819
|
+
});
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
2827
3822
|
const effectiveRelationTypes = (symType === 'Class' || symType === 'Interface') &&
|
|
2828
3823
|
!hasExplicitRelationTypes &&
|
|
2829
3824
|
!relationTypes.includes('ACCESSES')
|
|
2830
3825
|
? [...relationTypes, 'ACCESSES']
|
|
2831
3826
|
: relationTypes;
|
|
3827
|
+
// (4) single → route the resolved symbol to the engine selected by `mode`.
|
|
3828
|
+
if (mode === 'pdg') {
|
|
3829
|
+
const pdgResult = await this._runImpactPDG({
|
|
3830
|
+
repo,
|
|
3831
|
+
sym,
|
|
3832
|
+
symType,
|
|
3833
|
+
direction,
|
|
3834
|
+
maxDepth,
|
|
3835
|
+
line: params.line,
|
|
3836
|
+
limit: Number.isFinite(params.limit) ? params.limit : 100,
|
|
3837
|
+
// KTD2 extraction-seam discipline: hand the engine its DB dependency
|
|
3838
|
+
// explicitly rather than `this.`-binding it. LocalBackend owns repo
|
|
3839
|
+
// lifecycle; `pdg-impact.ts` owns traversal/projection.
|
|
3840
|
+
executeParameterized,
|
|
3841
|
+
// FU-C: thread the CALL_SUMMARY layer presence read above (meta stamp) so
|
|
3842
|
+
// the engine notes "no return-value ascent (re-index)" on a pre-FU-C (v3)
|
|
3843
|
+
// index. `layer.hasCallSummary` is set on every meta-readable state.
|
|
3844
|
+
callSummaryAvailable: layer?.hasCallSummary === true,
|
|
3845
|
+
});
|
|
3846
|
+
// Statement-precise inter-procedural reach: a first-hop callee is "proven"
|
|
3847
|
+
// iff it is invoked in a block of the criterion's dependence slice. The
|
|
3848
|
+
// slice = the seed block(s) (the changed line itself) UNION the dependent
|
|
3849
|
+
// reachable blocks — both carry the leaf callee names they call
|
|
3850
|
+
// (`BasicBlock.callees`). The seed block is included because a callee
|
|
3851
|
+
// invoked directly on the changed line is the most-directly-impacted one,
|
|
3852
|
+
// yet `reachableBlocks` excludes the seed by the seed-minus-reachable
|
|
3853
|
+
// convention. Upstream seeds carry no discriminating slice, so the bridge
|
|
3854
|
+
// falls back to preserving callgraph reach.
|
|
3855
|
+
// `_runImpactPDG` returns the PdgImpactResult union; only the success/empty
|
|
3856
|
+
// slice results carry intraReachableBlocks/seedBlocks (degraded and error
|
|
3857
|
+
// results do not). Narrow via the same discriminant the composer uses, then
|
|
3858
|
+
// read the typed string[] slices — no `as any`.
|
|
3859
|
+
const sliceResult = 'error' in pdgResult || 'pdgLayer' in pdgResult ? null : pdgResult;
|
|
3860
|
+
// FIX 6: key the "first-hop proven" set on the INTRA-procedural slice only
|
|
3861
|
+
// (seed ∪ intra-reachable), NOT `reachableBlocks` — which the U1 descent now
|
|
3862
|
+
// EXPANDS with inter-procedurally-reached callee blocks. Using the expanded
|
|
3863
|
+
// superset would mark transitively-reached (2+ hop) callgraph targets as
|
|
3864
|
+
// first-hop "proven", silently shifting the established statementPrecision
|
|
3865
|
+
// semantics. The interproc-reached blocks are routed into the statement
|
|
3866
|
+
// slice / block→symbol projection inside `_runImpactPDG` only.
|
|
3867
|
+
const intraReachableBlocks = sliceResult?.intraReachableBlocks ?? [];
|
|
3868
|
+
const seedBlocks = sliceResult?.seedBlocks ?? [];
|
|
3869
|
+
const sliceBlocks = [...seedBlocks, ...intraReachableBlocks];
|
|
3870
|
+
const sliceCalleeNames = direction === 'downstream' && sliceBlocks.length > 0
|
|
3871
|
+
? await this.calleesOfBlocks(repo, sliceBlocks)
|
|
3872
|
+
: new Set();
|
|
3873
|
+
// Resolved-id slice set (sound primary key, KTD3): unioned from the SAME
|
|
3874
|
+
// seed ∪ reachable block set as the names — so a callee invoked only on the
|
|
3875
|
+
// seeded line is still provable by id. Absent on a pre-v3 index (no
|
|
3876
|
+
// `calleeIds` column) → empty → the bridge falls back to the leaf-name match.
|
|
3877
|
+
const sliceCalleeIds = direction === 'downstream' && sliceBlocks.length > 0
|
|
3878
|
+
? await this.calleeIdsOfBlocks(repo, sliceBlocks)
|
|
3879
|
+
: new Set();
|
|
3880
|
+
// Build the bridge when EITHER the name fallback or the id key has signal —
|
|
3881
|
+
// an id-only index (names empty but ids present) must still seed the bridge.
|
|
3882
|
+
const pdgBridge = sliceCalleeNames.size > 0 || sliceCalleeIds.size > 0
|
|
3883
|
+
? { sliceCalleeNames, sliceCalleeIds }
|
|
3884
|
+
: undefined;
|
|
3885
|
+
try {
|
|
3886
|
+
const interproceduralResult = await this._runImpactBFS(repo, sym, symType, direction, {
|
|
3887
|
+
maxDepth,
|
|
3888
|
+
relationTypes: effectiveRelationTypes,
|
|
3889
|
+
includeTests,
|
|
3890
|
+
minConfidence,
|
|
3891
|
+
limit: Number.isFinite(params.limit) ? params.limit : 100,
|
|
3892
|
+
offset: Number.isFinite(params.offset) ? params.offset : 0,
|
|
3893
|
+
pdgBridge,
|
|
3894
|
+
});
|
|
3895
|
+
return composeUnifiedPdgImpactResult(pdgResult, interproceduralResult);
|
|
3896
|
+
}
|
|
3897
|
+
catch (e) {
|
|
3898
|
+
logQueryError('impact:pdg-interprocedural-reach', e);
|
|
3899
|
+
return composeUnifiedPdgImpactResult(pdgResult, null, e);
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
2832
3902
|
return this._runImpactBFS(repo, sym, symType, direction, {
|
|
2833
3903
|
maxDepth,
|
|
2834
3904
|
relationTypes: effectiveRelationTypes,
|
|
@@ -2839,6 +3909,79 @@ export class LocalBackend {
|
|
|
2839
3909
|
summaryOnly: params.summaryOnly,
|
|
2840
3910
|
});
|
|
2841
3911
|
}
|
|
3912
|
+
/**
|
|
3913
|
+
* Union of the leaf callee names invoked across a set of dependence-slice
|
|
3914
|
+
* blocks (`BasicBlock.callees`, space-joined at emit). Drives statement-precise
|
|
3915
|
+
* inter-procedural evidence: a first-hop callee reached from the criterion is
|
|
3916
|
+
* "proven" (callgraph-bridge) iff its name is in this set, else unproven-bridge.
|
|
3917
|
+
* Empty when the slice blocks call nothing or carry no harvested callees
|
|
3918
|
+
* (non-TS/JS or synthetic ENTRY/EXIT blocks) — the bridge then preserves
|
|
3919
|
+
* callgraph reach. A query failure is logged and degrades to empty (no proof),
|
|
3920
|
+
* never throws (the inter-procedural reach is still returned).
|
|
3921
|
+
*/
|
|
3922
|
+
async calleesOfBlocks(repo, blockIds) {
|
|
3923
|
+
const names = new Set();
|
|
3924
|
+
if (blockIds.length === 0)
|
|
3925
|
+
return names;
|
|
3926
|
+
try {
|
|
3927
|
+
const rows = await executeParameterized(repo.lbugPath, `MATCH (b:BasicBlock) WHERE b.id IN $ids RETURN b.callees AS callees`, { ids: blockIds });
|
|
3928
|
+
for (const r of rows) {
|
|
3929
|
+
const raw = String(r.callees ?? r[0] ?? '');
|
|
3930
|
+
for (const n of raw.split(' '))
|
|
3931
|
+
if (n)
|
|
3932
|
+
names.add(n);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
catch (e) {
|
|
3936
|
+
logQueryError('impact:pdg-slice-callees', e);
|
|
3937
|
+
}
|
|
3938
|
+
return names;
|
|
3939
|
+
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Union of the RESOLVED callee symbol ids invoked across a set of
|
|
3942
|
+
* dependence-slice blocks (`BasicBlock.calleeIds`, space-joined at emit —
|
|
3943
|
+
* sibling of `callees`). This is the SOUND key the bridge prefers: a first-hop
|
|
3944
|
+
* callee is proven statement-precise iff its resolved id is in this set, which
|
|
3945
|
+
* eliminates the same-leaf-name collision (false-positive) and import-alias
|
|
3946
|
+
* (false-negative) the name set cannot distinguish. Empty when the slice blocks
|
|
3947
|
+
* carry no captured ids (pre-v3 index without the `calleeIds` column, or
|
|
3948
|
+
* non-overloading/synthetic blocks) — the bridge then falls back to the
|
|
3949
|
+
* leaf-name match per U5. A query failure is logged and degrades to empty (no
|
|
3950
|
+
* proof), never throws (the inter-procedural reach is still returned). Mirrors
|
|
3951
|
+
* `calleesOfBlocks` exactly — same shape, same swallow-on-error contract.
|
|
3952
|
+
*/
|
|
3953
|
+
async calleeIdsOfBlocks(repo, blockIds) {
|
|
3954
|
+
const ids = new Set();
|
|
3955
|
+
if (blockIds.length === 0)
|
|
3956
|
+
return ids;
|
|
3957
|
+
try {
|
|
3958
|
+
const rows = await executeParameterized(repo.lbugPath, `MATCH (b:BasicBlock) WHERE b.id IN $ids RETURN b.calleeIds AS calleeIds`, { ids: blockIds });
|
|
3959
|
+
for (const r of rows) {
|
|
3960
|
+
// Shared split-and-drop-sentinel logic (`splitCalleeIds`) so this bridge
|
|
3961
|
+
// key and the inter-procedural descent cannot diverge. The sentinel marks
|
|
3962
|
+
// a capped block (handled by the names-sentinel check in the bridge) and
|
|
3963
|
+
// is not a resolved symbol id, so it never enters the `has(realId)` set.
|
|
3964
|
+
for (const id of splitCalleeIds(r.calleeIds ?? r[0]))
|
|
3965
|
+
ids.add(id);
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
catch (e) {
|
|
3969
|
+
logQueryError('impact:pdg-slice-callee-ids', e);
|
|
3970
|
+
}
|
|
3971
|
+
return ids;
|
|
3972
|
+
}
|
|
3973
|
+
/**
|
|
3974
|
+
* Delegates the PDG impact engine to `pdg-impact.ts`.
|
|
3975
|
+
*
|
|
3976
|
+
* The private method remains as the LocalBackend dispatch seam so existing
|
|
3977
|
+
* tests can keep asserting that `mode:'pdg'` routes through the PDG
|
|
3978
|
+
* statement engine before LocalBackend attaches interprocedural symbol reach.
|
|
3979
|
+
* The traversal/projection/result assembly lives in the extracted helper
|
|
3980
|
+
* module.
|
|
3981
|
+
*/
|
|
3982
|
+
async _runImpactPDG(deps) {
|
|
3983
|
+
return runImpactPDG(deps);
|
|
3984
|
+
}
|
|
2842
3985
|
/**
|
|
2843
3986
|
* #1858 — epistemic lower-bound detection.
|
|
2844
3987
|
*
|
|
@@ -2972,6 +4115,7 @@ export class LocalBackend {
|
|
|
2972
4115
|
: this.computeEpistemicBoundary(repo, symId, symType, (sym.name || sym[1]));
|
|
2973
4116
|
const impacted = [];
|
|
2974
4117
|
const visited = new Set([symId]);
|
|
4118
|
+
const pdgBridgeEvidenceById = new Map();
|
|
2975
4119
|
let frontier = [symId];
|
|
2976
4120
|
let traversalComplete = true;
|
|
2977
4121
|
// Fix #480: For Java (and other JVM) Class/Interface nodes, CALLS edges
|
|
@@ -3049,10 +4193,31 @@ export class LocalBackend {
|
|
|
3049
4193
|
...(safeMinConfidence > 0 ? { minConfidence: safeMinConfidence } : {}),
|
|
3050
4194
|
});
|
|
3051
4195
|
for (const rel of related) {
|
|
4196
|
+
const sourceId = String(rel.sourceId ?? rel[0] ?? '');
|
|
3052
4197
|
const relId = rel.id || rel[1];
|
|
3053
4198
|
const filePath = rel.filePath || rel[4] || '';
|
|
3054
4199
|
if (!includeTests && isTestFilePath(filePath))
|
|
3055
4200
|
continue;
|
|
4201
|
+
// Bridge evidence is computed for EVERY edge (not just the first to
|
|
4202
|
+
// reach a node) and the strongest verdict across all parents is kept
|
|
4203
|
+
// (`callgraph-bridge` wins). This makes a diamond-reachable node's
|
|
4204
|
+
// proven/unproven label order-independent of DB row iteration; the
|
|
4205
|
+
// final label is stamped onto the impacted items after the depth loop.
|
|
4206
|
+
if (opts.pdgBridge) {
|
|
4207
|
+
const ev = pdgBridgeEvidenceForImpact({
|
|
4208
|
+
bridge: opts.pdgBridge,
|
|
4209
|
+
depth,
|
|
4210
|
+
calleeName: rel.name || rel[2],
|
|
4211
|
+
// Sound primary key (KTD3): the reached callee's RESOLVED id — the
|
|
4212
|
+
// same `relId` (`rel.id`) the BFS keys its visited/frontier sets on,
|
|
4213
|
+
// which equals the CALLS targetId captured into `BasicBlock.calleeIds`.
|
|
4214
|
+
// The bridge proves by id ∈ `sliceCalleeIds` first, falling back to
|
|
4215
|
+
// `calleeName` only when ids are absent or the block is capped.
|
|
4216
|
+
calleeId: relId,
|
|
4217
|
+
inherited: pdgBridgeEvidenceById.get(sourceId),
|
|
4218
|
+
});
|
|
4219
|
+
pdgBridgeEvidenceById.set(String(relId), betterBridgeEvidence(pdgBridgeEvidenceById.get(String(relId)), ev));
|
|
4220
|
+
}
|
|
3056
4221
|
if (!visited.has(relId)) {
|
|
3057
4222
|
visited.add(relId);
|
|
3058
4223
|
nextFrontier.push(relId);
|
|
@@ -3063,6 +4228,8 @@ export class LocalBackend {
|
|
|
3063
4228
|
const effectiveConfidence = typeof storedConfidence === 'number' && storedConfidence > 0
|
|
3064
4229
|
? storedConfidence
|
|
3065
4230
|
: confidenceForRelType(relationType);
|
|
4231
|
+
// pdgEvidence is stamped after the depth loop from the finalized,
|
|
4232
|
+
// order-independent pdgBridgeEvidenceById map.
|
|
3066
4233
|
impacted.push({
|
|
3067
4234
|
depth,
|
|
3068
4235
|
id: relId,
|
|
@@ -3084,6 +4251,18 @@ export class LocalBackend {
|
|
|
3084
4251
|
}
|
|
3085
4252
|
frontier = nextFrontier;
|
|
3086
4253
|
}
|
|
4254
|
+
// Stamp the finalized, order-independent bridge evidence (strongest across
|
|
4255
|
+
// all parents) onto each impacted item. Deferred from the BFS loop so a
|
|
4256
|
+
// diamond-reachable node reflects a proven parent regardless of visit order.
|
|
4257
|
+
if (opts.pdgBridge) {
|
|
4258
|
+
for (const item of impacted) {
|
|
4259
|
+
const ev = pdgBridgeEvidenceById.get(String(item.id));
|
|
4260
|
+
if (ev) {
|
|
4261
|
+
item.pdgEvidence = ev.evidence;
|
|
4262
|
+
item.pdgBridgeBasis = ev.basis;
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
3087
4266
|
const grouped = {};
|
|
3088
4267
|
for (const item of impacted) {
|
|
3089
4268
|
if (!grouped[item.depth])
|
|
@@ -3577,6 +4756,30 @@ export class LocalBackend {
|
|
|
3577
4756
|
return { error: resolved.error };
|
|
3578
4757
|
const svc = this.getGroupService();
|
|
3579
4758
|
if (method === 'impact') {
|
|
4759
|
+
// KTD5/KTD12 — validate `mode` at the group-forward boundary too (the
|
|
4760
|
+
// JSON-schema enum is advisory). An invalid mode errors; `mode:'pdg'` is
|
|
4761
|
+
// rejected for @group targets because PDG impact is single-repo and
|
|
4762
|
+
// intra-procedural — there is no cross-repo dependence graph to walk.
|
|
4763
|
+
// Rejecting here (before groupImpact) is the KTD12 @group hard error.
|
|
4764
|
+
const groupModeResult = validateImpactMode(params.mode);
|
|
4765
|
+
if ('error' in groupModeResult)
|
|
4766
|
+
return { error: groupModeResult.error };
|
|
4767
|
+
if (groupModeResult.mode === 'pdg') {
|
|
4768
|
+
// @group reject: no single-repo symbol is ever resolved on the group path,
|
|
4769
|
+
// so the envelope carries the partial-but-typed target (PdgImpactTarget).
|
|
4770
|
+
// Routed through the typed builder so this exit is a PdgImpactResult union
|
|
4771
|
+
// member, never a bare { error } object.
|
|
4772
|
+
const groupRejectTarget = { name: String(params.target ?? '') };
|
|
4773
|
+
const pdgErr = makePdgImpactErrorResult({
|
|
4774
|
+
mode: 'pdg',
|
|
4775
|
+
error: "mode:'pdg' is not supported for @group targets — PDG impact is " +
|
|
4776
|
+
'single-repo and intra-procedural. Run pdg impact against an ' +
|
|
4777
|
+
'individual indexed repository instead.',
|
|
4778
|
+
target: groupRejectTarget,
|
|
4779
|
+
direction: (params.direction === 'downstream' ? 'downstream' : 'upstream'),
|
|
4780
|
+
});
|
|
4781
|
+
return pdgErr;
|
|
4782
|
+
}
|
|
3580
4783
|
const impactArgs = {
|
|
3581
4784
|
name: groupName,
|
|
3582
4785
|
repo: resolved.repoPath,
|
|
@@ -3609,7 +4812,10 @@ export class LocalBackend {
|
|
|
3609
4812
|
if (method === 'query') {
|
|
3610
4813
|
const queryArgs = {
|
|
3611
4814
|
name: groupName,
|
|
3612
|
-
|
|
4815
|
+
// #2175: resolve the search_query alias here (new name wins, same rule as the
|
|
4816
|
+
// local query() handler) so the group path is self-contained and does not depend
|
|
4817
|
+
// on params being normalized upstream. groupQuery() reads `query`.
|
|
4818
|
+
query: resolveAliasString(params.search_query, params.query),
|
|
3613
4819
|
};
|
|
3614
4820
|
if (typeof params.task_context === 'string')
|
|
3615
4821
|
queryArgs.task_context = params.task_context;
|