gitnexus 1.6.8-rc.9 → 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 +11 -0
- package/dist/core/ingestion/languages/c-cpp.js +6 -1
- 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 +3 -0
- 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 +23 -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/parse-worker.d.ts +13 -0
- package/dist/core/ingestion/workers/parse-worker.js +76 -17
- package/dist/core/ingestion/workers/worker-pool.d.ts +9 -40
- package/dist/core/ingestion/workers/worker-pool.js +15 -2
- 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.d.ts +17 -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 +41 -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
|
@@ -0,0 +1,1972 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDG-backed impact helpers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `local-backend.ts` so LocalBackend owns dispatch/repo lifecycle
|
|
5
|
+
* while this module owns the PDG layer probe, statement traversal, block
|
|
6
|
+
* projection, and result assembly contract.
|
|
7
|
+
*/
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { loadMeta } from '../../storage/repo-manager.js';
|
|
10
|
+
import { IMPACT_MAX_DEPTH, PDG_QUERY_DEFAULT_LIMIT, PDG_QUERY_MAX_LIMIT } from '../tools.js';
|
|
11
|
+
import { CALLEES_TRUNCATED_SENTINEL, CALLEE_ID_SEP } from '../../core/ingestion/cfg/emit.js';
|
|
12
|
+
import { decodeCallSummary } from '../../core/ingestion/taint/call-summary-codec.js';
|
|
13
|
+
import { decodeReachingDefReason } from '../../core/ingestion/cfg/reaching-def-reason-codec.js';
|
|
14
|
+
import { getProviderForFile } from '../../core/ingestion/languages/index.js';
|
|
15
|
+
import { SupportedLanguages } from '../../_shared/index.js';
|
|
16
|
+
/**
|
|
17
|
+
* Parse the `<fnLine>` segment out of a `BasicBlock` id (1-based function start
|
|
18
|
+
* line). The id template is
|
|
19
|
+
* `BasicBlock:<filePath>:<fnLine>:<fnCol>:<blockIdx>`
|
|
20
|
+
* and `<filePath>` may itself contain `':'` (a Windows drive letter), so the
|
|
21
|
+
* segments are taken from the RIGHT: `<blockIdx>` is last, `<fnCol>` second-last,
|
|
22
|
+
* `<fnLine>` third-last. Extracted from the `_pdgQueryImpl` closure (#2086) into
|
|
23
|
+
* a shared module-scope helper so the PDG impact traversal (U3/U4) reuses the
|
|
24
|
+
* exact same parse — the `pdg_query` read path is byte-identical to before.
|
|
25
|
+
*/
|
|
26
|
+
export function fnLineOf(id) {
|
|
27
|
+
const parts = id.split(':');
|
|
28
|
+
return Number(parts[parts.length - 3]);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse the `<filePath>` segment out of a `BasicBlock` id, the COUNTERPART to
|
|
32
|
+
* `fnLineOf`. The id template is `BasicBlock:<filePath>:<fnLine>:<fnCol>:<blockIdx>`,
|
|
33
|
+
* so the file path is everything BETWEEN the `BasicBlock:` prefix and the last
|
|
34
|
+
* THREE colon-segments (`<fnLine>:<fnCol>:<blockIdx>`). `<filePath>` may itself
|
|
35
|
+
* contain `':'` (a Windows drive letter), so we strip from both ends rather than
|
|
36
|
+
* split-and-pick. Returns `''` for an unparseable id (treated as unresolved).
|
|
37
|
+
*/
|
|
38
|
+
function fnFileOf(id) {
|
|
39
|
+
const parts = id.split(':');
|
|
40
|
+
// Need: `BasicBlock` + filePath(≥1) + fnLine + fnCol + blockIdx ⇒ ≥5 segments.
|
|
41
|
+
if (parts.length < 5 || parts[0] !== 'BasicBlock')
|
|
42
|
+
return '';
|
|
43
|
+
// Drop the leading `BasicBlock` token and the trailing fnLine/fnCol/blockIdx,
|
|
44
|
+
// rejoin the middle on ':' to restore a path that itself contained colons.
|
|
45
|
+
return parts.slice(1, parts.length - 3).join(':');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Default inter-procedural FUNCTION-hop budget for the U1 forward closure (the
|
|
49
|
+
* number of call boundaries the slice descends through). Distinct from the per-
|
|
50
|
+
* hop intra-callee step/edge budget (which reuses `stepLimit`). Kept small (3)
|
|
51
|
+
* because context-insensitive descent over-includes with depth.
|
|
52
|
+
*/
|
|
53
|
+
const INTERPROC_DEPTH_BUDGET = 3;
|
|
54
|
+
/**
|
|
55
|
+
* Total newly-reached-block cap across all inter-procedural hops — the secondary
|
|
56
|
+
* termination guard against a pathological fan-out inside the depth budget.
|
|
57
|
+
* Stamps `truncated` with the `'limit'` reason when it fires (it is a SIZE/budget
|
|
58
|
+
* cap, not dependence-level depth exhaustion — FIX 7).
|
|
59
|
+
*/
|
|
60
|
+
const INTERPROC_NODE_BUDGET = 5000;
|
|
61
|
+
/**
|
|
62
|
+
* Split a tab-joined ({@link CALLEE_ID_SEP}) `BasicBlock.calleeIds` cell into its resolved callee
|
|
63
|
+
* symbol ids, dropping the truncation sentinel (a capped block carries the
|
|
64
|
+
* sentinel to mark an incomplete call-site list; it is NOT a resolved symbol id
|
|
65
|
+
* and must never enter a `has(realId)` set). Empty/whitespace cells yield no ids.
|
|
66
|
+
*
|
|
67
|
+
* Extracted here (U1) so the two callers — `LocalBackend.calleeIdsOfBlocks` (the
|
|
68
|
+
* statement-precise bridge key) and the inter-procedural descent's
|
|
69
|
+
* `calleeIdsFromCalleeRows` — cannot diverge on the split-and-drop-sentinel
|
|
70
|
+
* logic. Both consume rows of `BasicBlock.calleeIds`; this is the single source.
|
|
71
|
+
*/
|
|
72
|
+
export function splitCalleeIds(raw) {
|
|
73
|
+
const out = [];
|
|
74
|
+
// Split on the SHARED CALLEE_ID_SEP (tab) — ids embed file paths / multi-word
|
|
75
|
+
// C++ type tokens that can contain a space, so a space split would fragment
|
|
76
|
+
// them. Producer (calleeIdsOfBlock) joins with the same constant.
|
|
77
|
+
for (const id of String(raw ?? '').split(CALLEE_ID_SEP)) {
|
|
78
|
+
if (id && id !== CALLEES_TRUNCATED_SENTINEL)
|
|
79
|
+
out.push(id);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Contract version of the mode:'pdg' impact result shape. A stable discriminator
|
|
85
|
+
* for external MCP/agent consumers — distinct from the DB INCREMENTAL_SCHEMA_VERSION.
|
|
86
|
+
* Bump on any breaking change to the PDG result fields.
|
|
87
|
+
*/
|
|
88
|
+
export const PDG_RESULT_VERSION = 1;
|
|
89
|
+
/**
|
|
90
|
+
* FU-B-2 intra-block def→use line walk. Given a block's self REACHING_DEF
|
|
91
|
+
* def→use line PAIRS (every `defLine → useLine` step decoded from the edge
|
|
92
|
+
* `reason`'s pair list), walk FORWARD from a set of entry lines and return every
|
|
93
|
+
* interior USE line transitively reached. This is the principled statement-
|
|
94
|
+
* granular recovery for a coalesced straight-line BasicBlock, for BOTH chain
|
|
95
|
+
* shapes:
|
|
96
|
+
* - DISTINCT bindings: `chainCompute` coalesces `a@7; b@8; c@9` into one block;
|
|
97
|
+
* each binding is its own (block-pair, binding) group, so `a@7→b@8` and
|
|
98
|
+
* `b@8→c@9` arrive as separate self-edges, and walking from line 7 recovers
|
|
99
|
+
* {8, 9}.
|
|
100
|
+
* - SAME binding (reassignment): `acc = f(acc); acc = g(acc); acc = h(acc)`
|
|
101
|
+
* coalesces into one self-block whose `acc@24→acc@25`, `acc@25→acc@26`,
|
|
102
|
+
* `acc@26→acc@27` steps ALL share the one (self-block, accIdx) group — the
|
|
103
|
+
* dedup collapses them onto one edge, but the FU-B-2 pair LIST carries all
|
|
104
|
+
* three, so walking from line 24 chains 24→25→26→27 to fixpoint. (A
|
|
105
|
+
* first-pair-only annotation would stop at 25.)
|
|
106
|
+
*
|
|
107
|
+
* Pure: forward adjacency `defLine → Set<useLine>`, BFS from `entryLines`,
|
|
108
|
+
* bounded by the (finite) pair set + a `reached` set so it always terminates,
|
|
109
|
+
* even on a cycle — a self-referential line (`x = x + 1`, def@L→use@L) re-adds
|
|
110
|
+
* nothing new. The entry lines themselves are NOT emitted (they are the seed /
|
|
111
|
+
* the block's representative line already surfaced elsewhere); only newly-reached
|
|
112
|
+
* interior use lines are.
|
|
113
|
+
*/
|
|
114
|
+
function walkIntraBlockChain(selfEdges, entryLines) {
|
|
115
|
+
const succ = new Map();
|
|
116
|
+
for (const { defLine, useLine } of selfEdges) {
|
|
117
|
+
if (!Number.isInteger(defLine) || !Number.isInteger(useLine))
|
|
118
|
+
continue;
|
|
119
|
+
const set = succ.get(defLine) ?? new Set();
|
|
120
|
+
set.add(useLine);
|
|
121
|
+
succ.set(defLine, set);
|
|
122
|
+
}
|
|
123
|
+
const reached = new Set();
|
|
124
|
+
const frontier = [];
|
|
125
|
+
for (const e of entryLines)
|
|
126
|
+
frontier.push(e);
|
|
127
|
+
const seenSeed = new Set(frontier);
|
|
128
|
+
while (frontier.length > 0) {
|
|
129
|
+
const cur = frontier.pop();
|
|
130
|
+
for (const next of succ.get(cur) ?? []) {
|
|
131
|
+
if (reached.has(next) || seenSeed.has(next))
|
|
132
|
+
continue;
|
|
133
|
+
reached.add(next);
|
|
134
|
+
frontier.push(next);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return reached;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Fetch the self REACHING_DEF edges (`(a)-[REACHING_DEF]->(a)`) for a set of
|
|
141
|
+
* blocks, grouped by block id, with each edge's def/use source LINES decoded
|
|
142
|
+
* from the FU-B-2 `reason` annotation (`<name>|1:<defLine>:<useLine>`). A
|
|
143
|
+
* pre-FU-B-2 (un-annotated) `reason` decodes to no line info → the edge is
|
|
144
|
+
* dropped (the block then projects at block-start granularity exactly as before,
|
|
145
|
+
* the documented graceful degrade for an older index). A query error propagates.
|
|
146
|
+
*/
|
|
147
|
+
async function selfReachingDefEdgesByBlock(lbugPath, blockIds, exec) {
|
|
148
|
+
const out = new Map();
|
|
149
|
+
if (blockIds.length === 0)
|
|
150
|
+
return out;
|
|
151
|
+
const rows = await exec(lbugPath, `MATCH (a:BasicBlock)-[r:CodeRelation]->(a)
|
|
152
|
+
WHERE r.type = 'REACHING_DEF' AND a.id IN $ids
|
|
153
|
+
RETURN a.id AS id, r.reason AS reason`, { ids: blockIds });
|
|
154
|
+
for (const r of rows) {
|
|
155
|
+
const id = String(r['id'] ?? '');
|
|
156
|
+
if (!id)
|
|
157
|
+
continue;
|
|
158
|
+
const decoded = decodeReachingDefReason(r['reason']);
|
|
159
|
+
// FU-B-2: the annotation carries the FULL ordered (defLine, useLine) pair
|
|
160
|
+
// list for this (block-pair, binding) group — push EVERY pair so the walk can
|
|
161
|
+
// chain a same-binding reassignment (`acc@24->acc@25->acc@26`), which the
|
|
162
|
+
// dedup coalesces onto this one edge. A pre-FU-B-2 (un-annotated) reason
|
|
163
|
+
// decodes to no pairs → contributes nothing (block-start granularity).
|
|
164
|
+
if (decoded.pairs.length === 0)
|
|
165
|
+
continue;
|
|
166
|
+
const list = out.get(id) ?? [];
|
|
167
|
+
for (const p of decoded.pairs)
|
|
168
|
+
list.push({ defLine: p.defLine, useLine: p.useLine });
|
|
169
|
+
out.set(id, list);
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Resolve a set of BasicBlock ids to their source statements (line + text),
|
|
175
|
+
* deduped by `(filePath, line, block id)` and sorted by line. This is the useful
|
|
176
|
+
* output of a statement-anchored PDG slice — the dependent statements the change
|
|
177
|
+
* reaches. A query error propagates (no `.catch` swallow) so a DB failure is
|
|
178
|
+
* never silently reported as "no affected statements".
|
|
179
|
+
*
|
|
180
|
+
* FU-B-2 statement granularity: a coalesced straight-line BasicBlock is projected
|
|
181
|
+
* to its single `startLine` by default, which UNDER-reports the interior
|
|
182
|
+
* statements that genuinely depend on the criterion (lines 8/9 of a `7-9` block).
|
|
183
|
+
* For each block in `chainWalkBlocks` we walk its self REACHING_DEF def→use LINE
|
|
184
|
+
* chain (decoded from the FU-B-2 `reason` annotation) FORWARD from that block's
|
|
185
|
+
* entry line(s) and emit one statement per reached INTERIOR line (its own text
|
|
186
|
+
* from the block text). This is the SINGLE principled statement-granular
|
|
187
|
+
* mechanism — it replaces FU-C's blind all-interior-lines expansion of ascent
|
|
188
|
+
* blocks (now routed through the same walk, seeded at the block's start line) and
|
|
189
|
+
* also recovers the seed block's own interior dependents (the U2/intra-dataflow-
|
|
190
|
+
* chain recall gap). A pre-FU-B-2 index (no line annotation) yields no self-edge
|
|
191
|
+
* lines → the block degrades to its block-start projection (byte-identical to the
|
|
192
|
+
* old behavior).
|
|
193
|
+
*/
|
|
194
|
+
async function pdgStatementsForBlocks(lbugPath, blockIds, exec, criterionFile, ownerFnLine,
|
|
195
|
+
/**
|
|
196
|
+
* Blocks whose interior should be expanded to statement granularity via the
|
|
197
|
+
* self-edge def→use line walk, each mapped to the entry line(s) the walk
|
|
198
|
+
* starts from. Two contributors:
|
|
199
|
+
* - the SEED block, seeded at the criterion line — recovers the interior
|
|
200
|
+
* dependents of the changed statement inside its own coalesced block (the
|
|
201
|
+
* U2 intra-dataflow-chain gap: line 7's block 7-9 yields {8,9});
|
|
202
|
+
* - each U-C4 ascent-confirmed CALL block, seeded at its start line — the
|
|
203
|
+
* statement-granular realisation of the return-value ascent (replacing the
|
|
204
|
+
* FU-C blind interior-line stop-gap with the principled walk).
|
|
205
|
+
* Empty/absent ⇒ every block projects at block-start granularity (byte-
|
|
206
|
+
* identical to the pre-FU-B-2 behavior).
|
|
207
|
+
*/
|
|
208
|
+
chainWalkBlocks = new Map()) {
|
|
209
|
+
if (blockIds.length === 0 && chainWalkBlocks.size === 0)
|
|
210
|
+
return [];
|
|
211
|
+
// The seed block(s) we walk are NOT in `blockIds` (seeds are excluded from the
|
|
212
|
+
// reachable slice), so the block-text fetch must cover BOTH the reachable
|
|
213
|
+
// blocks and the chain-walk blocks. Union, de-duplicated.
|
|
214
|
+
const fetchIds = [...new Set([...blockIds, ...chainWalkBlocks.keys()])];
|
|
215
|
+
const [rows, selfEdgesByBlock] = await Promise.all([
|
|
216
|
+
exec(lbugPath, `MATCH (b:BasicBlock) WHERE b.id IN $ids
|
|
217
|
+
RETURN b.id AS id, b.startLine AS line, b.endLine AS endLine, b.text AS text`, { ids: fetchIds }),
|
|
218
|
+
chainWalkBlocks.size > 0
|
|
219
|
+
? selfReachingDefEdgesByBlock(lbugPath, [...chainWalkBlocks.keys()], exec)
|
|
220
|
+
: Promise.resolve(new Map()),
|
|
221
|
+
]);
|
|
222
|
+
const byKey = new Map();
|
|
223
|
+
const reachableIds = new Set(blockIds);
|
|
224
|
+
// Narrow the awaited rows ONCE at the boundary to a typed record shape; read
|
|
225
|
+
// the aliased cells via bracket access with String()/Number() coercion.
|
|
226
|
+
for (const r of rows) {
|
|
227
|
+
const id = String(r['id'] ?? '');
|
|
228
|
+
const line = Number(r['line'] ?? 0);
|
|
229
|
+
if (!id || !Number.isFinite(line) || line <= 0)
|
|
230
|
+
continue;
|
|
231
|
+
const filePath = fnFileOf(id);
|
|
232
|
+
const text = String(r['text'] ?? '').trim();
|
|
233
|
+
// INTRA iff this block's owning function (file + 1-based start line) is the
|
|
234
|
+
// criterion's own function; otherwise it was reached across a call boundary
|
|
235
|
+
// (INTER). Pure key comparison parsed from the block id — no extra DB query.
|
|
236
|
+
const scope = filePath === criterionFile && fnLineOf(id) === ownerFnLine ? 'intra' : 'inter';
|
|
237
|
+
const textLines = String(r['text'] ?? '').split('\n');
|
|
238
|
+
// ── FU-B-2 chain-walk block: expand to its interior dependent lines ───────
|
|
239
|
+
// Walk the block's self REACHING_DEF def→use LINE chain forward from the
|
|
240
|
+
// entry line(s). Each reached interior line is a statement transitively
|
|
241
|
+
// data-dependent on the entry (the chain the coalesced block lost). Emit it
|
|
242
|
+
// with its own physical-line text (`BasicBlock.text` is `lines.join('\n')`,
|
|
243
|
+
// 1-based from `startLine`). This is the principled replacement for FU-C's
|
|
244
|
+
// blind all-interior expansion — only lines the def→use chain proves are
|
|
245
|
+
// surfaced. A block that is ALSO a reachable block keeps its block-start
|
|
246
|
+
// statement too (added below); the seed block (not reachable) contributes
|
|
247
|
+
// ONLY its walked interior lines.
|
|
248
|
+
const entryLines = chainWalkBlocks.get(id);
|
|
249
|
+
if (entryLines !== undefined) {
|
|
250
|
+
// An empty entry set means "seed from the block's own start line" — the
|
|
251
|
+
// FU-C ascent blocks, whose call statements chain forward from the block
|
|
252
|
+
// start (the caller does not know the start line at map-build time). A
|
|
253
|
+
// non-empty set (the seed block, seeded at the criterion line) is used
|
|
254
|
+
// verbatim. `line` is the resolved block start line for this row.
|
|
255
|
+
const seeds = entryLines.size > 0 ? entryLines : new Set([line]);
|
|
256
|
+
const reached = walkIntraBlockChain(selfEdgesByBlock.get(id) ?? [], seeds);
|
|
257
|
+
for (const ln of reached) {
|
|
258
|
+
const idx = ln - line;
|
|
259
|
+
if (idx < 0 || idx >= textLines.length)
|
|
260
|
+
continue; // out of this block's text
|
|
261
|
+
const lineText = textLines[idx].trim();
|
|
262
|
+
const key = `${filePath}:${ln}:${id}`;
|
|
263
|
+
if (!byKey.has(key))
|
|
264
|
+
byKey.set(key, { line: ln, filePath, text: lineText, scope });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// A reachable block always surfaces its representative block-start statement.
|
|
268
|
+
// A pure chain-walk seed block (not reachable) does NOT (its start line is
|
|
269
|
+
// the criterion / the call block already surfaced elsewhere — only its
|
|
270
|
+
// walked-forward dependents are new). When the reachable block is ALSO a
|
|
271
|
+
// chain-walk block (a coalesced call block whose interior lines we expanded),
|
|
272
|
+
// its block-start statement is surfaced at the SAME statement granularity —
|
|
273
|
+
// the first physical line's own text, not the whole multi-statement block
|
|
274
|
+
// text — so all of its statements are consistently single-line. A reachable
|
|
275
|
+
// block that is NOT chain-walked keeps the full trimmed block text (byte-
|
|
276
|
+
// identical to the pre-FU-B-2 projection).
|
|
277
|
+
if (reachableIds.has(id)) {
|
|
278
|
+
const key = `${filePath}:${line}:${id}`;
|
|
279
|
+
const startText = entryLines !== undefined && textLines.length > 0 ? textLines[0].trim() : text;
|
|
280
|
+
if (!byKey.has(key))
|
|
281
|
+
byKey.set(key, { line, filePath, text: startText, scope });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return [...byKey.values()].sort((a, b) => {
|
|
285
|
+
if (a.filePath !== b.filePath)
|
|
286
|
+
return a.filePath < b.filePath ? -1 : 1;
|
|
287
|
+
if (a.line !== b.line)
|
|
288
|
+
return a.line - b.line;
|
|
289
|
+
return a.text < b.text ? -1 : a.text > b.text ? 1 : 0;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Net-new block → owning-symbol resolver (U4) — the REVERSE of
|
|
294
|
+
* `resolveBlockAnchor` (which goes symbol→blocks). No precedent exists:
|
|
295
|
+
* `_pdgQueryImpl` only ever extracts a raw `functionLine`, never an owning
|
|
296
|
+
* symbol. Lives in the extracted PDG impact engine and takes injected deps so
|
|
297
|
+
* LocalBackend keeps repo lifecycle/dispatch while this module owns projection.
|
|
298
|
+
*
|
|
299
|
+
* For each reachable block id `BasicBlock:<filePath>:<fnLine>:<fnCol>:<blockIdx>`:
|
|
300
|
+
* - `fnLineOf` → 1-based function start line; `fnFileOf` → file path.
|
|
301
|
+
* - Query `Function`/`Method`/`Constructor` `WHERE filePath = $f AND startLine = (fnLine-1)`
|
|
302
|
+
* — block `fnLine` is 1-based, symbol `startLine` is 0-based, so subtract one
|
|
303
|
+
* (the `[symStart+1]` convention from `resolveBlockAnchor`, applied in
|
|
304
|
+
* reverse; NOT re-derived).
|
|
305
|
+
*
|
|
306
|
+
* Two non-happy paths, BOTH surfaced (never silent):
|
|
307
|
+
* - **>1 match** (same-line different-name functions): `fnCol` rides the block
|
|
308
|
+
* id but the schema has NO `startColumn` column and the symbol id encodes only
|
|
309
|
+
* the name, so a `(filePath, startLine)` join cannot disambiguate. Report ALL
|
|
310
|
+
* colliding symbols, each `ambiguous: true` (R4 / Feasibility Finding 1).
|
|
311
|
+
* - **0 matches** (top-level/free-statement block, or a lambda whose start line
|
|
312
|
+
* ≠ a symbol `startLine`): one `unresolved` entry (`id: null`) under the
|
|
313
|
+
* block's file (R9 shadow path).
|
|
314
|
+
*
|
|
315
|
+
* Distinct `(filePath, fnLine)` pairs are queried once each (a block and its
|
|
316
|
+
* siblings in the same function share a pair), so the cost is O(distinct
|
|
317
|
+
* functions), not O(blocks).
|
|
318
|
+
*/
|
|
319
|
+
async function projectBlocksToSymbols(deps) {
|
|
320
|
+
const { lbugPath, blockIds, executeParameterized: exec } = deps;
|
|
321
|
+
// Group blocks by their (filePath, fnLine) owning-function key so each owning
|
|
322
|
+
// function is resolved with a single query regardless of block count.
|
|
323
|
+
const byFnKey = new Map();
|
|
324
|
+
for (const id of blockIds) {
|
|
325
|
+
const filePath = fnFileOf(id);
|
|
326
|
+
const fnLine = fnLineOf(id); // 1-based
|
|
327
|
+
if (!filePath || !Number.isFinite(fnLine)) {
|
|
328
|
+
// Unparseable block id — record an unresolved key so it is reported, never
|
|
329
|
+
// dropped. Use the raw id as the key so duplicates collapse.
|
|
330
|
+
byFnKey.set(`#bad#${id}`, { filePath: filePath || id, symStart: NaN });
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const symStart = fnLine - 1; // 0-based symbol startLine (reverse [symStart+1])
|
|
334
|
+
byFnKey.set(`${filePath}#${symStart}`, { filePath, symStart });
|
|
335
|
+
}
|
|
336
|
+
const resolved = [];
|
|
337
|
+
let unresolvedCount = 0;
|
|
338
|
+
let ambiguousCount = 0;
|
|
339
|
+
await Promise.all(Array.from(byFnKey.values()).map(async ({ filePath, symStart }) => {
|
|
340
|
+
if (!Number.isFinite(symStart)) {
|
|
341
|
+
// Unparseable id — shadow-path unresolved under (best-effort) file.
|
|
342
|
+
resolved.push({ id: null, name: '(unresolved)', type: 'unresolved', filePath });
|
|
343
|
+
unresolvedCount += 1;
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// `Function`/`Method`/`Constructor` carry name+filePath+startLine; the schema has NO
|
|
347
|
+
// `startColumn`, so the join is on (filePath, startLine) only. `filePath`
|
|
348
|
+
// and `symStart` are BOUND as params (KTD11 — never interpolated). A
|
|
349
|
+
// UNION ALL across explicit labels is used rather than a
|
|
350
|
+
// `(s:Function OR s:Method OR s:Constructor)` disjunction (unsupported in the LadybugDB
|
|
351
|
+
// Cypher subset — the established cross-label pattern, see
|
|
352
|
+
// `enrichCandidateLabels`).
|
|
353
|
+
// FIX 6: do NOT swallow a query failure as `[]`. A DB error (lock /
|
|
354
|
+
// corruption / missing path) must NOT masquerade as a genuine no-owning-
|
|
355
|
+
// symbol result — that would silently inflate `unresolvedCount` and hide
|
|
356
|
+
// the failure. Letting it reject propagates through `Promise.all` →
|
|
357
|
+
// `projectBlocksToSymbols` → `_runImpactPDG` → `_impactImpl` up to the
|
|
358
|
+
// `impact()` structured-error catch, where it surfaces as a real error
|
|
359
|
+
// with a recovery suggestion (rather than a clean-looking partial radius).
|
|
360
|
+
const rows = await exec(lbugPath, `MATCH (s:\`Function\`)
|
|
361
|
+
WHERE s.filePath = $filePath AND s.startLine = $symStart
|
|
362
|
+
RETURN s.id AS id, s.name AS name, 'Function' AS label, s.startLine AS startLine
|
|
363
|
+
UNION ALL
|
|
364
|
+
MATCH (s:\`Method\`)
|
|
365
|
+
WHERE s.filePath = $filePath AND s.startLine = $symStart
|
|
366
|
+
RETURN s.id AS id, s.name AS name, 'Method' AS label, s.startLine AS startLine
|
|
367
|
+
UNION ALL
|
|
368
|
+
MATCH (s:\`Constructor\`)
|
|
369
|
+
WHERE s.filePath = $filePath AND s.startLine = $symStart
|
|
370
|
+
RETURN s.id AS id, s.name AS name, 'Constructor' AS label, s.startLine AS startLine`, { filePath, symStart });
|
|
371
|
+
if (rows.length === 0) {
|
|
372
|
+
// No owning symbol — top-level/free-statement block or a lambda whose
|
|
373
|
+
// start line ≠ a symbol startLine. Shadow path: report under its file.
|
|
374
|
+
resolved.push({
|
|
375
|
+
id: null,
|
|
376
|
+
name: '(unresolved)',
|
|
377
|
+
type: 'unresolved',
|
|
378
|
+
filePath,
|
|
379
|
+
startLine: symStart,
|
|
380
|
+
});
|
|
381
|
+
unresolvedCount += 1;
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// >1 ⇒ ambiguous-projection (same-line, different-name functions). Report
|
|
385
|
+
// ALL colliding symbols, NEVER silently pick one (R4 / Feasibility 1).
|
|
386
|
+
const isAmbiguous = rows.length > 1;
|
|
387
|
+
// Narrow the rows ONCE at the boundary to a typed record shape and read the
|
|
388
|
+
// aliased cells via bracket access (with positional `['0']`… fallback for a
|
|
389
|
+
// non-aliased row shape) — no per-field `as any`, matching the typed-row
|
|
390
|
+
// pattern used elsewhere in this file (e.g. lines ~264, ~1309, ~1386).
|
|
391
|
+
for (const r of rows) {
|
|
392
|
+
resolved.push({
|
|
393
|
+
id: String(r['id'] ?? r['0'] ?? ''),
|
|
394
|
+
name: String(r['name'] ?? r['1'] ?? ''),
|
|
395
|
+
type: String(r['label'] ?? r['2'] ?? 'Function'),
|
|
396
|
+
filePath,
|
|
397
|
+
startLine: Number(r['startLine'] ?? r['3'] ?? symStart),
|
|
398
|
+
...(isAmbiguous ? { ambiguous: true } : {}),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (isAmbiguous)
|
|
402
|
+
ambiguousCount += 1;
|
|
403
|
+
}));
|
|
404
|
+
// Deterministic order: by filePath, then startLine, then id (unresolved last
|
|
405
|
+
// within a file). Order-independence matters for the parity/fingerprint
|
|
406
|
+
// contract (KTD8 standing interchangeability) and for stable consumer output.
|
|
407
|
+
resolved.sort((a, b) => {
|
|
408
|
+
if (a.filePath !== b.filePath)
|
|
409
|
+
return a.filePath < b.filePath ? -1 : 1;
|
|
410
|
+
const al = a.startLine ?? Number.MAX_SAFE_INTEGER;
|
|
411
|
+
const bl = b.startLine ?? Number.MAX_SAFE_INTEGER;
|
|
412
|
+
if (al !== bl)
|
|
413
|
+
return al - bl;
|
|
414
|
+
const ai = a.id ?? '';
|
|
415
|
+
const bi = b.id ?? '';
|
|
416
|
+
return ai < bi ? -1 : ai > bi ? 1 : 0;
|
|
417
|
+
});
|
|
418
|
+
return { symbols: resolved, unresolvedCount, ambiguousCount };
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* The KTD8 parity fields a PDG impact result carries even when it short-circuits
|
|
422
|
+
* to an empty radius (degraded layer / no PDG body / no dependence reachability).
|
|
423
|
+
*
|
|
424
|
+
* A programmatic consumer iterating `byDepth`, reading `byDepthCounts[1]`, or
|
|
425
|
+
* coalescing `affected_processes`/`affected_modules` must find a well-formed
|
|
426
|
+
* (empty) shape on EVERY early return, not `undefined` (which would render as
|
|
427
|
+
* "isolated"/"no data" instead of "inconclusive"). The CLI branches on
|
|
428
|
+
* `pdgLayer` first so it is safe regardless, but the JSON contract must be
|
|
429
|
+
* uniform across all three early returns — this single source guarantees that.
|
|
430
|
+
*/
|
|
431
|
+
function emptyPdgParityFields() {
|
|
432
|
+
return {
|
|
433
|
+
byDepth: {},
|
|
434
|
+
byDepthCounts: { 1: 0 },
|
|
435
|
+
summary: { direct: 0, processes_affected: 0, modules_affected: 0 },
|
|
436
|
+
affected_processes: [],
|
|
437
|
+
affected_modules: [],
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
export function makePdgImpactErrorResult(input) {
|
|
441
|
+
return {
|
|
442
|
+
...(input.mode ? { mode: input.mode } : {}),
|
|
443
|
+
pdgResultVersion: PDG_RESULT_VERSION,
|
|
444
|
+
error: input.error,
|
|
445
|
+
target: input.target,
|
|
446
|
+
direction: input.direction,
|
|
447
|
+
impactedCount: 0,
|
|
448
|
+
risk: 'UNKNOWN',
|
|
449
|
+
...(input.suggestion ? { suggestion: input.suggestion } : {}),
|
|
450
|
+
...(input.recoverySuggestion ? { recoverySuggestion: input.recoverySuggestion } : {}),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
export function isPdgDegradedLayerStatus(layer) {
|
|
454
|
+
return layer.state !== 'ready';
|
|
455
|
+
}
|
|
456
|
+
export function makePdgLayerDegradedResult(input) {
|
|
457
|
+
return {
|
|
458
|
+
mode: input.mode,
|
|
459
|
+
pdgResultVersion: PDG_RESULT_VERSION,
|
|
460
|
+
pdgLayer: input.layer.state,
|
|
461
|
+
...(input.layer.missingSubLayer ? { missingSubLayer: input.layer.missingSubLayer } : {}),
|
|
462
|
+
...(input.layer.probeError ? { probeError: input.layer.probeError } : {}),
|
|
463
|
+
...(input.layer.recoverySuggestion
|
|
464
|
+
? { recoverySuggestion: input.layer.recoverySuggestion }
|
|
465
|
+
: {}),
|
|
466
|
+
note: input.layer.note,
|
|
467
|
+
target: input.target,
|
|
468
|
+
direction: input.direction,
|
|
469
|
+
impactedCount: 0,
|
|
470
|
+
risk: 'UNKNOWN',
|
|
471
|
+
...emptyPdgParityFields(),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Assemble the consumer-safe PDG impact result (U4 / KTD8 parity matrix).
|
|
476
|
+
*
|
|
477
|
+
* Takes the U3 traversal output (reachable block set + truncation signalling)
|
|
478
|
+
* plus the U4 block→symbol projection, and shapes a result STRUCTURALLY
|
|
479
|
+
* substitutable for the call-graph `_runImpactBFS` result so every consumer
|
|
480
|
+
* (CLI `formatImpactResult`, group `collectImpactSymbolUids`/`mergeRisk`,
|
|
481
|
+
* `impactByUid`) renders it without misrendering. This is a STANDING
|
|
482
|
+
* interchangeability contract, not a one-time check.
|
|
483
|
+
*
|
|
484
|
+
* Field-by-field vs the call-graph result (KTD8):
|
|
485
|
+
* - `target.id/name/type/filePath` — identical shape (`collectImpactSymbolUids`
|
|
486
|
+
* keys on `target.id`/`target.filePath`).
|
|
487
|
+
* - `byDepth` — same `{ [depth]: item[] }` map shape, but COLLAPSED to a single
|
|
488
|
+
* bucket (`1`): intra-procedural dependence has no meaningful inter-symbol hop
|
|
489
|
+
* count (block-hops are NOT call-hops). Items carry `{ id, name, type,
|
|
490
|
+
* filePath, … }` exactly like the call-graph items so `collectImpactSymbolUids`
|
|
491
|
+
* collects their UIDs. `unresolved` shadow-path entries keep `id: null` (they
|
|
492
|
+
* are surfaced, never dropped — but collect as no UID).
|
|
493
|
+
* - `byDepthCounts` — `{ 1: <symbolCount> }`, same shape.
|
|
494
|
+
* - `affected_processes` / `affected_modules` — empty `[]` (no
|
|
495
|
+
* STEP_IN_PROCESS/module edges originate from BasicBlocks; consumers coalesce
|
|
496
|
+
* `[]` safely).
|
|
497
|
+
* - `epistemic` — a PDG-specific marker (`'pdg-intra-procedural'`), NOT the
|
|
498
|
+
* callgraph DI/dynamic-dispatch `'lower-bound'` copy. `note` carries the
|
|
499
|
+
* PDG framing so the CLI prints PDG text, not callgraph boundary text.
|
|
500
|
+
* - `risk` — the existing `'UNKNOWN'` sentinel (NOT a new label). `mergeRisk`
|
|
501
|
+
* already coalesces `'UNKNOWN'` correctly (never a confident `LOW`).
|
|
502
|
+
* - `impactedCount` — count of DISTINCT owning SYMBOLS (resolved UIDs), the
|
|
503
|
+
* meaningful unit for the impact question ("which symbols are affected").
|
|
504
|
+
* `blockCount` is retained separately as the raw reachable-block count.
|
|
505
|
+
*/
|
|
506
|
+
function assemblePdgImpactResult(input) {
|
|
507
|
+
const { target, direction, reachableBlocks, projection } = input;
|
|
508
|
+
const { symbols, unresolvedCount, ambiguousCount } = projection;
|
|
509
|
+
const affectedStatements = input.affectedStatements ?? [];
|
|
510
|
+
const statementMode = typeof input.criterionLine === 'number';
|
|
511
|
+
// Items for the single collapsed bucket. Shaped like the call-graph byDepth
|
|
512
|
+
// items (`{ depth, id, name, type, filePath, processes }`) so consumers that
|
|
513
|
+
// iterate byDepth read the same fields. `unresolved` entries keep `id: null`
|
|
514
|
+
// (surfaced under their file; `collectImpactSymbolUids` skips a null id, which
|
|
515
|
+
// is correct — there is no symbol UID to attribute).
|
|
516
|
+
const items = symbols.map((s) => ({
|
|
517
|
+
depth: 1,
|
|
518
|
+
id: s.id,
|
|
519
|
+
name: s.name,
|
|
520
|
+
type: s.type,
|
|
521
|
+
filePath: s.filePath,
|
|
522
|
+
...(s.startLine !== undefined ? { startLine: s.startLine } : {}),
|
|
523
|
+
...(s.ambiguous ? { ambiguous: true } : {}),
|
|
524
|
+
...(s.id === null ? { unresolved: true } : {}),
|
|
525
|
+
pdgEvidence: (s.id === null ? 'degraded' : 'owner-projection'),
|
|
526
|
+
pdgEvidenceReason: s.id === null
|
|
527
|
+
? 'reachable BasicBlock has no owning Function/Method/Constructor projection'
|
|
528
|
+
: 'reachable BasicBlock projected to its owning symbol',
|
|
529
|
+
processes: [],
|
|
530
|
+
}));
|
|
531
|
+
// impactedCount = distinct owning SYMBOLS (resolved UIDs). Unresolved shadow
|
|
532
|
+
// entries are surfaced in byDepth but do NOT inflate the symbol count.
|
|
533
|
+
const resolvedUids = new Set(symbols.filter((s) => s.id !== null).map((s) => s.id));
|
|
534
|
+
const impactedCount = resolvedUids.size;
|
|
535
|
+
const byDepth = items.length > 0 ? { 1: items } : {};
|
|
536
|
+
const byDepthCounts = { 1: items.length };
|
|
537
|
+
const noteParts = statementMode
|
|
538
|
+
? [
|
|
539
|
+
`mode:'pdg' — intra-procedural slice from line ${input.criterionLine} of ` +
|
|
540
|
+
`'${target.name}'. ${affectedStatements.length} ` +
|
|
541
|
+
`${affectedStatements.length === 1 ? 'statement is' : 'statements are'} ${direction}-` +
|
|
542
|
+
`dependent on it (over CDG + REACHING_DEF). Inter-procedural symbol reach ` +
|
|
543
|
+
`is attached by impact mode's unified PDG dispatcher in interproceduralByDepth/byDepth.`,
|
|
544
|
+
]
|
|
545
|
+
: [
|
|
546
|
+
`mode:'pdg' — intra-procedural Program Dependence Graph. ${impactedCount} owning ` +
|
|
547
|
+
`${impactedCount === 1 ? 'symbol' : 'symbols'} reached via ${reachableBlocks.length} ` +
|
|
548
|
+
`dependence ${reachableBlocks.length === 1 ? 'block' : 'blocks'} ` +
|
|
549
|
+
`(${direction} over CDG + REACHING_DEF). Inter-procedural symbol reach ` +
|
|
550
|
+
`is attached by impact mode's unified PDG dispatcher in interproceduralByDepth/byDepth.`,
|
|
551
|
+
];
|
|
552
|
+
const interproceduralHops = input.interproceduralHops ?? 0;
|
|
553
|
+
if (interproceduralHops > 0) {
|
|
554
|
+
noteParts.push(`The statement slice (affectedStatements) crosses ${interproceduralHops} ` +
|
|
555
|
+
`inter-procedural ${interproceduralHops === 1 ? 'hop' : 'hops'} via resolved call ` +
|
|
556
|
+
`sites (HRB context-insensitive forward closure, downstream). SOUNDNESS CAVEATS: ` +
|
|
557
|
+
`(1) context-insensitive — a dependence may be attributed to a callee only reachable ` +
|
|
558
|
+
`from a DIFFERENT call site of the same function (bounded over-inclusion, the same ` +
|
|
559
|
+
`imprecision callgraph mode has); (2) return-value ascent IS captured (via ` +
|
|
560
|
+
`CALL_SUMMARY): a caller statement that depends on a callee's RETURN value is in the ` +
|
|
561
|
+
`slice when the callee has a persisted return-flow summary. Out-parameter / mutated-` +
|
|
562
|
+
`argument ascent, callee-written shared / captured variables, and exception ascent (a ` +
|
|
563
|
+
`throw the caller catches) remain deferred (they need an alias / try-catch model). A ` +
|
|
564
|
+
`PRE-FU-C (v3) --pdg index has no CALL_SUMMARY edges, so return-value ascent is absent ` +
|
|
565
|
+
`there until a re-index; (3) no cross-boundary alias ` +
|
|
566
|
+
`model; (4) precision is bounded by the call RESOLVER's precision (multi-candidate ` +
|
|
567
|
+
`dispatch / C++ overload under-resolution flow through faithfully — sound, never drops a ` +
|
|
568
|
+
`real target).`);
|
|
569
|
+
// FU-C degradation: the slice crossed a call boundary but this index predates
|
|
570
|
+
// CALL_SUMMARY (a v3 `--pdg` index). The intra slice is served, but no
|
|
571
|
+
// return-value ascent ran — steer to a re-index.
|
|
572
|
+
if (input.callSummaryAvailable === false) {
|
|
573
|
+
noteParts.push(`no return-value ascent (re-index for CALL_SUMMARY): this --pdg index predates the ` +
|
|
574
|
+
`FU-C return-value-ascent layer, so a caller statement depending on a callee's ` +
|
|
575
|
+
`RETURN value is NOT in the slice. Re-run gitnexus analyze --pdg to record ` +
|
|
576
|
+
`CALL_SUMMARY edges and enable it.`);
|
|
577
|
+
}
|
|
578
|
+
else if (input.callSummaryAvailable === true) {
|
|
579
|
+
// The CALL_SUMMARY layer is present, but return-value ascent is populated
|
|
580
|
+
// ONLY for TypeScript/JavaScript today (the formal-index it needs is set
|
|
581
|
+
// solely by the TS/JS harvester). For a criterion in any other language the
|
|
582
|
+
// ascent is structurally empty, so say so rather than letting the omission
|
|
583
|
+
// read as "ascent ran and found nothing". Sound — never claims ascent fired.
|
|
584
|
+
// Language is derived HERE in mcp/local, which may name languages; the
|
|
585
|
+
// shared core/ingestion pipeline must not.
|
|
586
|
+
const lang = getProviderForFile(target.filePath)?.id;
|
|
587
|
+
const ascentLanguage = lang === SupportedLanguages.TypeScript || lang === SupportedLanguages.JavaScript;
|
|
588
|
+
if (!ascentLanguage) {
|
|
589
|
+
noteParts.push(`return-value ascent is currently TypeScript/JavaScript-only (only the TS/JS harvester ` +
|
|
590
|
+
`records the formal-index it needs), so a caller statement depending on a non-TS/JS ` +
|
|
591
|
+
`callee's RETURN value is not in the slice. Descent and the intra slice are unaffected.`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (ambiguousCount > 0) {
|
|
596
|
+
noteParts.push(`${ambiguousCount} owning-symbol ${ambiguousCount === 1 ? 'projection is' : 'projections are'} ` +
|
|
597
|
+
`ambiguous: same-line functions cannot be disambiguated by start line alone (no startColumn ` +
|
|
598
|
+
`in the schema), so ALL colliding symbols are reported — none is silently picked.`);
|
|
599
|
+
}
|
|
600
|
+
if (unresolvedCount > 0) {
|
|
601
|
+
noteParts.push(`${unresolvedCount} reachable ${unresolvedCount === 1 ? 'block maps' : 'blocks map'} to no ` +
|
|
602
|
+
`owning Function/Method/Constructor (top-level statement or a lambda whose start line is not a symbol ` +
|
|
603
|
+
`start) — surfaced under their file as 'unresolved', never dropped.`);
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
mode: 'pdg',
|
|
607
|
+
pdgResultVersion: PDG_RESULT_VERSION,
|
|
608
|
+
target,
|
|
609
|
+
direction,
|
|
610
|
+
impactedCount,
|
|
611
|
+
// KTD8: reuse the existing UNKNOWN sentinel — never a confident LOW (which
|
|
612
|
+
// would read as "safe to refactor"; #2129/#1858 false-safe lineage). PDG
|
|
613
|
+
// mode is intra-procedural, so its count is a per-function lower bound on the
|
|
614
|
+
// true blast radius and risk is genuinely UNKNOWN at the program level.
|
|
615
|
+
risk: 'UNKNOWN',
|
|
616
|
+
// PDG-specific epistemic marker — NOT the callgraph 'lower-bound'/DI copy.
|
|
617
|
+
epistemic: 'pdg-intra-procedural',
|
|
618
|
+
note: noteParts.join(' '),
|
|
619
|
+
pdgEvidence: {
|
|
620
|
+
statements: 'local-dependence',
|
|
621
|
+
localSymbols: unresolvedCount > 0 ? 'degraded' : 'owner-projection',
|
|
622
|
+
localSymbolCount: impactedCount,
|
|
623
|
+
unresolvedBlockCount: unresolvedCount,
|
|
624
|
+
ambiguousProjectionCount: ambiguousCount,
|
|
625
|
+
},
|
|
626
|
+
// Statement-level slice: the dependent source statements (line + text) the
|
|
627
|
+
// change reaches. This is the primary useful output of statement mode; the
|
|
628
|
+
// accuracy harness scores against these lines.
|
|
629
|
+
...(statementMode ? { criterionLine: input.criterionLine } : {}),
|
|
630
|
+
affectedStatements,
|
|
631
|
+
affectedStatementCount: affectedStatements.length,
|
|
632
|
+
// Raw block-level detail retained alongside the symbol projection (U3 tests
|
|
633
|
+
// and the accuracy harness read these).
|
|
634
|
+
reachableBlocks,
|
|
635
|
+
intraReachableBlocks: input.intraReachableBlocks,
|
|
636
|
+
seedBlocks: input.seedBlocks,
|
|
637
|
+
blockCount: reachableBlocks.length,
|
|
638
|
+
depthReached: input.depthReached,
|
|
639
|
+
unresolvedBlockCount: unresolvedCount,
|
|
640
|
+
ambiguousProjectionCount: ambiguousCount,
|
|
641
|
+
...(input.truncated ? { truncated: true } : {}),
|
|
642
|
+
...(input.truncatedBy ? { truncatedBy: input.truncatedBy } : {}),
|
|
643
|
+
...(input.truncatedByReasons ? { truncatedByReasons: input.truncatedByReasons } : {}),
|
|
644
|
+
summary: {
|
|
645
|
+
direct: impactedCount,
|
|
646
|
+
processes_affected: 0,
|
|
647
|
+
modules_affected: 0,
|
|
648
|
+
},
|
|
649
|
+
byDepthCounts,
|
|
650
|
+
affected_processes: [],
|
|
651
|
+
affected_modules: [],
|
|
652
|
+
byDepth,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Validate the `impact` `mode` param (KTD5 — backend hard-gate).
|
|
657
|
+
*
|
|
658
|
+
* The MCP JSON-schema `enum` is advisory only (server.ts forwards args
|
|
659
|
+
* unvalidated and `callTool` is reachable directly), so this backend check is
|
|
660
|
+
* the real boundary — mirroring `_pdgQueryImpl`'s `mode` enum validation. A
|
|
661
|
+
* typo'd mode silently running callgraph is exactly the silent fallback this
|
|
662
|
+
* forbids (it would make the accuracy harness compare callgraph-vs-callgraph
|
|
663
|
+
* and report perfect parity).
|
|
664
|
+
*
|
|
665
|
+
* Absent / `undefined` / `'callgraph'` all resolve to `'callgraph'` (the
|
|
666
|
+
* unchanged default path). `'pdg'` is valid. Anything else — `'PDG'`, `'pgd'`,
|
|
667
|
+
* `''`, or a non-string (`0`, `null`, …) — returns a structured `{ error }`,
|
|
668
|
+
* never a callgraph result.
|
|
669
|
+
*/
|
|
670
|
+
export function validateImpactMode(rawMode) {
|
|
671
|
+
if (rawMode === undefined || rawMode === 'callgraph')
|
|
672
|
+
return { mode: 'callgraph' };
|
|
673
|
+
if (rawMode === 'pdg')
|
|
674
|
+
return { mode: 'pdg' };
|
|
675
|
+
return {
|
|
676
|
+
error: `Invalid "mode": expected "callgraph" or "pdg", got ${JSON.stringify(rawMode)}.`,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Read the two PDG sub-layer caps from the on-disk `meta.json` stamp — the
|
|
681
|
+
* single shared meta-probe both `_pdgQueryImpl` (one cap) and the PDG impact
|
|
682
|
+
* mode (both caps) key on. Never scans the DB. An unreadable / missing meta
|
|
683
|
+
* yields `metaReadable: false` (the `'unknown'` seam); a readable meta with no
|
|
684
|
+
* `pdg` stamp yields `metaReadable: true` with both caps `false` (no-layer).
|
|
685
|
+
*/
|
|
686
|
+
async function readPdgMetaCaps(lbugPath, loadMetaFn) {
|
|
687
|
+
try {
|
|
688
|
+
const meta = await loadMetaFn(path.dirname(lbugPath));
|
|
689
|
+
if (!meta)
|
|
690
|
+
return { metaReadable: false, cdg: false, rd: false, callSummary: false };
|
|
691
|
+
return {
|
|
692
|
+
metaReadable: true,
|
|
693
|
+
cdg: meta.pdg?.maxCdgEdgesPerFunction !== undefined,
|
|
694
|
+
rd: meta.pdg?.maxReachingDefEdgesPerFunction !== undefined,
|
|
695
|
+
callSummary: meta.pdg?.hasCallSummary === true,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
// Meta unreadable — the caller decides from the DB (the `'unknown'` state).
|
|
700
|
+
return { metaReadable: false, cdg: false, rd: false, callSummary: false };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Project the both-caps PDG meta read down to the single mode-relevant cap that
|
|
705
|
+
* `_pdgQueryImpl` keys on (`controls` → CDG, `flows` → REACHING_DEF), preserving
|
|
706
|
+
* its established tri-state `boolean | undefined` contract byte-for-byte
|
|
707
|
+
* (Feasibility Issue 4):
|
|
708
|
+
* - `false` — meta readable and the relevant cap absent → definitive
|
|
709
|
+
* no-layer (short-circuits before any DB scan).
|
|
710
|
+
* - `true` — meta readable and the relevant cap present → proceed.
|
|
711
|
+
* - `undefined` — meta unreadable → defer to the post-anchored-query probe.
|
|
712
|
+
*
|
|
713
|
+
* `_pdgQueryImpl` needs only ONE cap, so it collapses the both-caps read here
|
|
714
|
+
* rather than consuming `pdgLayerStatus` directly (whose `'unknown'` state does
|
|
715
|
+
* an upfront global probe — wrong timing/order for the anchored-query path).
|
|
716
|
+
*/
|
|
717
|
+
export async function pdgStampForMode(lbugPath, mode, loadMetaFn = loadMeta) {
|
|
718
|
+
const caps = await readPdgMetaCaps(lbugPath, loadMetaFn);
|
|
719
|
+
if (!caps.metaReadable)
|
|
720
|
+
return undefined;
|
|
721
|
+
return mode === 'controls' ? caps.cdg : caps.rd;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* PDG-layer presence/degradation check for the `impact` PDG mode (KTD7).
|
|
725
|
+
*
|
|
726
|
+
* Returns the four distinct states WITHOUT scanning the DB except for the single
|
|
727
|
+
* bounded `LIMIT 1` probe the `'unknown'` (meta-unreadable) case requires. The
|
|
728
|
+
* caller (`_impactImpl` PDG branch, and the accuracy harness) surfaces a
|
|
729
|
+
* distinct signal per state so a missing `--pdg` layer / partial layer is never
|
|
730
|
+
* silently misread as a confident empty blast radius. Impact needs BOTH the CDG
|
|
731
|
+
* and the REACHING_DEF sub-layer, so a partial stamp degrades, not proceeds.
|
|
732
|
+
*/
|
|
733
|
+
export async function pdgLayerStatus(deps) {
|
|
734
|
+
const loadMetaFn = deps.loadMetaFn ?? loadMeta;
|
|
735
|
+
const caps = await readPdgMetaCaps(deps.lbugPath, loadMetaFn);
|
|
736
|
+
if (caps.metaReadable) {
|
|
737
|
+
// Meta is readable — the stamp is authoritative, no DB scan needed.
|
|
738
|
+
// `hasCallSummary` rides on every meta-readable state (it is NOT part of the
|
|
739
|
+
// `'ready'` gate — a v3 index without it is still ready for the intra slice).
|
|
740
|
+
if (caps.cdg && caps.rd)
|
|
741
|
+
return { state: 'ready', hasCallSummary: caps.callSummary };
|
|
742
|
+
if (caps.cdg !== caps.rd) {
|
|
743
|
+
// Exactly one sub-layer stamped (XOR) — partial layer; impact needs both.
|
|
744
|
+
const missingSubLayer = caps.cdg ? 'REACHING_DEF' : 'CDG';
|
|
745
|
+
return {
|
|
746
|
+
state: 'sub-layer-missing',
|
|
747
|
+
missingSubLayer,
|
|
748
|
+
hasCallSummary: caps.callSummary,
|
|
749
|
+
note: `PDG layer is incomplete — the ${missingSubLayer} sub-layer is missing ` +
|
|
750
|
+
`(impact's PDG mode needs both CDG and REACHING_DEF). ` +
|
|
751
|
+
`Re-run gitnexus analyze --pdg to record it.`,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
// Neither cap stamped (meta.pdg absent, or present with no caps) → the layer
|
|
755
|
+
// was never recorded. Definitive, no DB scan.
|
|
756
|
+
return {
|
|
757
|
+
state: 'no-layer',
|
|
758
|
+
hasCallSummary: caps.callSummary,
|
|
759
|
+
note: 'no PDG layer — run gitnexus analyze --pdg to record CDG + REACHING_DEF edges for this repo',
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
// Meta unreadable (e.g. a seeded test DB): one bounded probe confirms the
|
|
763
|
+
// layer status is genuinely undeterminable from the DB. A missing layer is
|
|
764
|
+
// indistinguishable from an all-linear (edge-free) one (#2188), so whether the
|
|
765
|
+
// probe finds a row or not the state stays `'unknown'` (never the definitive
|
|
766
|
+
// no-layer wording). The probe is bounded (`LIMIT 1`) and anchored on the
|
|
767
|
+
// BasicBlock→BasicBlock partition (the `(:BasicBlock)…(:BasicBlock)` label pair
|
|
768
|
+
// restricts it to the sparse pdg-edge partition, never a global rel scan — the
|
|
769
|
+
// established `_explainImpl` anchoring pattern), and it is wrapped so a db-lock
|
|
770
|
+
// / missing-path throw degrades to the same `'unknown'` signal rather than
|
|
771
|
+
// propagating and losing it.
|
|
772
|
+
//
|
|
773
|
+
// The probe result is NOT discarded: a visible CDG/REACHING_DEF edge (with
|
|
774
|
+
// meta unreadable) is a weak-but-real "edges are present, but completeness is
|
|
775
|
+
// unprovable" signal, distinct from "no edges visible at all". Both stay
|
|
776
|
+
// `'unknown'` (inconclusive), but the note distinguishes them so the operator
|
|
777
|
+
// gets the more useful hint.
|
|
778
|
+
let edgesVisible = false;
|
|
779
|
+
let probeError;
|
|
780
|
+
try {
|
|
781
|
+
const rows = await deps.executeParameterized(deps.lbugPath, `MATCH (:BasicBlock)-[r:CodeRelation]->(:BasicBlock) WHERE r.type IN ['CDG', 'REACHING_DEF'] RETURN r.type AS type LIMIT 1`, {});
|
|
782
|
+
edgesVisible = Array.isArray(rows) && rows.length > 0;
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
// db-lock / missing-path / corrupt probe — fall through as not-visible, but
|
|
786
|
+
// keep the `'unknown'` signal AND preserve the probe failure. Reporting the
|
|
787
|
+
// failed probe as "no edges visible" hides a DB-health problem from operators.
|
|
788
|
+
probeError = err instanceof Error ? err.message : String(err);
|
|
789
|
+
edgesVisible = false;
|
|
790
|
+
}
|
|
791
|
+
if (probeError) {
|
|
792
|
+
return {
|
|
793
|
+
state: 'unknown',
|
|
794
|
+
probeError,
|
|
795
|
+
recoverySuggestion: 'Check for a LadybugDB lock/corruption or missing index path. Stop overlapping GitNexus processes, retry, or re-run gitnexus analyze --pdg.',
|
|
796
|
+
note: `PDG layer status unknown — CDG/REACHING_DEF probe failed: ${probeError}. ` +
|
|
797
|
+
`The layer cannot be confirmed complete; this is distinct from "no edges visible".`,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
state: 'unknown',
|
|
802
|
+
note: edgesVisible
|
|
803
|
+
? 'PDG layer status unknown — CDG/REACHING_DEF edges ARE visible but meta is unreadable, so the layer cannot be confirmed complete (a partial layer looks the same); was this repo fully indexed with gitnexus analyze --pdg?'
|
|
804
|
+
: 'PDG layer status unknown — no CDG/REACHING_DEF edges visible and meta is unreadable; was this repo indexed with gitnexus analyze --pdg?',
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Build the SAME BasicBlock seed anchor (`anchorClause` + `queryParams`) as
|
|
809
|
+
* `resolveBlockAnchor`'s symbol branch, but from an ALREADY-RESOLVED symbol —
|
|
810
|
+
* WITHOUT re-running `resolveSymbolCandidates`.
|
|
811
|
+
*
|
|
812
|
+
* Why this exists (correctness keystone): `_impactImpl` already resolves the
|
|
813
|
+
* target to a confident single symbol honoring the caller's
|
|
814
|
+
* `target_uid`/`file_path`/`kind` hints. Re-resolving by the bare `sym.name`
|
|
815
|
+
* inside `_runImpactPDG` would (a) RE-AMBIGUATE a globally-ambiguous name the
|
|
816
|
+
* caller had disambiguated (returning the "ambiguous" early payload instead of
|
|
817
|
+
* the PDG result), or (b) anchor the seed on a DIFFERENT same-name symbol in
|
|
818
|
+
* another file → a wrong-symbol blast radius. Anchoring directly from the
|
|
819
|
+
* resolved `{ filePath, startLine, endLine }` preserves the disambiguation.
|
|
820
|
+
*
|
|
821
|
+
* The window is byte-identical to `resolveBlockAnchor`'s symbol branch: BOTH
|
|
822
|
+
* span bounds are shifted `+1` (1-based BasicBlock `startLine` vs the 0-based
|
|
823
|
+
* symbol span — the lower `+1` excludes a neighbor's block on the line above,
|
|
824
|
+
* the upper `+1` keeps a guard/def/use on the final line). A symbol with no
|
|
825
|
+
* usable span degrades to the same file-level id-prefix filter. This is the
|
|
826
|
+
* resolved-symbol counterpart, NOT a second window convention.
|
|
827
|
+
*/
|
|
828
|
+
function blockAnchorForResolvedSymbol(sym) {
|
|
829
|
+
const idPrefix = `BasicBlock:${sym.filePath}:`;
|
|
830
|
+
if (typeof sym.startLine === 'number' &&
|
|
831
|
+
typeof sym.endLine === 'number' &&
|
|
832
|
+
sym.endLine >= sym.startLine) {
|
|
833
|
+
return {
|
|
834
|
+
anchorClause: 'a.id STARTS WITH $idPrefix AND a.startLine >= $symStart AND a.startLine <= $symEnd',
|
|
835
|
+
queryParams: { idPrefix, symStart: sym.startLine + 1, symEnd: sym.endLine + 1 },
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
return { anchorClause: 'a.id STARTS WITH $idPrefix', queryParams: { idPrefix } };
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Build a STATEMENT seed anchor: the BasicBlock(s) starting at a specific
|
|
842
|
+
* 1-based source `line` WITHIN the resolved symbol. This is what makes
|
|
843
|
+
* `mode:'pdg'` useful — seeding the dependence slice on a single statement
|
|
844
|
+
* (the thing being changed) rather than the whole symbol. A whole-symbol seed
|
|
845
|
+
* captures every intra-procedural block, so the reachable-minus-seed set is
|
|
846
|
+
* empty (all intra reach is within the seed); a statement seed leaves the
|
|
847
|
+
* other dependent statements reachable. `BasicBlock.startLine` is 1-based and
|
|
848
|
+
* matches the source line, so no `+1` offset applies here (unlike the symbol
|
|
849
|
+
* span, where the 0-based symbol bounds are shifted). Bounded to the symbol's
|
|
850
|
+
* own span when known, so a line shared with a sibling symbol can't leak.
|
|
851
|
+
*/
|
|
852
|
+
function blockAnchorForStatement(sym, line) {
|
|
853
|
+
const idPrefix = `BasicBlock:${sym.filePath}:`;
|
|
854
|
+
if (typeof sym.startLine === 'number' &&
|
|
855
|
+
typeof sym.endLine === 'number' &&
|
|
856
|
+
sym.endLine >= sym.startLine) {
|
|
857
|
+
return {
|
|
858
|
+
anchorClause: 'a.id STARTS WITH $idPrefix AND a.startLine = $line AND a.startLine >= $symStart AND a.startLine <= $symEnd',
|
|
859
|
+
queryParams: { idPrefix, line, symStart: sym.startLine + 1, symEnd: sym.endLine + 1 },
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
anchorClause: 'a.id STARTS WITH $idPrefix AND a.startLine = $line',
|
|
864
|
+
queryParams: { idPrefix, line },
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* One bounded, direction-aware BFS over CDG + REACHING_DEF starting from a set
|
|
869
|
+
* of seed blocks. Extracted (U1) from `runImpactPDG`'s intra loop so the
|
|
870
|
+
* inter-procedural descent reuses the EXACT same edge query and step/limit
|
|
871
|
+
* semantics — no reimplementation. The query, the one-past-`stepLimit` probe,
|
|
872
|
+
* the per-step truncation flag, and the depth-exhaustion flag are byte-identical
|
|
873
|
+
* to the original inline loop.
|
|
874
|
+
*
|
|
875
|
+
* `visited` is the caller's shared cycle/recursion guard: seeds are pre-added so
|
|
876
|
+
* they are never re-collected (the seed-minus-reachable convention), and any
|
|
877
|
+
* block already visited (a prior callee's seed, or already-reached) is skipped.
|
|
878
|
+
* Newly-discovered blocks are added to `visited` AND returned in `reachable`.
|
|
879
|
+
*/
|
|
880
|
+
async function bfsReachableBlocks(input) {
|
|
881
|
+
const { lbugPath, exec, seedBlocks, visited, direction, depthBudget, stepLimit, probeLimit } = input;
|
|
882
|
+
const reachable = new Set();
|
|
883
|
+
// Seeds are pre-added to `visited` so they are never re-COLLECTED (the
|
|
884
|
+
// seed-minus-reachable convention), but the seed list still drives the first
|
|
885
|
+
// step's frontier — the guard blocks re-collection, not seed re-expansion.
|
|
886
|
+
for (const id of seedBlocks)
|
|
887
|
+
visited.add(id);
|
|
888
|
+
let frontier = [...seedBlocks];
|
|
889
|
+
let depthReached = 0;
|
|
890
|
+
let truncatedByDepth = false;
|
|
891
|
+
let truncatedByLimit = false;
|
|
892
|
+
const matchEndpoint = direction === 'downstream' ? 'a' : 'b';
|
|
893
|
+
const collectEndpoint = direction === 'downstream' ? 'b' : 'a';
|
|
894
|
+
for (let depth = 0; depth < depthBudget; depth++) {
|
|
895
|
+
if (frontier.length === 0)
|
|
896
|
+
break;
|
|
897
|
+
const rawRows = await exec(lbugPath, `MATCH (a:BasicBlock)-[r:CodeRelation]->(b:BasicBlock)
|
|
898
|
+
WHERE r.type IN ['CDG', 'REACHING_DEF'] AND ${matchEndpoint}.id IN $frontier
|
|
899
|
+
RETURN DISTINCT ${collectEndpoint}.id AS id
|
|
900
|
+
LIMIT ${probeLimit}`, { frontier });
|
|
901
|
+
// Narrow the awaited rows ONCE at the boundary (executeParameterized returns
|
|
902
|
+
// any[]) to a typed record shape, then read the aliased `id` via bracket
|
|
903
|
+
// access — no `as any` sprayed per field.
|
|
904
|
+
const rows = rawRows.slice(0, stepLimit);
|
|
905
|
+
depthReached = depth + 1;
|
|
906
|
+
if (rawRows.length > stepLimit)
|
|
907
|
+
truncatedByLimit = true;
|
|
908
|
+
const next = [];
|
|
909
|
+
for (const r of rows) {
|
|
910
|
+
const id = String(r['id'] ?? '');
|
|
911
|
+
if (!id || visited.has(id))
|
|
912
|
+
continue;
|
|
913
|
+
visited.add(id);
|
|
914
|
+
reachable.add(id);
|
|
915
|
+
next.push(id);
|
|
916
|
+
}
|
|
917
|
+
frontier = next;
|
|
918
|
+
}
|
|
919
|
+
if (frontier.length > 0)
|
|
920
|
+
truncatedByDepth = true;
|
|
921
|
+
return { reachable, depthReached, truncatedByDepth, truncatedByLimit };
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Gather the resolved callee symbol ids invoked across a set of slice blocks
|
|
925
|
+
* (`BasicBlock.calleeIds`). Reuses the SHARED `splitCalleeIds` so the descent
|
|
926
|
+
* cannot diverge from `LocalBackend.calleeIdsOfBlocks` on the split/drop-sentinel
|
|
927
|
+
* logic. A pre-namespace-v4 index (no `calleeIds` column → empty cells) yields no
|
|
928
|
+
* ids, so the descent degrades cleanly to intra-only (no inter-procedural hop).
|
|
929
|
+
*/
|
|
930
|
+
async function calleeIdsFromBlocks(lbugPath, blockIds, exec) {
|
|
931
|
+
const ids = new Set();
|
|
932
|
+
for (const { calleeIds } of await calleeIdsByBlock(lbugPath, blockIds, exec)) {
|
|
933
|
+
for (const id of calleeIds)
|
|
934
|
+
ids.add(id);
|
|
935
|
+
}
|
|
936
|
+
return ids;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Per-block variant of {@link calleeIdsFromBlocks}: keep the CALL block → its
|
|
940
|
+
* `calleeIds` mapping rather than flattening it. The return-value ascent (U-C4)
|
|
941
|
+
* needs this association — it re-seeds the caller's intra closure FROM the
|
|
942
|
+
* specific call block whose callee's `CALL_SUMMARY` licenses the ascent, so the
|
|
943
|
+
* flattened id-only set is insufficient. Reuses the SHARED `splitCalleeIds` so
|
|
944
|
+
* the split/drop-sentinel logic cannot diverge from the flattening caller. A
|
|
945
|
+
* block with no callee ids (empty/whitespace cell, or a pre-v4 index with no
|
|
946
|
+
* `calleeIds` column) yields an empty `calleeIds` — skipped by the consumer.
|
|
947
|
+
*/
|
|
948
|
+
async function calleeIdsByBlock(lbugPath, blockIds, exec) {
|
|
949
|
+
if (blockIds.length === 0)
|
|
950
|
+
return [];
|
|
951
|
+
const rows = await exec(lbugPath, `MATCH (b:BasicBlock) WHERE b.id IN $ids RETURN b.id AS id, b.calleeIds AS calleeIds`, { ids: blockIds });
|
|
952
|
+
const out = [];
|
|
953
|
+
// Narrow the awaited rows ONCE at the boundary to a typed record shape; read
|
|
954
|
+
// the aliased cells via bracket access — no per-field `as any`.
|
|
955
|
+
for (const r of rows) {
|
|
956
|
+
const blockId = String(r['id'] ?? '');
|
|
957
|
+
if (!blockId)
|
|
958
|
+
continue;
|
|
959
|
+
const calleeIds = splitCalleeIds(r['calleeIds']);
|
|
960
|
+
if (calleeIds.length > 0)
|
|
961
|
+
out.push({ blockId, calleeIds });
|
|
962
|
+
}
|
|
963
|
+
return out;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Of a set of resolved callee symbol ids, which ones have a persisted
|
|
967
|
+
* `CALL_SUMMARY` self-loop edge recording a NON-EMPTY return-value ascent
|
|
968
|
+
* (≥1 formal parameter flows to the callee's return). This is the FU-C consumer
|
|
969
|
+
* side of the producer's per-callee summary (see `call-summary-codec.ts`).
|
|
970
|
+
*
|
|
971
|
+
* The summary is a self-loop on the Function/Method/Constructor node:
|
|
972
|
+
* `(c)-[r:CodeRelation {type:'CALL_SUMMARY'}]->(c) WHERE c.id IN $ids`. The
|
|
973
|
+
* `reason` carries the param→return bitset; `decodeCallSummary` unpacks it and
|
|
974
|
+
* NEVER throws — a malformed / absent / empty (`r:0`) summary yields NO entry
|
|
975
|
+
* (the sound default: never claim a false return-flow). A PRE-FU-C (v3) `--pdg`
|
|
976
|
+
* index has NO `CALL_SUMMARY` edges, so this returns the empty set and the
|
|
977
|
+
* ascent is a clean no-op (the intra slice is unchanged — the documented
|
|
978
|
+
* "re-index for CALL_SUMMARY" degradation).
|
|
979
|
+
*/
|
|
980
|
+
async function calleesWithReturnFlow(lbugPath, calleeIds, exec) {
|
|
981
|
+
const out = new Set();
|
|
982
|
+
if (calleeIds.length === 0)
|
|
983
|
+
return out;
|
|
984
|
+
const rows = await exec(lbugPath, `MATCH (c)-[r:CodeRelation]->(c)
|
|
985
|
+
WHERE r.type = 'CALL_SUMMARY' AND c.id IN $ids
|
|
986
|
+
RETURN c.id AS id, r.reason AS reason`, { ids: calleeIds });
|
|
987
|
+
for (const r of rows) {
|
|
988
|
+
const id = String(r['id'] ?? '');
|
|
989
|
+
if (!id)
|
|
990
|
+
continue;
|
|
991
|
+
const decoded = decodeCallSummary(r['reason']);
|
|
992
|
+
// ARG→FORMAL trace precision: the conservative-but-sound default — ascend if
|
|
993
|
+
// ANY formal is return-flowing (the call site's argument is, by construction
|
|
994
|
+
// of the descent, in the slice: the call block is itself a slice block). A
|
|
995
|
+
// specific positional arg↔formal mapping is not cleanly recoverable at the
|
|
996
|
+
// coalesced call block (`BasicBlock.calleeIds` is an unordered set, not a
|
|
997
|
+
// per-arg list), so this never drops a real ascent; it may over-include
|
|
998
|
+
// (bounded — the result still flows to a slice statement). See the descent
|
|
999
|
+
// doc-comment + the result `note` caveat.
|
|
1000
|
+
if (decoded.ok && decoded.returnFlowParams.length > 0)
|
|
1001
|
+
out.add(id);
|
|
1002
|
+
}
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Batch-resolve resolved callee symbol ids → their `{id,filePath,startLine,endLine}`
|
|
1007
|
+
* spans via ONE `s.id IN $ids` UNION-ALL query over Function/Method/Constructor —
|
|
1008
|
+
* the SAME query shape as `projectBlocksToSymbols`, but keyed on the RESOLVED
|
|
1009
|
+
* `s.id` (no same-line ambiguity, unlike the reverse block-to-symbol join). Ids
|
|
1010
|
+
* with no matching symbol (a callee resolved to a node kind without a CFG body,
|
|
1011
|
+
* or an out-of-repo id) simply produce no span and are skipped — never an error.
|
|
1012
|
+
*/
|
|
1013
|
+
async function resolveCalleeSpans(lbugPath, calleeIds, exec) {
|
|
1014
|
+
if (calleeIds.length === 0)
|
|
1015
|
+
return [];
|
|
1016
|
+
const rows = await exec(lbugPath, `MATCH (s:\`Function\`) WHERE s.id IN $ids
|
|
1017
|
+
RETURN s.id AS id, s.filePath AS filePath, s.startLine AS startLine, s.endLine AS endLine
|
|
1018
|
+
UNION ALL
|
|
1019
|
+
MATCH (s:\`Method\`) WHERE s.id IN $ids
|
|
1020
|
+
RETURN s.id AS id, s.filePath AS filePath, s.startLine AS startLine, s.endLine AS endLine
|
|
1021
|
+
UNION ALL
|
|
1022
|
+
MATCH (s:\`Constructor\`) WHERE s.id IN $ids
|
|
1023
|
+
RETURN s.id AS id, s.filePath AS filePath, s.startLine AS startLine, s.endLine AS endLine`, { ids: calleeIds });
|
|
1024
|
+
const spans = [];
|
|
1025
|
+
// Narrow the awaited rows ONCE at the boundary to a typed record shape; read
|
|
1026
|
+
// the aliased columns via bracket access with Number()/String() coercion —
|
|
1027
|
+
// no per-field `as any` (the same boundary-narrowing the typed helpers use).
|
|
1028
|
+
for (const r of rows) {
|
|
1029
|
+
const id = String(r['id'] ?? '');
|
|
1030
|
+
const filePath = String(r['filePath'] ?? '');
|
|
1031
|
+
const startLine = Number(r['startLine']);
|
|
1032
|
+
const endLine = Number(r['endLine']);
|
|
1033
|
+
if (!id || !filePath || !Number.isFinite(startLine) || !Number.isFinite(endLine))
|
|
1034
|
+
continue;
|
|
1035
|
+
spans.push({ id, filePath, startLine, endLine });
|
|
1036
|
+
}
|
|
1037
|
+
return spans;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Bounded inter-procedural forward closure (U1) — HRB context-INSENSITIVE
|
|
1041
|
+
* forward slicing, the shipped Joern approach. Starting from the intra-procedural
|
|
1042
|
+
* slice blocks, descend DOWNSTREAM through resolved call sites: per hop, gather
|
|
1043
|
+
* the slice blocks' `calleeIds`, resolve them to callee spans, seed each callee's
|
|
1044
|
+
* blocks, run the SAME CDG+REACHING_DEF BFS within the callee, and union the
|
|
1045
|
+
* newly-reachable blocks into the slice. Recurses to `depthBudget` FUNCTION hops.
|
|
1046
|
+
*
|
|
1047
|
+
* Termination is guaranteed by the shared `visited` set (a block is expanded at
|
|
1048
|
+
* most once across all hops) plus the depth cap. A total node cap (`nodeBudget`)
|
|
1049
|
+
* is the secondary guard against a pathological fan-out within the budget.
|
|
1050
|
+
*
|
|
1051
|
+
* Caveats (documented in the result note + bench/impact-pdg/README.md):
|
|
1052
|
+
* (1) context-insensitive — a dependence may be attributed to a callee only
|
|
1053
|
+
* reachable from a DIFFERENT call site of the same function (bounded
|
|
1054
|
+
* over-inclusion, the same imprecision callgraph mode already has);
|
|
1055
|
+
* (2) return-value ascent IS captured (via CALL_SUMMARY, U-C4): a caller
|
|
1056
|
+
* statement that depends on a callee's RETURN value is re-seeded into the
|
|
1057
|
+
* caller's continuation when the callee carries a persisted return-flow
|
|
1058
|
+
* summary. Out-parameter / mutated-argument ascent, callee-written shared /
|
|
1059
|
+
* captured variables, and exception ascent (a throw the caller catches)
|
|
1060
|
+
* remain deferred (they need an alias / try-catch model). A pre-FU-C (v3)
|
|
1061
|
+
* --pdg index has no CALL_SUMMARY edges → no return-value ascent there;
|
|
1062
|
+
* (3) no cross-boundary alias model;
|
|
1063
|
+
* (4) precision is bounded by the call RESOLVER's precision (multi-candidate
|
|
1064
|
+
* dispatch / C++ overload under-resolution flow through faithfully — sound,
|
|
1065
|
+
* never drops a real target).
|
|
1066
|
+
*/
|
|
1067
|
+
async function interproceduralDescent(input) {
|
|
1068
|
+
const { lbugPath, exec, visited, depthBudget, intraDepthBudget, stepLimit, probeLimit, nodeBudget, } = input;
|
|
1069
|
+
const reachable = new Set();
|
|
1070
|
+
let truncatedByDepth = false;
|
|
1071
|
+
let truncatedByLimit = false;
|
|
1072
|
+
let truncatedByNodeCap = false;
|
|
1073
|
+
let hopsReached = 0;
|
|
1074
|
+
// The slice the next hop gathers callees FROM — starts as the intra slice, then
|
|
1075
|
+
// becomes each hop's newly-reached callee blocks.
|
|
1076
|
+
let sliceBlocks = [...input.initialSliceBlocks];
|
|
1077
|
+
// Guard against re-seeding the same callee function across hops. Pre-seeded
|
|
1078
|
+
// with the statement seed's owner function so recursion never re-broadens it.
|
|
1079
|
+
const seededCalleeIds = new Set(input.preSeededCalleeIds ?? []);
|
|
1080
|
+
// U-C4 return-value ascent: CALL blocks whose callee has a non-empty
|
|
1081
|
+
// CALL_SUMMARY return-flow → the call's result depends on the slice.
|
|
1082
|
+
const ascentBlocks = new Set();
|
|
1083
|
+
hopLoop: for (let hop = 0; hop < depthBudget; hop++) {
|
|
1084
|
+
if (sliceBlocks.length === 0)
|
|
1085
|
+
break;
|
|
1086
|
+
// Keep the CALL block → callee association (U-C4 needs it to re-seed the
|
|
1087
|
+
// caller's intra closure FROM the specific call block the ascent licenses);
|
|
1088
|
+
// the flattened id set still drives the descent's fresh-callee bookkeeping.
|
|
1089
|
+
const blockCallees = await calleeIdsByBlock(lbugPath, sliceBlocks, exec);
|
|
1090
|
+
const calleeIds = new Set();
|
|
1091
|
+
for (const { calleeIds: ids } of blockCallees)
|
|
1092
|
+
for (const id of ids)
|
|
1093
|
+
calleeIds.add(id);
|
|
1094
|
+
// ── U-C4 ascent: re-seed the caller's intra closure THROUGH call results ──
|
|
1095
|
+
// For each call block in THIS hop's slice, if ANY of its callees' return
|
|
1096
|
+
// values flow back (a non-empty CALL_SUMMARY), the call result is
|
|
1097
|
+
// slice-dependent (the call block is itself a slice block). Re-seed a
|
|
1098
|
+
// BOUNDED downstream BFS FROM the call block so the caller's continuation
|
|
1099
|
+
// that consumes the result is captured. Monotone: only ADDS to `reachable`,
|
|
1100
|
+
// reusing the shared `visited` set, so it stays bounded + terminating. A
|
|
1101
|
+
// pre-v4 index (no CALL_SUMMARY) yields no return-flowing callees → no-op.
|
|
1102
|
+
const returnFlowing = await calleesWithReturnFlow(lbugPath, [...calleeIds], exec);
|
|
1103
|
+
if (returnFlowing.size > 0) {
|
|
1104
|
+
for (const { blockId, calleeIds: ids } of blockCallees) {
|
|
1105
|
+
// Bound the ascent re-seeds the same way the descent bounds its per-span
|
|
1106
|
+
// BFS (line ~1496): a wide fan-out of return-flowing call blocks must not
|
|
1107
|
+
// run unbounded re-seeds inside a single hop. Mirror the descent's
|
|
1108
|
+
// node-cap short-circuit at the TOP of the loop, stamping the same flag.
|
|
1109
|
+
if (reachable.size > nodeBudget) {
|
|
1110
|
+
truncatedByNodeCap = true;
|
|
1111
|
+
break hopLoop;
|
|
1112
|
+
}
|
|
1113
|
+
if (!ids.some((id) => returnFlowing.has(id)))
|
|
1114
|
+
continue;
|
|
1115
|
+
// The call block is in the slice by construction; record it so the
|
|
1116
|
+
// statement projection surfaces the call statements whose results chain
|
|
1117
|
+
// through it (a coalesced call block spans several call lines).
|
|
1118
|
+
ascentBlocks.add(blockId);
|
|
1119
|
+
// Continue the caller's intra closure from the call block. The block is
|
|
1120
|
+
// already in `visited` (it is a slice block), so the shared-visited BFS
|
|
1121
|
+
// only adds genuinely-new downstream caller blocks (e.g. a SEPARATE
|
|
1122
|
+
// statement that uses the call result), never re-expanding the seed.
|
|
1123
|
+
const ascent = await bfsReachableBlocks({
|
|
1124
|
+
lbugPath,
|
|
1125
|
+
exec,
|
|
1126
|
+
seedBlocks: [blockId],
|
|
1127
|
+
visited,
|
|
1128
|
+
direction: 'downstream',
|
|
1129
|
+
depthBudget: intraDepthBudget,
|
|
1130
|
+
stepLimit,
|
|
1131
|
+
probeLimit,
|
|
1132
|
+
});
|
|
1133
|
+
if (ascent.truncatedByLimit)
|
|
1134
|
+
truncatedByLimit = true;
|
|
1135
|
+
for (const id of ascent.reachable)
|
|
1136
|
+
reachable.add(id);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const freshIds = [...calleeIds].filter((id) => !seededCalleeIds.has(id));
|
|
1140
|
+
if (freshIds.length === 0)
|
|
1141
|
+
break;
|
|
1142
|
+
for (const id of freshIds)
|
|
1143
|
+
seededCalleeIds.add(id);
|
|
1144
|
+
const spans = await resolveCalleeSpans(lbugPath, freshIds, exec);
|
|
1145
|
+
if (spans.length === 0)
|
|
1146
|
+
break;
|
|
1147
|
+
hopsReached = hop + 1;
|
|
1148
|
+
// U13: fetch every callee's seed blocks CONCURRENTLY. The seed fetch was the
|
|
1149
|
+
// only per-span round-trip; running them in one wave collapses N sequential
|
|
1150
|
+
// round-trips. Each span runs the IDENTICAL query as before — same anchor,
|
|
1151
|
+
// same LIMIT, same slice — so the per-span seed set and the `exceeded`
|
|
1152
|
+
// (truncatedByLimit) flag are byte-identical; only the latency changes. The
|
|
1153
|
+
// flag is still APPLIED per span in the sequential loop below, so a span past
|
|
1154
|
+
// the node-budget short-circuit never sets it (prior semantics preserved).
|
|
1155
|
+
const spanSeeds = await Promise.all(spans.map(async (span) => {
|
|
1156
|
+
const { anchorClause, queryParams } = blockAnchorForResolvedSymbol(span);
|
|
1157
|
+
const rawSeedRows = await exec(lbugPath, `MATCH (a:BasicBlock) WHERE ${anchorClause} RETURN a.id AS id LIMIT ${probeLimit}`, queryParams);
|
|
1158
|
+
const exceeded = rawSeedRows.length > stepLimit;
|
|
1159
|
+
const seeds = rawSeedRows
|
|
1160
|
+
.slice(0, stepLimit)
|
|
1161
|
+
.map((r) => String(r['id'] ?? ''))
|
|
1162
|
+
.filter((id) => id.length > 0);
|
|
1163
|
+
return { exceeded, seeds };
|
|
1164
|
+
}));
|
|
1165
|
+
// U14: run each callee's BFS CONCURRENTLY against a PRIVATE clone of the
|
|
1166
|
+
// hop-start `visited` snapshot, then merge IN SPAN ORDER below. Cross-span
|
|
1167
|
+
// sharing of the mutable `visited` set was the ONLY thing forcing the BFS
|
|
1168
|
+
// sequential; the clones remove the race. The reachable set is the monotone
|
|
1169
|
+
// union of the per-callee closures (order-independent — `bfsReachableBlocks`
|
|
1170
|
+
// keys its query on the frontier, not `visited`; `visited` only gates
|
|
1171
|
+
// re-collection), so only the per-callee BFS DB round-trips run in parallel
|
|
1172
|
+
// while the merged result stays byte-identical. In a degraded case where a
|
|
1173
|
+
// per-callee BFS hits its depth/step limit, a sibling may expand through the
|
|
1174
|
+
// truncated region — a bounded, sound OVER-approximation, never fewer blocks.
|
|
1175
|
+
const visitedSnapshot = new Set(visited);
|
|
1176
|
+
const spanBfs = await Promise.all(spanSeeds.map(async ({ seeds }) => seeds.length === 0
|
|
1177
|
+
? null
|
|
1178
|
+
: bfsReachableBlocks({
|
|
1179
|
+
lbugPath,
|
|
1180
|
+
exec,
|
|
1181
|
+
seedBlocks: seeds,
|
|
1182
|
+
visited: new Set(visitedSnapshot), // private clone — no cross-span race
|
|
1183
|
+
direction: 'downstream',
|
|
1184
|
+
// Per-callee intra DEPTH clamp (block-hops within the callee), NOT
|
|
1185
|
+
// the row budget — mirrors the top-level intra BFS depth clamp.
|
|
1186
|
+
depthBudget: intraDepthBudget,
|
|
1187
|
+
stepLimit,
|
|
1188
|
+
probeLimit,
|
|
1189
|
+
})));
|
|
1190
|
+
const hopReached = new Set();
|
|
1191
|
+
for (let si = 0; si < spans.length; si++) {
|
|
1192
|
+
// Node budget is checked INSIDE the per-span MERGE (in span order) so the
|
|
1193
|
+
// mid-hop short-circuit stays byte-identical: the cumulative reachable size
|
|
1194
|
+
// after merging spans 0..k equals the sequential path's, so the break fires
|
|
1195
|
+
// at the SAME span. A span past the break is never merged (its parallel BFS
|
|
1196
|
+
// result is discarded), exactly as the sequential loop never processed it.
|
|
1197
|
+
if (reachable.size > nodeBudget) {
|
|
1198
|
+
truncatedByNodeCap = true;
|
|
1199
|
+
break hopLoop;
|
|
1200
|
+
}
|
|
1201
|
+
const { exceeded, seeds: calleeSeeds } = spanSeeds[si];
|
|
1202
|
+
if (exceeded)
|
|
1203
|
+
truncatedByLimit = true;
|
|
1204
|
+
if (calleeSeeds.length === 0)
|
|
1205
|
+
continue;
|
|
1206
|
+
// A callee seed block IS reachable (the callee is invoked from the slice),
|
|
1207
|
+
// unlike the top-level seed which is the target itself.
|
|
1208
|
+
for (const id of calleeSeeds) {
|
|
1209
|
+
if (!visited.has(id)) {
|
|
1210
|
+
visited.add(id);
|
|
1211
|
+
reachable.add(id);
|
|
1212
|
+
hopReached.add(id);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
const bfs = spanBfs[si];
|
|
1216
|
+
if (bfs === null)
|
|
1217
|
+
continue;
|
|
1218
|
+
if (bfs.truncatedByLimit)
|
|
1219
|
+
truncatedByLimit = true;
|
|
1220
|
+
// The per-callee BFS ran against a clone, so fold its discovered blocks
|
|
1221
|
+
// into the shared `visited`/`reachable` here (the sequential path did this
|
|
1222
|
+
// inside the BFS); Sets dedup, so order across siblings is irrelevant.
|
|
1223
|
+
for (const id of bfs.reachable) {
|
|
1224
|
+
visited.add(id);
|
|
1225
|
+
reachable.add(id);
|
|
1226
|
+
hopReached.add(id);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (reachable.size > nodeBudget) {
|
|
1230
|
+
truncatedByNodeCap = true;
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
sliceBlocks = [...hopReached];
|
|
1234
|
+
}
|
|
1235
|
+
// Frontier of callees still expandable after the hop budget ⇒ depth truncation.
|
|
1236
|
+
// (Conservative: if the last hop reached blocks AND we used the full budget,
|
|
1237
|
+
// deeper callees may exist.)
|
|
1238
|
+
if (hopsReached >= depthBudget && sliceBlocks.length > 0)
|
|
1239
|
+
truncatedByDepth = true;
|
|
1240
|
+
return {
|
|
1241
|
+
reachable,
|
|
1242
|
+
hopsReached,
|
|
1243
|
+
truncatedByDepth,
|
|
1244
|
+
truncatedByLimit,
|
|
1245
|
+
truncatedByNodeCap,
|
|
1246
|
+
ascentBlocks,
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
export async function runImpactPDG(deps) {
|
|
1250
|
+
const { repo, sym, direction, maxDepth, line, executeParameterized: exec } = deps;
|
|
1251
|
+
const callSummaryAvailable = deps.callSummaryAvailable === true;
|
|
1252
|
+
// `line` present ⇒ statement-anchored slice (the useful mode); absent ⇒
|
|
1253
|
+
// whole-symbol seed (intra-procedural reach collapses to empty for a
|
|
1254
|
+
// function — kept for back-compat, with a note steering the caller to `line`).
|
|
1255
|
+
const statementMode = typeof line === 'number' && Number.isInteger(line) && line >= 1;
|
|
1256
|
+
// `target` carries the call-graph-compatible shape (id/name/type/filePath) so
|
|
1257
|
+
// `collectImpactSymbolUids` keys on it identically to a callgraph result.
|
|
1258
|
+
const target = {
|
|
1259
|
+
id: sym.id,
|
|
1260
|
+
name: sym.name,
|
|
1261
|
+
type: deps.symType || 'Function',
|
|
1262
|
+
filePath: sym.filePath,
|
|
1263
|
+
};
|
|
1264
|
+
// Validate the per-step LIMIT as a positive integer (KTD11 — interpolated,
|
|
1265
|
+
// so it must be sanitised, never user-string-passed). A non-integer / out-of
|
|
1266
|
+
// range value (NaN, 1.5, negative, huge) is CLAMPED to the bounded default
|
|
1267
|
+
// rather than rejected: impact's `limit` is a soft page hint, and a clamp
|
|
1268
|
+
// keeps the safety tool producing a (flagged-bounded) radius instead of a
|
|
1269
|
+
// hard error. The clamp ceiling matches `pdg_query`'s validated max.
|
|
1270
|
+
const rawLimit = deps.limit;
|
|
1271
|
+
const stepLimit = Number.isInteger(rawLimit) && rawLimit >= 1 && rawLimit <= PDG_QUERY_MAX_LIMIT
|
|
1272
|
+
? rawLimit
|
|
1273
|
+
: PDG_QUERY_DEFAULT_LIMIT;
|
|
1274
|
+
// Depth: clamp to the documented impact server max. The BFS issues one DB
|
|
1275
|
+
// query per depth level, so direct callTool callers must not bypass the
|
|
1276
|
+
// schema's maxDepth cap.
|
|
1277
|
+
const depthBudget = Number.isInteger(maxDepth) && maxDepth >= 1 ? Math.min(maxDepth, IMPACT_MAX_DEPTH) : 3;
|
|
1278
|
+
// ── Seed: anchor the target's BasicBlocks from the ALREADY-RESOLVED symbol ─
|
|
1279
|
+
// `_impactImpl` already resolved `sym` to a confident single match honoring
|
|
1280
|
+
// the caller's target_uid/file_path/kind hints. Re-resolving by the bare
|
|
1281
|
+
// `sym.name` here would RE-AMBIGUATE a disambiguated name (returning the
|
|
1282
|
+
// "ambiguous" early payload instead of the PDG result) or anchor the seed on
|
|
1283
|
+
// a DIFFERENT same-name symbol in another file (wrong-symbol blast radius).
|
|
1284
|
+
// So build the seed anchor DIRECTLY from the resolved symbol's
|
|
1285
|
+
// [startLine+1, endLine+1] window — the same window `resolveBlockAnchor`'s
|
|
1286
|
+
// symbol branch produces, without re-running `resolveSymbolCandidates`.
|
|
1287
|
+
const { anchorClause, queryParams } = statementMode
|
|
1288
|
+
? blockAnchorForStatement(sym, line)
|
|
1289
|
+
: blockAnchorForResolvedSymbol(sym);
|
|
1290
|
+
const probeLimit = stepLimit + 1;
|
|
1291
|
+
const rawSeedRows = await exec(repo.lbugPath, `MATCH (a:BasicBlock) WHERE ${anchorClause} RETURN a.id AS id LIMIT ${probeLimit}`, queryParams);
|
|
1292
|
+
const seedRows = rawSeedRows.slice(0, stepLimit);
|
|
1293
|
+
let seedBlocks = seedRows
|
|
1294
|
+
.map((r) => String(r['id'] ?? ''))
|
|
1295
|
+
.filter((id) => id.length > 0);
|
|
1296
|
+
// Pin the OWNING function for a statement seed: a closure body block that
|
|
1297
|
+
// starts on the SAME source line as the seeded statement satisfies the
|
|
1298
|
+
// (forgiving) symbol-span window but belongs to a different function, so its
|
|
1299
|
+
// intra slice would leak in. The block id encodes the 1-based function start
|
|
1300
|
+
// line, and a block of THIS symbol has fnLine === sym.startLine + 1 (block
|
|
1301
|
+
// lines 1-based, symbol startLine 0-based). Drop foreign-fn seed blocks —
|
|
1302
|
+
// defensively: only when it leaves ≥1 seed, so a kind whose fnLine convention
|
|
1303
|
+
// differs never loses a real seed (it keeps the prior, slightly-loose set).
|
|
1304
|
+
if (statementMode && typeof sym.startLine === 'number') {
|
|
1305
|
+
const ownerFnLine = sym.startLine + 1;
|
|
1306
|
+
const owned = seedBlocks.filter((id) => fnLineOf(id) === ownerFnLine);
|
|
1307
|
+
if (owned.length > 0)
|
|
1308
|
+
seedBlocks = owned;
|
|
1309
|
+
}
|
|
1310
|
+
// FIX 7: the seed query probes one row past `stepLimit`, then processes at
|
|
1311
|
+
// most `stepLimit` rows like every BFS step. A function with more seed blocks
|
|
1312
|
+
// than `stepLimit` would silently under-seed (and thus under-report) — flag
|
|
1313
|
+
// it so the result carries the same truncation
|
|
1314
|
+
// signal the BFS steps do, never a silent partial seed.
|
|
1315
|
+
const seedTruncated = rawSeedRows.length > stepLimit;
|
|
1316
|
+
// ── KTD6 no-body contract: distinguish "no PDG body" from "no dependence" ──
|
|
1317
|
+
// A symbol that resolves but produces ZERO anchored blocks has no CFG body
|
|
1318
|
+
// (interface / type alias / abstract / ambient / one-line const). A bare
|
|
1319
|
+
// impactedCount:0 / risk:'LOW' would read as "safe to refactor" — the exact
|
|
1320
|
+
// false-safe `impact` exists to prevent (#2129/#1858). Surface an explicit
|
|
1321
|
+
// note + a non-LOW epistemic marker, never a silent confident zero.
|
|
1322
|
+
if (seedBlocks.length === 0) {
|
|
1323
|
+
return {
|
|
1324
|
+
mode: 'pdg',
|
|
1325
|
+
pdgResultVersion: PDG_RESULT_VERSION,
|
|
1326
|
+
target,
|
|
1327
|
+
direction,
|
|
1328
|
+
...(statementMode ? { criterionLine: line } : {}),
|
|
1329
|
+
reachableBlocks: [],
|
|
1330
|
+
intraReachableBlocks: [],
|
|
1331
|
+
seedBlocks: [],
|
|
1332
|
+
blockCount: 0,
|
|
1333
|
+
affectedStatements: [],
|
|
1334
|
+
affectedStatementCount: 0,
|
|
1335
|
+
truncated: false,
|
|
1336
|
+
depthReached: 0,
|
|
1337
|
+
// statementMode: the requested line has no statement block inside the
|
|
1338
|
+
// symbol (blank line, comment, outside the body, or a line the CFG did
|
|
1339
|
+
// not materialise). Distinct from "no PDG body".
|
|
1340
|
+
epistemic: statementMode ? 'pdg-no-block-at-line' : 'no-pdg-body',
|
|
1341
|
+
note: statementMode
|
|
1342
|
+
? `No PDG statement block starts at line ${line} within '${sym.name}' ` +
|
|
1343
|
+
`(${sym.filePath}). The line may be blank, a comment, a brace, or outside ` +
|
|
1344
|
+
`the symbol's body. Pass a line that begins an executable statement.`
|
|
1345
|
+
: `'${sym.name}' has no PDG body — no BasicBlocks / control- or data-dependence ` +
|
|
1346
|
+
`edges exist for this symbol (e.g. an interface, type alias, abstract/ambient ` +
|
|
1347
|
+
`member, or a one-line declaration with no CFG). This is NOT a confident ` +
|
|
1348
|
+
`"no impact": the local PDG statement slice cannot model this symbol kind. ` +
|
|
1349
|
+
`Inter-procedural symbol reach may still be attached by the unified impact dispatcher.`,
|
|
1350
|
+
impactedCount: 0,
|
|
1351
|
+
risk: 'UNKNOWN',
|
|
1352
|
+
// KTD8 parity fields so a consumer iterating byDepth / reading the
|
|
1353
|
+
// depth counts on a no-body result still finds a well-formed (empty)
|
|
1354
|
+
// shape rather than `undefined` (which would render as "isolated").
|
|
1355
|
+
...emptyPdgParityFields(),
|
|
1356
|
+
unresolvedBlockCount: 0,
|
|
1357
|
+
ambiguousProjectionCount: 0,
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
// ── Bounded direction-aware BFS over CDG + REACHING_DEF (KTD4, KTD11) ──────
|
|
1361
|
+
// Seed blocks are NOT counted as reachable (they ARE the target); the
|
|
1362
|
+
// reachable set is everything the BFS discovers from them. Visited tracks
|
|
1363
|
+
// BOTH seeds and discovered blocks so a cycle never re-expands. The traversal
|
|
1364
|
+
// is the shared `bfsReachableBlocks` — the SAME edge query / step-limit / probe
|
|
1365
|
+
// semantics the inter-procedural descent (U1) reuses, so the two cannot diverge.
|
|
1366
|
+
const visited = new Set(seedBlocks);
|
|
1367
|
+
const intra = await bfsReachableBlocks({
|
|
1368
|
+
lbugPath: repo.lbugPath,
|
|
1369
|
+
exec,
|
|
1370
|
+
seedBlocks,
|
|
1371
|
+
visited,
|
|
1372
|
+
direction,
|
|
1373
|
+
depthBudget,
|
|
1374
|
+
stepLimit,
|
|
1375
|
+
probeLimit,
|
|
1376
|
+
});
|
|
1377
|
+
const reachable = intra.reachable;
|
|
1378
|
+
// FIX 6: snapshot the INTRA-procedural reachable set BEFORE the U1 descent
|
|
1379
|
+
// expands `reachable` with inter-procedurally-reached callee blocks. The
|
|
1380
|
+
// callgraph bridge keys its "first-hop proven" set on seed ∪ intra-reachable
|
|
1381
|
+
// (the original statement slice) — feeding it the interproc-expanded superset
|
|
1382
|
+
// would mark transitively-reached (2+ hop) callgraph targets as first-hop
|
|
1383
|
+
// proven, silently shifting the established statementPrecision semantics.
|
|
1384
|
+
const intraReachableBlocks = [...reachable].sort();
|
|
1385
|
+
const depthReached = intra.depthReached;
|
|
1386
|
+
// `truncatedByDepth`: the BFS still had a non-empty frontier when the depth
|
|
1387
|
+
// budget ran out (more reachable blocks exist past `maxDepth`).
|
|
1388
|
+
// `truncatedByLimit`: a step's neighbour query hit the one-past LIMIT probe.
|
|
1389
|
+
// The SEED query is one-past-probed too, so `seedTruncated` seeds the flag — a
|
|
1390
|
+
// partial seed is a lower-bound expansion just like a partial step.
|
|
1391
|
+
let truncatedByDepth = intra.truncatedByDepth;
|
|
1392
|
+
let truncatedByLimit = seedTruncated || intra.truncatedByLimit;
|
|
1393
|
+
// ── U1: bounded inter-procedural forward closure (DOWNSTREAM only) ──────────
|
|
1394
|
+
// After the intra slice completes and BEFORE block→symbol projection, descend
|
|
1395
|
+
// through resolved call sites so the slice crosses function boundaries (HRB
|
|
1396
|
+
// context-insensitive forward closure — Joern's shipped approach). Gated to
|
|
1397
|
+
// downstream (the dispatcher's direction gate mirrors this): forward closure is
|
|
1398
|
+
// only meaningful when following calls forward. A pre-namespace-v4 index (no
|
|
1399
|
+
// `calleeIds`) yields no callee ids → the descent is a no-op → byte-identical
|
|
1400
|
+
// intra-only behavior. The inter-procedural reach DEEPENS `reachableBlocks`
|
|
1401
|
+
// (and thus `affectedStatements`); the owning-symbol `byDepth` stays a single
|
|
1402
|
+
// collapsed bucket (block-hops are not call-hops — the standing KTD8 contract;
|
|
1403
|
+
// every byDepth consumer iterates generically, so deepening the statement slice
|
|
1404
|
+
// — not the bucket count — is the correctness-preserving surface).
|
|
1405
|
+
let interproceduralHops = 0;
|
|
1406
|
+
// U-C4: CALL blocks whose callee's CALL_SUMMARY licensed a return-value ascent
|
|
1407
|
+
// (empty for an upstream slice, a pre-FU-C v3 index, or no return-flowing
|
|
1408
|
+
// callee). The statement projection expands these blocks to their interior
|
|
1409
|
+
// call lines (a coalesced call block spans several statements whose results
|
|
1410
|
+
// chain through it — the statement-granularity realisation of the ascent).
|
|
1411
|
+
let ascentBlocks = new Set();
|
|
1412
|
+
if (direction === 'downstream') {
|
|
1413
|
+
const interproc = await interproceduralDescent({
|
|
1414
|
+
lbugPath: repo.lbugPath,
|
|
1415
|
+
exec,
|
|
1416
|
+
// The slice the first hop gathers callees from = seeds ∪ intra reach. The
|
|
1417
|
+
// seed block(s) are included because a callee invoked directly on the
|
|
1418
|
+
// changed line is the most-directly-impacted one (seed-minus-reachable
|
|
1419
|
+
// excludes seeds from `reachable`, but they still call out to callees).
|
|
1420
|
+
initialSliceBlocks: [...seedBlocks, ...reachable],
|
|
1421
|
+
visited,
|
|
1422
|
+
depthBudget: INTERPROC_DEPTH_BUDGET,
|
|
1423
|
+
// FIX 1: the per-callee intra BFS depth is the SAME depth clamp the
|
|
1424
|
+
// top-level intra BFS uses — NOT `stepLimit` (a row/probe budget). A callee
|
|
1425
|
+
// must not be traversable up to PDG_QUERY_MAX_LIMIT dependence-levels deep.
|
|
1426
|
+
intraDepthBudget: depthBudget,
|
|
1427
|
+
stepLimit,
|
|
1428
|
+
probeLimit,
|
|
1429
|
+
nodeBudget: INTERPROC_NODE_BUDGET,
|
|
1430
|
+
// FIX 3: pre-seed the descent with the statement seed's OWNER FUNCTION so
|
|
1431
|
+
// direct/mutual recursion back to it never re-seeds the whole-function span
|
|
1432
|
+
// the statement slice deliberately excluded. The resolved target symbol id
|
|
1433
|
+
// (`sym.id`) is exactly that owner — `_impactImpl` already resolved it.
|
|
1434
|
+
preSeededCalleeIds: statementMode ? [sym.id] : [],
|
|
1435
|
+
});
|
|
1436
|
+
interproceduralHops = interproc.hopsReached;
|
|
1437
|
+
ascentBlocks = interproc.ascentBlocks;
|
|
1438
|
+
for (const id of interproc.reachable)
|
|
1439
|
+
reachable.add(id);
|
|
1440
|
+
if (interproc.truncatedByDepth)
|
|
1441
|
+
truncatedByDepth = true;
|
|
1442
|
+
if (interproc.truncatedByLimit)
|
|
1443
|
+
truncatedByLimit = true;
|
|
1444
|
+
// FIX 7: the node cap is a SIZE/budget limit, semantically 'limit' — NOT
|
|
1445
|
+
// 'depth' (which means dependence-level exhaustion). Map it to truncatedByLimit.
|
|
1446
|
+
if (interproc.truncatedByNodeCap)
|
|
1447
|
+
truncatedByLimit = true;
|
|
1448
|
+
// FIX 5: do NOT fold the inter-procedural FUNCTION-hop count into
|
|
1449
|
+
// `depthReached` (which means intra BLOCK-hop depth — a different unit). The
|
|
1450
|
+
// function-hop count is plumbed separately via `interproceduralHops`.
|
|
1451
|
+
}
|
|
1452
|
+
const reachableBlocks = [...reachable].sort();
|
|
1453
|
+
const truncated = truncatedByDepth || truncatedByLimit;
|
|
1454
|
+
// truncatedBy PRECEDENCE (U3): when BOTH depth and limit truncation fire, the
|
|
1455
|
+
// scalar `truncatedBy` reports 'depth' (the depth ternary is tested first), while
|
|
1456
|
+
// `truncatedByReasons` lists BOTH ['depth','limit']. Depth wins the scalar slot
|
|
1457
|
+
// because dependence-level exhaustion is the stronger "the slice is incomplete"
|
|
1458
|
+
// signal; the reasons array preserves that a size/limit cap also fired. This is a
|
|
1459
|
+
// standing precedence contract — keep the depth-first ternary and the both-fire
|
|
1460
|
+
// reasons array consistent.
|
|
1461
|
+
const truncatedBy = truncatedByDepth
|
|
1462
|
+
? 'depth'
|
|
1463
|
+
: truncatedByLimit
|
|
1464
|
+
? 'limit'
|
|
1465
|
+
: undefined;
|
|
1466
|
+
const truncatedByReasons = truncatedByDepth && truncatedByLimit ? ['depth', 'limit'] : undefined;
|
|
1467
|
+
// ── Resolve the reachable blocks to source statements (line + text) ────────
|
|
1468
|
+
// This is the useful output of statement mode: the dependent statements the
|
|
1469
|
+
// change at `line` reaches. Fetched once for the whole reachable set; sorted
|
|
1470
|
+
// by line. Failure surfaces (no `.catch` swallow) rather than masquerading
|
|
1471
|
+
// as "no affected statements".
|
|
1472
|
+
// Scope tag (FU-A): the criterion's OWN function is (sym.filePath, fnLine),
|
|
1473
|
+
// where fnLine follows the BasicBlock 1-based convention sym.startLine + 1
|
|
1474
|
+
// (the same window used to anchor the seed above). A symbol without a numeric
|
|
1475
|
+
// startLine has no owning-fn line to match, so `ownerFnLine` is NaN and every
|
|
1476
|
+
// statement tags as 'inter' (no false intra claim).
|
|
1477
|
+
const criterionFile = sym.filePath;
|
|
1478
|
+
const ownerFnLine = typeof sym.startLine === 'number' ? sym.startLine + 1 : Number.NaN;
|
|
1479
|
+
// ── FU-B-2 chain-walk blocks: statement-granular interior recovery ─────────
|
|
1480
|
+
// The SINGLE principled mechanism that expands a coalesced straight-line
|
|
1481
|
+
// BasicBlock to its interior dependent statements via the self REACHING_DEF
|
|
1482
|
+
// def→use LINE chain. Two contributors:
|
|
1483
|
+
// - the SEED block(s), seeded at the criterion `line` (statement mode only):
|
|
1484
|
+
// recovers the interior statements of the criterion's own coalesced block
|
|
1485
|
+
// that the block-granular slice lost (the U2 intra-dataflow-chain gap —
|
|
1486
|
+
// line 7's `7-9` block yields {8,9}). The seed block is NOT in
|
|
1487
|
+
// `reachableBlocks` (seeds are excluded), so the walk surfaces lines no
|
|
1488
|
+
// other projection reaches.
|
|
1489
|
+
// - each U-C4 ascent CALL block, seeded from its own start line (empty entry
|
|
1490
|
+
// set ⇒ the projection default-seeds at the block start): the principled
|
|
1491
|
+
// replacement for FU-C's blind all-interior-lines expansion.
|
|
1492
|
+
const chainWalkBlocks = new Map();
|
|
1493
|
+
if (statementMode) {
|
|
1494
|
+
for (const id of seedBlocks)
|
|
1495
|
+
chainWalkBlocks.set(id, new Set([line]));
|
|
1496
|
+
}
|
|
1497
|
+
// Every REACHABLE coalesced block is also walked, seeded from its own start
|
|
1498
|
+
// line (empty entry set ⇒ the projection default-seeds at the block start): a
|
|
1499
|
+
// block reached via CDG (a control-dependent body block) or REACHING_DEF can
|
|
1500
|
+
// itself coalesce a straight-line data-dep chain whose interior statements the
|
|
1501
|
+
// block-start projection would lose (e.g. the guard fixture's `const y; const
|
|
1502
|
+
// z = y + 1;` post-guard body — line 12 chains from line 11 via `y`). This is
|
|
1503
|
+
// the SAME principled self-edge walk, applied to the whole slice, not just the
|
|
1504
|
+
// seed.
|
|
1505
|
+
for (const id of reachableBlocks) {
|
|
1506
|
+
if (!chainWalkBlocks.has(id))
|
|
1507
|
+
chainWalkBlocks.set(id, new Set());
|
|
1508
|
+
}
|
|
1509
|
+
for (const id of ascentBlocks) {
|
|
1510
|
+
if (!chainWalkBlocks.has(id))
|
|
1511
|
+
chainWalkBlocks.set(id, new Set());
|
|
1512
|
+
}
|
|
1513
|
+
const affectedStatements = await pdgStatementsForBlocks(repo.lbugPath, reachableBlocks, exec, criterionFile, ownerFnLine, chainWalkBlocks);
|
|
1514
|
+
// ── Has a PDG body but no inter-block dependence reachability ──────────────
|
|
1515
|
+
// Distinct from "no PDG body": the function exists and has blocks, but no
|
|
1516
|
+
// CDG/REACHING_DEF edge leaves the target's blocks in this direction (no
|
|
1517
|
+
// DISTINCT downstream BasicBlock is reached). For a WHOLE-SYMBOL seed this is
|
|
1518
|
+
// the expected (and uninformative) result — every intra-procedural block is
|
|
1519
|
+
// already a seed — so the note steers to `line`. Still not a confident zero —
|
|
1520
|
+
// explicit note + UNKNOWN (KTD6/KTD8).
|
|
1521
|
+
//
|
|
1522
|
+
// Item 5 robustness: `affectedStatements` was ALREADY computed above with the
|
|
1523
|
+
// seed block(s) in `chainWalkBlocks`, so a criterion whose only dependents are
|
|
1524
|
+
// INTERIOR to its own coalesced seed block (a straight-line chain with no
|
|
1525
|
+
// distinct downstream block — e.g. `const a; const b = a*2; const c = b-3`
|
|
1526
|
+
// with no `return`) still has those recovered interior statements here. Surface
|
|
1527
|
+
// them rather than hardcoding `[]` (which would silently drop the seed-block
|
|
1528
|
+
// chain-walk on this path). `reachableBlocks` stays `[]` — those lines belong to
|
|
1529
|
+
// the seed block, correctly not a reachable BLOCK — but the statement slice is
|
|
1530
|
+
// not lost.
|
|
1531
|
+
if (reachableBlocks.length === 0) {
|
|
1532
|
+
return {
|
|
1533
|
+
mode: 'pdg',
|
|
1534
|
+
pdgResultVersion: PDG_RESULT_VERSION,
|
|
1535
|
+
target,
|
|
1536
|
+
direction,
|
|
1537
|
+
...(statementMode ? { criterionLine: line } : {}),
|
|
1538
|
+
impactedCount: 0,
|
|
1539
|
+
risk: 'UNKNOWN',
|
|
1540
|
+
epistemic: 'pdg-intra-procedural',
|
|
1541
|
+
note: statementMode
|
|
1542
|
+
? `No statement in '${sym.name}' is in a DISTINCT ${direction}-dependent block of ` +
|
|
1543
|
+
`line ${line} (no CDG/REACHING_DEF edge leaves the seed block in this direction). ` +
|
|
1544
|
+
`Any interior statements of the seed's own coalesced block that depend on line ${line} ` +
|
|
1545
|
+
`are surfaced in affectedStatements.`
|
|
1546
|
+
: `'${sym.name}' has a PDG body but a WHOLE-SYMBOL ${direction} slice is empty: ` +
|
|
1547
|
+
`intra-procedural dependence stays inside the function, so every reachable block ` +
|
|
1548
|
+
`is already part of the seed. Pass line:<N> to slice from a specific statement ` +
|
|
1549
|
+
`(what depends on the code at that line). Inter-procedural symbol reach is attached ` +
|
|
1550
|
+
`separately by the unified impact dispatcher.`,
|
|
1551
|
+
reachableBlocks: [],
|
|
1552
|
+
// Empty intra reach (reachableBlocks is empty here) — the bridge keys on
|
|
1553
|
+
// seed ∪ intra-reachable, so an empty intra slice leaves the bridge to seed
|
|
1554
|
+
// from the seed blocks alone (FIX 6 parity field).
|
|
1555
|
+
intraReachableBlocks: [],
|
|
1556
|
+
// Carry the real seed blocks (non-empty here — the function HAS blocks, they
|
|
1557
|
+
// are all seeds): a callee invoked directly on the seeded line must still be
|
|
1558
|
+
// provable even when the line has no downstream dependents (the seed-line FN
|
|
1559
|
+
// the tri-review found). Empty reachableBlocks must NOT zero the seed callees.
|
|
1560
|
+
seedBlocks,
|
|
1561
|
+
blockCount: 0,
|
|
1562
|
+
affectedStatements,
|
|
1563
|
+
affectedStatementCount: affectedStatements.length,
|
|
1564
|
+
depthReached,
|
|
1565
|
+
unresolvedBlockCount: 0,
|
|
1566
|
+
ambiguousProjectionCount: 0,
|
|
1567
|
+
...(truncated ? { truncated: true } : {}),
|
|
1568
|
+
...(truncatedBy ? { truncatedBy } : {}),
|
|
1569
|
+
...(truncatedByReasons ? { truncatedByReasons } : {}),
|
|
1570
|
+
...emptyPdgParityFields(),
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
// ── U4: project reachable blocks → owning symbols, assemble parity result ──
|
|
1574
|
+
const projection = await projectBlocksToSymbols({
|
|
1575
|
+
lbugPath: repo.lbugPath,
|
|
1576
|
+
blockIds: reachableBlocks,
|
|
1577
|
+
executeParameterized: exec,
|
|
1578
|
+
});
|
|
1579
|
+
return assemblePdgImpactResult({
|
|
1580
|
+
target: {
|
|
1581
|
+
id: sym.id,
|
|
1582
|
+
name: sym.name,
|
|
1583
|
+
type: deps.symType || 'Function',
|
|
1584
|
+
filePath: sym.filePath,
|
|
1585
|
+
},
|
|
1586
|
+
direction,
|
|
1587
|
+
reachableBlocks,
|
|
1588
|
+
intraReachableBlocks,
|
|
1589
|
+
seedBlocks,
|
|
1590
|
+
affectedStatements,
|
|
1591
|
+
criterionLine: statementMode ? line : undefined,
|
|
1592
|
+
projection,
|
|
1593
|
+
depthReached,
|
|
1594
|
+
truncated,
|
|
1595
|
+
truncatedBy,
|
|
1596
|
+
truncatedByReasons,
|
|
1597
|
+
interproceduralHops,
|
|
1598
|
+
callSummaryAvailable,
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
export function pdgBridgeEvidenceForImpact(input) {
|
|
1602
|
+
const { bridge, depth, calleeName, calleeId, inherited } = input;
|
|
1603
|
+
if (depth > 1) {
|
|
1604
|
+
return (inherited ?? {
|
|
1605
|
+
evidence: 'unproven-bridge',
|
|
1606
|
+
basis: 'first-hop evidence unavailable for inherited symbol-graph reach',
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
const sliceCalleeNames = bridge.sliceCalleeNames;
|
|
1610
|
+
// Whole-symbol compatibility bridge ONLY when NEITHER key discriminates. The
|
|
1611
|
+
// empty-names guard must also require empty ids: `local-backend` builds the
|
|
1612
|
+
// bridge when names OR ids have signal, so an id-only slice (names empty/absent,
|
|
1613
|
+
// ids present — e.g. a block whose calls resolve to ids but carry no static leaf
|
|
1614
|
+
// name) must fall through to the resolved-id branch, not short-circuit to
|
|
1615
|
+
// "prove everything". (PR #2227 tri-review-2 headline.)
|
|
1616
|
+
if (sliceCalleeNames.size === 0 && bridge.sliceCalleeIds.size === 0) {
|
|
1617
|
+
return {
|
|
1618
|
+
evidence: 'callgraph-bridge',
|
|
1619
|
+
basis: 'whole-symbol PDG result uses symbol graph as compatibility bridge',
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
// A slice block whose call sites were truncated at the per-statement cap has an
|
|
1623
|
+
// INCOMPLETE callee list, so absence from the set does not prove absence from
|
|
1624
|
+
// the slice. Keep such reach callgraph-equal rather than under-proving. (A capped
|
|
1625
|
+
// block always carries the sentinel in `callees`/names — the per-statement cap is
|
|
1626
|
+
// name-agnostic — so checking names suffices. `sliceCalleeNames` is always
|
|
1627
|
+
// present now (an id-only slice has it as an empty set, never capped), so the
|
|
1628
|
+
// direct `.has` is sound.)
|
|
1629
|
+
if (sliceCalleeNames.has(CALLEES_TRUNCATED_SENTINEL)) {
|
|
1630
|
+
return {
|
|
1631
|
+
evidence: 'callgraph-bridge',
|
|
1632
|
+
basis: 'a slice block truncated its call sites — callee set is incomplete (callee-unknown)',
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
// Sound primary key (KTD3): when the slice carries resolved callee ids and the
|
|
1636
|
+
// block is not capped (sentinel handled above), prove by exact symbol-id match.
|
|
1637
|
+
// An id that is NOT in the present set is a real proof failure (`unproven-bridge`),
|
|
1638
|
+
// NOT a fall-through to the name predicate — the name path would re-leak the
|
|
1639
|
+
// same-name collision this key exists to eliminate.
|
|
1640
|
+
const sliceCalleeIds = bridge.sliceCalleeIds;
|
|
1641
|
+
if (sliceCalleeIds.size > 0) {
|
|
1642
|
+
const id = typeof calleeId === 'string' ? calleeId : '';
|
|
1643
|
+
if (id && sliceCalleeIds.has(id)) {
|
|
1644
|
+
return {
|
|
1645
|
+
evidence: 'callgraph-bridge',
|
|
1646
|
+
basis: 'callee id is invoked in a block of the local PDG dependence slice (resolved-symbol match)',
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
return {
|
|
1650
|
+
evidence: 'unproven-bridge',
|
|
1651
|
+
basis: 'callee id is not invoked in any block of the local PDG dependence slice',
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
// R3 graceful fallback: no captured ids (pre-v3 index / upstream / whole-symbol)
|
|
1655
|
+
// ⇒ use the leaf-name match.
|
|
1656
|
+
const name = typeof calleeName === 'string' ? calleeName : '';
|
|
1657
|
+
if (name && sliceCalleeNames.has(name)) {
|
|
1658
|
+
return {
|
|
1659
|
+
evidence: 'callgraph-bridge',
|
|
1660
|
+
basis: 'callee is invoked in a block of the local PDG dependence slice',
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
evidence: 'unproven-bridge',
|
|
1665
|
+
basis: 'callee is not invoked in any block of the local PDG dependence slice',
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Pick the stronger of two bridge-evidence verdicts for the same reached symbol.
|
|
1670
|
+
* `callgraph-bridge` (proven) beats `unproven-bridge`, so a node reachable from
|
|
1671
|
+
* multiple parents is proven if ANY parent proves it. This makes the
|
|
1672
|
+
* proven/unproven label order-independent of DB row iteration — a diamond-reached
|
|
1673
|
+
* symbol gets the same label regardless of which parent the BFS visits first
|
|
1674
|
+
* (PR #2227 tri-review, P3).
|
|
1675
|
+
*/
|
|
1676
|
+
export function betterBridgeEvidence(existing, candidate) {
|
|
1677
|
+
if (!existing)
|
|
1678
|
+
return candidate;
|
|
1679
|
+
if (existing.evidence === 'callgraph-bridge')
|
|
1680
|
+
return existing;
|
|
1681
|
+
if (candidate.evidence === 'callgraph-bridge')
|
|
1682
|
+
return candidate;
|
|
1683
|
+
return existing;
|
|
1684
|
+
}
|
|
1685
|
+
function normalizePdgBridgeByDepth(byDepth) {
|
|
1686
|
+
const normalized = {};
|
|
1687
|
+
for (const [depthKey, items] of Object.entries(byDepth ?? {})) {
|
|
1688
|
+
const depth = Number(depthKey);
|
|
1689
|
+
if (!Number.isFinite(depth) || !Array.isArray(items))
|
|
1690
|
+
continue;
|
|
1691
|
+
normalized[depth] = items.map((item) => {
|
|
1692
|
+
if (!item || typeof item !== 'object')
|
|
1693
|
+
return item;
|
|
1694
|
+
const record = item;
|
|
1695
|
+
const evidence = typeof record.pdgEvidence === 'string'
|
|
1696
|
+
? record.pdgEvidence
|
|
1697
|
+
: 'callgraph-bridge';
|
|
1698
|
+
return {
|
|
1699
|
+
...record,
|
|
1700
|
+
pdgEvidence: evidence,
|
|
1701
|
+
...(record.pdgEvidenceReason
|
|
1702
|
+
? {}
|
|
1703
|
+
: {
|
|
1704
|
+
pdgEvidenceReason: evidence === 'unproven-bridge'
|
|
1705
|
+
? 'symbol reached through the resolved symbol graph, but the existing graph did not prove the first-hop call site is in the local PDG slice'
|
|
1706
|
+
: 'symbol reached through the resolved symbol graph compatibility bridge',
|
|
1707
|
+
}),
|
|
1708
|
+
};
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
return normalized;
|
|
1712
|
+
}
|
|
1713
|
+
function countPdgEvidence(byDepth) {
|
|
1714
|
+
const counts = {};
|
|
1715
|
+
for (const items of Object.values(byDepth ?? {})) {
|
|
1716
|
+
if (!Array.isArray(items))
|
|
1717
|
+
continue;
|
|
1718
|
+
for (const item of items) {
|
|
1719
|
+
if (!item || typeof item !== 'object')
|
|
1720
|
+
continue;
|
|
1721
|
+
const evidence = item.pdgEvidence;
|
|
1722
|
+
if (typeof evidence !== 'string')
|
|
1723
|
+
continue;
|
|
1724
|
+
counts[evidence] = (counts[evidence] ?? 0) + 1;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
return counts;
|
|
1728
|
+
}
|
|
1729
|
+
function dominantInterproceduralEvidence(counts) {
|
|
1730
|
+
if ((counts['unproven-bridge'] ?? 0) > 0)
|
|
1731
|
+
return 'unproven-bridge';
|
|
1732
|
+
if ((counts['callgraph-bridge'] ?? 0) > 0)
|
|
1733
|
+
return 'callgraph-bridge';
|
|
1734
|
+
return undefined;
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Statement-precise projection of the inter-procedural bridge: the subset PROVEN
|
|
1738
|
+
* to be invoked from the criterion's dependence slice (`callgraph-bridge`),
|
|
1739
|
+
* dropping `unproven-bridge` symbols — reachable in the call graph but only from
|
|
1740
|
+
* statements the changed line does not reach. Additive: the full
|
|
1741
|
+
* `interproceduralByDepth` is unchanged and still preserves callgraph reach; this
|
|
1742
|
+
* answers the tighter "which other functions does changing THIS line affect?".
|
|
1743
|
+
* For an upstream / whole-symbol seed there is no discriminating slice, so every
|
|
1744
|
+
* symbol is `callgraph-bridge` and the projection equals the full reach
|
|
1745
|
+
* (`statementPrecision` = 1). When the slice carries resolved callee ids the
|
|
1746
|
+
* projection is SOUND — a reached symbol is proven iff its resolved id matches a
|
|
1747
|
+
* slice-block id (resolved-symbol match), so neither a same-named out-of-slice
|
|
1748
|
+
* callee nor an aliased import perturbs the set. The leaf-name match is the
|
|
1749
|
+
* documented fallback (pre-v3 index / no captured ids), and only in that
|
|
1750
|
+
* name-fallback path is the projection a conservative SUPERSET (a callee invoked
|
|
1751
|
+
* from both an in-slice and out-of-slice site resolves to proven).
|
|
1752
|
+
*/
|
|
1753
|
+
function projectStatementPreciseByDepth(byDepth) {
|
|
1754
|
+
const out = {};
|
|
1755
|
+
for (const [depthKey, items] of Object.entries(byDepth ?? {})) {
|
|
1756
|
+
const depth = Number(depthKey);
|
|
1757
|
+
if (!Number.isFinite(depth) || !Array.isArray(items))
|
|
1758
|
+
continue;
|
|
1759
|
+
const proven = items.filter((item) => item &&
|
|
1760
|
+
typeof item === 'object' &&
|
|
1761
|
+
item.pdgEvidence !== 'unproven-bridge');
|
|
1762
|
+
if (proven.length > 0)
|
|
1763
|
+
out[depth] = proven;
|
|
1764
|
+
}
|
|
1765
|
+
return out;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Compose `mode:'pdg'` into one user-facing impact result:
|
|
1769
|
+
*
|
|
1770
|
+
* - `affectedStatements` / `reachableBlocks` stay owned by the persisted PDG
|
|
1771
|
+
* layer (CDG + REACHING_DEF), preserving the statement-level intra result.
|
|
1772
|
+
* - `interproceduralByDepth` / `pdgInterprocedural` expose the symbol reach;
|
|
1773
|
+
* `byDepth` stays as the compatibility symbol bucket so existing consumers
|
|
1774
|
+
* still see one PDG result shape.
|
|
1775
|
+
*
|
|
1776
|
+
* The callgraph option remains available as the comparator/default path; this
|
|
1777
|
+
* helper only changes the `pdg` result contract from intra-only to unified.
|
|
1778
|
+
*/
|
|
1779
|
+
export function composeUnifiedPdgImpactResult(pdgResult, interproceduralResult, interproceduralError) {
|
|
1780
|
+
if ('error' in pdgResult || 'pdgLayer' in pdgResult)
|
|
1781
|
+
return pdgResult;
|
|
1782
|
+
const localByDepth = pdgResult.byDepth ?? {};
|
|
1783
|
+
const localByDepthCounts = pdgResult.byDepthCounts ?? {};
|
|
1784
|
+
const interproceduralByDepth = normalizePdgBridgeByDepth(interproceduralResult?.byDepth ?? {});
|
|
1785
|
+
const interproceduralByDepthCounts = interproceduralResult?.byDepthCounts ?? {};
|
|
1786
|
+
const interproceduralEvidenceCounts = countPdgEvidence(interproceduralByDepth);
|
|
1787
|
+
const interproceduralEvidence = dominantInterproceduralEvidence(interproceduralEvidenceCounts);
|
|
1788
|
+
// Additive statement-precise projection (see projectStatementPreciseByDepth):
|
|
1789
|
+
// the proven subset of the inter-procedural reach. `interproceduralByDepth`
|
|
1790
|
+
// above is unchanged and still preserves full callgraph reach.
|
|
1791
|
+
const statementPreciseByDepth = projectStatementPreciseByDepth(interproceduralByDepth);
|
|
1792
|
+
const statementPreciseByDepthCounts = {};
|
|
1793
|
+
for (const [depthKey, items] of Object.entries(statementPreciseByDepth)) {
|
|
1794
|
+
statementPreciseByDepthCounts[Number(depthKey)] = items.length;
|
|
1795
|
+
}
|
|
1796
|
+
const provenBridgeCount = interproceduralEvidenceCounts['callgraph-bridge'] ?? 0;
|
|
1797
|
+
const unprovenBridgeCount = interproceduralEvidenceCounts['unproven-bridge'] ?? 0;
|
|
1798
|
+
const statementPrecision = provenBridgeCount + unprovenBridgeCount > 0
|
|
1799
|
+
? provenBridgeCount / (provenBridgeCount + unprovenBridgeCount)
|
|
1800
|
+
: null;
|
|
1801
|
+
const byDepth = {};
|
|
1802
|
+
const byDepthCounts = {};
|
|
1803
|
+
const depthKeys = Array.from(new Set([
|
|
1804
|
+
...Object.keys(localByDepth),
|
|
1805
|
+
...Object.keys(interproceduralByDepth),
|
|
1806
|
+
...Object.keys(localByDepthCounts),
|
|
1807
|
+
...Object.keys(interproceduralByDepthCounts),
|
|
1808
|
+
]))
|
|
1809
|
+
.map((d) => Number(d))
|
|
1810
|
+
.filter((d) => Number.isFinite(d))
|
|
1811
|
+
.sort((a, b) => a - b);
|
|
1812
|
+
// Resolved symbol id of a byDepth item, or null for an unresolved/shadow item.
|
|
1813
|
+
const itemId = (item) => {
|
|
1814
|
+
if (item && typeof item === 'object' && 'id' in item) {
|
|
1815
|
+
const id = item.id;
|
|
1816
|
+
return typeof id === 'string' && id.length > 0 ? id : null;
|
|
1817
|
+
}
|
|
1818
|
+
return null;
|
|
1819
|
+
};
|
|
1820
|
+
// Cross-bucket dedup: a callee can surface in BOTH the local PDG block-expansion
|
|
1821
|
+
// AND the inter-procedural callgraph reach, so the raw `local + interproc` sums
|
|
1822
|
+
// double-count it. Track each layer's resolved ids to subtract the overlap from
|
|
1823
|
+
// the headline (cross-depth) and the per-depth counts.
|
|
1824
|
+
const localAllIds = new Set();
|
|
1825
|
+
const interAllIds = new Set();
|
|
1826
|
+
for (const depth of depthKeys) {
|
|
1827
|
+
const localItems = localByDepth[depth] ?? localByDepth[String(depth)] ?? [];
|
|
1828
|
+
const interItems = interproceduralByDepth[depth] ?? interproceduralByDepth[String(depth)] ?? [];
|
|
1829
|
+
const localDepthIds = new Set();
|
|
1830
|
+
for (const it of localItems) {
|
|
1831
|
+
const id = itemId(it);
|
|
1832
|
+
if (id !== null) {
|
|
1833
|
+
localDepthIds.add(id);
|
|
1834
|
+
localAllIds.add(id);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
// Drop an interproc item whose resolved id is already present locally AT THIS
|
|
1838
|
+
// DEPTH — keep the local item (richer projection). The SAME id reached at a
|
|
1839
|
+
// DIFFERENT depth is a legitimate distinct per-depth bucket and is retained
|
|
1840
|
+
// (so sum(byDepthCounts) can exceed the cross-depth `impactedCount`).
|
|
1841
|
+
let perDepthOverlap = 0;
|
|
1842
|
+
const dedupedInter = [];
|
|
1843
|
+
for (const it of interItems) {
|
|
1844
|
+
const id = itemId(it);
|
|
1845
|
+
if (id !== null)
|
|
1846
|
+
interAllIds.add(id);
|
|
1847
|
+
if (id !== null && localDepthIds.has(id)) {
|
|
1848
|
+
perDepthOverlap += 1;
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
dedupedInter.push(it);
|
|
1852
|
+
}
|
|
1853
|
+
const items = [...localItems, ...dedupedInter];
|
|
1854
|
+
if (items.length > 0)
|
|
1855
|
+
byDepth[depth] = items;
|
|
1856
|
+
const localCount = localByDepthCounts[depth] ?? localByDepthCounts[String(depth)] ?? localItems.length;
|
|
1857
|
+
const interCount = interproceduralByDepthCounts[depth] ??
|
|
1858
|
+
interproceduralByDepthCounts[String(depth)] ??
|
|
1859
|
+
interItems.length;
|
|
1860
|
+
const totalCount = Math.max(0, localCount + interCount - perDepthOverlap);
|
|
1861
|
+
if (totalCount > 0)
|
|
1862
|
+
byDepthCounts[depth] = totalCount;
|
|
1863
|
+
}
|
|
1864
|
+
// Cross-depth overlap of resolved ids reached by BOTH layers. Computed from the
|
|
1865
|
+
// visible byDepth ids; if a layer's byDepth was display-truncated this is a
|
|
1866
|
+
// lower bound, so the resulting count is a safe over-estimate (never the old
|
|
1867
|
+
// double-count, never below the larger single layer).
|
|
1868
|
+
let crossOverlap = 0;
|
|
1869
|
+
for (const id of interAllIds)
|
|
1870
|
+
if (localAllIds.has(id))
|
|
1871
|
+
crossOverlap += 1;
|
|
1872
|
+
if (Object.keys(byDepthCounts).length === 0) {
|
|
1873
|
+
const localZero = localByDepthCounts[1] ?? localByDepthCounts['1'];
|
|
1874
|
+
const interZero = interproceduralByDepthCounts[1] ?? interproceduralByDepthCounts['1'];
|
|
1875
|
+
if (typeof localZero === 'number' || typeof interZero === 'number') {
|
|
1876
|
+
byDepthCounts[1] =
|
|
1877
|
+
(typeof localZero === 'number' ? localZero : 0) +
|
|
1878
|
+
(typeof interZero === 'number' ? interZero : 0);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
const localImpactedCount = typeof pdgResult.impactedCount === 'number' ? pdgResult.impactedCount : 0;
|
|
1882
|
+
const interproceduralImpactedCount = typeof interproceduralResult?.impactedCount === 'number'
|
|
1883
|
+
? interproceduralResult.impactedCount
|
|
1884
|
+
: 0;
|
|
1885
|
+
// DISTINCT owning symbols across both layers (was localImpactedCount +
|
|
1886
|
+
// interproceduralImpactedCount, which double-counted a symbol reached by both).
|
|
1887
|
+
const impactedCount = Math.max(0, localImpactedCount + interproceduralImpactedCount - crossOverlap);
|
|
1888
|
+
// `direct` = distinct depth-1 reach. The deduped byDepthCounts[1] is exactly
|
|
1889
|
+
// that union, so it stays consistent with impactedCount (no separate layer sum).
|
|
1890
|
+
const directCount = byDepthCounts[1] ?? pdgResult.summary?.direct ?? localImpactedCount;
|
|
1891
|
+
const summary = interproceduralResult?.summary
|
|
1892
|
+
? {
|
|
1893
|
+
...interproceduralResult.summary,
|
|
1894
|
+
direct: directCount,
|
|
1895
|
+
}
|
|
1896
|
+
: {
|
|
1897
|
+
direct: directCount,
|
|
1898
|
+
processes_affected: 0,
|
|
1899
|
+
modules_affected: 0,
|
|
1900
|
+
};
|
|
1901
|
+
const affectedProcesses = interproceduralResult?.affected_processes ?? [];
|
|
1902
|
+
const affectedModules = interproceduralResult?.affected_modules ?? [];
|
|
1903
|
+
const partial = Boolean(interproceduralResult?.partial || interproceduralError);
|
|
1904
|
+
const errorMessage = interproceduralError instanceof Error
|
|
1905
|
+
? interproceduralError.message
|
|
1906
|
+
: interproceduralError
|
|
1907
|
+
? String(interproceduralError)
|
|
1908
|
+
: undefined;
|
|
1909
|
+
const noteParts = [
|
|
1910
|
+
pdgResult.note,
|
|
1911
|
+
`Inter-procedural symbol reach is included using the resolved symbol graph; ` +
|
|
1912
|
+
`statement-level PDG reach remains in affectedStatements. The symbol reach is ` +
|
|
1913
|
+
`labeled as a PDG evidence bridge, not as pure statement-level dependence.`,
|
|
1914
|
+
];
|
|
1915
|
+
if (errorMessage) {
|
|
1916
|
+
noteParts.push(`Inter-procedural symbol reach failed (${errorMessage}); byDepth is therefore a lower bound.`);
|
|
1917
|
+
}
|
|
1918
|
+
else if (interproceduralResult?.epistemic === 'lower-bound') {
|
|
1919
|
+
noteParts.push(`The inter-procedural symbol reach is a lower bound because unresolved indirection was detected.`);
|
|
1920
|
+
}
|
|
1921
|
+
if ((interproceduralEvidenceCounts['unproven-bridge'] ?? 0) > 0) {
|
|
1922
|
+
noteParts.push(`${interproceduralEvidenceCounts['unproven-bridge']} inter-procedural ` +
|
|
1923
|
+
`symbol(s) are labeled unproven-bridge: the resolved symbol graph reaches them, ` +
|
|
1924
|
+
`but the current graph did not prove their first-hop call site is in the local PDG slice.`);
|
|
1925
|
+
}
|
|
1926
|
+
return {
|
|
1927
|
+
...pdgResult,
|
|
1928
|
+
// Explicit (also carried by the `...pdgResult` spread) so the unified
|
|
1929
|
+
// mode:'pdg' exit always advertises the contract version.
|
|
1930
|
+
pdgResultVersion: PDG_RESULT_VERSION,
|
|
1931
|
+
impactedCount,
|
|
1932
|
+
note: noteParts.filter(Boolean).join(' '),
|
|
1933
|
+
summary,
|
|
1934
|
+
byDepthCounts,
|
|
1935
|
+
interproceduralByDepth,
|
|
1936
|
+
interproceduralByDepthCounts,
|
|
1937
|
+
// Statement-precise (proven) inter-procedural reach is emitted ONLY under
|
|
1938
|
+
// `pdgInterprocedural` below — a single source, no top-level duplicate.
|
|
1939
|
+
affected_processes: affectedProcesses,
|
|
1940
|
+
affected_modules: affectedModules,
|
|
1941
|
+
byDepth,
|
|
1942
|
+
...(partial ? { partial: true } : {}),
|
|
1943
|
+
...(interproceduralResult?.epistemic
|
|
1944
|
+
? { interproceduralEpistemic: interproceduralResult.epistemic }
|
|
1945
|
+
: {}),
|
|
1946
|
+
...(interproceduralResult?.boundaries
|
|
1947
|
+
? { interproceduralBoundaries: interproceduralResult.boundaries }
|
|
1948
|
+
: {}),
|
|
1949
|
+
...(errorMessage ? { interproceduralError: errorMessage } : {}),
|
|
1950
|
+
pdgEvidence: {
|
|
1951
|
+
// pdgResult is narrowed to the success/empty slice result by the
|
|
1952
|
+
// `'error' in / 'pdgLayer' in` guard at the top of this function, so
|
|
1953
|
+
// `pdgEvidence` is typed (optional) — no `as any`.
|
|
1954
|
+
...(pdgResult.pdgEvidence ?? {}),
|
|
1955
|
+
...(interproceduralEvidence ? { interprocedural: interproceduralEvidence } : {}),
|
|
1956
|
+
interproceduralEvidenceCounts,
|
|
1957
|
+
},
|
|
1958
|
+
pdgInterprocedural: {
|
|
1959
|
+
engine: 'symbol-graph',
|
|
1960
|
+
evidence: interproceduralEvidence ?? 'callgraph-bridge',
|
|
1961
|
+
impactedCount: interproceduralImpactedCount,
|
|
1962
|
+
byDepthCounts: interproceduralByDepthCounts,
|
|
1963
|
+
byDepth: interproceduralByDepth,
|
|
1964
|
+
evidenceCounts: interproceduralEvidenceCounts,
|
|
1965
|
+
statementPreciseByDepth,
|
|
1966
|
+
statementPreciseByDepthCounts,
|
|
1967
|
+
statementPreciseImpactedCount: provenBridgeCount,
|
|
1968
|
+
statementPrecision,
|
|
1969
|
+
partial,
|
|
1970
|
+
},
|
|
1971
|
+
};
|
|
1972
|
+
}
|