arceus-s 1.6.4
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 +395 -0
- package/dist/_shared/graph/types.d.ts +81 -0
- package/dist/_shared/graph/types.d.ts.map +1 -0
- package/dist/_shared/graph/types.js +8 -0
- package/dist/_shared/graph/types.js.map +1 -0
- package/dist/_shared/index.d.ts +63 -0
- package/dist/_shared/index.d.ts.map +1 -0
- package/dist/_shared/index.js +48 -0
- package/dist/_shared/index.js.map +1 -0
- package/dist/_shared/integrations/circuit-breaker.d.ts +183 -0
- package/dist/_shared/integrations/circuit-breaker.d.ts.map +1 -0
- package/dist/_shared/integrations/circuit-breaker.js +236 -0
- package/dist/_shared/integrations/circuit-breaker.js.map +1 -0
- package/dist/_shared/integrations/resilient-fetch.d.ts +99 -0
- package/dist/_shared/integrations/resilient-fetch.d.ts.map +1 -0
- package/dist/_shared/integrations/resilient-fetch.js +204 -0
- package/dist/_shared/integrations/resilient-fetch.js.map +1 -0
- package/dist/_shared/integrations/retry.d.ts +60 -0
- package/dist/_shared/integrations/retry.d.ts.map +1 -0
- package/dist/_shared/integrations/retry.js +67 -0
- package/dist/_shared/integrations/retry.js.map +1 -0
- package/dist/_shared/integrations/understand-quickly.d.ts +77 -0
- package/dist/_shared/integrations/understand-quickly.d.ts.map +1 -0
- package/dist/_shared/integrations/understand-quickly.js +176 -0
- package/dist/_shared/integrations/understand-quickly.js.map +1 -0
- package/dist/_shared/language-detection.d.ts +23 -0
- package/dist/_shared/language-detection.d.ts.map +1 -0
- package/dist/_shared/language-detection.js +139 -0
- package/dist/_shared/language-detection.js.map +1 -0
- package/dist/_shared/languages.d.ts +26 -0
- package/dist/_shared/languages.d.ts.map +1 -0
- package/dist/_shared/languages.js +27 -0
- package/dist/_shared/languages.js.map +1 -0
- package/dist/_shared/lbug/schema-constants.d.ts +16 -0
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -0
- package/dist/_shared/lbug/schema-constants.js +67 -0
- package/dist/_shared/lbug/schema-constants.js.map +1 -0
- package/dist/_shared/mro-strategy.d.ts +41 -0
- package/dist/_shared/mro-strategy.d.ts.map +1 -0
- package/dist/_shared/mro-strategy.js +2 -0
- package/dist/_shared/mro-strategy.js.map +1 -0
- package/dist/_shared/pipeline.d.ts +16 -0
- package/dist/_shared/pipeline.d.ts.map +1 -0
- package/dist/_shared/pipeline.js +5 -0
- package/dist/_shared/pipeline.js.map +1 -0
- package/dist/_shared/scope-resolution/def-index.d.ts +36 -0
- package/dist/_shared/scope-resolution/def-index.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/def-index.js +51 -0
- package/dist/_shared/scope-resolution/def-index.js.map +1 -0
- package/dist/_shared/scope-resolution/evidence-weights.d.ts +69 -0
- package/dist/_shared/scope-resolution/evidence-weights.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/evidence-weights.js +84 -0
- package/dist/_shared/scope-resolution/evidence-weights.js.map +1 -0
- package/dist/_shared/scope-resolution/finalize-algorithm.d.ts +149 -0
- package/dist/_shared/scope-resolution/finalize-algorithm.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/finalize-algorithm.js +795 -0
- package/dist/_shared/scope-resolution/finalize-algorithm.js.map +1 -0
- package/dist/_shared/scope-resolution/language-classification.d.ts +26 -0
- package/dist/_shared/scope-resolution/language-classification.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/language-classification.js +44 -0
- package/dist/_shared/scope-resolution/language-classification.js.map +1 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +106 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.js +98 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -0
- package/dist/_shared/scope-resolution/module-scope-index.d.ts +46 -0
- package/dist/_shared/scope-resolution/module-scope-index.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/module-scope-index.js +58 -0
- package/dist/_shared/scope-resolution/module-scope-index.js.map +1 -0
- package/dist/_shared/scope-resolution/origin-priority.d.ts +14 -0
- package/dist/_shared/scope-resolution/origin-priority.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/origin-priority.js +21 -0
- package/dist/_shared/scope-resolution/origin-priority.js.map +1 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts +76 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/parsed-file.js +54 -0
- package/dist/_shared/scope-resolution/parsed-file.js.map +1 -0
- package/dist/_shared/scope-resolution/position-index.d.ts +62 -0
- package/dist/_shared/scope-resolution/position-index.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/position-index.js +134 -0
- package/dist/_shared/scope-resolution/position-index.js.map +1 -0
- package/dist/_shared/scope-resolution/qualified-name-index.d.ts +44 -0
- package/dist/_shared/scope-resolution/qualified-name-index.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/qualified-name-index.js +75 -0
- package/dist/_shared/scope-resolution/qualified-name-index.js.map +1 -0
- package/dist/_shared/scope-resolution/reference-site.d.ts +75 -0
- package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/reference-site.js +24 -0
- package/dist/_shared/scope-resolution/reference-site.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts +27 -0
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js +30 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts +69 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.js +44 -0
- package/dist/_shared/scope-resolution/registries/context.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts +56 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.js +150 -0
- package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts +26 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js +31 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +81 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js +349 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts +33 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js +56 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts +36 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js +32 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts +43 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js +60 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js.map +1 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts +53 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.js +126 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -0
- package/dist/_shared/scope-resolution/scope-id.d.ts +43 -0
- package/dist/_shared/scope-resolution/scope-id.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/scope-id.js +46 -0
- package/dist/_shared/scope-resolution/scope-id.js.map +1 -0
- package/dist/_shared/scope-resolution/scope-tree.d.ts +83 -0
- package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/scope-tree.js +220 -0
- package/dist/_shared/scope-resolution/scope-tree.js.map +1 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts +63 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.js +122 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.js.map +1 -0
- package/dist/_shared/scope-resolution/shadow/diff.d.ts +59 -0
- package/dist/_shared/scope-resolution/shadow/diff.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/shadow/diff.js +79 -0
- package/dist/_shared/scope-resolution/shadow/diff.js.map +1 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +34 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/symbol-definition.js +12 -0
- package/dist/_shared/scope-resolution/symbol-definition.js.map +1 -0
- package/dist/_shared/scope-resolution/types.d.ts +400 -0
- package/dist/_shared/scope-resolution/types.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/types.js +25 -0
- package/dist/_shared/scope-resolution/types.js.map +1 -0
- package/dist/_shared/test-helpers.d.ts +13 -0
- package/dist/_shared/test-helpers.d.ts.map +1 -0
- package/dist/_shared/test-helpers.js +13 -0
- package/dist/_shared/test-helpers.js.map +1 -0
- package/dist/cli/ai-context.d.ts +28 -0
- package/dist/cli/ai-context.js +289 -0
- package/dist/cli/analyze.d.ts +80 -0
- package/dist/cli/analyze.js +525 -0
- package/dist/cli/augment.d.ts +13 -0
- package/dist/cli/augment.js +33 -0
- package/dist/cli/clean.d.ts +10 -0
- package/dist/cli/clean.js +79 -0
- package/dist/cli/cli-message.d.ts +16 -0
- package/dist/cli/cli-message.js +71 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +31 -0
- package/dist/cli/eval-server.d.ts +37 -0
- package/dist/cli/eval-server.js +423 -0
- package/dist/cli/group.d.ts +1 -0
- package/dist/cli/group.js +323 -0
- package/dist/cli/index-repo.d.ts +15 -0
- package/dist/cli/index-repo.js +120 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +492 -0
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +18 -0
- package/dist/cli/list.d.ts +6 -0
- package/dist/cli/list.js +40 -0
- package/dist/cli/mcp.d.ts +29 -0
- package/dist/cli/mcp.js +73 -0
- package/dist/cli/optional-grammars.d.ts +47 -0
- package/dist/cli/optional-grammars.js +87 -0
- package/dist/cli/publish.d.ts +29 -0
- package/dist/cli/publish.js +174 -0
- package/dist/cli/remove.d.ts +30 -0
- package/dist/cli/remove.js +102 -0
- package/dist/cli/serve.d.ts +4 -0
- package/dist/cli/serve.js +55 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +642 -0
- package/dist/cli/skill-gen.d.ts +26 -0
- package/dist/cli/skill-gen.js +555 -0
- package/dist/cli/status.d.ts +6 -0
- package/dist/cli/status.js +36 -0
- package/dist/cli/tool.d.ts +43 -0
- package/dist/cli/tool.js +169 -0
- package/dist/cli/wiki.d.ts +23 -0
- package/dist/cli/wiki.js +616 -0
- package/dist/config/ignore-service.d.ts +35 -0
- package/dist/config/ignore-service.js +439 -0
- package/dist/config/supported-languages.d.ts +13 -0
- package/dist/config/supported-languages.js +13 -0
- package/dist/core/augmentation/engine.d.ts +26 -0
- package/dist/core/augmentation/engine.js +272 -0
- package/dist/core/embedding-mode.d.ts +51 -0
- package/dist/core/embedding-mode.js +48 -0
- package/dist/core/embeddings/ast-utils.d.ts +22 -0
- package/dist/core/embeddings/ast-utils.js +106 -0
- package/dist/core/embeddings/character-chunk.d.ts +12 -0
- package/dist/core/embeddings/character-chunk.js +43 -0
- package/dist/core/embeddings/chunker.d.ts +14 -0
- package/dist/core/embeddings/chunker.js +239 -0
- package/dist/core/embeddings/config.d.ts +2 -0
- package/dist/core/embeddings/config.js +36 -0
- package/dist/core/embeddings/embedder.d.ts +65 -0
- package/dist/core/embeddings/embedder.js +349 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +68 -0
- package/dist/core/embeddings/embedding-pipeline.js +552 -0
- package/dist/core/embeddings/exact-search.d.ts +15 -0
- package/dist/core/embeddings/exact-search.js +27 -0
- package/dist/core/embeddings/hf-env.d.ts +135 -0
- package/dist/core/embeddings/hf-env.js +232 -0
- package/dist/core/embeddings/http-client.d.ts +37 -0
- package/dist/core/embeddings/http-client.js +199 -0
- package/dist/core/embeddings/index.d.ts +10 -0
- package/dist/core/embeddings/index.js +10 -0
- package/dist/core/embeddings/line-index.d.ts +7 -0
- package/dist/core/embeddings/line-index.js +42 -0
- package/dist/core/embeddings/server-mapping.d.ts +15 -0
- package/dist/core/embeddings/server-mapping.js +33 -0
- package/dist/core/embeddings/structural-extractor.d.ts +15 -0
- package/dist/core/embeddings/structural-extractor.js +58 -0
- package/dist/core/embeddings/text-generator.d.ts +31 -0
- package/dist/core/embeddings/text-generator.js +208 -0
- package/dist/core/embeddings/types.d.ts +211 -0
- package/dist/core/embeddings/types.js +202 -0
- package/dist/core/git-staleness.d.ts +37 -0
- package/dist/core/git-staleness.js +167 -0
- package/dist/core/graph/graph.d.ts +2 -0
- package/dist/core/graph/graph.js +173 -0
- package/dist/core/graph/types.d.ts +36 -0
- package/dist/core/graph/types.js +1 -0
- package/dist/core/group/bridge-db.d.ts +82 -0
- package/dist/core/group/bridge-db.js +609 -0
- package/dist/core/group/bridge-schema.d.ts +27 -0
- package/dist/core/group/bridge-schema.js +55 -0
- package/dist/core/group/config-parser.d.ts +7 -0
- package/dist/core/group/config-parser.js +122 -0
- package/dist/core/group/contract-extractor.d.ts +7 -0
- package/dist/core/group/contract-extractor.js +1 -0
- package/dist/core/group/cross-impact.d.ts +85 -0
- package/dist/core/group/cross-impact.js +515 -0
- package/dist/core/group/extractors/elixir-workspace-extractor.d.ts +15 -0
- package/dist/core/group/extractors/elixir-workspace-extractor.js +204 -0
- package/dist/core/group/extractors/fs-utils.d.ts +10 -0
- package/dist/core/group/extractors/fs-utils.js +24 -0
- package/dist/core/group/extractors/go-workspace-extractor.d.ts +14 -0
- package/dist/core/group/extractors/go-workspace-extractor.js +217 -0
- package/dist/core/group/extractors/grpc-extractor.d.ts +25 -0
- package/dist/core/group/extractors/grpc-extractor.js +414 -0
- package/dist/core/group/extractors/grpc-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/go.js +97 -0
- package/dist/core/group/extractors/grpc-patterns/index.d.ts +19 -0
- package/dist/core/group/extractors/grpc-patterns/index.js +46 -0
- package/dist/core/group/extractors/grpc-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/java.js +173 -0
- package/dist/core/group/extractors/grpc-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/grpc-patterns/node.js +290 -0
- package/dist/core/group/extractors/grpc-patterns/proto.d.ts +9 -0
- package/dist/core/group/extractors/grpc-patterns/proto.js +134 -0
- package/dist/core/group/extractors/grpc-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/python.js +67 -0
- package/dist/core/group/extractors/grpc-patterns/types.d.ts +50 -0
- package/dist/core/group/extractors/grpc-patterns/types.js +1 -0
- package/dist/core/group/extractors/http-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/go.js +215 -0
- package/dist/core/group/extractors/http-patterns/index.d.ts +17 -0
- package/dist/core/group/extractors/http-patterns/index.js +44 -0
- package/dist/core/group/extractors/http-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/java.js +253 -0
- package/dist/core/group/extractors/http-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/http-patterns/node.js +484 -0
- package/dist/core/group/extractors/http-patterns/php.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/php.js +178 -0
- package/dist/core/group/extractors/http-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/python.js +308 -0
- package/dist/core/group/extractors/http-patterns/types.d.ts +61 -0
- package/dist/core/group/extractors/http-patterns/types.js +1 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +21 -0
- package/dist/core/group/extractors/http-route-extractor.js +430 -0
- package/dist/core/group/extractors/include-extractor.d.ts +39 -0
- package/dist/core/group/extractors/include-extractor.js +566 -0
- package/dist/core/group/extractors/java-workspace-extractor.d.ts +16 -0
- package/dist/core/group/extractors/java-workspace-extractor.js +204 -0
- package/dist/core/group/extractors/manifest-extractor.d.ts +54 -0
- package/dist/core/group/extractors/manifest-extractor.js +320 -0
- package/dist/core/group/extractors/node-workspace-extractor.d.ts +14 -0
- package/dist/core/group/extractors/node-workspace-extractor.js +207 -0
- package/dist/core/group/extractors/python-workspace-extractor.d.ts +15 -0
- package/dist/core/group/extractors/python-workspace-extractor.js +205 -0
- package/dist/core/group/extractors/rust-workspace-extractor.d.ts +44 -0
- package/dist/core/group/extractors/rust-workspace-extractor.js +240 -0
- package/dist/core/group/extractors/thrift-extractor.d.ts +22 -0
- package/dist/core/group/extractors/thrift-extractor.js +283 -0
- package/dist/core/group/extractors/thrift-patterns/index.d.ts +4 -0
- package/dist/core/group/extractors/thrift-patterns/index.js +10 -0
- package/dist/core/group/extractors/thrift-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/thrift-patterns/java.js +220 -0
- package/dist/core/group/extractors/thrift-patterns/types.d.ts +17 -0
- package/dist/core/group/extractors/thrift-patterns/types.js +1 -0
- package/dist/core/group/extractors/topic-extractor.d.ts +8 -0
- package/dist/core/group/extractors/topic-extractor.js +97 -0
- package/dist/core/group/extractors/topic-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/go.js +120 -0
- package/dist/core/group/extractors/topic-patterns/index.d.ts +14 -0
- package/dist/core/group/extractors/topic-patterns/index.js +38 -0
- package/dist/core/group/extractors/topic-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/java.js +80 -0
- package/dist/core/group/extractors/topic-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/topic-patterns/node.js +155 -0
- package/dist/core/group/extractors/topic-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/python.js +116 -0
- package/dist/core/group/extractors/topic-patterns/types.d.ts +25 -0
- package/dist/core/group/extractors/topic-patterns/types.js +10 -0
- package/dist/core/group/extractors/tree-sitter-scanner.d.ts +113 -0
- package/dist/core/group/extractors/tree-sitter-scanner.js +95 -0
- package/dist/core/group/extractors/workspace-extractor.d.ts +13 -0
- package/dist/core/group/extractors/workspace-extractor.js +65 -0
- package/dist/core/group/group-path-utils.d.ts +17 -0
- package/dist/core/group/group-path-utils.js +40 -0
- package/dist/core/group/matching.d.ts +13 -0
- package/dist/core/group/matching.js +284 -0
- package/dist/core/group/normalization.d.ts +3 -0
- package/dist/core/group/normalization.js +115 -0
- package/dist/core/group/resolve-at-member.d.ts +10 -0
- package/dist/core/group/resolve-at-member.js +31 -0
- package/dist/core/group/service-boundary-detector.d.ts +8 -0
- package/dist/core/group/service-boundary-detector.js +155 -0
- package/dist/core/group/service.d.ts +56 -0
- package/dist/core/group/service.js +395 -0
- package/dist/core/group/storage.d.ts +9 -0
- package/dist/core/group/storage.js +157 -0
- package/dist/core/group/sync.d.ts +21 -0
- package/dist/core/group/sync.js +267 -0
- package/dist/core/group/types.d.ts +181 -0
- package/dist/core/group/types.js +1 -0
- package/dist/core/incremental/shadow-candidates.d.ts +44 -0
- package/dist/core/incremental/shadow-candidates.js +74 -0
- package/dist/core/incremental/subgraph-extract.d.ts +64 -0
- package/dist/core/incremental/subgraph-extract.js +111 -0
- package/dist/core/ingestion/ast-cache.d.ts +26 -0
- package/dist/core/ingestion/ast-cache.js +48 -0
- package/dist/core/ingestion/binding-accumulator.d.ts +212 -0
- package/dist/core/ingestion/binding-accumulator.js +336 -0
- package/dist/core/ingestion/call-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/call-extractors/configs/c-cpp.js +8 -0
- package/dist/core/ingestion/call-extractors/configs/csharp.d.ts +2 -0
- package/dist/core/ingestion/call-extractors/configs/csharp.js +6 -0
- package/dist/core/ingestion/call-extractors/configs/dart.d.ts +2 -0
- package/dist/core/ingestion/call-extractors/configs/dart.js +5 -0
- package/dist/core/ingestion/call-extractors/configs/go.d.ts +2 -0
- package/dist/core/ingestion/call-extractors/configs/go.js +5 -0
- package/dist/core/ingestion/call-extractors/configs/jvm.d.ts +3 -0
- package/dist/core/ingestion/call-extractors/configs/jvm.js +51 -0
- package/dist/core/ingestion/call-extractors/configs/php.d.ts +2 -0
- package/dist/core/ingestion/call-extractors/configs/php.js +5 -0
- package/dist/core/ingestion/call-extractors/configs/python.d.ts +2 -0
- package/dist/core/ingestion/call-extractors/configs/python.js +5 -0
- package/dist/core/ingestion/call-extractors/configs/ruby.d.ts +2 -0
- package/dist/core/ingestion/call-extractors/configs/ruby.js +5 -0
- package/dist/core/ingestion/call-extractors/configs/rust.d.ts +2 -0
- package/dist/core/ingestion/call-extractors/configs/rust.js +5 -0
- package/dist/core/ingestion/call-extractors/configs/swift.d.ts +2 -0
- package/dist/core/ingestion/call-extractors/configs/swift.js +5 -0
- package/dist/core/ingestion/call-extractors/configs/typescript-javascript.d.ts +3 -0
- package/dist/core/ingestion/call-extractors/configs/typescript-javascript.js +8 -0
- package/dist/core/ingestion/call-extractors/generic.d.ts +5 -0
- package/dist/core/ingestion/call-extractors/generic.js +59 -0
- package/dist/core/ingestion/call-processor.d.ts +235 -0
- package/dist/core/ingestion/call-processor.js +2754 -0
- package/dist/core/ingestion/call-routing.d.ts +55 -0
- package/dist/core/ingestion/call-routing.js +95 -0
- package/dist/core/ingestion/call-types.d.ts +135 -0
- package/dist/core/ingestion/call-types.js +2 -0
- package/dist/core/ingestion/class-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/class-extractors/configs/c-cpp.js +11 -0
- package/dist/core/ingestion/class-extractors/configs/csharp.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/configs/csharp.js +21 -0
- package/dist/core/ingestion/class-extractors/configs/dart.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/configs/dart.js +7 -0
- package/dist/core/ingestion/class-extractors/configs/go.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/configs/go.js +20 -0
- package/dist/core/ingestion/class-extractors/configs/jvm.d.ts +3 -0
- package/dist/core/ingestion/class-extractors/configs/jvm.js +35 -0
- package/dist/core/ingestion/class-extractors/configs/php.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/configs/php.js +7 -0
- package/dist/core/ingestion/class-extractors/configs/python.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/configs/python.js +7 -0
- package/dist/core/ingestion/class-extractors/configs/ruby.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/configs/ruby.js +7 -0
- package/dist/core/ingestion/class-extractors/configs/rust.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/configs/rust.js +7 -0
- package/dist/core/ingestion/class-extractors/configs/swift.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/configs/swift.js +18 -0
- package/dist/core/ingestion/class-extractors/configs/typescript-javascript.d.ts +4 -0
- package/dist/core/ingestion/class-extractors/configs/typescript-javascript.js +28 -0
- package/dist/core/ingestion/class-extractors/generic.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/generic.js +135 -0
- package/dist/core/ingestion/class-types.d.ts +34 -0
- package/dist/core/ingestion/class-types.js +1 -0
- package/dist/core/ingestion/cluster-enricher.d.ts +38 -0
- package/dist/core/ingestion/cluster-enricher.js +169 -0
- package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
- package/dist/core/ingestion/cobol/cobol-copy-expander.js +376 -0
- package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +212 -0
- package/dist/core/ingestion/cobol/cobol-preprocessor.js +1727 -0
- package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
- package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
- package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
- package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
- package/dist/core/ingestion/cobol-processor.d.ts +54 -0
- package/dist/core/ingestion/cobol-processor.js +1232 -0
- package/dist/core/ingestion/community-processor.d.ts +39 -0
- package/dist/core/ingestion/community-processor.js +336 -0
- package/dist/core/ingestion/constants.d.ts +17 -0
- package/dist/core/ingestion/constants.js +21 -0
- package/dist/core/ingestion/cpp-ue-preprocessor.d.ts +12 -0
- package/dist/core/ingestion/cpp-ue-preprocessor.js +260 -0
- package/dist/core/ingestion/emit-references.d.ts +88 -0
- package/dist/core/ingestion/emit-references.js +229 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +40 -0
- package/dist/core/ingestion/entry-point-scoring.js +196 -0
- package/dist/core/ingestion/export-detection.d.ts +57 -0
- package/dist/core/ingestion/export-detection.js +233 -0
- package/dist/core/ingestion/field-extractor.d.ts +29 -0
- package/dist/core/ingestion/field-extractor.js +25 -0
- package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/c-cpp.js +104 -0
- package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/csharp.js +121 -0
- package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/dart.js +78 -0
- package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
- package/dist/core/ingestion/field-extractors/configs/go.js +60 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +53 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.js +158 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
- package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/php.js +65 -0
- package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
- package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
- package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
- package/dist/core/ingestion/field-extractors/configs/ruby.js +76 -0
- package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
- package/dist/core/ingestion/field-extractors/configs/rust.js +52 -0
- package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/swift.js +65 -0
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +56 -0
- package/dist/core/ingestion/field-extractors/generic.d.ts +49 -0
- package/dist/core/ingestion/field-extractors/generic.js +117 -0
- package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
- package/dist/core/ingestion/field-extractors/typescript.js +291 -0
- package/dist/core/ingestion/field-types.d.ts +61 -0
- package/dist/core/ingestion/field-types.js +2 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +28 -0
- package/dist/core/ingestion/filesystem-walker.js +92 -0
- package/dist/core/ingestion/finalize-orchestrator.d.ts +63 -0
- package/dist/core/ingestion/finalize-orchestrator.js +142 -0
- package/dist/core/ingestion/framework-detection.d.ts +30 -0
- package/dist/core/ingestion/framework-detection.js +428 -0
- package/dist/core/ingestion/heritage-extractors/configs/go.d.ts +13 -0
- package/dist/core/ingestion/heritage-extractors/configs/go.js +20 -0
- package/dist/core/ingestion/heritage-extractors/configs/ruby.d.ts +18 -0
- package/dist/core/ingestion/heritage-extractors/configs/ruby.js +65 -0
- package/dist/core/ingestion/heritage-extractors/generic.d.ts +23 -0
- package/dist/core/ingestion/heritage-extractors/generic.js +47 -0
- package/dist/core/ingestion/heritage-processor.d.ts +54 -0
- package/dist/core/ingestion/heritage-processor.js +367 -0
- package/dist/core/ingestion/heritage-types.d.ts +73 -0
- package/dist/core/ingestion/heritage-types.js +2 -0
- package/dist/core/ingestion/import-processor.d.ts +23 -0
- package/dist/core/ingestion/import-processor.js +377 -0
- package/dist/core/ingestion/import-resolvers/configs/c-cpp.d.ts +7 -0
- package/dist/core/ingestion/import-resolvers/configs/c-cpp.js +14 -0
- package/dist/core/ingestion/import-resolvers/configs/csharp.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/configs/csharp.js +27 -0
- package/dist/core/ingestion/import-resolvers/configs/dart.d.ts +17 -0
- package/dist/core/ingestion/import-resolvers/configs/dart.js +54 -0
- package/dist/core/ingestion/import-resolvers/configs/go.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/configs/go.js +26 -0
- package/dist/core/ingestion/import-resolvers/configs/jvm.d.ts +13 -0
- package/dist/core/ingestion/import-resolvers/configs/jvm.js +68 -0
- package/dist/core/ingestion/import-resolvers/configs/php.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/configs/php.js +15 -0
- package/dist/core/ingestion/import-resolvers/configs/python.d.ts +12 -0
- package/dist/core/ingestion/import-resolvers/configs/python.js +41 -0
- package/dist/core/ingestion/import-resolvers/configs/ruby.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/configs/ruby.js +16 -0
- package/dist/core/ingestion/import-resolvers/configs/rust.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/configs/rust.js +54 -0
- package/dist/core/ingestion/import-resolvers/configs/swift.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/configs/swift.js +29 -0
- package/dist/core/ingestion/import-resolvers/configs/typescript-javascript.d.ts +9 -0
- package/dist/core/ingestion/import-resolvers/configs/typescript-javascript.js +23 -0
- package/dist/core/ingestion/import-resolvers/csharp.d.ts +18 -0
- package/dist/core/ingestion/import-resolvers/csharp.js +115 -0
- package/dist/core/ingestion/import-resolvers/go.d.ts +17 -0
- package/dist/core/ingestion/import-resolvers/go.js +46 -0
- package/dist/core/ingestion/import-resolvers/jvm.d.ts +27 -0
- package/dist/core/ingestion/import-resolvers/jvm.js +106 -0
- package/dist/core/ingestion/import-resolvers/php.d.ts +24 -0
- package/dist/core/ingestion/import-resolvers/php.js +77 -0
- package/dist/core/ingestion/import-resolvers/python.d.ts +22 -0
- package/dist/core/ingestion/import-resolvers/python.js +72 -0
- package/dist/core/ingestion/import-resolvers/resolver-factory.d.ts +24 -0
- package/dist/core/ingestion/import-resolvers/resolver-factory.js +33 -0
- package/dist/core/ingestion/import-resolvers/ruby.d.ts +14 -0
- package/dist/core/ingestion/import-resolvers/ruby.js +17 -0
- package/dist/core/ingestion/import-resolvers/rust.d.ts +17 -0
- package/dist/core/ingestion/import-resolvers/rust.js +75 -0
- package/dist/core/ingestion/import-resolvers/standard.d.ts +35 -0
- package/dist/core/ingestion/import-resolvers/standard.js +168 -0
- package/dist/core/ingestion/import-resolvers/types.d.ts +68 -0
- package/dist/core/ingestion/import-resolvers/types.js +6 -0
- package/dist/core/ingestion/import-resolvers/utils.d.ts +35 -0
- package/dist/core/ingestion/import-resolvers/utils.js +153 -0
- package/dist/core/ingestion/import-target-adapter.d.ts +73 -0
- package/dist/core/ingestion/import-target-adapter.js +95 -0
- package/dist/core/ingestion/language-config.d.ts +52 -0
- package/dist/core/ingestion/language-config.js +182 -0
- package/dist/core/ingestion/language-provider.d.ts +465 -0
- package/dist/core/ingestion/language-provider.js +24 -0
- package/dist/core/ingestion/languages/c/arity-metadata.d.ts +14 -0
- package/dist/core/ingestion/languages/c/arity-metadata.js +94 -0
- package/dist/core/ingestion/languages/c/arity.d.ts +6 -0
- package/dist/core/ingestion/languages/c/arity.js +18 -0
- package/dist/core/ingestion/languages/c/captures.d.ts +2 -0
- package/dist/core/ingestion/languages/c/captures.js +105 -0
- package/dist/core/ingestion/languages/c/header-scan.d.ts +7 -0
- package/dist/core/ingestion/languages/c/header-scan.js +55 -0
- package/dist/core/ingestion/languages/c/import-decomposer.d.ts +8 -0
- package/dist/core/ingestion/languages/c/import-decomposer.js +50 -0
- package/dist/core/ingestion/languages/c/import-target.d.ts +14 -0
- package/dist/core/ingestion/languages/c/import-target.js +57 -0
- package/dist/core/ingestion/languages/c/index.d.ts +11 -0
- package/dist/core/ingestion/languages/c/index.js +11 -0
- package/dist/core/ingestion/languages/c/interpret.d.ts +14 -0
- package/dist/core/ingestion/languages/c/interpret.js +48 -0
- package/dist/core/ingestion/languages/c/merge-bindings.d.ts +7 -0
- package/dist/core/ingestion/languages/c/merge-bindings.js +23 -0
- package/dist/core/ingestion/languages/c/query.d.ts +3 -0
- package/dist/core/ingestion/languages/c/query.js +161 -0
- package/dist/core/ingestion/languages/c/scope-resolver.d.ts +13 -0
- package/dist/core/ingestion/languages/c/scope-resolver.js +60 -0
- package/dist/core/ingestion/languages/c/simple-hooks.d.ts +14 -0
- package/dist/core/ingestion/languages/c/simple-hooks.js +19 -0
- package/dist/core/ingestion/languages/c/static-linkage.d.ts +13 -0
- package/dist/core/ingestion/languages/c/static-linkage.js +57 -0
- package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
- package/dist/core/ingestion/languages/c-cpp.js +411 -0
- package/dist/core/ingestion/languages/cobol.d.ts +1 -0
- package/dist/core/ingestion/languages/cobol.js +28 -0
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.d.ts +21 -0
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.js +56 -0
- package/dist/core/ingestion/languages/csharp/arity-metadata.d.ts +26 -0
- package/dist/core/ingestion/languages/csharp/arity-metadata.js +46 -0
- package/dist/core/ingestion/languages/csharp/arity.d.ts +23 -0
- package/dist/core/ingestion/languages/csharp/arity.js +37 -0
- package/dist/core/ingestion/languages/csharp/cache-stats.d.ts +15 -0
- package/dist/core/ingestion/languages/csharp/cache-stats.js +26 -0
- package/dist/core/ingestion/languages/csharp/captures.d.ts +19 -0
- package/dist/core/ingestion/languages/csharp/captures.js +346 -0
- package/dist/core/ingestion/languages/csharp/import-decomposer.d.ts +19 -0
- package/dist/core/ingestion/languages/csharp/import-decomposer.js +93 -0
- package/dist/core/ingestion/languages/csharp/import-target.d.ts +25 -0
- package/dist/core/ingestion/languages/csharp/import-target.js +123 -0
- package/dist/core/ingestion/languages/csharp/index.d.ts +82 -0
- package/dist/core/ingestion/languages/csharp/index.js +82 -0
- package/dist/core/ingestion/languages/csharp/interpret.d.ts +15 -0
- package/dist/core/ingestion/languages/csharp/interpret.js +132 -0
- package/dist/core/ingestion/languages/csharp/merge-bindings.d.ts +27 -0
- package/dist/core/ingestion/languages/csharp/merge-bindings.js +55 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +51 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.js +387 -0
- package/dist/core/ingestion/languages/csharp/query.d.ts +35 -0
- package/dist/core/ingestion/languages/csharp/query.js +521 -0
- package/dist/core/ingestion/languages/csharp/receiver-binding.d.ts +31 -0
- package/dist/core/ingestion/languages/csharp/receiver-binding.js +135 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.d.ts +10 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +63 -0
- package/dist/core/ingestion/languages/csharp/simple-hooks.d.ts +53 -0
- package/dist/core/ingestion/languages/csharp/simple-hooks.js +76 -0
- package/dist/core/ingestion/languages/csharp.d.ts +8 -0
- package/dist/core/ingestion/languages/csharp.js +201 -0
- package/dist/core/ingestion/languages/dart.d.ts +12 -0
- package/dist/core/ingestion/languages/dart.js +138 -0
- package/dist/core/ingestion/languages/go/arity-metadata.d.ts +8 -0
- package/dist/core/ingestion/languages/go/arity-metadata.js +37 -0
- package/dist/core/ingestion/languages/go/arity.d.ts +2 -0
- package/dist/core/ingestion/languages/go/arity.js +14 -0
- package/dist/core/ingestion/languages/go/cache-stats.d.ts +7 -0
- package/dist/core/ingestion/languages/go/cache-stats.js +15 -0
- package/dist/core/ingestion/languages/go/captures.d.ts +2 -0
- package/dist/core/ingestion/languages/go/captures.js +129 -0
- package/dist/core/ingestion/languages/go/expand-wildcards.d.ts +15 -0
- package/dist/core/ingestion/languages/go/expand-wildcards.js +93 -0
- package/dist/core/ingestion/languages/go/import-decomposer.d.ts +3 -0
- package/dist/core/ingestion/languages/go/import-decomposer.js +44 -0
- package/dist/core/ingestion/languages/go/import-target.d.ts +21 -0
- package/dist/core/ingestion/languages/go/import-target.js +67 -0
- package/dist/core/ingestion/languages/go/index.d.ts +17 -0
- package/dist/core/ingestion/languages/go/index.js +17 -0
- package/dist/core/ingestion/languages/go/interface-impls.d.ts +4 -0
- package/dist/core/ingestion/languages/go/interface-impls.js +72 -0
- package/dist/core/ingestion/languages/go/interpret.d.ts +11 -0
- package/dist/core/ingestion/languages/go/interpret.js +146 -0
- package/dist/core/ingestion/languages/go/merge-bindings.d.ts +2 -0
- package/dist/core/ingestion/languages/go/merge-bindings.js +18 -0
- package/dist/core/ingestion/languages/go/method-owners.d.ts +17 -0
- package/dist/core/ingestion/languages/go/method-owners.js +96 -0
- package/dist/core/ingestion/languages/go/namespace-mirror.d.ts +15 -0
- package/dist/core/ingestion/languages/go/namespace-mirror.js +53 -0
- package/dist/core/ingestion/languages/go/package-siblings.d.ts +11 -0
- package/dist/core/ingestion/languages/go/package-siblings.js +84 -0
- package/dist/core/ingestion/languages/go/query.d.ts +3 -0
- package/dist/core/ingestion/languages/go/query.js +207 -0
- package/dist/core/ingestion/languages/go/range-binding.d.ts +8 -0
- package/dist/core/ingestion/languages/go/range-binding.js +109 -0
- package/dist/core/ingestion/languages/go/receiver-binding.d.ts +3 -0
- package/dist/core/ingestion/languages/go/receiver-binding.js +21 -0
- package/dist/core/ingestion/languages/go/scope-resolver.d.ts +2 -0
- package/dist/core/ingestion/languages/go/scope-resolver.js +33 -0
- package/dist/core/ingestion/languages/go/simple-hooks.d.ts +4 -0
- package/dist/core/ingestion/languages/go/simple-hooks.js +21 -0
- package/dist/core/ingestion/languages/go/type-binding.d.ts +3 -0
- package/dist/core/ingestion/languages/go/type-binding.js +237 -0
- package/dist/core/ingestion/languages/go.d.ts +11 -0
- package/dist/core/ingestion/languages/go.js +94 -0
- package/dist/core/ingestion/languages/index.d.ts +39 -0
- package/dist/core/ingestion/languages/index.js +64 -0
- package/dist/core/ingestion/languages/java/arity-metadata.d.ts +18 -0
- package/dist/core/ingestion/languages/java/arity-metadata.js +40 -0
- package/dist/core/ingestion/languages/java/arity.d.ts +10 -0
- package/dist/core/ingestion/languages/java/arity.js +24 -0
- package/dist/core/ingestion/languages/java/cache-stats.d.ts +15 -0
- package/dist/core/ingestion/languages/java/cache-stats.js +26 -0
- package/dist/core/ingestion/languages/java/captures.d.ts +17 -0
- package/dist/core/ingestion/languages/java/captures.js +187 -0
- package/dist/core/ingestion/languages/java/import-decomposer.d.ts +18 -0
- package/dist/core/ingestion/languages/java/import-decomposer.js +85 -0
- package/dist/core/ingestion/languages/java/import-target.d.ts +17 -0
- package/dist/core/ingestion/languages/java/import-target.js +100 -0
- package/dist/core/ingestion/languages/java/index.d.ts +29 -0
- package/dist/core/ingestion/languages/java/index.js +29 -0
- package/dist/core/ingestion/languages/java/interpret.d.ts +13 -0
- package/dist/core/ingestion/languages/java/interpret.js +131 -0
- package/dist/core/ingestion/languages/java/merge-bindings.d.ts +12 -0
- package/dist/core/ingestion/languages/java/merge-bindings.js +40 -0
- package/dist/core/ingestion/languages/java/query.d.ts +30 -0
- package/dist/core/ingestion/languages/java/query.js +192 -0
- package/dist/core/ingestion/languages/java/receiver-binding.d.ts +11 -0
- package/dist/core/ingestion/languages/java/receiver-binding.js +95 -0
- package/dist/core/ingestion/languages/java/scope-resolver.d.ts +50 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +74 -0
- package/dist/core/ingestion/languages/java/simple-hooks.d.ts +13 -0
- package/dist/core/ingestion/languages/java/simple-hooks.js +34 -0
- package/dist/core/ingestion/languages/java.d.ts +9 -0
- package/dist/core/ingestion/languages/java.js +76 -0
- package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
- package/dist/core/ingestion/languages/kotlin.js +164 -0
- package/dist/core/ingestion/languages/php/arity-metadata.d.ts +28 -0
- package/dist/core/ingestion/languages/php/arity-metadata.js +63 -0
- package/dist/core/ingestion/languages/php/arity.d.ts +25 -0
- package/dist/core/ingestion/languages/php/arity.js +40 -0
- package/dist/core/ingestion/languages/php/cache-stats.d.ts +15 -0
- package/dist/core/ingestion/languages/php/cache-stats.js +26 -0
- package/dist/core/ingestion/languages/php/captures.d.ts +34 -0
- package/dist/core/ingestion/languages/php/captures.js +739 -0
- package/dist/core/ingestion/languages/php/import-decomposer.d.ts +28 -0
- package/dist/core/ingestion/languages/php/import-decomposer.js +265 -0
- package/dist/core/ingestion/languages/php/import-target.d.ts +47 -0
- package/dist/core/ingestion/languages/php/import-target.js +100 -0
- package/dist/core/ingestion/languages/php/index.d.ts +68 -0
- package/dist/core/ingestion/languages/php/index.js +72 -0
- package/dist/core/ingestion/languages/php/interpret.d.ts +36 -0
- package/dist/core/ingestion/languages/php/interpret.js +241 -0
- package/dist/core/ingestion/languages/php/merge-bindings.d.ts +19 -0
- package/dist/core/ingestion/languages/php/merge-bindings.js +47 -0
- package/dist/core/ingestion/languages/php/namespace-siblings.d.ts +51 -0
- package/dist/core/ingestion/languages/php/namespace-siblings.js +288 -0
- package/dist/core/ingestion/languages/php/query.d.ts +32 -0
- package/dist/core/ingestion/languages/php/query.js +326 -0
- package/dist/core/ingestion/languages/php/receiver-binding.d.ts +36 -0
- package/dist/core/ingestion/languages/php/receiver-binding.js +128 -0
- package/dist/core/ingestion/languages/php/scope-resolver.d.ts +23 -0
- package/dist/core/ingestion/languages/php/scope-resolver.js +358 -0
- package/dist/core/ingestion/languages/php/simple-hooks.d.ts +42 -0
- package/dist/core/ingestion/languages/php/simple-hooks.js +111 -0
- package/dist/core/ingestion/languages/php.d.ts +1 -0
- package/dist/core/ingestion/languages/php.js +290 -0
- package/dist/core/ingestion/languages/python/arity-metadata.d.ts +24 -0
- package/dist/core/ingestion/languages/python/arity-metadata.js +45 -0
- package/dist/core/ingestion/languages/python/arity.d.ts +22 -0
- package/dist/core/ingestion/languages/python/arity.js +38 -0
- package/dist/core/ingestion/languages/python/cache-stats.d.ts +17 -0
- package/dist/core/ingestion/languages/python/cache-stats.js +28 -0
- package/dist/core/ingestion/languages/python/captures.d.ts +19 -0
- package/dist/core/ingestion/languages/python/captures.js +130 -0
- package/dist/core/ingestion/languages/python/import-decomposer.d.ts +15 -0
- package/dist/core/ingestion/languages/python/import-decomposer.js +112 -0
- package/dist/core/ingestion/languages/python/import-target.d.ts +21 -0
- package/dist/core/ingestion/languages/python/import-target.js +195 -0
- package/dist/core/ingestion/languages/python/index.d.ts +80 -0
- package/dist/core/ingestion/languages/python/index.js +80 -0
- package/dist/core/ingestion/languages/python/interpret.d.ts +15 -0
- package/dist/core/ingestion/languages/python/interpret.js +191 -0
- package/dist/core/ingestion/languages/python/merge-bindings.d.ts +16 -0
- package/dist/core/ingestion/languages/python/merge-bindings.js +44 -0
- package/dist/core/ingestion/languages/python/query.d.ts +9 -0
- package/dist/core/ingestion/languages/python/query.js +267 -0
- package/dist/core/ingestion/languages/python/receiver-binding.d.ts +21 -0
- package/dist/core/ingestion/languages/python/receiver-binding.js +116 -0
- package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -0
- package/dist/core/ingestion/languages/python/scope-resolver.js +53 -0
- package/dist/core/ingestion/languages/python/simple-hooks.d.ts +25 -0
- package/dist/core/ingestion/languages/python/simple-hooks.js +43 -0
- package/dist/core/ingestion/languages/python.d.ts +12 -0
- package/dist/core/ingestion/languages/python.js +133 -0
- package/dist/core/ingestion/languages/ruby.d.ts +9 -0
- package/dist/core/ingestion/languages/ruby.js +235 -0
- package/dist/core/ingestion/languages/rust.d.ts +12 -0
- package/dist/core/ingestion/languages/rust.js +167 -0
- package/dist/core/ingestion/languages/swift.d.ts +12 -0
- package/dist/core/ingestion/languages/swift.js +312 -0
- package/dist/core/ingestion/languages/typescript/arity-metadata.d.ts +59 -0
- package/dist/core/ingestion/languages/typescript/arity-metadata.js +103 -0
- package/dist/core/ingestion/languages/typescript/arity.d.ts +37 -0
- package/dist/core/ingestion/languages/typescript/arity.js +54 -0
- package/dist/core/ingestion/languages/typescript/cache-stats.d.ts +17 -0
- package/dist/core/ingestion/languages/typescript/cache-stats.js +28 -0
- package/dist/core/ingestion/languages/typescript/captures.d.ts +28 -0
- package/dist/core/ingestion/languages/typescript/captures.js +474 -0
- package/dist/core/ingestion/languages/typescript/import-decomposer.d.ts +49 -0
- package/dist/core/ingestion/languages/typescript/import-decomposer.js +371 -0
- package/dist/core/ingestion/languages/typescript/import-target.d.ts +50 -0
- package/dist/core/ingestion/languages/typescript/import-target.js +61 -0
- package/dist/core/ingestion/languages/typescript/index.d.ts +94 -0
- package/dist/core/ingestion/languages/typescript/index.js +94 -0
- package/dist/core/ingestion/languages/typescript/interpret.d.ts +35 -0
- package/dist/core/ingestion/languages/typescript/interpret.js +317 -0
- package/dist/core/ingestion/languages/typescript/merge-bindings.d.ts +62 -0
- package/dist/core/ingestion/languages/typescript/merge-bindings.js +158 -0
- package/dist/core/ingestion/languages/typescript/query.d.ts +84 -0
- package/dist/core/ingestion/languages/typescript/query.js +978 -0
- package/dist/core/ingestion/languages/typescript/receiver-binding.d.ts +59 -0
- package/dist/core/ingestion/languages/typescript/receiver-binding.js +171 -0
- package/dist/core/ingestion/languages/typescript/scope-resolver.d.ts +16 -0
- package/dist/core/ingestion/languages/typescript/scope-resolver.js +113 -0
- package/dist/core/ingestion/languages/typescript/simple-hooks.d.ts +71 -0
- package/dist/core/ingestion/languages/typescript/simple-hooks.js +131 -0
- package/dist/core/ingestion/languages/typescript.d.ts +11 -0
- package/dist/core/ingestion/languages/typescript.js +324 -0
- package/dist/core/ingestion/languages/vue.d.ts +13 -0
- package/dist/core/ingestion/languages/vue.js +79 -0
- package/dist/core/ingestion/markdown-processor.d.ts +17 -0
- package/dist/core/ingestion/markdown-processor.js +124 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
- package/dist/core/ingestion/method-extractors/configs/csharp.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/csharp.js +287 -0
- package/dist/core/ingestion/method-extractors/configs/dart.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/dart.js +376 -0
- package/dist/core/ingestion/method-extractors/configs/go.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/go.js +176 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.js +336 -0
- package/dist/core/ingestion/method-extractors/configs/php.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/php.js +304 -0
- package/dist/core/ingestion/method-extractors/configs/python.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/python.js +309 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.js +286 -0
- package/dist/core/ingestion/method-extractors/configs/rust.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/rust.js +195 -0
- package/dist/core/ingestion/method-extractors/configs/swift.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/swift.js +276 -0
- package/dist/core/ingestion/method-extractors/configs/typescript-javascript.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/typescript-javascript.js +338 -0
- package/dist/core/ingestion/method-extractors/generic.d.ts +11 -0
- package/dist/core/ingestion/method-extractors/generic.js +205 -0
- package/dist/core/ingestion/method-types.d.ts +90 -0
- package/dist/core/ingestion/method-types.js +2 -0
- package/dist/core/ingestion/model/field-registry.d.ts +18 -0
- package/dist/core/ingestion/model/field-registry.js +22 -0
- package/dist/core/ingestion/model/heritage-map.d.ts +105 -0
- package/dist/core/ingestion/model/heritage-map.js +260 -0
- package/dist/core/ingestion/model/index.d.ts +20 -0
- package/dist/core/ingestion/model/index.js +43 -0
- package/dist/core/ingestion/model/method-registry.d.ts +71 -0
- package/dist/core/ingestion/model/method-registry.js +134 -0
- package/dist/core/ingestion/model/registration-table.d.ts +138 -0
- package/dist/core/ingestion/model/registration-table.js +224 -0
- package/dist/core/ingestion/model/resolution-context.d.ts +93 -0
- package/dist/core/ingestion/model/resolution-context.js +337 -0
- package/dist/core/ingestion/model/resolve.d.ts +61 -0
- package/dist/core/ingestion/model/resolve.js +401 -0
- package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +72 -0
- package/dist/core/ingestion/model/scope-resolution-indexes.js +42 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +150 -0
- package/dist/core/ingestion/model/semantic-model.js +175 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +200 -0
- package/dist/core/ingestion/model/symbol-table.js +206 -0
- package/dist/core/ingestion/model/type-registry.d.ts +39 -0
- package/dist/core/ingestion/model/type-registry.js +62 -0
- package/dist/core/ingestion/mro-processor.d.ts +46 -0
- package/dist/core/ingestion/mro-processor.js +597 -0
- package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/csharp.js +37 -0
- package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/java.js +29 -0
- package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
- package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/php.js +61 -0
- package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/python.js +49 -0
- package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/rust.js +66 -0
- package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
- package/dist/core/ingestion/named-bindings/types.js +6 -0
- package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/typescript.js +58 -0
- package/dist/core/ingestion/parsing-processor.d.ts +60 -0
- package/dist/core/ingestion/parsing-processor.js +627 -0
- package/dist/core/ingestion/pipeline-phases/cobol.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/cobol.js +46 -0
- package/dist/core/ingestion/pipeline-phases/communities.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/communities.js +63 -0
- package/dist/core/ingestion/pipeline-phases/cross-file-impl.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/cross-file-impl.js +157 -0
- package/dist/core/ingestion/pipeline-phases/cross-file.d.ts +37 -0
- package/dist/core/ingestion/pipeline-phases/cross-file.js +64 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +22 -0
- package/dist/core/ingestion/pipeline-phases/index.js +23 -0
- package/dist/core/ingestion/pipeline-phases/markdown.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/markdown.js +34 -0
- package/dist/core/ingestion/pipeline-phases/mro.d.ts +18 -0
- package/dist/core/ingestion/pipeline-phases/mro.js +37 -0
- package/dist/core/ingestion/pipeline-phases/orm-extraction.d.ts +22 -0
- package/dist/core/ingestion/pipeline-phases/orm-extraction.js +92 -0
- package/dist/core/ingestion/pipeline-phases/orm.d.ts +15 -0
- package/dist/core/ingestion/pipeline-phases/orm.js +75 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +63 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +567 -0
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +88 -0
- package/dist/core/ingestion/pipeline-phases/parse.js +33 -0
- package/dist/core/ingestion/pipeline-phases/processes.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/processes.js +149 -0
- package/dist/core/ingestion/pipeline-phases/routes.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +244 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +22 -0
- package/dist/core/ingestion/pipeline-phases/runner.js +204 -0
- package/dist/core/ingestion/pipeline-phases/scan.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/scan.js +46 -0
- package/dist/core/ingestion/pipeline-phases/structure.d.ts +27 -0
- package/dist/core/ingestion/pipeline-phases/structure.js +35 -0
- package/dist/core/ingestion/pipeline-phases/tools.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/tools.js +86 -0
- package/dist/core/ingestion/pipeline-phases/types.d.ts +79 -0
- package/dist/core/ingestion/pipeline-phases/types.js +37 -0
- package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.d.ts +70 -0
- package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.js +312 -0
- package/dist/core/ingestion/pipeline.d.ts +49 -0
- package/dist/core/ingestion/pipeline.js +89 -0
- package/dist/core/ingestion/process-processor.d.ts +51 -0
- package/dist/core/ingestion/process-processor.js +318 -0
- package/dist/core/ingestion/registry-primary-flag.d.ts +88 -0
- package/dist/core/ingestion/registry-primary-flag.js +117 -0
- package/dist/core/ingestion/resolve-references.d.ts +63 -0
- package/dist/core/ingestion/resolve-references.js +175 -0
- package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
- package/dist/core/ingestion/route-extractors/expo.js +36 -0
- package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
- package/dist/core/ingestion/route-extractors/middleware.js +167 -0
- package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
- package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
- package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
- package/dist/core/ingestion/route-extractors/php.js +22 -0
- package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
- package/dist/core/ingestion/route-extractors/response-shapes.js +294 -0
- package/dist/core/ingestion/scope-extractor-bridge.d.ts +35 -0
- package/dist/core/ingestion/scope-extractor-bridge.js +49 -0
- package/dist/core/ingestion/scope-extractor.d.ts +86 -0
- package/dist/core/ingestion/scope-extractor.js +772 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +558 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +250 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +43 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +79 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +57 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +142 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +46 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +19 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +40 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +37 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +118 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +38 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +73 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +467 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +53 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +251 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.d.ts +75 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.js +202 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.js +102 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +30 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +81 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +46 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +377 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +47 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +152 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +68 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +125 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +31 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +91 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +218 -0
- package/dist/core/ingestion/scope-resolution/pipeline/validate-bindings-immutability.d.ts +39 -0
- package/dist/core/ingestion/scope-resolution/pipeline/validate-bindings-immutability.js +65 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +36 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +58 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +170 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +447 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +52 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.js +61 -0
- package/dist/core/ingestion/shadow-harness.d.ts +113 -0
- package/dist/core/ingestion/shadow-harness.js +148 -0
- package/dist/core/ingestion/structure-processor.d.ts +2 -0
- package/dist/core/ingestion/structure-processor.js +36 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +16 -0
- package/dist/core/ingestion/tree-sitter-queries.js +1497 -0
- package/dist/core/ingestion/type-env.d.ts +86 -0
- package/dist/core/ingestion/type-env.js +1129 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +7 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +532 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +583 -0
- package/dist/core/ingestion/type-extractors/dart.d.ts +15 -0
- package/dist/core/ingestion/type-extractors/dart.js +369 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/go.js +513 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
- package/dist/core/ingestion/type-extractors/jvm.js +856 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/php.js +534 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/python.js +474 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +377 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/rust.js +515 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +131 -0
- package/dist/core/ingestion/type-extractors/shared.js +796 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/swift.js +487 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +172 -0
- package/dist/core/ingestion/type-extractors/types.js +1 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/typescript.js +661 -0
- package/dist/core/ingestion/utils/ast-helpers.d.ts +102 -0
- package/dist/core/ingestion/utils/ast-helpers.js +561 -0
- package/dist/core/ingestion/utils/call-analysis.d.ts +75 -0
- package/dist/core/ingestion/utils/call-analysis.js +574 -0
- package/dist/core/ingestion/utils/env.d.ts +20 -0
- package/dist/core/ingestion/utils/env.js +24 -0
- package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
- package/dist/core/ingestion/utils/event-loop.js +5 -0
- package/dist/core/ingestion/utils/graph-sort.d.ts +58 -0
- package/dist/core/ingestion/utils/graph-sort.js +100 -0
- package/dist/core/ingestion/utils/max-file-size.d.ts +20 -0
- package/dist/core/ingestion/utils/max-file-size.js +53 -0
- package/dist/core/ingestion/utils/method-props.d.ts +32 -0
- package/dist/core/ingestion/utils/method-props.js +147 -0
- package/dist/core/ingestion/utils/ruby-self-call.d.ts +52 -0
- package/dist/core/ingestion/utils/ruby-self-call.js +59 -0
- package/dist/core/ingestion/utils/verbose.d.ts +1 -0
- package/dist/core/ingestion/utils/verbose.js +7 -0
- package/dist/core/ingestion/variable-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/variable-extractors/configs/c-cpp.js +81 -0
- package/dist/core/ingestion/variable-extractors/configs/csharp.d.ts +9 -0
- package/dist/core/ingestion/variable-extractors/configs/csharp.js +63 -0
- package/dist/core/ingestion/variable-extractors/configs/dart.d.ts +2 -0
- package/dist/core/ingestion/variable-extractors/configs/dart.js +94 -0
- package/dist/core/ingestion/variable-extractors/configs/go.d.ts +2 -0
- package/dist/core/ingestion/variable-extractors/configs/go.js +83 -0
- package/dist/core/ingestion/variable-extractors/configs/jvm.d.ts +18 -0
- package/dist/core/ingestion/variable-extractors/configs/jvm.js +115 -0
- package/dist/core/ingestion/variable-extractors/configs/php.d.ts +14 -0
- package/dist/core/ingestion/variable-extractors/configs/php.js +58 -0
- package/dist/core/ingestion/variable-extractors/configs/python.d.ts +2 -0
- package/dist/core/ingestion/variable-extractors/configs/python.js +101 -0
- package/dist/core/ingestion/variable-extractors/configs/ruby.d.ts +11 -0
- package/dist/core/ingestion/variable-extractors/configs/ruby.js +52 -0
- package/dist/core/ingestion/variable-extractors/configs/rust.d.ts +2 -0
- package/dist/core/ingestion/variable-extractors/configs/rust.js +76 -0
- package/dist/core/ingestion/variable-extractors/configs/swift.d.ts +2 -0
- package/dist/core/ingestion/variable-extractors/configs/swift.js +88 -0
- package/dist/core/ingestion/variable-extractors/configs/typescript-javascript.d.ts +3 -0
- package/dist/core/ingestion/variable-extractors/configs/typescript-javascript.js +83 -0
- package/dist/core/ingestion/variable-extractors/generic.d.ts +5 -0
- package/dist/core/ingestion/variable-extractors/generic.js +80 -0
- package/dist/core/ingestion/variable-types.d.ts +82 -0
- package/dist/core/ingestion/variable-types.js +2 -0
- package/dist/core/ingestion/vue-sfc-extractor.d.ts +44 -0
- package/dist/core/ingestion/vue-sfc-extractor.js +111 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +199 -0
- package/dist/core/ingestion/workers/parse-worker.js +1940 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +23 -0
- package/dist/core/ingestion/workers/worker-pool.js +380 -0
- package/dist/core/lbug/csv-generator.d.ts +33 -0
- package/dist/core/lbug/csv-generator.js +463 -0
- package/dist/core/lbug/extension-loader.d.ts +86 -0
- package/dist/core/lbug/extension-loader.js +186 -0
- package/dist/core/lbug/lbug-adapter.d.ts +243 -0
- package/dist/core/lbug/lbug-adapter.js +1377 -0
- package/dist/core/lbug/lbug-config.d.ts +102 -0
- package/dist/core/lbug/lbug-config.js +303 -0
- package/dist/core/lbug/pool-adapter.d.ts +90 -0
- package/dist/core/lbug/pool-adapter.js +592 -0
- package/dist/core/lbug/schema.d.ts +62 -0
- package/dist/core/lbug/schema.js +495 -0
- package/dist/core/logger.d.ts +125 -0
- package/dist/core/logger.js +323 -0
- package/dist/core/platform/capabilities.d.ts +24 -0
- package/dist/core/platform/capabilities.js +54 -0
- package/dist/core/run-analyze.d.ts +92 -0
- package/dist/core/run-analyze.js +672 -0
- package/dist/core/search/bm25-index.d.ts +29 -0
- package/dist/core/search/bm25-index.js +118 -0
- package/dist/core/search/fts-indexes.d.ts +1 -0
- package/dist/core/search/fts-indexes.js +7 -0
- package/dist/core/search/fts-schema.d.ts +6 -0
- package/dist/core/search/fts-schema.js +7 -0
- package/dist/core/search/hybrid-search.d.ts +53 -0
- package/dist/core/search/hybrid-search.js +136 -0
- package/dist/core/search/phase-timer.d.ts +72 -0
- package/dist/core/search/phase-timer.js +106 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +8 -0
- package/dist/core/tree-sitter/parser-loader.js +189 -0
- package/dist/core/tree-sitter/safe-parse.d.ts +6 -0
- package/dist/core/tree-sitter/safe-parse.js +32 -0
- package/dist/core/wiki/cursor-client.d.ts +31 -0
- package/dist/core/wiki/cursor-client.js +123 -0
- package/dist/core/wiki/generator.d.ts +129 -0
- package/dist/core/wiki/generator.js +899 -0
- package/dist/core/wiki/graph-queries.d.ts +84 -0
- package/dist/core/wiki/graph-queries.js +244 -0
- package/dist/core/wiki/html-viewer.d.ts +10 -0
- package/dist/core/wiki/html-viewer.js +304 -0
- package/dist/core/wiki/llm-client.d.ts +83 -0
- package/dist/core/wiki/llm-client.js +267 -0
- package/dist/core/wiki/mermaid-sanitizer.d.ts +2 -0
- package/dist/core/wiki/mermaid-sanitizer.js +100 -0
- package/dist/core/wiki/prompts.d.ts +53 -0
- package/dist/core/wiki/prompts.js +181 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +3 -0
- package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
- package/dist/mcp/compatible-stdio-transport.js +206 -0
- package/dist/mcp/core/embedder.d.ts +27 -0
- package/dist/mcp/core/embedder.js +145 -0
- package/dist/mcp/core/lbug-adapter.d.ts +11 -0
- package/dist/mcp/core/lbug-adapter.js +11 -0
- package/dist/mcp/local/local-backend.d.ts +356 -0
- package/dist/mcp/local/local-backend.js +3251 -0
- package/dist/mcp/resources.d.ts +62 -0
- package/dist/mcp/resources.js +512 -0
- package/dist/mcp/server.d.ts +23 -0
- package/dist/mcp/server.js +314 -0
- package/dist/mcp/staleness.d.ts +5 -0
- package/dist/mcp/staleness.js +4 -0
- package/dist/mcp/stdio-capture.d.ts +40 -0
- package/dist/mcp/stdio-capture.js +53 -0
- package/dist/mcp/stdio-context.d.ts +47 -0
- package/dist/mcp/stdio-context.js +145 -0
- package/dist/mcp/tools.d.ts +29 -0
- package/dist/mcp/tools.js +506 -0
- package/dist/server/analyze-job.d.ts +55 -0
- package/dist/server/analyze-job.js +150 -0
- package/dist/server/analyze-worker.d.ts +13 -0
- package/dist/server/analyze-worker.js +59 -0
- package/dist/server/api.d.ts +72 -0
- package/dist/server/api.js +1638 -0
- package/dist/server/git-clone.d.ts +99 -0
- package/dist/server/git-clone.js +397 -0
- package/dist/server/mcp-http.d.ts +13 -0
- package/dist/server/mcp-http.js +101 -0
- package/dist/server/validation.d.ts +98 -0
- package/dist/server/validation.js +142 -0
- package/dist/storage/file-hash.d.ts +47 -0
- package/dist/storage/file-hash.js +86 -0
- package/dist/storage/git.d.ts +148 -0
- package/dist/storage/git.js +346 -0
- package/dist/storage/parse-cache.d.ts +67 -0
- package/dist/storage/parse-cache.js +182 -0
- package/dist/storage/repo-manager.d.ts +467 -0
- package/dist/storage/repo-manager.js +804 -0
- package/dist/types/pipeline.d.ts +18 -0
- package/dist/types/pipeline.js +1 -0
- package/hooks/claude/arc-hook.cjs +334 -0
- package/hooks/claude/hook-lock.cjs +119 -0
- package/hooks/claude/pre-tool-use.sh +79 -0
- package/hooks/claude/session-start.sh +42 -0
- package/package.json +122 -0
- package/scripts/bench-scope-resolution.ts +134 -0
- package/scripts/build-tree-sitter-dart.cjs +53 -0
- package/scripts/build-tree-sitter-proto.cjs +93 -0
- package/scripts/build.js +99 -0
- package/scripts/ci-list-migrated-languages.ts +24 -0
- package/scripts/install-duckdb-extension.mjs +48 -0
- package/skills/arc-cli.md +83 -0
- package/skills/arc-debugging.md +89 -0
- package/skills/arc-exploring.md +78 -0
- package/skills/arc-guide.md +64 -0
- package/skills/arc-impact-analysis.md +97 -0
- package/skills/arc-pr-review.md +163 -0
- package/skills/arc-refactoring.md +121 -0
- package/vendor/leiden/index.cjs +355 -0
- package/vendor/leiden/utils.cjs +392 -0
- package/vendor/tree-sitter-dart/README.md +18 -0
- package/vendor/tree-sitter-dart/binding.gyp +31 -0
- package/vendor/tree-sitter-dart/bindings/node/binding.cc +20 -0
- package/vendor/tree-sitter-dart/bindings/node/index.d.ts +28 -0
- package/vendor/tree-sitter-dart/bindings/node/index.js +7 -0
- package/vendor/tree-sitter-dart/grammar.js +2895 -0
- package/vendor/tree-sitter-dart/package.json +18 -0
- package/vendor/tree-sitter-dart/queries/highlights.scm +246 -0
- package/vendor/tree-sitter-dart/queries/tags.scm +92 -0
- package/vendor/tree-sitter-dart/queries/test.scm +1 -0
- package/vendor/tree-sitter-dart/src/grammar.json +12459 -0
- package/vendor/tree-sitter-dart/src/node-types.json +15055 -0
- package/vendor/tree-sitter-dart/src/parser.c +196127 -0
- package/vendor/tree-sitter-dart/src/scanner.c +130 -0
- package/vendor/tree-sitter-dart/src/tree_sitter/alloc.h +54 -0
- package/vendor/tree-sitter-dart/src/tree_sitter/array.h +290 -0
- package/vendor/tree-sitter-dart/src/tree_sitter/parser.h +265 -0
- package/vendor/tree-sitter-proto/binding.gyp +30 -0
- package/vendor/tree-sitter-proto/bindings/node/binding.cc +20 -0
- package/vendor/tree-sitter-proto/bindings/node/index.d.ts +28 -0
- package/vendor/tree-sitter-proto/bindings/node/index.js +7 -0
- package/vendor/tree-sitter-proto/package.json +12 -0
- package/vendor/tree-sitter-proto/src/node-types.json +1145 -0
- package/vendor/tree-sitter-proto/src/parser.c +10149 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/alloc.h +54 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/array.h +291 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/parser.h +266 -0
- package/vendor/tree-sitter-swift/LICENSE +21 -0
- package/vendor/tree-sitter-swift/README.md +139 -0
- package/vendor/tree-sitter-swift/bindings/node/index.d.ts +28 -0
- package/vendor/tree-sitter-swift/bindings/node/index.js +7 -0
- package/vendor/tree-sitter-swift/package.json +28 -0
- package/vendor/tree-sitter-swift/prebuilds/darwin-arm64/tree-sitter-swift.node +0 -0
- package/vendor/tree-sitter-swift/prebuilds/darwin-x64/tree-sitter-swift.node +0 -0
- package/vendor/tree-sitter-swift/prebuilds/linux-arm64/tree-sitter-swift.node +0 -0
- package/vendor/tree-sitter-swift/prebuilds/linux-x64/tree-sitter-swift.node +0 -0
- package/vendor/tree-sitter-swift/prebuilds/win32-arm64/tree-sitter-swift.node +0 -0
- package/vendor/tree-sitter-swift/prebuilds/win32-x64/tree-sitter-swift.node +0 -0
- package/vendor/tree-sitter-swift/src/node-types.json +30694 -0
- package/web/assets/__vite-browser-external-CLwMvL_q.js +1 -0
- package/web/assets/agent-DaYmiVrk.js +601 -0
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-aGTGQQQG.js +1 -0
- package/web/assets/architectureDiagram-UL44E2DR-613o-OfM.js +36 -0
- package/web/assets/blockDiagram-7IZFK4PR-BBJRt4vF.js +132 -0
- package/web/assets/c4Diagram-Y2BXMSZH-BhR2CErx.js +10 -0
- package/web/assets/chunk-3SSMPTDK-DWfEAoKy.js +321 -0
- package/web/assets/chunk-6764PJDD-NOXEgi3n.js +1 -0
- package/web/assets/chunk-AZZRMDJM-DVvcxwI7.js +15 -0
- package/web/assets/chunk-JQRUD6KW-CQgkrimK.js +1 -0
- package/web/assets/chunk-KGYTTC2M-DxOdSoAJ.js +161 -0
- package/web/assets/chunk-KRXBNO2N-BlnQTnxv.js +1 -0
- package/web/assets/chunk-LCXTWHL2-Dhf_u-1F.js +231 -0
- package/web/assets/chunk-LII3EMHJ-Cb3HLCZX.js +1 -0
- package/web/assets/chunk-RG4AUYOV-DLCfNede.js +206 -0
- package/web/assets/chunk-T5OCTHI4-B0CGAG7q.js +1 -0
- package/web/assets/chunk-W44A43WB-ZyrAMwtT.js +1 -0
- package/web/assets/chunk-ZXARS5L4-B0TJPmj5.js +1 -0
- package/web/assets/classDiagram-KGZ6W3CR-CvSnsfJD.js +1 -0
- package/web/assets/classDiagram-v2-72OJOZXJ-CvSnsfJD.js +1 -0
- package/web/assets/context-builder-BREgwful.js +15 -0
- package/web/assets/cose-bilkent-UX7MHV2Q-BsPIaeag.js +1 -0
- package/web/assets/dagre-ND4H6XIP-CV4l9vOZ.js +4 -0
- package/web/assets/diagram-3NCE3AQN-9kSzEbS8.js +43 -0
- package/web/assets/diagram-GF46GFSD-qRvqbex6.js +24 -0
- package/web/assets/diagram-HNR7UZ2L-Dj_ye4Ua.js +3 -0
- package/web/assets/diagram-QXG6HAR7-COwBV6B0.js +24 -0
- package/web/assets/diagram-WEQXMOUZ-C9xjn5dU.js +10 -0
- package/web/assets/erDiagram-L5TCEMPS-fRp0t1Yd.js +85 -0
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-BR0Ygfrw.js +1 -0
- package/web/assets/flowDiagram-H6V6AXG4-Ccr8FDLD.js +162 -0
- package/web/assets/ganttDiagram-JCBTUEKG-DfBPqAGN.js +292 -0
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-CYihBz6Z.js +1 -0
- package/web/assets/gitGraphDiagram-S2ZK5IYY-CHvG_UQ0.js +106 -0
- package/web/assets/index-B7cw1L6-.css +2 -0
- package/web/assets/index-CJJQgfSH.js +886 -0
- package/web/assets/info-J43DQDTF-KCYPFFUO-BmmoeX4D.js +1 -0
- package/web/assets/infoDiagram-3YFTVSEB-C7cMy-GP.js +2 -0
- package/web/assets/ishikawaDiagram-BNXS4ZKH-C80yCPYi.js +70 -0
- package/web/assets/journeyDiagram-M6C3CM3L-BHxH1zjE.js +139 -0
- package/web/assets/kanban-definition-75IXJCU3-DNZo1hOE.js +89 -0
- package/web/assets/katex-K3KEBU37-CbyuvTf1.js +261 -0
- package/web/assets/mindmap-definition-2TDM6QVE-Dpgl3Elt.js +96 -0
- package/web/assets/packet-YPE3B663-LP52Z2RK-7JAqDnUy.js +1 -0
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-Bv9vE7io.js +1 -0
- package/web/assets/pieDiagram-CU6KROY3-BW0mr0ek.js +30 -0
- package/web/assets/quadrantDiagram-VICAPDV7-C1dCMBbk.js +7 -0
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-dtZpcOZd.js +1 -0
- package/web/assets/requirementDiagram-JXO7QTGE-Dyqqny4j.js +84 -0
- package/web/assets/sankeyDiagram-URQDO5SZ-B3FGr5SL.js +40 -0
- package/web/assets/sequenceDiagram-VS2MUI6T-B4LlGP9C.js +162 -0
- package/web/assets/stateDiagram-7D4R322I-V9F-klBP.js +1 -0
- package/web/assets/stateDiagram-v2-36443NZ5-CKDYYzqR.js +1 -0
- package/web/assets/timeline-definition-O6YCAMPW-CX2WjkZA.js +120 -0
- package/web/assets/treeView-BLDUP644-QA4HXRO3-BQaKTdhr.js +1 -0
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-Bqlxdyrq.js +1 -0
- package/web/assets/vennDiagram-MWXL3ELB-BxZPYqOF.js +34 -0
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-dofeprUr.js +1 -0
- package/web/assets/wardleyDiagram-CUQ6CDDI-BLdJJYkV.js +78 -0
- package/web/assets/xychartDiagram-N2JHSOCM-DqDgigLa.js +7 -0
- package/web/index.html +19 -0
|
@@ -0,0 +1,3251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Backend (Multi-Repo)
|
|
3
|
+
*
|
|
4
|
+
* Provides tool implementations using local .arc/ indexes.
|
|
5
|
+
* Supports multiple indexed repositories via a global registry.
|
|
6
|
+
* LadybugDB connections are opened lazily per repo on first query.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, isWriteQuery, } from '../../core/lbug/pool-adapter.js';
|
|
11
|
+
import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/lbug-config.js';
|
|
12
|
+
export { isWriteQuery };
|
|
13
|
+
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
|
14
|
+
// at MCP server startup — crashes on unsupported Node ABI versions (#89)
|
|
15
|
+
// git utilities available if needed
|
|
16
|
+
// import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
|
|
17
|
+
import { parseDiffHunks } from '../../storage/git.js';
|
|
18
|
+
import { listRegisteredRepos, cleanupOldKuzuFiles, } from '../../storage/repo-manager.js';
|
|
19
|
+
import { GroupService } from '../../core/group/service.js';
|
|
20
|
+
import { resolveAtGroupMemberRepoPath } from '../../core/group/resolve-at-member.js';
|
|
21
|
+
import { collectBestChunks } from '../../core/embeddings/types.js';
|
|
22
|
+
import { rankExactEmbeddingRows, } from '../../core/embeddings/exact-search.js';
|
|
23
|
+
import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME } from '../../core/lbug/schema.js';
|
|
24
|
+
import { getExactScanLimit, isVectorExtensionSupportedByPlatform, } from '../../core/platform/capabilities.js';
|
|
25
|
+
import { PhaseTimer } from '../../core/search/phase-timer.js';
|
|
26
|
+
import { checkStalenessAsync, checkCwdMatch } from '../../core/git-staleness.js';
|
|
27
|
+
import { logger } from '../../core/logger.js';
|
|
28
|
+
// AI context generation is CLI-only (arc analyze)
|
|
29
|
+
// import { generateAIContextFiles } from '../../cli/ai-context.js';
|
|
30
|
+
/**
|
|
31
|
+
* Quick test-file detection for filtering impact results.
|
|
32
|
+
* Matches common test file patterns across all supported languages.
|
|
33
|
+
*/
|
|
34
|
+
export function isTestFilePath(filePath) {
|
|
35
|
+
const p = filePath.toLowerCase().replace(/\\/g, '/');
|
|
36
|
+
return (p.includes('.test.') ||
|
|
37
|
+
p.includes('.spec.') ||
|
|
38
|
+
p.includes('__tests__/') ||
|
|
39
|
+
p.includes('__mocks__/') ||
|
|
40
|
+
p.includes('/test/') ||
|
|
41
|
+
p.includes('/tests/') ||
|
|
42
|
+
p.includes('/testing/') ||
|
|
43
|
+
p.includes('/fixtures/') ||
|
|
44
|
+
p.endsWith('_test.go') ||
|
|
45
|
+
p.endsWith('_test.py') ||
|
|
46
|
+
p.endsWith('_spec.rb') ||
|
|
47
|
+
p.endsWith('_test.rb') ||
|
|
48
|
+
p.includes('/spec/') ||
|
|
49
|
+
p.includes('/test_') ||
|
|
50
|
+
p.includes('/conftest.'));
|
|
51
|
+
}
|
|
52
|
+
/** Valid LadybugDB node labels for safe Cypher query construction */
|
|
53
|
+
export const VALID_NODE_LABELS = new Set([
|
|
54
|
+
'File',
|
|
55
|
+
'Folder',
|
|
56
|
+
'Function',
|
|
57
|
+
'Class',
|
|
58
|
+
'Interface',
|
|
59
|
+
'Method',
|
|
60
|
+
'CodeElement',
|
|
61
|
+
'Community',
|
|
62
|
+
'Process',
|
|
63
|
+
'Struct',
|
|
64
|
+
'Enum',
|
|
65
|
+
'Macro',
|
|
66
|
+
'Typedef',
|
|
67
|
+
'Union',
|
|
68
|
+
'Namespace',
|
|
69
|
+
'Trait',
|
|
70
|
+
'Impl',
|
|
71
|
+
'TypeAlias',
|
|
72
|
+
'Const',
|
|
73
|
+
'Static',
|
|
74
|
+
'Property',
|
|
75
|
+
'Record',
|
|
76
|
+
'Delegate',
|
|
77
|
+
'Annotation',
|
|
78
|
+
'Constructor',
|
|
79
|
+
'Template',
|
|
80
|
+
'Module',
|
|
81
|
+
'Route',
|
|
82
|
+
'Tool',
|
|
83
|
+
]);
|
|
84
|
+
/** Valid relation types for impact analysis filtering */
|
|
85
|
+
export const VALID_RELATION_TYPES = new Set([
|
|
86
|
+
'CALLS',
|
|
87
|
+
'IMPORTS',
|
|
88
|
+
'EXTENDS',
|
|
89
|
+
'IMPLEMENTS',
|
|
90
|
+
'HAS_METHOD',
|
|
91
|
+
'HAS_PROPERTY',
|
|
92
|
+
'METHOD_OVERRIDES',
|
|
93
|
+
'OVERRIDES', // Legacy alias — dual-read for pre-rename indexes
|
|
94
|
+
'METHOD_IMPLEMENTS',
|
|
95
|
+
'ACCESSES',
|
|
96
|
+
'HANDLES_ROUTE',
|
|
97
|
+
'FETCHES',
|
|
98
|
+
'HANDLES_TOOL',
|
|
99
|
+
'ENTRY_POINT_OF',
|
|
100
|
+
'WRAPS',
|
|
101
|
+
]);
|
|
102
|
+
/**
|
|
103
|
+
* Per-relation-type confidence floor for impact analysis.
|
|
104
|
+
*
|
|
105
|
+
* When the graph stores a relation with a confidence value, that stored
|
|
106
|
+
* value is used as-is (it reflects resolution-tier accuracy from analysis
|
|
107
|
+
* time). This map provides the floor for each edge type when no stored
|
|
108
|
+
* confidence is available, and is also used for display / tooltip hints.
|
|
109
|
+
*
|
|
110
|
+
* Rationale:
|
|
111
|
+
* CALLS / IMPORTS – direct, strongly-typed references → 0.9
|
|
112
|
+
* EXTENDS – class hierarchy, statically verifiable → 0.85
|
|
113
|
+
* IMPLEMENTS – interface contract, statically verifiable → 0.85
|
|
114
|
+
* METHOD_OVERRIDES – method override, statically verifiable → 0.85
|
|
115
|
+
* METHOD_IMPLEMENTS – interface method implementation, statically verifiable → 0.85
|
|
116
|
+
* HAS_METHOD – structural containment → 0.95
|
|
117
|
+
* HAS_PROPERTY – structural containment → 0.95
|
|
118
|
+
* ACCESSES – field read/write, may be indirect → 0.8
|
|
119
|
+
* CONTAINS – folder/file containment → 0.95
|
|
120
|
+
* (unknown type) – conservative fallback → 0.5
|
|
121
|
+
*/
|
|
122
|
+
export const IMPACT_RELATION_CONFIDENCE = {
|
|
123
|
+
CALLS: 0.9,
|
|
124
|
+
IMPORTS: 0.9,
|
|
125
|
+
EXTENDS: 0.85,
|
|
126
|
+
IMPLEMENTS: 0.85,
|
|
127
|
+
METHOD_OVERRIDES: 0.85,
|
|
128
|
+
METHOD_IMPLEMENTS: 0.85,
|
|
129
|
+
HAS_METHOD: 0.95,
|
|
130
|
+
HAS_PROPERTY: 0.95,
|
|
131
|
+
ACCESSES: 0.8,
|
|
132
|
+
CONTAINS: 0.95,
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Return the confidence floor for a given relation type.
|
|
136
|
+
* Falls back to 0.5 for unknown types so they are not silently elevated.
|
|
137
|
+
*/
|
|
138
|
+
const confidenceForRelType = (relType) => IMPACT_RELATION_CONFIDENCE[relType ?? ''] ?? 0.5;
|
|
139
|
+
/** Structured error logging for query failures — replaces empty catch blocks */
|
|
140
|
+
function logQueryError(context, err) {
|
|
141
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
142
|
+
logger.error({ context, err: msg }, 'Arceus query failed');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Per-query latency telemetry for production aggregation (#553).
|
|
146
|
+
*
|
|
147
|
+
* Logged at `debug` level — timing is observability/telemetry, not an
|
|
148
|
+
* error. Operators wanting per-query timing set `ARC_LOG_LEVEL=debug`
|
|
149
|
+
* (or equivalent). Emitting at `error` level (the original migration
|
|
150
|
+
* artifact) caused alerting rules to fire on every successful query and
|
|
151
|
+
* inflated stderr noise for every MCP/CLI invocation.
|
|
152
|
+
*
|
|
153
|
+
* Emitted via the project logger which routes to stderr — never stdout —
|
|
154
|
+
* because the MCP stdio transport uses stdout exclusively for JSON-RPC
|
|
155
|
+
* responses (#324) and the CLI e2e test `tool output goes to stdout via
|
|
156
|
+
* fd 1` asserts stdout parses cleanly as JSON.
|
|
157
|
+
*/
|
|
158
|
+
function logQueryTiming(query, phases) {
|
|
159
|
+
const totalMs = phases.wall ?? Object.values(phases).reduce((a, b) => a + b, 0);
|
|
160
|
+
const truncated = query.length > 80 ? `${query.slice(0, 80)}…` : query;
|
|
161
|
+
logger.debug({ query: truncated, totalMs, phases }, 'Arceus query timing');
|
|
162
|
+
}
|
|
163
|
+
export class LocalBackend {
|
|
164
|
+
repos = new Map();
|
|
165
|
+
contextCache = new Map();
|
|
166
|
+
initializedRepos = new Set();
|
|
167
|
+
reinitPromises = new Map();
|
|
168
|
+
lastStalenessCheck = new Map();
|
|
169
|
+
groupToolSvc = null;
|
|
170
|
+
/**
|
|
171
|
+
* One-shot stderr warnings for sibling-clone drift, keyed by
|
|
172
|
+
* `${repoId}|${cwdGitRoot}`. Without this guard every tool call
|
|
173
|
+
* from inside a sibling clone would print the same warning,
|
|
174
|
+
* making MCP stderr unreadable.
|
|
175
|
+
*/
|
|
176
|
+
warnedSiblingDrift = new Set();
|
|
177
|
+
/**
|
|
178
|
+
* One-shot stderr warning for the VECTOR-extension fallback. Without this
|
|
179
|
+
* guard the diagnostic would fire on every `semanticSearch()` call on
|
|
180
|
+
* platforms where the extension is unsupported (e.g. Windows), making MCP
|
|
181
|
+
* stderr noisy per DoD §2.8.
|
|
182
|
+
*/
|
|
183
|
+
warnedVectorUnsupported = false;
|
|
184
|
+
/**
|
|
185
|
+
* Cross-repo group tools (CLI). Shares logic with MCP `group_*` handlers.
|
|
186
|
+
*/
|
|
187
|
+
getGroupService() {
|
|
188
|
+
if (!this.groupToolSvc) {
|
|
189
|
+
const port = {
|
|
190
|
+
resolveRepo: (p) => this.resolveRepo(p),
|
|
191
|
+
impact: (r, p) => this.impact(r, p),
|
|
192
|
+
query: (r, p) => this.query(r, p),
|
|
193
|
+
impactByUid: (id, uid, d, o) => this.impactByUid(id, uid, d, o),
|
|
194
|
+
context: (r, p) => this.context(r, p),
|
|
195
|
+
};
|
|
196
|
+
this.groupToolSvc = new GroupService(port);
|
|
197
|
+
}
|
|
198
|
+
return this.groupToolSvc;
|
|
199
|
+
}
|
|
200
|
+
/** Close all pooled LadybugDB connections (CLI one-shot; optional for long-lived MCP). */
|
|
201
|
+
async dispose() {
|
|
202
|
+
await closeLbug();
|
|
203
|
+
}
|
|
204
|
+
// ─── Initialization ──────────────────────────────────────────────
|
|
205
|
+
/**
|
|
206
|
+
* Initialize from the global registry.
|
|
207
|
+
* Returns true if at least one repo is available.
|
|
208
|
+
*/
|
|
209
|
+
async init() {
|
|
210
|
+
await this.refreshRepos();
|
|
211
|
+
return this.repos.size > 0;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Re-read the global registry and update the in-memory repo map.
|
|
215
|
+
* New repos are added, existing repos are updated, removed repos are pruned.
|
|
216
|
+
* LadybugDB connections for removed repos are NOT closed (they idle-timeout naturally).
|
|
217
|
+
*/
|
|
218
|
+
async refreshRepos() {
|
|
219
|
+
const entries = await listRegisteredRepos({ validate: true });
|
|
220
|
+
const freshIds = new Set();
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
const id = this.repoId(entry.name, entry.path);
|
|
223
|
+
freshIds.add(id);
|
|
224
|
+
const storagePath = entry.storagePath;
|
|
225
|
+
const lbugPath = path.join(storagePath, 'lbug');
|
|
226
|
+
// Clean up any leftover KuzuDB files from before the LadybugDB migration.
|
|
227
|
+
// If kuzu exists but lbug doesn't, warn so the user knows to re-analyze.
|
|
228
|
+
const kuzu = await cleanupOldKuzuFiles(storagePath);
|
|
229
|
+
if (kuzu.found && kuzu.needsReindex) {
|
|
230
|
+
logger.error(`Arceus: "${entry.name}" has a stale KuzuDB index. Run: arc analyze ${entry.path}`);
|
|
231
|
+
}
|
|
232
|
+
const handle = {
|
|
233
|
+
id,
|
|
234
|
+
name: entry.name,
|
|
235
|
+
repoPath: entry.path,
|
|
236
|
+
storagePath,
|
|
237
|
+
lbugPath,
|
|
238
|
+
indexedAt: entry.indexedAt,
|
|
239
|
+
lastCommit: entry.lastCommit,
|
|
240
|
+
remoteUrl: entry.remoteUrl,
|
|
241
|
+
stats: entry.stats,
|
|
242
|
+
};
|
|
243
|
+
this.repos.set(id, handle);
|
|
244
|
+
// Build lightweight context (no LadybugDB needed)
|
|
245
|
+
const s = entry.stats || {};
|
|
246
|
+
this.contextCache.set(id, {
|
|
247
|
+
projectName: entry.name,
|
|
248
|
+
stats: {
|
|
249
|
+
fileCount: s.files || 0,
|
|
250
|
+
functionCount: s.nodes || 0,
|
|
251
|
+
communityCount: s.communities || 0,
|
|
252
|
+
processCount: s.processes || 0,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// Prune repos that no longer exist in the registry
|
|
257
|
+
for (const id of this.repos.keys()) {
|
|
258
|
+
if (!freshIds.has(id)) {
|
|
259
|
+
this.repos.delete(id);
|
|
260
|
+
this.contextCache.delete(id);
|
|
261
|
+
this.initializedRepos.delete(id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Generate a stable repo ID from name + path.
|
|
267
|
+
* If names collide, append a hash of the path.
|
|
268
|
+
*/
|
|
269
|
+
repoId(name, repoPath) {
|
|
270
|
+
const base = name.toLowerCase();
|
|
271
|
+
// Check for name collision with a different path
|
|
272
|
+
for (const [id, handle] of this.repos) {
|
|
273
|
+
if (id === base && handle.repoPath !== path.resolve(repoPath)) {
|
|
274
|
+
// Collision — use path hash
|
|
275
|
+
const hash = Buffer.from(repoPath).toString('base64url').slice(0, 6);
|
|
276
|
+
return `${base}-${hash}`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return base;
|
|
280
|
+
}
|
|
281
|
+
// ─── Repo Resolution ─────────────────────────────────────────────
|
|
282
|
+
/**
|
|
283
|
+
* Resolve which repo to use.
|
|
284
|
+
* - If repoParam is given, match by name or path
|
|
285
|
+
* - If only 1 repo, use it
|
|
286
|
+
* - If 0 or multiple without param, throw with helpful message
|
|
287
|
+
*
|
|
288
|
+
* On a miss, re-reads the registry once in case a new repo was indexed
|
|
289
|
+
* while the MCP server was running.
|
|
290
|
+
*/
|
|
291
|
+
async resolveRepo(repoParam) {
|
|
292
|
+
const result = this.resolveRepoFromCache(repoParam);
|
|
293
|
+
if (result) {
|
|
294
|
+
// Issue: silent graph drift across sibling clones.
|
|
295
|
+
// If the caller's cwd lives in a *different* on-disk clone of
|
|
296
|
+
// the same repo (matched by `remoteUrl`), warn once per
|
|
297
|
+
// (repo, cwd) pair on stderr. We do not fail or refuse to
|
|
298
|
+
// serve — the index is still the best answer we have — but
|
|
299
|
+
// the operator/agent has to know the answer may be stale.
|
|
300
|
+
this.maybeWarnSiblingDrift(result).catch(() => {
|
|
301
|
+
/* best-effort; never throw from resolveRepo */
|
|
302
|
+
});
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
// Miss — refresh registry and try once more
|
|
306
|
+
await this.refreshRepos();
|
|
307
|
+
const retried = this.resolveRepoFromCache(repoParam);
|
|
308
|
+
if (retried) {
|
|
309
|
+
this.maybeWarnSiblingDrift(retried).catch(() => { });
|
|
310
|
+
return retried;
|
|
311
|
+
}
|
|
312
|
+
// Still no match — throw with helpful message
|
|
313
|
+
if (this.repos.size === 0) {
|
|
314
|
+
throw new Error('No indexed repositories. Run: arc analyze');
|
|
315
|
+
}
|
|
316
|
+
// Build a disambiguated "Available: …" list (#829). When two handles
|
|
317
|
+
// share a name, annotate each colliding label with its path so the
|
|
318
|
+
// caller can actually pick the right one. Single-name entries render
|
|
319
|
+
// identically to pre-#829 output.
|
|
320
|
+
const nameCounts = new Map();
|
|
321
|
+
for (const h of this.repos.values()) {
|
|
322
|
+
const key = h.name.toLowerCase();
|
|
323
|
+
nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);
|
|
324
|
+
}
|
|
325
|
+
const labels = [...this.repos.values()].map((h) => (nameCounts.get(h.name.toLowerCase()) ?? 0) > 1 ? `${h.name} (${h.repoPath})` : h.name);
|
|
326
|
+
if (repoParam) {
|
|
327
|
+
throw new Error(`Repository "${repoParam}" not found. Available: ${labels.join(', ')}`);
|
|
328
|
+
}
|
|
329
|
+
throw new Error(`Multiple repositories indexed. Specify which one with the "repo" parameter. Available: ${labels.join(', ')}`);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Try to resolve a repo from the in-memory cache. Returns null on miss.
|
|
333
|
+
*/
|
|
334
|
+
resolveRepoFromCache(repoParam) {
|
|
335
|
+
if (this.repos.size === 0)
|
|
336
|
+
return null;
|
|
337
|
+
if (repoParam) {
|
|
338
|
+
const paramLower = repoParam.toLowerCase();
|
|
339
|
+
// Match by id
|
|
340
|
+
if (this.repos.has(paramLower))
|
|
341
|
+
return this.repos.get(paramLower);
|
|
342
|
+
// Match by name (case-insensitive)
|
|
343
|
+
for (const handle of this.repos.values()) {
|
|
344
|
+
if (handle.name.toLowerCase() === paramLower)
|
|
345
|
+
return handle;
|
|
346
|
+
}
|
|
347
|
+
// Match by path (substring)
|
|
348
|
+
const resolved = path.resolve(repoParam);
|
|
349
|
+
for (const handle of this.repos.values()) {
|
|
350
|
+
if (handle.repoPath === resolved)
|
|
351
|
+
return handle;
|
|
352
|
+
}
|
|
353
|
+
// Match by partial name
|
|
354
|
+
for (const handle of this.repos.values()) {
|
|
355
|
+
if (handle.name.toLowerCase().includes(paramLower))
|
|
356
|
+
return handle;
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
if (this.repos.size === 1) {
|
|
361
|
+
return this.repos.values().next().value;
|
|
362
|
+
}
|
|
363
|
+
return null; // Multiple repos, no param — ambiguous
|
|
364
|
+
}
|
|
365
|
+
// ─── Lazy LadybugDB Init ────────────────────────────────────────────
|
|
366
|
+
async ensureInitialized(repoId) {
|
|
367
|
+
// If a reinit is already in progress for this repo, wait for it
|
|
368
|
+
const pending = this.reinitPromises.get(repoId);
|
|
369
|
+
if (pending)
|
|
370
|
+
return pending;
|
|
371
|
+
const handle = this.repos.get(repoId);
|
|
372
|
+
if (!handle)
|
|
373
|
+
throw new Error(`Unknown repo: ${repoId}`);
|
|
374
|
+
// Check if the index was rebuilt since we opened the connection (#297).
|
|
375
|
+
// Throttle staleness checks to at most once per 5 seconds per repo to
|
|
376
|
+
// avoid an fs.readFile round-trip on every tool invocation.
|
|
377
|
+
if (this.initializedRepos.has(repoId) && isLbugReady(repoId)) {
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
const lastCheck = this.lastStalenessCheck.get(repoId) ?? 0;
|
|
380
|
+
if (now - lastCheck < 5000)
|
|
381
|
+
return; // Checked recently — skip
|
|
382
|
+
this.lastStalenessCheck.set(repoId, now);
|
|
383
|
+
try {
|
|
384
|
+
const metaPath = path.join(handle.storagePath, 'meta.json');
|
|
385
|
+
const metaRaw = await fs.readFile(metaPath, 'utf-8');
|
|
386
|
+
const meta = JSON.parse(metaRaw);
|
|
387
|
+
if (meta.indexedAt && meta.indexedAt !== handle.indexedAt) {
|
|
388
|
+
// Index was rebuilt — close stale connection and re-init.
|
|
389
|
+
// Wrap in reinitPromises to prevent TOCTOU race where concurrent
|
|
390
|
+
// callers both detect staleness and double-close the pool.
|
|
391
|
+
const reinit = (async () => {
|
|
392
|
+
try {
|
|
393
|
+
await closeLbug(repoId);
|
|
394
|
+
this.initializedRepos.delete(repoId);
|
|
395
|
+
handle.indexedAt = meta.indexedAt;
|
|
396
|
+
await initLbug(repoId, handle.lbugPath);
|
|
397
|
+
this.initializedRepos.add(repoId);
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
this.reinitPromises.delete(repoId);
|
|
401
|
+
}
|
|
402
|
+
})();
|
|
403
|
+
this.reinitPromises.set(repoId, reinit);
|
|
404
|
+
return reinit;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
return; // Pool is current
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return; // Can't read meta — assume pool is fine
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
await initLbug(repoId, handle.lbugPath);
|
|
416
|
+
this.initializedRepos.add(repoId);
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
// If lock error, mark as not initialized so next call retries
|
|
420
|
+
this.initializedRepos.delete(repoId);
|
|
421
|
+
throw err;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// ─── Public Getters ──────────────────────────────────────────────
|
|
425
|
+
/**
|
|
426
|
+
* Get context for a specific repo (or the single repo if only one).
|
|
427
|
+
*/
|
|
428
|
+
getContext(repoId) {
|
|
429
|
+
if (repoId && this.contextCache.has(repoId)) {
|
|
430
|
+
return this.contextCache.get(repoId);
|
|
431
|
+
}
|
|
432
|
+
if (this.repos.size === 1) {
|
|
433
|
+
return this.contextCache.values().next().value ?? null;
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* List all registered repos with their metadata.
|
|
439
|
+
* Re-reads the global registry so newly indexed repos are discovered
|
|
440
|
+
* without restarting the MCP server.
|
|
441
|
+
*
|
|
442
|
+
* Each entry includes:
|
|
443
|
+
* - `staleness`: if the indexed clone's own HEAD has moved past
|
|
444
|
+
* the recorded `lastCommit` (option D in the issue's fix list).
|
|
445
|
+
* - `siblings`: other registered entries sharing the same
|
|
446
|
+
* `remoteUrl` (option B's payoff: callers can see at a glance
|
|
447
|
+
* that another clone of the same logical repo is registered).
|
|
448
|
+
* - `remoteUrl`: the canonical origin URL recorded at index time.
|
|
449
|
+
*/
|
|
450
|
+
async listRepos() {
|
|
451
|
+
await this.refreshRepos();
|
|
452
|
+
const handles = [...this.repos.values()];
|
|
453
|
+
// Pre-group registered handles by `remoteUrl` so the sibling
|
|
454
|
+
// lookup is O(1) per handle. We reuse the in-memory `this.repos`
|
|
455
|
+
// (already populated by `refreshRepos`) instead of doing a fresh
|
|
456
|
+
// `readRegistry()` per entry — that would be N file reads for N
|
|
457
|
+
// registered repos.
|
|
458
|
+
const isWin = process.platform === 'win32';
|
|
459
|
+
const norm = (p) => (isWin ? path.resolve(p).toLowerCase() : path.resolve(p));
|
|
460
|
+
const byRemote = new Map();
|
|
461
|
+
for (const h of handles) {
|
|
462
|
+
if (!h.remoteUrl)
|
|
463
|
+
continue;
|
|
464
|
+
const list = byRemote.get(h.remoteUrl) ?? [];
|
|
465
|
+
list.push(h);
|
|
466
|
+
byRemote.set(h.remoteUrl, list);
|
|
467
|
+
}
|
|
468
|
+
// Check staleness for all repos in parallel instead of sequentially.
|
|
469
|
+
// Each check spawns an async `git rev-list` — with 200 repos the sync
|
|
470
|
+
// variant took ~50 s; parallel async brings it under a second (#1363).
|
|
471
|
+
const stalenessResults = await Promise.all(handles.map((h) => checkStalenessAsync(h.repoPath, h.lastCommit)));
|
|
472
|
+
return handles.map((h, i) => {
|
|
473
|
+
const stale = stalenessResults[i];
|
|
474
|
+
const selfNorm = norm(h.repoPath);
|
|
475
|
+
const siblings = h.remoteUrl
|
|
476
|
+
? (byRemote.get(h.remoteUrl) ?? []).filter((e) => norm(e.repoPath) !== selfNorm)
|
|
477
|
+
: [];
|
|
478
|
+
return {
|
|
479
|
+
name: h.name,
|
|
480
|
+
path: h.repoPath,
|
|
481
|
+
indexedAt: h.indexedAt,
|
|
482
|
+
lastCommit: h.lastCommit,
|
|
483
|
+
remoteUrl: h.remoteUrl,
|
|
484
|
+
stats: h.stats,
|
|
485
|
+
staleness: stale.isStale
|
|
486
|
+
? { commitsBehind: stale.commitsBehind, hint: stale.hint }
|
|
487
|
+
: undefined,
|
|
488
|
+
siblings: siblings.length > 0
|
|
489
|
+
? siblings.map((s) => ({
|
|
490
|
+
name: s.name,
|
|
491
|
+
path: s.repoPath,
|
|
492
|
+
lastCommit: s.lastCommit,
|
|
493
|
+
}))
|
|
494
|
+
: undefined,
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Best-effort sibling-clone drift warning.
|
|
500
|
+
*
|
|
501
|
+
* When the resolved index has a `remoteUrl` recorded and the caller's
|
|
502
|
+
* `process.cwd()` is inside a *different* clone of the same repo, emit
|
|
503
|
+
* one stderr line per (repo, cwd) pair so the operator knows the
|
|
504
|
+
* graph may be stale relative to what's actually on disk under their
|
|
505
|
+
* cwd. Silent on path matches and on repos without a remote URL.
|
|
506
|
+
*
|
|
507
|
+
* Limitation: in MCP stdio server mode `process.cwd()` is the
|
|
508
|
+
* server's CWD at start time, *not* the agent client's CWD. The
|
|
509
|
+
* warning therefore only fires when the MCP server itself was
|
|
510
|
+
* launched from inside a sibling clone (typical for `npx arc
|
|
511
|
+
* serve` from a polecat workspace). Surfacing the client's CWD
|
|
512
|
+
* would require a per-tool-call `cwd` parameter — out of scope for
|
|
513
|
+
* the current MCP contract.
|
|
514
|
+
*
|
|
515
|
+
* Pure side-effect (stderr); never affects the returned handle.
|
|
516
|
+
* After the first computation for a given (repo, cwd) pair the
|
|
517
|
+
* result is cached so subsequent `resolveRepo()` calls don't
|
|
518
|
+
* re-shell-out to git.
|
|
519
|
+
*/
|
|
520
|
+
async maybeWarnSiblingDrift(handle) {
|
|
521
|
+
if (!handle.remoteUrl)
|
|
522
|
+
return;
|
|
523
|
+
let cwd;
|
|
524
|
+
try {
|
|
525
|
+
cwd = process.cwd();
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
// Early-exit cache: keyed on (repo, cwd) BEFORE any git shellout.
|
|
531
|
+
// After the first call for a given cwd, this short-circuits the
|
|
532
|
+
// up-to-four `execSync`/`execFileSync` calls inside `checkCwdMatch`
|
|
533
|
+
// — important for MCP-server mode where `process.cwd()` is constant
|
|
534
|
+
// and `resolveRepo` runs on every tool call.
|
|
535
|
+
const cacheKey = `${handle.id}|${cwd}`;
|
|
536
|
+
if (this.warnedSiblingDrift.has(cacheKey))
|
|
537
|
+
return;
|
|
538
|
+
const match = await checkCwdMatch(cwd);
|
|
539
|
+
if (match.match !== 'sibling-by-remote' ||
|
|
540
|
+
!match.entry ||
|
|
541
|
+
!match.cwdGitRoot ||
|
|
542
|
+
match.entry.path !== handle.repoPath ||
|
|
543
|
+
!match.hint) {
|
|
544
|
+
// Cache "nothing to warn about" outcomes too — `checkCwdMatch`
|
|
545
|
+
// is deterministic for a fixed (registry, cwd) pair, so re-running
|
|
546
|
+
// it yields nothing new.
|
|
547
|
+
this.warnedSiblingDrift.add(cacheKey);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
this.warnedSiblingDrift.add(cacheKey);
|
|
551
|
+
logger.error(`Arceus: ${match.hint}`);
|
|
552
|
+
}
|
|
553
|
+
// ─── Tool Dispatch ───────────────────────────────────────────────
|
|
554
|
+
async callTool(method, params) {
|
|
555
|
+
if (method === 'list_repos') {
|
|
556
|
+
return this.listRepos();
|
|
557
|
+
}
|
|
558
|
+
if (method.startsWith('group_')) {
|
|
559
|
+
return this.handleGroupTool(method, params || {});
|
|
560
|
+
}
|
|
561
|
+
const p = params && typeof params === 'object' ? params : {};
|
|
562
|
+
if ((method === 'impact' || method === 'query' || method === 'context') &&
|
|
563
|
+
typeof p.repo === 'string' &&
|
|
564
|
+
p.repo.startsWith('@')) {
|
|
565
|
+
return this.callToolAtGroupRepo(method, p);
|
|
566
|
+
}
|
|
567
|
+
// Resolve repo from optional param (re-reads registry on miss)
|
|
568
|
+
const repo = await this.resolveRepo(params?.repo);
|
|
569
|
+
switch (method) {
|
|
570
|
+
case 'query':
|
|
571
|
+
return this.query(repo, params);
|
|
572
|
+
case 'cypher': {
|
|
573
|
+
const raw = await this.cypher(repo, params);
|
|
574
|
+
return this.formatCypherAsMarkdown(raw);
|
|
575
|
+
}
|
|
576
|
+
case 'context':
|
|
577
|
+
return this.context(repo, params);
|
|
578
|
+
case 'impact':
|
|
579
|
+
return this.impact(repo, params);
|
|
580
|
+
case 'detect_changes':
|
|
581
|
+
return this.detectChanges(repo, params);
|
|
582
|
+
case 'rename':
|
|
583
|
+
return this.rename(repo, params);
|
|
584
|
+
// Legacy aliases for backwards compatibility
|
|
585
|
+
case 'search':
|
|
586
|
+
return this.query(repo, params);
|
|
587
|
+
case 'explore':
|
|
588
|
+
return this.context(repo, { name: params?.name, ...params });
|
|
589
|
+
case 'overview':
|
|
590
|
+
return this.overview(repo, params);
|
|
591
|
+
case 'route_map':
|
|
592
|
+
return this.routeMap(repo, params);
|
|
593
|
+
case 'shape_check':
|
|
594
|
+
return this.shapeCheck(repo, params);
|
|
595
|
+
case 'tool_map':
|
|
596
|
+
return this.toolMap(repo, params);
|
|
597
|
+
case 'api_impact':
|
|
598
|
+
return this.apiImpact(repo, params);
|
|
599
|
+
default:
|
|
600
|
+
throw new Error(`Unknown tool: ${method}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// ─── Tool Implementations ────────────────────────────────────────
|
|
604
|
+
/**
|
|
605
|
+
* Query tool — process-grouped search.
|
|
606
|
+
*
|
|
607
|
+
* 1. Hybrid search (BM25 + semantic) to find matching symbols
|
|
608
|
+
* 2. Trace each match to its process(es) via STEP_IN_PROCESS
|
|
609
|
+
* 3. Group by process, rank by aggregate relevance + internal cluster cohesion
|
|
610
|
+
* 4. Return: { processes, process_symbols, definitions }
|
|
611
|
+
*/
|
|
612
|
+
async query(repo, params) {
|
|
613
|
+
if (!params.query?.trim()) {
|
|
614
|
+
return { error: 'query parameter is required and cannot be empty.' };
|
|
615
|
+
}
|
|
616
|
+
await this.ensureInitialized(repo.id);
|
|
617
|
+
const processLimit = params.limit || 5;
|
|
618
|
+
const maxSymbolsPerProcess = params.max_symbols || 10;
|
|
619
|
+
const includeContent = params.include_content ?? false;
|
|
620
|
+
const searchQuery = params.query.trim();
|
|
621
|
+
// Per-phase timing instrumentation (#553). Records wall time for each
|
|
622
|
+
// observable sub-step of the search pipeline so production latency can
|
|
623
|
+
// be aggregated offline for Pareto analysis and bottleneck detection.
|
|
624
|
+
// Overhead is <0.1 ms per phase; the timer is passive and never alters
|
|
625
|
+
// query behaviour.
|
|
626
|
+
const timer = new PhaseTimer();
|
|
627
|
+
const wallStart = performance.now();
|
|
628
|
+
// Step 1: Run hybrid search to get matching symbols. BM25 and vector
|
|
629
|
+
// search run concurrently via Promise.all — use `timer.time()` for
|
|
630
|
+
// each so both get independent wall-time records without fighting
|
|
631
|
+
// over a single `current` phase slot.
|
|
632
|
+
const searchLimit = processLimit * maxSymbolsPerProcess; // fetch enough raw results
|
|
633
|
+
const [bm25SearchResult, semanticResults] = await Promise.all([
|
|
634
|
+
timer.time('bm25', this.bm25Search(repo, searchQuery, searchLimit)),
|
|
635
|
+
timer.time('vector', this.semanticSearch(repo, searchQuery, searchLimit)),
|
|
636
|
+
]);
|
|
637
|
+
// Guard against undefined results (#1489) — when FTS is entirely
|
|
638
|
+
// unavailable the search helper may return an unexpected shape.
|
|
639
|
+
const bm25Results = bm25SearchResult?.results ?? [];
|
|
640
|
+
const ftsUsed = bm25SearchResult?.ftsUsed ?? false;
|
|
641
|
+
// Merge via reciprocal rank fusion
|
|
642
|
+
timer.start('merge');
|
|
643
|
+
const scoreMap = new Map();
|
|
644
|
+
for (let i = 0; i < bm25Results.length; i++) {
|
|
645
|
+
const result = bm25Results[i];
|
|
646
|
+
const key = result.nodeId || result.filePath;
|
|
647
|
+
const rrfScore = 1 / (60 + i);
|
|
648
|
+
const existing = scoreMap.get(key);
|
|
649
|
+
if (existing) {
|
|
650
|
+
existing.score += rrfScore;
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
scoreMap.set(key, { score: rrfScore, data: result });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const safeSemanticResults = semanticResults ?? [];
|
|
657
|
+
for (let i = 0; i < safeSemanticResults.length; i++) {
|
|
658
|
+
const result = safeSemanticResults[i];
|
|
659
|
+
const key = result.nodeId || result.filePath;
|
|
660
|
+
const rrfScore = 1 / (60 + i);
|
|
661
|
+
const existing = scoreMap.get(key);
|
|
662
|
+
if (existing) {
|
|
663
|
+
existing.score += rrfScore;
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
scoreMap.set(key, { score: rrfScore, data: result });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const merged = Array.from(scoreMap.entries())
|
|
670
|
+
.sort((a, b) => b[1].score - a[1].score)
|
|
671
|
+
.slice(0, searchLimit);
|
|
672
|
+
timer.stop(); // merge
|
|
673
|
+
// Step 2: For each match with a nodeId, trace to process(es)
|
|
674
|
+
timer.start('symbol_lookup');
|
|
675
|
+
const processMap = new Map();
|
|
676
|
+
const definitions = []; // standalone symbols not in any process
|
|
677
|
+
for (const [_, item] of merged) {
|
|
678
|
+
const sym = item.data;
|
|
679
|
+
if (!sym.nodeId) {
|
|
680
|
+
// File-level results go to definitions
|
|
681
|
+
definitions.push({
|
|
682
|
+
name: sym.name,
|
|
683
|
+
type: sym.type || 'File',
|
|
684
|
+
filePath: sym.filePath,
|
|
685
|
+
});
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
// Find processes this symbol participates in
|
|
689
|
+
let processRows = [];
|
|
690
|
+
try {
|
|
691
|
+
processRows = await executeParameterized(repo.id, `
|
|
692
|
+
MATCH (n {id: $nodeId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
693
|
+
RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
694
|
+
`, { nodeId: sym.nodeId });
|
|
695
|
+
}
|
|
696
|
+
catch (e) {
|
|
697
|
+
logQueryError('query:process-lookup', e);
|
|
698
|
+
}
|
|
699
|
+
// Get cluster membership + cohesion (cohesion used as internal ranking signal)
|
|
700
|
+
let cohesion = 0;
|
|
701
|
+
let module;
|
|
702
|
+
try {
|
|
703
|
+
const cohesionRows = await executeParameterized(repo.id, `
|
|
704
|
+
MATCH (n {id: $nodeId})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
705
|
+
RETURN c.cohesion AS cohesion, c.heuristicLabel AS module
|
|
706
|
+
LIMIT 1
|
|
707
|
+
`, { nodeId: sym.nodeId });
|
|
708
|
+
if (cohesionRows.length > 0) {
|
|
709
|
+
cohesion = (cohesionRows[0].cohesion ?? cohesionRows[0][0]) || 0;
|
|
710
|
+
module = cohesionRows[0].module ?? cohesionRows[0][1];
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
catch (e) {
|
|
714
|
+
logQueryError('query:cluster-info', e);
|
|
715
|
+
}
|
|
716
|
+
// Optionally fetch content
|
|
717
|
+
let content;
|
|
718
|
+
if (includeContent) {
|
|
719
|
+
try {
|
|
720
|
+
const contentRows = await executeParameterized(repo.id, `
|
|
721
|
+
MATCH (n {id: $nodeId})
|
|
722
|
+
RETURN n.content AS content
|
|
723
|
+
`, { nodeId: sym.nodeId });
|
|
724
|
+
if (contentRows.length > 0) {
|
|
725
|
+
content = contentRows[0].content ?? contentRows[0][0];
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
catch (e) {
|
|
729
|
+
logQueryError('query:content-fetch', e);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const symbolEntry = {
|
|
733
|
+
id: sym.nodeId,
|
|
734
|
+
name: sym.name,
|
|
735
|
+
type: sym.type,
|
|
736
|
+
filePath: sym.filePath,
|
|
737
|
+
startLine: sym.startLine,
|
|
738
|
+
endLine: sym.endLine,
|
|
739
|
+
...(module ? { module } : {}),
|
|
740
|
+
...(includeContent && content ? { content } : {}),
|
|
741
|
+
};
|
|
742
|
+
if (processRows.length === 0) {
|
|
743
|
+
// Symbol not in any process — goes to definitions
|
|
744
|
+
definitions.push(symbolEntry);
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
// Add to each process it belongs to
|
|
748
|
+
for (const row of processRows) {
|
|
749
|
+
const pid = row.pid ?? row[0];
|
|
750
|
+
const label = row.label ?? row[1];
|
|
751
|
+
const hLabel = row.heuristicLabel ?? row[2];
|
|
752
|
+
const pType = row.processType ?? row[3];
|
|
753
|
+
const stepCount = row.stepCount ?? row[4];
|
|
754
|
+
const step = row.step ?? row[5];
|
|
755
|
+
if (!processMap.has(pid)) {
|
|
756
|
+
processMap.set(pid, {
|
|
757
|
+
id: pid,
|
|
758
|
+
label,
|
|
759
|
+
heuristicLabel: hLabel,
|
|
760
|
+
processType: pType,
|
|
761
|
+
stepCount,
|
|
762
|
+
totalScore: 0,
|
|
763
|
+
cohesionBoost: 0,
|
|
764
|
+
symbols: [],
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
const proc = processMap.get(pid);
|
|
768
|
+
proc.totalScore += item.score;
|
|
769
|
+
proc.cohesionBoost = Math.max(proc.cohesionBoost, cohesion);
|
|
770
|
+
proc.symbols.push({
|
|
771
|
+
...symbolEntry,
|
|
772
|
+
process_id: pid,
|
|
773
|
+
step_index: step,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
timer.stop(); // symbol_lookup
|
|
779
|
+
// Step 3: Rank processes by aggregate score + internal cohesion boost
|
|
780
|
+
timer.start('ranking');
|
|
781
|
+
const rankedProcesses = Array.from(processMap.values())
|
|
782
|
+
.map((p) => ({
|
|
783
|
+
...p,
|
|
784
|
+
priority: p.totalScore + p.cohesionBoost * 0.1, // cohesion as subtle ranking signal
|
|
785
|
+
}))
|
|
786
|
+
.sort((a, b) => b.priority - a.priority)
|
|
787
|
+
.slice(0, processLimit);
|
|
788
|
+
timer.stop(); // ranking
|
|
789
|
+
// Step 4: Build response
|
|
790
|
+
timer.start('formatting');
|
|
791
|
+
const processes = rankedProcesses.map((p) => ({
|
|
792
|
+
id: p.id,
|
|
793
|
+
summary: p.heuristicLabel || p.label,
|
|
794
|
+
priority: Math.round(p.priority * 1000) / 1000,
|
|
795
|
+
symbol_count: p.symbols.length,
|
|
796
|
+
process_type: p.processType,
|
|
797
|
+
step_count: p.stepCount,
|
|
798
|
+
}));
|
|
799
|
+
const processSymbols = rankedProcesses.flatMap((p) => p.symbols.slice(0, maxSymbolsPerProcess).map((s) => ({
|
|
800
|
+
...s,
|
|
801
|
+
// remove internal fields
|
|
802
|
+
})));
|
|
803
|
+
// Deduplicate process_symbols by id
|
|
804
|
+
const seen = new Set();
|
|
805
|
+
const dedupedSymbols = processSymbols.filter((s) => {
|
|
806
|
+
if (seen.has(s.id))
|
|
807
|
+
return false;
|
|
808
|
+
seen.add(s.id);
|
|
809
|
+
return true;
|
|
810
|
+
});
|
|
811
|
+
timer.stop(); // formatting
|
|
812
|
+
// End-to-end wall time — deliberately a separate mark so callers can
|
|
813
|
+
// compare sum(phases) vs wall to see how much Promise.all concurrency
|
|
814
|
+
// saved. Must come before summary() so it's included.
|
|
815
|
+
timer.mark('wall', performance.now() - wallStart);
|
|
816
|
+
const timing = timer.summary();
|
|
817
|
+
logQueryTiming(searchQuery, timing);
|
|
818
|
+
return {
|
|
819
|
+
processes,
|
|
820
|
+
process_symbols: dedupedSymbols,
|
|
821
|
+
definitions: definitions.slice(0, 20), // cap standalone definitions
|
|
822
|
+
timing,
|
|
823
|
+
...(!ftsUsed && {
|
|
824
|
+
warning: 'FTS indexes missing — keyword search degraded. Run: arc analyze --force to rebuild indexes.',
|
|
825
|
+
}),
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* BM25 keyword search helper - uses LadybugDB FTS for always-fresh results
|
|
830
|
+
*/
|
|
831
|
+
async bm25Search(repo, query, limit) {
|
|
832
|
+
let searchFTSFromLbug;
|
|
833
|
+
try {
|
|
834
|
+
({ searchFTSFromLbug } = await import('../../core/search/bm25-index.js'));
|
|
835
|
+
}
|
|
836
|
+
catch (err) {
|
|
837
|
+
// Module import can fail in sandboxed MCP contexts (#1489)
|
|
838
|
+
logger.warn({ err: err?.message }, 'Arceus: bm25-index.js import failed — falling back to semantic-only');
|
|
839
|
+
return { results: [], ftsUsed: false };
|
|
840
|
+
}
|
|
841
|
+
let ftsResponse;
|
|
842
|
+
try {
|
|
843
|
+
ftsResponse = await searchFTSFromLbug(query, limit, repo.id);
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
logger.error({ err: err.message }, 'Arceus: BM25/FTS search failed (FTS indexes may not exist) -');
|
|
847
|
+
return { results: [], ftsUsed: false };
|
|
848
|
+
}
|
|
849
|
+
// Guard against unexpected response shape (#1489) — ftsResponse.results
|
|
850
|
+
// could be undefined when the FTS extension is unavailable in the MCP process.
|
|
851
|
+
const bm25Results = ftsResponse?.results ?? [];
|
|
852
|
+
const ftsUsed = ftsResponse?.ftsAvailable ?? false;
|
|
853
|
+
const results = [];
|
|
854
|
+
for (const bm25Result of bm25Results) {
|
|
855
|
+
const fullPath = bm25Result.filePath;
|
|
856
|
+
try {
|
|
857
|
+
// Prefer direct nodeId lookup (exact FTS-matched nodes) over filePath fallback.
|
|
858
|
+
// Without this, LIMIT 3 on filePath returns arbitrary symbols rather than
|
|
859
|
+
// the nodes that actually scored highest in the BM25 index.
|
|
860
|
+
const nodeIds = bm25Result.nodeIds?.length ? bm25Result.nodeIds : null;
|
|
861
|
+
const symbols = nodeIds
|
|
862
|
+
? await executeParameterized(repo.id, `
|
|
863
|
+
MATCH (n)
|
|
864
|
+
WHERE n.id IN $nodeIds
|
|
865
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
866
|
+
`, { nodeIds })
|
|
867
|
+
: await executeParameterized(repo.id, `
|
|
868
|
+
MATCH (n)
|
|
869
|
+
WHERE n.filePath = $filePath
|
|
870
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
871
|
+
LIMIT 3
|
|
872
|
+
`, { filePath: fullPath });
|
|
873
|
+
if (symbols.length > 0) {
|
|
874
|
+
for (const sym of symbols) {
|
|
875
|
+
results.push({
|
|
876
|
+
nodeId: sym.id || sym[0],
|
|
877
|
+
name: sym.name || sym[1],
|
|
878
|
+
type: sym.type || sym[2],
|
|
879
|
+
filePath: sym.filePath || sym[3],
|
|
880
|
+
startLine: sym.startLine || sym[4],
|
|
881
|
+
endLine: sym.endLine || sym[5],
|
|
882
|
+
bm25Score: bm25Result.score,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
const fileName = fullPath.split('/').pop() || fullPath;
|
|
888
|
+
results.push({
|
|
889
|
+
name: fileName,
|
|
890
|
+
type: 'File',
|
|
891
|
+
filePath: bm25Result.filePath,
|
|
892
|
+
bm25Score: bm25Result.score,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
const fileName = fullPath.split('/').pop() || fullPath;
|
|
898
|
+
results.push({
|
|
899
|
+
name: fileName,
|
|
900
|
+
type: 'File',
|
|
901
|
+
filePath: bm25Result.filePath,
|
|
902
|
+
bm25Score: bm25Result.score,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return { results, ftsUsed };
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Semantic vector search helper
|
|
910
|
+
*/
|
|
911
|
+
async semanticSearch(repo, query, limit) {
|
|
912
|
+
try {
|
|
913
|
+
// Check if embedding table exists before loading the model (avoids heavy model init when embeddings are off)
|
|
914
|
+
const tableCheck = await executeQuery(repo.id, `MATCH (e:${EMBEDDING_TABLE_NAME}) RETURN COUNT(*) AS cnt LIMIT 1`);
|
|
915
|
+
if (!tableCheck.length || (tableCheck[0].cnt ?? tableCheck[0][0]) === 0)
|
|
916
|
+
return [];
|
|
917
|
+
const { embedQuery, getEmbeddingDims } = await import('../core/embedder.js');
|
|
918
|
+
const queryVec = await embedQuery(query);
|
|
919
|
+
const dims = getEmbeddingDims();
|
|
920
|
+
const queryVecStr = `[${queryVec.join(',')}]`;
|
|
921
|
+
let bestChunks = new Map();
|
|
922
|
+
if (isVectorExtensionSupportedByPlatform()) {
|
|
923
|
+
try {
|
|
924
|
+
bestChunks = await collectBestChunks(limit, async (fetchLimit) => {
|
|
925
|
+
const vectorQuery = `
|
|
926
|
+
CALL QUERY_VECTOR_INDEX('${EMBEDDING_TABLE_NAME}', '${EMBEDDING_INDEX_NAME}',
|
|
927
|
+
CAST(${queryVecStr} AS FLOAT[${dims}]), ${fetchLimit})
|
|
928
|
+
YIELD node AS emb, distance
|
|
929
|
+
WITH emb, distance
|
|
930
|
+
WHERE distance < 0.6
|
|
931
|
+
RETURN emb.nodeId AS nodeId, emb.chunkIndex AS chunkIndex,
|
|
932
|
+
emb.startLine AS startLine, emb.endLine AS endLine, distance
|
|
933
|
+
ORDER BY distance
|
|
934
|
+
`;
|
|
935
|
+
const embResults = await executeQuery(repo.id, vectorQuery);
|
|
936
|
+
return embResults.map((row) => ({
|
|
937
|
+
nodeId: row.nodeId ?? row[0],
|
|
938
|
+
chunkIndex: row.chunkIndex ?? row[1] ?? 0,
|
|
939
|
+
startLine: row.startLine ?? row[2] ?? 0,
|
|
940
|
+
endLine: row.endLine ?? row[3] ?? 0,
|
|
941
|
+
distance: row.distance ?? row[4],
|
|
942
|
+
}));
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
catch {
|
|
946
|
+
bestChunks = new Map();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
else if (!this.warnedVectorUnsupported) {
|
|
950
|
+
// Rare diagnostic: surface why we fell back to the exact scan path so
|
|
951
|
+
// operators can see at a glance that VECTOR is disabled by platform
|
|
952
|
+
// policy. Emitted once per `LocalBackend` instance lifetime to avoid
|
|
953
|
+
// noisy stderr on hot semantic-search paths (DoD §2.8).
|
|
954
|
+
this.warnedVectorUnsupported = true;
|
|
955
|
+
logger.warn('Arceus [query:vector]: VECTOR extension not supported on this platform; using exact scan fallback');
|
|
956
|
+
}
|
|
957
|
+
if (bestChunks.size === 0) {
|
|
958
|
+
const embeddingCount = Number(tableCheck[0].cnt ?? tableCheck[0][0] ?? 0);
|
|
959
|
+
const exactLimit = getExactScanLimit();
|
|
960
|
+
if (embeddingCount > exactLimit)
|
|
961
|
+
return [];
|
|
962
|
+
const rows = await executeQuery(repo.id, `
|
|
963
|
+
MATCH (e:${EMBEDDING_TABLE_NAME})
|
|
964
|
+
RETURN e.nodeId AS nodeId, e.chunkIndex AS chunkIndex,
|
|
965
|
+
e.startLine AS startLine, e.endLine AS endLine, e.embedding AS embedding
|
|
966
|
+
`);
|
|
967
|
+
const exactRows = rows.map((row) => ({
|
|
968
|
+
nodeId: row.nodeId ?? row[0],
|
|
969
|
+
chunkIndex: row.chunkIndex ?? row[1] ?? 0,
|
|
970
|
+
startLine: row.startLine ?? row[2] ?? 0,
|
|
971
|
+
endLine: row.endLine ?? row[3] ?? 0,
|
|
972
|
+
embedding: row.embedding ?? row[4] ?? [],
|
|
973
|
+
}));
|
|
974
|
+
bestChunks = new Map(rankExactEmbeddingRows(exactRows, queryVec, limit, 0.6).map((row) => [
|
|
975
|
+
row.nodeId,
|
|
976
|
+
{
|
|
977
|
+
distance: row.distance,
|
|
978
|
+
chunkIndex: row.chunkIndex,
|
|
979
|
+
startLine: row.startLine,
|
|
980
|
+
endLine: row.endLine,
|
|
981
|
+
},
|
|
982
|
+
]));
|
|
983
|
+
}
|
|
984
|
+
if (bestChunks.size === 0)
|
|
985
|
+
return [];
|
|
986
|
+
const results = [];
|
|
987
|
+
for (const [nodeId, chunk] of Array.from(bestChunks.entries()).slice(0, limit)) {
|
|
988
|
+
const labelEndIdx = nodeId.indexOf(':');
|
|
989
|
+
const label = labelEndIdx > 0 ? nodeId.substring(0, labelEndIdx) : 'Unknown';
|
|
990
|
+
// Validate label against known node types to prevent Cypher injection
|
|
991
|
+
if (!VALID_NODE_LABELS.has(label))
|
|
992
|
+
continue;
|
|
993
|
+
try {
|
|
994
|
+
const nodeQuery = label === 'File'
|
|
995
|
+
? `MATCH (n:File {id: $nodeId}) RETURN n.name AS name, n.filePath AS filePath`
|
|
996
|
+
: `MATCH (n:\`${label}\` {id: $nodeId}) RETURN n.name AS name, n.filePath AS filePath`;
|
|
997
|
+
const nodeRows = await executeParameterized(repo.id, nodeQuery, { nodeId });
|
|
998
|
+
if (nodeRows.length > 0) {
|
|
999
|
+
const nodeRow = nodeRows[0];
|
|
1000
|
+
results.push({
|
|
1001
|
+
nodeId,
|
|
1002
|
+
name: nodeRow.name ?? nodeRow[0] ?? '',
|
|
1003
|
+
type: label,
|
|
1004
|
+
filePath: nodeRow.filePath ?? nodeRow[1] ?? '',
|
|
1005
|
+
distance: chunk.distance,
|
|
1006
|
+
startLine: chunk.startLine,
|
|
1007
|
+
endLine: chunk.endLine,
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
catch { }
|
|
1012
|
+
}
|
|
1013
|
+
return results;
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
// Expected when embeddings are disabled — silently fall back to BM25-only
|
|
1017
|
+
return [];
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async executeCypher(repoName, query) {
|
|
1021
|
+
const repo = await this.resolveRepo(repoName);
|
|
1022
|
+
return this.cypher(repo, { query });
|
|
1023
|
+
}
|
|
1024
|
+
async cypher(repo, params) {
|
|
1025
|
+
await this.ensureInitialized(repo.id);
|
|
1026
|
+
if (!isLbugReady(repo.id)) {
|
|
1027
|
+
return { error: 'LadybugDB not ready. Index may be corrupted.' };
|
|
1028
|
+
}
|
|
1029
|
+
// Block write operations (defense-in-depth — DB is already read-only)
|
|
1030
|
+
if (isWriteQuery(params.query)) {
|
|
1031
|
+
return {
|
|
1032
|
+
error: 'Write operations (CREATE, DELETE, SET, MERGE, REMOVE, DROP, ALTER, COPY, DETACH) are not allowed. The knowledge graph is read-only.',
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
try {
|
|
1036
|
+
const result = await executeQuery(repo.id, params.query);
|
|
1037
|
+
return result;
|
|
1038
|
+
}
|
|
1039
|
+
catch (err) {
|
|
1040
|
+
const msg = err.message || 'Query failed';
|
|
1041
|
+
if (isWalCorruptionError(err)) {
|
|
1042
|
+
return {
|
|
1043
|
+
error: msg,
|
|
1044
|
+
recoverySuggestion: WAL_RECOVERY_SUGGESTION,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
return { error: msg };
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Format raw Cypher result rows as a markdown table for LLM readability.
|
|
1052
|
+
* Falls back to raw result if rows aren't tabular objects.
|
|
1053
|
+
*/
|
|
1054
|
+
formatCypherAsMarkdown(result) {
|
|
1055
|
+
if (!Array.isArray(result) || result.length === 0)
|
|
1056
|
+
return result;
|
|
1057
|
+
const firstRow = result[0];
|
|
1058
|
+
if (typeof firstRow !== 'object' || firstRow === null)
|
|
1059
|
+
return result;
|
|
1060
|
+
const keys = Object.keys(firstRow);
|
|
1061
|
+
if (keys.length === 0)
|
|
1062
|
+
return result;
|
|
1063
|
+
const header = '| ' + keys.join(' | ') + ' |';
|
|
1064
|
+
const separator = '| ' + keys.map(() => '---').join(' | ') + ' |';
|
|
1065
|
+
const dataRows = result.map((row) => '| ' +
|
|
1066
|
+
keys
|
|
1067
|
+
.map((k) => {
|
|
1068
|
+
const v = row[k];
|
|
1069
|
+
if (v === null || v === undefined)
|
|
1070
|
+
return '';
|
|
1071
|
+
if (typeof v === 'object')
|
|
1072
|
+
return JSON.stringify(v);
|
|
1073
|
+
return String(v);
|
|
1074
|
+
})
|
|
1075
|
+
.join(' | ') +
|
|
1076
|
+
' |');
|
|
1077
|
+
return {
|
|
1078
|
+
markdown: [header, separator, ...dataRows].join('\n'),
|
|
1079
|
+
row_count: result.length,
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Aggregate same-named clusters: group by heuristicLabel, sum symbols,
|
|
1084
|
+
* weighted-average cohesion, filter out tiny clusters (<5 symbols).
|
|
1085
|
+
* Raw communities stay intact in LadybugDB for Cypher queries.
|
|
1086
|
+
*/
|
|
1087
|
+
aggregateClusters(clusters) {
|
|
1088
|
+
const groups = new Map();
|
|
1089
|
+
for (const c of clusters) {
|
|
1090
|
+
const label = c.heuristicLabel || c.label || 'Unknown';
|
|
1091
|
+
const symbols = c.symbolCount || 0;
|
|
1092
|
+
const cohesion = c.cohesion || 0;
|
|
1093
|
+
const existing = groups.get(label);
|
|
1094
|
+
if (!existing) {
|
|
1095
|
+
groups.set(label, {
|
|
1096
|
+
ids: [c.id],
|
|
1097
|
+
totalSymbols: symbols,
|
|
1098
|
+
weightedCohesion: cohesion * symbols,
|
|
1099
|
+
largest: c,
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
existing.ids.push(c.id);
|
|
1104
|
+
existing.totalSymbols += symbols;
|
|
1105
|
+
existing.weightedCohesion += cohesion * symbols;
|
|
1106
|
+
if (symbols > (existing.largest.symbolCount || 0)) {
|
|
1107
|
+
existing.largest = c;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return Array.from(groups.entries())
|
|
1112
|
+
.map(([label, g]) => ({
|
|
1113
|
+
id: g.largest.id,
|
|
1114
|
+
label,
|
|
1115
|
+
heuristicLabel: label,
|
|
1116
|
+
symbolCount: g.totalSymbols,
|
|
1117
|
+
cohesion: g.totalSymbols > 0 ? g.weightedCohesion / g.totalSymbols : 0,
|
|
1118
|
+
subCommunities: g.ids.length,
|
|
1119
|
+
}))
|
|
1120
|
+
.filter((c) => c.symbolCount >= 5)
|
|
1121
|
+
.sort((a, b) => b.symbolCount - a.symbolCount);
|
|
1122
|
+
}
|
|
1123
|
+
async overview(repo, params) {
|
|
1124
|
+
await this.ensureInitialized(repo.id);
|
|
1125
|
+
const limit = params.limit || 20;
|
|
1126
|
+
const result = {
|
|
1127
|
+
repo: repo.name,
|
|
1128
|
+
repoPath: repo.repoPath,
|
|
1129
|
+
stats: repo.stats,
|
|
1130
|
+
indexedAt: repo.indexedAt,
|
|
1131
|
+
lastCommit: repo.lastCommit,
|
|
1132
|
+
};
|
|
1133
|
+
if (params.showClusters !== false) {
|
|
1134
|
+
try {
|
|
1135
|
+
// Fetch more raw communities than the display limit so aggregation has enough data
|
|
1136
|
+
const rawLimit = Math.max(limit * 5, 200);
|
|
1137
|
+
const clusters = await executeQuery(repo.id, `
|
|
1138
|
+
MATCH (c:Community)
|
|
1139
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1140
|
+
ORDER BY c.symbolCount DESC
|
|
1141
|
+
LIMIT ${rawLimit}
|
|
1142
|
+
`);
|
|
1143
|
+
const rawClusters = clusters.map((c) => ({
|
|
1144
|
+
id: c.id || c[0],
|
|
1145
|
+
label: c.label || c[1],
|
|
1146
|
+
heuristicLabel: c.heuristicLabel || c[2],
|
|
1147
|
+
cohesion: c.cohesion || c[3],
|
|
1148
|
+
symbolCount: c.symbolCount || c[4],
|
|
1149
|
+
}));
|
|
1150
|
+
result.clusters = this.aggregateClusters(rawClusters).slice(0, limit);
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
result.clusters = [];
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (params.showProcesses !== false) {
|
|
1157
|
+
try {
|
|
1158
|
+
const processes = await executeQuery(repo.id, `
|
|
1159
|
+
MATCH (p:Process)
|
|
1160
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1161
|
+
ORDER BY p.stepCount DESC
|
|
1162
|
+
LIMIT ${limit}
|
|
1163
|
+
`);
|
|
1164
|
+
result.processes = processes.map((p) => ({
|
|
1165
|
+
id: p.id || p[0],
|
|
1166
|
+
label: p.label || p[1],
|
|
1167
|
+
heuristicLabel: p.heuristicLabel || p[2],
|
|
1168
|
+
processType: p.processType || p[3],
|
|
1169
|
+
stepCount: p.stepCount || p[4],
|
|
1170
|
+
}));
|
|
1171
|
+
}
|
|
1172
|
+
catch {
|
|
1173
|
+
result.processes = [];
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return result;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Patch the `type` field on candidates whose `labels(n)[0]` projection
|
|
1180
|
+
* came back empty — a known LadybugDB behaviour for several node types.
|
|
1181
|
+
*
|
|
1182
|
+
* Uses one scoped UNION query across the five priority labels rather
|
|
1183
|
+
* than per-candidate round-trips, so cost is a single DB call regardless
|
|
1184
|
+
* of how many candidates need enrichment. No-op when every candidate
|
|
1185
|
+
* already has a non-empty type.
|
|
1186
|
+
*
|
|
1187
|
+
* Failures are swallowed: label enrichment is an optimisation for
|
|
1188
|
+
* downstream scoring and #480 Class/Interface BFS seeding; if it fails
|
|
1189
|
+
* the symbol still resolves, just without the kind-priority bonus.
|
|
1190
|
+
*/
|
|
1191
|
+
async enrichCandidateLabels(repo, candidates) {
|
|
1192
|
+
const ids = candidates.filter((c) => c.type === '' && c.id).map((c) => c.id);
|
|
1193
|
+
if (ids.length === 0)
|
|
1194
|
+
return;
|
|
1195
|
+
try {
|
|
1196
|
+
const rows = await executeParameterized(repo.id, `
|
|
1197
|
+
MATCH (n:\`Class\`) WHERE n.id IN $ids RETURN n.id AS id, 'Class' AS label
|
|
1198
|
+
UNION ALL
|
|
1199
|
+
MATCH (n:\`Interface\`) WHERE n.id IN $ids RETURN n.id AS id, 'Interface' AS label
|
|
1200
|
+
UNION ALL
|
|
1201
|
+
MATCH (n:\`Function\`) WHERE n.id IN $ids RETURN n.id AS id, 'Function' AS label
|
|
1202
|
+
UNION ALL
|
|
1203
|
+
MATCH (n:\`Method\`) WHERE n.id IN $ids RETURN n.id AS id, 'Method' AS label
|
|
1204
|
+
UNION ALL
|
|
1205
|
+
MATCH (n:\`Constructor\`) WHERE n.id IN $ids RETURN n.id AS id, 'Constructor' AS label
|
|
1206
|
+
`, { ids });
|
|
1207
|
+
const labelById = new Map();
|
|
1208
|
+
for (const r of rows) {
|
|
1209
|
+
const id = (r.id ?? r[0]);
|
|
1210
|
+
const label = (r.label ?? r[1]);
|
|
1211
|
+
if (id && label && !labelById.has(id))
|
|
1212
|
+
labelById.set(id, label);
|
|
1213
|
+
}
|
|
1214
|
+
for (const c of candidates) {
|
|
1215
|
+
if (c.type === '' && labelById.has(c.id))
|
|
1216
|
+
c.type = labelById.get(c.id);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
catch {
|
|
1220
|
+
/* best-effort — downstream resolvers still work without the label */
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Score a symbol candidate for disambiguation ranking.
|
|
1225
|
+
*
|
|
1226
|
+
* Deterministic, no DB round-trip:
|
|
1227
|
+
* - base 0.50
|
|
1228
|
+
* - +0.40 when file_path hint matches (substring, case-insensitive)
|
|
1229
|
+
* - +0.20 when kind hint exactly matches the candidate's kind
|
|
1230
|
+
* - when no kind hint, a small priority bonus (Class > Interface >
|
|
1231
|
+
* Function > Method > Constructor) to preserve the intuition that
|
|
1232
|
+
* class-level names are usually what the user wanted.
|
|
1233
|
+
*
|
|
1234
|
+
* Capped at 1.0. Intentionally simple and inspectable — a future v2 can
|
|
1235
|
+
* plug in BM25/embedding signals here without changing the surrounding
|
|
1236
|
+
* resolver shape.
|
|
1237
|
+
*/
|
|
1238
|
+
scoreCandidate(c, hints) {
|
|
1239
|
+
let s = 0.5;
|
|
1240
|
+
if (hints.file_path && c.filePath && typeof c.filePath === 'string') {
|
|
1241
|
+
if (c.filePath.toLowerCase().includes(hints.file_path.toLowerCase())) {
|
|
1242
|
+
s += 0.4;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (hints.kind && c.kind === hints.kind) {
|
|
1246
|
+
s += 0.2;
|
|
1247
|
+
}
|
|
1248
|
+
if (!hints.kind) {
|
|
1249
|
+
const priority = {
|
|
1250
|
+
Class: 5,
|
|
1251
|
+
Interface: 4,
|
|
1252
|
+
Function: 3,
|
|
1253
|
+
Method: 2,
|
|
1254
|
+
Constructor: 1,
|
|
1255
|
+
};
|
|
1256
|
+
s += (priority[c.kind] ?? 0) * 0.02;
|
|
1257
|
+
}
|
|
1258
|
+
return Math.min(1.0, s);
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Shared symbol resolver used by `context` and `impact`.
|
|
1262
|
+
*
|
|
1263
|
+
* Returns one of:
|
|
1264
|
+
* - `{ kind: 'ok', symbol, resolvedLabel }` — single confident match
|
|
1265
|
+
* (either direct UID, only one candidate after filtering, Class/
|
|
1266
|
+
* Constructor collapse, or a top-scoring candidate with a clear gap
|
|
1267
|
+
* to the runner-up).
|
|
1268
|
+
* - `{ kind: 'ambiguous', candidates }` — multiple viable matches,
|
|
1269
|
+
* sorted by score desc. Each candidate carries a relevance score.
|
|
1270
|
+
* - `{ kind: 'not_found' }` — no matches at all.
|
|
1271
|
+
*
|
|
1272
|
+
* Preserves the #480 Class/Constructor preference: when the only
|
|
1273
|
+
* ambiguity is between a Class and its own Constructor (same name,
|
|
1274
|
+
* same filePath), the Class wins silently.
|
|
1275
|
+
*/
|
|
1276
|
+
async resolveSymbolCandidates(repo, query, hints) {
|
|
1277
|
+
const { uid, name, include_content } = query;
|
|
1278
|
+
const selectClause = `n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}`;
|
|
1279
|
+
// Direct UID — zero-ambiguity path.
|
|
1280
|
+
if (uid) {
|
|
1281
|
+
const rows = await executeParameterized(repo.id, `MATCH (n {id: $uid}) RETURN ${selectClause} LIMIT 1`, { uid });
|
|
1282
|
+
if (rows.length === 0)
|
|
1283
|
+
return { kind: 'not_found' };
|
|
1284
|
+
const r = rows[0];
|
|
1285
|
+
const symbol = {
|
|
1286
|
+
id: (r.id ?? r[0]),
|
|
1287
|
+
name: (r.name ?? r[1]),
|
|
1288
|
+
type: (r.type ?? r[2] ?? ''),
|
|
1289
|
+
filePath: (r.filePath ?? r[3]),
|
|
1290
|
+
startLine: (r.startLine ?? r[4]),
|
|
1291
|
+
endLine: (r.endLine ?? r[5]),
|
|
1292
|
+
...(include_content ? { content: (r.content ?? r[6]) } : {}),
|
|
1293
|
+
};
|
|
1294
|
+
// Same LadybugDB label-enrichment as the name-based path: a UID
|
|
1295
|
+
// pointing at a Class must still surface `type: 'Class'` so impact's
|
|
1296
|
+
// Class/Interface BFS seed fires. No-op when type is already set.
|
|
1297
|
+
await this.enrichCandidateLabels(repo, [symbol]);
|
|
1298
|
+
return { kind: 'ok', symbol, resolvedLabel: symbol.type };
|
|
1299
|
+
}
|
|
1300
|
+
if (!name)
|
|
1301
|
+
return { kind: 'not_found' };
|
|
1302
|
+
const isQualified = name.includes('/') || name.includes(':');
|
|
1303
|
+
let whereClause;
|
|
1304
|
+
const queryParams = { symName: name };
|
|
1305
|
+
if (hints.file_path) {
|
|
1306
|
+
whereClause = `WHERE n.name = $symName AND n.filePath CONTAINS $filePath`;
|
|
1307
|
+
queryParams.filePath = hints.file_path;
|
|
1308
|
+
}
|
|
1309
|
+
else if (isQualified) {
|
|
1310
|
+
whereClause = `WHERE n.id = $symName OR n.name = $symName`;
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
whereClause = `WHERE n.name = $symName`;
|
|
1314
|
+
}
|
|
1315
|
+
// LIMIT 20 (was 10) — scoring is the point now, so give the ranker
|
|
1316
|
+
// headroom instead of arbitrary truncation.
|
|
1317
|
+
const rows = await executeParameterized(repo.id, `MATCH (n) ${whereClause} RETURN ${selectClause} LIMIT 20`, queryParams);
|
|
1318
|
+
if (rows.length === 0)
|
|
1319
|
+
return { kind: 'not_found' };
|
|
1320
|
+
// Normalise row shape across object / tuple returns from LadybugDB.
|
|
1321
|
+
const normalized = rows.map((r) => ({
|
|
1322
|
+
id: (r.id ?? r[0]),
|
|
1323
|
+
name: (r.name ?? r[1]),
|
|
1324
|
+
type: (r.type ?? r[2] ?? ''),
|
|
1325
|
+
filePath: (r.filePath ?? r[3]),
|
|
1326
|
+
startLine: (r.startLine ?? r[4]),
|
|
1327
|
+
endLine: (r.endLine ?? r[5]),
|
|
1328
|
+
...(include_content ? { content: (r.content ?? r[6]) } : {}),
|
|
1329
|
+
}));
|
|
1330
|
+
// Enrich labels for any candidates where `labels(n)[0]` came back empty.
|
|
1331
|
+
// LadybugDB returns an empty string for that projection on certain node
|
|
1332
|
+
// types (notably Class), which left downstream consumers (impact's
|
|
1333
|
+
// Class/Interface BFS seed, the kind-priority scoring bonus) unable to
|
|
1334
|
+
// distinguish a Class target from "unknown kind". One scoped UNION
|
|
1335
|
+
// across the five priority labels patches the type in-place without
|
|
1336
|
+
// per-candidate round-trips.
|
|
1337
|
+
await this.enrichCandidateLabels(repo, normalized);
|
|
1338
|
+
// Preserve #480 Class/Constructor collapse: if we have exactly one
|
|
1339
|
+
// Class (or Interface) candidate and one Constructor sharing name +
|
|
1340
|
+
// filePath, fold into the Class. This used to require a follow-up
|
|
1341
|
+
// label query because LadybugDB sometimes returns an empty labels()[0]
|
|
1342
|
+
// for Class nodes — enrichment above handles the empty-type case, but
|
|
1343
|
+
// the `type === 'Constructor'` gate still correctly triggers when a
|
|
1344
|
+
// Class and its Constructor share the name.
|
|
1345
|
+
if (!hints.kind && normalized.length > 1) {
|
|
1346
|
+
const ambiguousType = normalized.some((s) => s.type === '' || s.type === 'Constructor');
|
|
1347
|
+
if (ambiguousType) {
|
|
1348
|
+
const candidateIds = normalized.map((s) => s.id).filter(Boolean);
|
|
1349
|
+
for (const label of ['Class', 'Interface']) {
|
|
1350
|
+
const labelRows = await executeParameterized(repo.id, `MATCH (n:\`${label}\`) WHERE n.id IN $candidateIds RETURN n.id AS id LIMIT 1`, { candidateIds }).catch(() => []);
|
|
1351
|
+
if (labelRows.length > 0) {
|
|
1352
|
+
const preferredId = labelRows[0].id ?? labelRows[0][0];
|
|
1353
|
+
const preferred = normalized.find((s) => s.id === preferredId);
|
|
1354
|
+
if (preferred) {
|
|
1355
|
+
return {
|
|
1356
|
+
kind: 'ok',
|
|
1357
|
+
symbol: preferred,
|
|
1358
|
+
resolvedLabel: label,
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
if (normalized.length === 1) {
|
|
1366
|
+
return {
|
|
1367
|
+
kind: 'ok',
|
|
1368
|
+
symbol: normalized[0],
|
|
1369
|
+
resolvedLabel: '',
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
// Score, sort desc, stable tiebreak on shorter filePath then lex uid.
|
|
1373
|
+
const scored = normalized.map((s) => ({
|
|
1374
|
+
...s,
|
|
1375
|
+
score: this.scoreCandidate({ kind: s.type, filePath: s.filePath || '' }, hints),
|
|
1376
|
+
}));
|
|
1377
|
+
scored.sort((a, b) => {
|
|
1378
|
+
if (b.score !== a.score)
|
|
1379
|
+
return b.score - a.score;
|
|
1380
|
+
const fpA = (a.filePath || '').length;
|
|
1381
|
+
const fpB = (b.filePath || '').length;
|
|
1382
|
+
if (fpA !== fpB)
|
|
1383
|
+
return fpA - fpB;
|
|
1384
|
+
return String(a.id).localeCompare(String(b.id));
|
|
1385
|
+
});
|
|
1386
|
+
// Confident single-result: top score ≥ 0.95 AND beats runner-up by a
|
|
1387
|
+
// clear margin. This lets a very strong file_path/kind hint resolve
|
|
1388
|
+
// cleanly instead of forcing the caller through a disambiguation
|
|
1389
|
+
// round-trip.
|
|
1390
|
+
//
|
|
1391
|
+
// The gap threshold uses `> 0.09` rather than `>= 0.10` on purpose:
|
|
1392
|
+
// IEEE754 addition of the scoring terms (0.50 + 0.40 + 0.20 - 0.90
|
|
1393
|
+
// yields 0.09999999999999998, not exactly 0.10) would otherwise break
|
|
1394
|
+
// the comparison for legitimate "top is 1.00, runner is 0.90" cases.
|
|
1395
|
+
// The intent is a clearly-dominant winner; 0.09 is a large enough
|
|
1396
|
+
// margin to mean that unambiguously.
|
|
1397
|
+
//
|
|
1398
|
+
// The `scored.length >= 2` guard is defensive. The `normalized.length === 1`
|
|
1399
|
+
// early return above already handles the single-candidate path, so in
|
|
1400
|
+
// practice `scored` always has at least two elements by the time we get
|
|
1401
|
+
// here — keeping the guard means changes to the upstream early-return
|
|
1402
|
+
// logic cannot accidentally index out of bounds at `scored[1]`.
|
|
1403
|
+
if (scored.length >= 2 && scored[0].score >= 0.95 && scored[0].score - scored[1].score > 0.09) {
|
|
1404
|
+
return { kind: 'ok', symbol: scored[0], resolvedLabel: scored[0].type };
|
|
1405
|
+
}
|
|
1406
|
+
return { kind: 'ambiguous', candidates: scored };
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Context tool — 360-degree symbol view with categorized refs.
|
|
1410
|
+
* Disambiguation (ranked) when multiple symbols share a name.
|
|
1411
|
+
* UID-based direct lookup. No cluster in output.
|
|
1412
|
+
*/
|
|
1413
|
+
async context(repo, params) {
|
|
1414
|
+
try {
|
|
1415
|
+
return await this._contextImpl(repo, params);
|
|
1416
|
+
}
|
|
1417
|
+
catch (err) {
|
|
1418
|
+
const msg = (err instanceof Error ? err.message : String(err)) || 'Context query failed';
|
|
1419
|
+
if (isWalCorruptionError(err)) {
|
|
1420
|
+
return {
|
|
1421
|
+
error: msg,
|
|
1422
|
+
recoverySuggestion: WAL_RECOVERY_SUGGESTION,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
throw err;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async _contextImpl(repo, params) {
|
|
1429
|
+
await this.ensureInitialized(repo.id);
|
|
1430
|
+
const { name, uid, file_path, kind, include_content } = params;
|
|
1431
|
+
if (!name && !uid) {
|
|
1432
|
+
return { error: 'Either "name" or "uid" parameter is required.' };
|
|
1433
|
+
}
|
|
1434
|
+
const outcome = await this.resolveSymbolCandidates(repo, { uid, name, include_content }, { file_path, kind });
|
|
1435
|
+
if (outcome.kind === 'not_found') {
|
|
1436
|
+
return { error: `Symbol '${name || uid}' not found` };
|
|
1437
|
+
}
|
|
1438
|
+
if (outcome.kind === 'ambiguous') {
|
|
1439
|
+
return {
|
|
1440
|
+
status: 'ambiguous',
|
|
1441
|
+
message: `Found ${outcome.candidates.length} symbols matching '${name}'. Use uid, file_path, or kind to disambiguate.`,
|
|
1442
|
+
candidates: outcome.candidates.map((c) => ({
|
|
1443
|
+
uid: c.id,
|
|
1444
|
+
name: c.name,
|
|
1445
|
+
kind: c.type,
|
|
1446
|
+
filePath: c.filePath,
|
|
1447
|
+
line: c.startLine,
|
|
1448
|
+
score: Number(c.score.toFixed(2)),
|
|
1449
|
+
})),
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
// Step 3: Build full context
|
|
1453
|
+
const sym = outcome.symbol;
|
|
1454
|
+
const resolvedLabel = outcome.resolvedLabel;
|
|
1455
|
+
const symId = sym.id;
|
|
1456
|
+
// Categorized incoming refs
|
|
1457
|
+
const incomingRows = await executeParameterized(repo.id, `
|
|
1458
|
+
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
|
|
1459
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
|
|
1460
|
+
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
1461
|
+
LIMIT 30
|
|
1462
|
+
`, { symId });
|
|
1463
|
+
let typedPropertyRows = [];
|
|
1464
|
+
// Fix #480: Class/Interface nodes have no direct CALLS/IMPORTS edges —
|
|
1465
|
+
// those point to Constructor and File nodes respectively. Fetch those
|
|
1466
|
+
// extra incoming refs and merge them in so context() shows real callers.
|
|
1467
|
+
//
|
|
1468
|
+
// Determine if this is a Class/Interface node. If resolvedLabel was set
|
|
1469
|
+
// during disambiguation (Step 2), use it directly — no extra round-trip.
|
|
1470
|
+
// Otherwise fall back to a single label check only when the type field is
|
|
1471
|
+
// empty (LadybugDB labels(n)[0] limitation).
|
|
1472
|
+
const symRawType = sym.type || sym[2] || '';
|
|
1473
|
+
let isClassLike = resolvedLabel === 'Class' || resolvedLabel === 'Interface';
|
|
1474
|
+
if (!isClassLike && symRawType === '') {
|
|
1475
|
+
try {
|
|
1476
|
+
// Single UNION query instead of two serial round-trips.
|
|
1477
|
+
const typeCheck = await executeParameterized(repo.id, `
|
|
1478
|
+
MATCH (n:Class) WHERE n.id = $symId RETURN 'Class' AS label LIMIT 1
|
|
1479
|
+
UNION ALL
|
|
1480
|
+
MATCH (n:Interface) WHERE n.id = $symId RETURN 'Interface' AS label LIMIT 1
|
|
1481
|
+
`, { symId });
|
|
1482
|
+
isClassLike = typeCheck.length > 0;
|
|
1483
|
+
}
|
|
1484
|
+
catch {
|
|
1485
|
+
/* not a Class/Interface node */
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
else if (!isClassLike) {
|
|
1489
|
+
isClassLike = symRawType === 'Class' || symRawType === 'Interface';
|
|
1490
|
+
}
|
|
1491
|
+
if (isClassLike) {
|
|
1492
|
+
try {
|
|
1493
|
+
// Run incoming-ref queries in parallel — they are independent.
|
|
1494
|
+
const [ctorIncoming, fileIncoming, typedPropertyIncoming, typedProperties] = await Promise.all([
|
|
1495
|
+
executeParameterized(repo.id, `
|
|
1496
|
+
MATCH (n)-[hm:CodeRelation]->(ctor:Constructor)
|
|
1497
|
+
WHERE n.id = $symId AND hm.type = 'HAS_METHOD'
|
|
1498
|
+
MATCH (caller)-[r:CodeRelation]->(ctor)
|
|
1499
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'ACCESSES']
|
|
1500
|
+
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
1501
|
+
LIMIT 30
|
|
1502
|
+
`, { symId }),
|
|
1503
|
+
executeParameterized(repo.id, `
|
|
1504
|
+
MATCH (f:File)-[rel:CodeRelation]->(n)
|
|
1505
|
+
WHERE n.id = $symId AND rel.type = 'DEFINES'
|
|
1506
|
+
MATCH (caller)-[r:CodeRelation]->(f)
|
|
1507
|
+
WHERE r.type IN ['CALLS', 'IMPORTS']
|
|
1508
|
+
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
1509
|
+
LIMIT 30
|
|
1510
|
+
`, { symId }),
|
|
1511
|
+
executeParameterized(repo.id, `
|
|
1512
|
+
MATCH (p:\`Property\`)
|
|
1513
|
+
WHERE p.declaredType = $name
|
|
1514
|
+
OR p.declaredType STARTS WITH $genericPrefix
|
|
1515
|
+
OR p.declaredType CONTAINS $genericArg
|
|
1516
|
+
MATCH (caller)-[r:CodeRelation]->(p)
|
|
1517
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'ACCESSES']
|
|
1518
|
+
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
1519
|
+
LIMIT 30
|
|
1520
|
+
`, {
|
|
1521
|
+
name: sym.name,
|
|
1522
|
+
genericPrefix: `${sym.name}<`,
|
|
1523
|
+
genericArg: `<${sym.name}>`,
|
|
1524
|
+
}),
|
|
1525
|
+
executeParameterized(repo.id, `
|
|
1526
|
+
MATCH (p:\`Property\`)
|
|
1527
|
+
WHERE p.declaredType = $name
|
|
1528
|
+
OR p.declaredType STARTS WITH $genericPrefix
|
|
1529
|
+
OR p.declaredType CONTAINS $genericArg
|
|
1530
|
+
RETURN p.id AS uid, p.name AS name, p.filePath AS filePath, labels(p)[0] AS kind,
|
|
1531
|
+
p.declaredType AS declaredType
|
|
1532
|
+
LIMIT 30
|
|
1533
|
+
`, {
|
|
1534
|
+
name: sym.name,
|
|
1535
|
+
genericPrefix: `${sym.name}<`,
|
|
1536
|
+
genericArg: `<${sym.name}>`,
|
|
1537
|
+
}),
|
|
1538
|
+
]);
|
|
1539
|
+
typedPropertyRows = typedProperties;
|
|
1540
|
+
// Deduplicate by (relType, uid) — a caller can have multiple relation
|
|
1541
|
+
// types to the same target (e.g. both IMPORTS and CALLS), and each
|
|
1542
|
+
// must be preserved so every category appears in the output.
|
|
1543
|
+
const seenKeys = new Set(incomingRows.map((r) => `${r.relType || r[0]}:${r.uid || r[1]}`));
|
|
1544
|
+
for (const r of [...ctorIncoming, ...fileIncoming, ...typedPropertyIncoming]) {
|
|
1545
|
+
const key = `${r.relType || r[0]}:${r.uid || r[1]}`;
|
|
1546
|
+
if (!seenKeys.has(key)) {
|
|
1547
|
+
seenKeys.add(key);
|
|
1548
|
+
incomingRows.push(r);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
catch (e) {
|
|
1553
|
+
logQueryError('context:class-incoming-expansion', e);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
// Categorized outgoing refs
|
|
1557
|
+
const outgoingRows = await executeParameterized(repo.id, `
|
|
1558
|
+
MATCH (n {id: $symId})-[r:CodeRelation]->(target)
|
|
1559
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
|
|
1560
|
+
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
|
1561
|
+
LIMIT 30
|
|
1562
|
+
`, { symId });
|
|
1563
|
+
// Process participation
|
|
1564
|
+
let processRows = [];
|
|
1565
|
+
try {
|
|
1566
|
+
processRows = await executeParameterized(repo.id, `
|
|
1567
|
+
MATCH (n {id: $symId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
1568
|
+
RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
|
|
1569
|
+
`, { symId });
|
|
1570
|
+
}
|
|
1571
|
+
catch (e) {
|
|
1572
|
+
logQueryError('context:process-participation', e);
|
|
1573
|
+
}
|
|
1574
|
+
// Helper to categorize refs
|
|
1575
|
+
const categorize = (rows) => {
|
|
1576
|
+
const cats = {};
|
|
1577
|
+
for (const row of rows) {
|
|
1578
|
+
const relType = (row.relType || row[0] || '').toLowerCase();
|
|
1579
|
+
const entry = {
|
|
1580
|
+
uid: row.uid || row[1],
|
|
1581
|
+
name: row.name || row[2],
|
|
1582
|
+
filePath: row.filePath || row[3],
|
|
1583
|
+
kind: row.kind || row[4],
|
|
1584
|
+
};
|
|
1585
|
+
if (!cats[relType])
|
|
1586
|
+
cats[relType] = [];
|
|
1587
|
+
cats[relType].push(entry);
|
|
1588
|
+
}
|
|
1589
|
+
return cats;
|
|
1590
|
+
};
|
|
1591
|
+
// Method/Function/Constructor enrichment: fetch method-specific properties
|
|
1592
|
+
const symKind = isClassLike ? resolvedLabel || 'Class' : sym.type || sym[2];
|
|
1593
|
+
const isMethodLike = symKind === 'Method' || symKind === 'Function' || symKind === 'Constructor';
|
|
1594
|
+
let methodMetadata;
|
|
1595
|
+
if (isMethodLike) {
|
|
1596
|
+
try {
|
|
1597
|
+
const metaRows = await executeParameterized(repo.id, `
|
|
1598
|
+
MATCH (n {id: $symId})
|
|
1599
|
+
RETURN n.visibility AS visibility, n.isStatic AS isStatic, n.isAbstract AS isAbstract,
|
|
1600
|
+
n.isFinal AS isFinal, n.isVirtual AS isVirtual, n.isOverride AS isOverride,
|
|
1601
|
+
n.isAsync AS isAsync, n.isPartial AS isPartial, n.returnType AS returnType,
|
|
1602
|
+
n.parameterCount AS parameterCount, n.isVariadic AS isVariadic,
|
|
1603
|
+
n.requiredParameterCount AS requiredParameterCount,
|
|
1604
|
+
n.parameterTypes AS parameterTypes, n.annotations AS annotations
|
|
1605
|
+
LIMIT 1
|
|
1606
|
+
`, { symId });
|
|
1607
|
+
if (metaRows.length > 0) {
|
|
1608
|
+
const row = metaRows[0];
|
|
1609
|
+
const meta = {};
|
|
1610
|
+
// Only include defined properties to distinguish "not applicable" from "not enriched"
|
|
1611
|
+
for (const key of Object.keys(row)) {
|
|
1612
|
+
const val = row[key];
|
|
1613
|
+
if (val !== null && val !== undefined)
|
|
1614
|
+
meta[key] = val;
|
|
1615
|
+
}
|
|
1616
|
+
if (Object.keys(meta).length > 0)
|
|
1617
|
+
methodMetadata = meta;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
catch {
|
|
1621
|
+
/* method metadata unavailable — omit silently */
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
return {
|
|
1625
|
+
status: 'found',
|
|
1626
|
+
symbol: {
|
|
1627
|
+
uid: sym.id || sym[0],
|
|
1628
|
+
name: sym.name || sym[1],
|
|
1629
|
+
kind: symKind,
|
|
1630
|
+
filePath: sym.filePath || sym[3],
|
|
1631
|
+
startLine: sym.startLine || sym[4],
|
|
1632
|
+
endLine: sym.endLine || sym[5],
|
|
1633
|
+
...(include_content && (sym.content || sym[6]) ? { content: sym.content || sym[6] } : {}),
|
|
1634
|
+
...(methodMetadata ? { methodMetadata } : {}),
|
|
1635
|
+
},
|
|
1636
|
+
incoming: categorize(incomingRows),
|
|
1637
|
+
outgoing: categorize(outgoingRows),
|
|
1638
|
+
...(typedPropertyRows.length > 0
|
|
1639
|
+
? {
|
|
1640
|
+
typed_properties: typedPropertyRows.map((r) => ({
|
|
1641
|
+
uid: r.uid || r[0],
|
|
1642
|
+
name: r.name || r[1],
|
|
1643
|
+
filePath: r.filePath || r[2],
|
|
1644
|
+
kind: r.kind || r[3],
|
|
1645
|
+
declaredType: r.declaredType || r[4],
|
|
1646
|
+
})),
|
|
1647
|
+
}
|
|
1648
|
+
: {}),
|
|
1649
|
+
processes: processRows.map((r) => ({
|
|
1650
|
+
id: r.pid || r[0],
|
|
1651
|
+
name: r.label || r[1],
|
|
1652
|
+
step_index: r.step || r[2],
|
|
1653
|
+
step_count: r.stepCount || r[3],
|
|
1654
|
+
})),
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Legacy explore — kept for backwards compatibility with resources.ts.
|
|
1659
|
+
* Routes cluster/process types to direct graph queries.
|
|
1660
|
+
*/
|
|
1661
|
+
async explore(repo, params) {
|
|
1662
|
+
await this.ensureInitialized(repo.id);
|
|
1663
|
+
const { name, type } = params;
|
|
1664
|
+
if (type === 'symbol') {
|
|
1665
|
+
return this.context(repo, { name });
|
|
1666
|
+
}
|
|
1667
|
+
if (type === 'cluster') {
|
|
1668
|
+
const clusters = await executeParameterized(repo.id, `
|
|
1669
|
+
MATCH (c:Community)
|
|
1670
|
+
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
|
|
1671
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1672
|
+
`, { clusterName: name });
|
|
1673
|
+
if (clusters.length === 0)
|
|
1674
|
+
return { error: `Cluster '${name}' not found` };
|
|
1675
|
+
const rawClusters = clusters.map((c) => ({
|
|
1676
|
+
id: c.id || c[0],
|
|
1677
|
+
label: c.label || c[1],
|
|
1678
|
+
heuristicLabel: c.heuristicLabel || c[2],
|
|
1679
|
+
cohesion: c.cohesion || c[3],
|
|
1680
|
+
symbolCount: c.symbolCount || c[4],
|
|
1681
|
+
}));
|
|
1682
|
+
let totalSymbols = 0, weightedCohesion = 0;
|
|
1683
|
+
for (const c of rawClusters) {
|
|
1684
|
+
const s = c.symbolCount || 0;
|
|
1685
|
+
totalSymbols += s;
|
|
1686
|
+
weightedCohesion += (c.cohesion || 0) * s;
|
|
1687
|
+
}
|
|
1688
|
+
const members = await executeParameterized(repo.id, `
|
|
1689
|
+
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
1690
|
+
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
|
|
1691
|
+
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1692
|
+
LIMIT 30
|
|
1693
|
+
`, { clusterName: name });
|
|
1694
|
+
return {
|
|
1695
|
+
cluster: {
|
|
1696
|
+
id: rawClusters[0].id,
|
|
1697
|
+
label: rawClusters[0].heuristicLabel || rawClusters[0].label,
|
|
1698
|
+
heuristicLabel: rawClusters[0].heuristicLabel || rawClusters[0].label,
|
|
1699
|
+
cohesion: totalSymbols > 0 ? weightedCohesion / totalSymbols : 0,
|
|
1700
|
+
symbolCount: totalSymbols,
|
|
1701
|
+
subCommunities: rawClusters.length,
|
|
1702
|
+
},
|
|
1703
|
+
members: members.map((m) => ({
|
|
1704
|
+
name: m.name || m[0],
|
|
1705
|
+
type: m.type || m[1],
|
|
1706
|
+
filePath: m.filePath || m[2],
|
|
1707
|
+
})),
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
if (type === 'process') {
|
|
1711
|
+
const processes = await executeParameterized(repo.id, `
|
|
1712
|
+
MATCH (p:Process)
|
|
1713
|
+
WHERE p.label = $processName OR p.heuristicLabel = $processName
|
|
1714
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1715
|
+
LIMIT 1
|
|
1716
|
+
`, { processName: name });
|
|
1717
|
+
if (processes.length === 0)
|
|
1718
|
+
return { error: `Process '${name}' not found` };
|
|
1719
|
+
const proc = processes[0];
|
|
1720
|
+
const procId = proc.id || proc[0];
|
|
1721
|
+
const steps = await executeParameterized(repo.id, `
|
|
1722
|
+
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: $procId})
|
|
1723
|
+
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
1724
|
+
ORDER BY r.step
|
|
1725
|
+
`, { procId });
|
|
1726
|
+
return {
|
|
1727
|
+
process: {
|
|
1728
|
+
id: procId,
|
|
1729
|
+
label: proc.label || proc[1],
|
|
1730
|
+
heuristicLabel: proc.heuristicLabel || proc[2],
|
|
1731
|
+
processType: proc.processType || proc[3],
|
|
1732
|
+
stepCount: proc.stepCount || proc[4],
|
|
1733
|
+
},
|
|
1734
|
+
steps: steps.map((s) => ({
|
|
1735
|
+
step: s.step || s[3],
|
|
1736
|
+
name: s.name || s[0],
|
|
1737
|
+
type: s.type || s[1],
|
|
1738
|
+
filePath: s.filePath || s[2],
|
|
1739
|
+
})),
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
return { error: 'Invalid type. Use: symbol, cluster, or process' };
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Detect changes — git-diff based impact analysis.
|
|
1746
|
+
* Maps changed lines to indexed symbols, then finds affected processes.
|
|
1747
|
+
*/
|
|
1748
|
+
async detectChanges(repo, params) {
|
|
1749
|
+
await this.ensureInitialized(repo.id);
|
|
1750
|
+
const scope = params.scope || 'unstaged';
|
|
1751
|
+
const { execFileSync } = await import('child_process');
|
|
1752
|
+
// Build git diff args based on scope (using execFileSync to avoid shell injection)
|
|
1753
|
+
let diffArgs;
|
|
1754
|
+
switch (scope) {
|
|
1755
|
+
case 'staged':
|
|
1756
|
+
diffArgs = ['diff', '--staged', '-U0'];
|
|
1757
|
+
break;
|
|
1758
|
+
case 'all':
|
|
1759
|
+
diffArgs = ['diff', 'HEAD', '-U0'];
|
|
1760
|
+
break;
|
|
1761
|
+
case 'compare':
|
|
1762
|
+
if (!params.base_ref)
|
|
1763
|
+
return { error: 'base_ref is required for "compare" scope' };
|
|
1764
|
+
diffArgs = ['diff', params.base_ref, '-U0'];
|
|
1765
|
+
break;
|
|
1766
|
+
case 'unstaged':
|
|
1767
|
+
default:
|
|
1768
|
+
diffArgs = ['diff', '-U0'];
|
|
1769
|
+
break;
|
|
1770
|
+
}
|
|
1771
|
+
let diffOutput;
|
|
1772
|
+
try {
|
|
1773
|
+
// maxBuffer raised from Node's 1MB default to 256MB to avoid ENOBUFS on
|
|
1774
|
+
// repos with large unstaged/untracked diffs (e.g. unignored build folders).
|
|
1775
|
+
// See issue: spawnSync git ENOBUFS in detect_changes(scope="unstaged").
|
|
1776
|
+
diffOutput = execFileSync('git', diffArgs, {
|
|
1777
|
+
cwd: repo.repoPath,
|
|
1778
|
+
encoding: 'utf-8',
|
|
1779
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
catch (err) {
|
|
1783
|
+
return { error: `Git diff failed: ${err.message}` };
|
|
1784
|
+
}
|
|
1785
|
+
const fileDiffs = parseDiffHunks(diffOutput);
|
|
1786
|
+
if (fileDiffs.length === 0) {
|
|
1787
|
+
return {
|
|
1788
|
+
summary: {
|
|
1789
|
+
changed_count: 0,
|
|
1790
|
+
affected_count: 0,
|
|
1791
|
+
risk_level: 'none',
|
|
1792
|
+
message: 'No changes detected.',
|
|
1793
|
+
},
|
|
1794
|
+
changed_symbols: [],
|
|
1795
|
+
affected_processes: [],
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
// Map diff hunks to indexed symbols via range overlap
|
|
1799
|
+
const changedSymbols = [];
|
|
1800
|
+
for (const fileDiff of fileDiffs) {
|
|
1801
|
+
if (fileDiff.hunks.length === 0)
|
|
1802
|
+
continue;
|
|
1803
|
+
// Build range overlap conditions for all hunks in this file
|
|
1804
|
+
const overlapConditions = fileDiff.hunks
|
|
1805
|
+
.map((_, i) => `(n.startLine <= $hunkEnd${i} AND n.endLine >= $hunkStart${i})`)
|
|
1806
|
+
.join(' OR ');
|
|
1807
|
+
const queryParams = { filePath: fileDiff.filePath };
|
|
1808
|
+
fileDiff.hunks.forEach((hunk, i) => {
|
|
1809
|
+
queryParams[`hunkStart${i}`] = hunk.startLine;
|
|
1810
|
+
queryParams[`hunkEnd${i}`] = hunk.endLine;
|
|
1811
|
+
});
|
|
1812
|
+
const symbolQuery = `
|
|
1813
|
+
MATCH (n) WHERE n.filePath ENDS WITH $filePath
|
|
1814
|
+
AND n.startLine IS NOT NULL AND n.endLine IS NOT NULL
|
|
1815
|
+
AND (${overlapConditions})
|
|
1816
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type,
|
|
1817
|
+
n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
1818
|
+
`;
|
|
1819
|
+
try {
|
|
1820
|
+
const rows = await executeParameterized(repo.id, symbolQuery, queryParams);
|
|
1821
|
+
for (const sym of rows) {
|
|
1822
|
+
changedSymbols.push({
|
|
1823
|
+
id: sym.id || sym[0],
|
|
1824
|
+
name: sym.name || sym[1],
|
|
1825
|
+
type: sym.type || sym[2],
|
|
1826
|
+
filePath: sym.filePath || sym[3],
|
|
1827
|
+
change_type: 'touched',
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
catch (e) {
|
|
1832
|
+
logQueryError('detect-changes:file-symbols', e);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
// Find affected processes -- single batched query instead of N+1
|
|
1836
|
+
const affectedProcesses = new Map();
|
|
1837
|
+
if (changedSymbols.length > 0) {
|
|
1838
|
+
const symIds = changedSymbols.map((s) => s.id);
|
|
1839
|
+
const symNameById = new Map(changedSymbols.map((s) => [s.id, s.name]));
|
|
1840
|
+
try {
|
|
1841
|
+
const procs = await executeParameterized(repo.id, `
|
|
1842
|
+
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
1843
|
+
WHERE n.id IN $ids
|
|
1844
|
+
RETURN n.id AS nodeId, p.id AS pid, p.heuristicLabel AS label,
|
|
1845
|
+
p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
1846
|
+
`, { ids: symIds });
|
|
1847
|
+
for (const proc of procs) {
|
|
1848
|
+
const nodeId = proc.nodeId || proc[0];
|
|
1849
|
+
const pid = proc.pid || proc[1];
|
|
1850
|
+
if (!affectedProcesses.has(pid)) {
|
|
1851
|
+
affectedProcesses.set(pid, {
|
|
1852
|
+
id: pid,
|
|
1853
|
+
name: proc.label || proc[2],
|
|
1854
|
+
process_type: proc.processType || proc[3],
|
|
1855
|
+
step_count: proc.stepCount || proc[4],
|
|
1856
|
+
changed_steps: [],
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
affectedProcesses.get(pid).changed_steps.push({
|
|
1860
|
+
symbol: symNameById.get(nodeId) ?? nodeId,
|
|
1861
|
+
step: proc.step || proc[5],
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
catch (e) {
|
|
1866
|
+
logQueryError('detect-changes:process-lookup', e);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
const processCount = affectedProcesses.size;
|
|
1870
|
+
const risk = processCount === 0
|
|
1871
|
+
? 'low'
|
|
1872
|
+
: processCount <= 5
|
|
1873
|
+
? 'medium'
|
|
1874
|
+
: processCount <= 15
|
|
1875
|
+
? 'high'
|
|
1876
|
+
: 'critical';
|
|
1877
|
+
return {
|
|
1878
|
+
summary: {
|
|
1879
|
+
changed_count: changedSymbols.length,
|
|
1880
|
+
affected_count: processCount,
|
|
1881
|
+
changed_files: fileDiffs.length,
|
|
1882
|
+
risk_level: risk,
|
|
1883
|
+
},
|
|
1884
|
+
changed_symbols: changedSymbols,
|
|
1885
|
+
affected_processes: Array.from(affectedProcesses.values()),
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Rename tool — multi-file coordinated rename using graph + text search.
|
|
1890
|
+
* Graph refs are tagged "graph" (high confidence).
|
|
1891
|
+
* Additional refs found via text search are tagged "text_search" (lower confidence).
|
|
1892
|
+
*/
|
|
1893
|
+
async rename(repo, params) {
|
|
1894
|
+
await this.ensureInitialized(repo.id);
|
|
1895
|
+
const { new_name, file_path } = params;
|
|
1896
|
+
const dry_run = params.dry_run ?? true;
|
|
1897
|
+
if (!params.symbol_name && !params.symbol_uid) {
|
|
1898
|
+
return { error: 'Either symbol_name or symbol_uid is required.' };
|
|
1899
|
+
}
|
|
1900
|
+
/** Guard: ensure a file path resolves within the repo root (prevents path traversal) */
|
|
1901
|
+
const assertSafePath = (filePath) => {
|
|
1902
|
+
const full = path.resolve(repo.repoPath, filePath);
|
|
1903
|
+
if (!full.startsWith(repo.repoPath + path.sep) && full !== repo.repoPath) {
|
|
1904
|
+
throw new Error(`Path traversal blocked: ${filePath}`);
|
|
1905
|
+
}
|
|
1906
|
+
return full;
|
|
1907
|
+
};
|
|
1908
|
+
// Step 1: Find the target symbol (reuse context's lookup)
|
|
1909
|
+
const lookupResult = await this.context(repo, {
|
|
1910
|
+
name: params.symbol_name,
|
|
1911
|
+
uid: params.symbol_uid,
|
|
1912
|
+
file_path,
|
|
1913
|
+
});
|
|
1914
|
+
if (lookupResult.status === 'ambiguous') {
|
|
1915
|
+
return lookupResult; // pass disambiguation through
|
|
1916
|
+
}
|
|
1917
|
+
if (lookupResult.error) {
|
|
1918
|
+
return lookupResult;
|
|
1919
|
+
}
|
|
1920
|
+
const sym = lookupResult.symbol;
|
|
1921
|
+
const oldName = sym.name;
|
|
1922
|
+
if (oldName === new_name) {
|
|
1923
|
+
return { error: 'New name is the same as the current name.' };
|
|
1924
|
+
}
|
|
1925
|
+
// Step 2: Collect edits from graph (high confidence)
|
|
1926
|
+
const changes = new Map();
|
|
1927
|
+
const addEdit = (filePath, line, oldText, newText, confidence) => {
|
|
1928
|
+
if (!changes.has(filePath)) {
|
|
1929
|
+
changes.set(filePath, { file_path: filePath, edits: [] });
|
|
1930
|
+
}
|
|
1931
|
+
changes.get(filePath).edits.push({ line, old_text: oldText, new_text: newText, confidence });
|
|
1932
|
+
};
|
|
1933
|
+
// The definition itself
|
|
1934
|
+
if (sym.filePath && sym.startLine) {
|
|
1935
|
+
try {
|
|
1936
|
+
const content = await fs.readFile(assertSafePath(sym.filePath), 'utf-8');
|
|
1937
|
+
const lines = content.split('\n');
|
|
1938
|
+
const lineIdx = sym.startLine - 1;
|
|
1939
|
+
if (lineIdx >= 0 && lineIdx < lines.length && lines[lineIdx].includes(oldName)) {
|
|
1940
|
+
const defRegex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
1941
|
+
addEdit(sym.filePath, sym.startLine, lines[lineIdx].trim(), lines[lineIdx].replace(defRegex, new_name).trim(), 'graph');
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
catch (e) {
|
|
1945
|
+
logQueryError('rename:read-definition', e);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
// All incoming refs from graph (callers, importers, etc.)
|
|
1949
|
+
const allIncoming = [
|
|
1950
|
+
...(lookupResult.incoming.calls || []),
|
|
1951
|
+
...(lookupResult.incoming.imports || []),
|
|
1952
|
+
...(lookupResult.incoming.extends || []),
|
|
1953
|
+
...(lookupResult.incoming.implements || []),
|
|
1954
|
+
];
|
|
1955
|
+
let graphEdits = changes.size > 0 ? 1 : 0; // count definition edit
|
|
1956
|
+
for (const ref of allIncoming) {
|
|
1957
|
+
if (!ref.filePath)
|
|
1958
|
+
continue;
|
|
1959
|
+
try {
|
|
1960
|
+
const content = await fs.readFile(assertSafePath(ref.filePath), 'utf-8');
|
|
1961
|
+
const lines = content.split('\n');
|
|
1962
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1963
|
+
if (lines[i].includes(oldName)) {
|
|
1964
|
+
addEdit(ref.filePath, i + 1, lines[i].trim(), lines[i]
|
|
1965
|
+
.replace(new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g'), new_name)
|
|
1966
|
+
.trim(), 'graph');
|
|
1967
|
+
graphEdits++;
|
|
1968
|
+
break; // one edit per file from graph refs
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
catch (e) {
|
|
1973
|
+
logQueryError('rename:read-ref', e);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
// Step 3: Text search for refs the graph might have missed
|
|
1977
|
+
let astSearchEdits = 0;
|
|
1978
|
+
const graphFiles = new Set([sym.filePath, ...allIncoming.map((r) => r.filePath)].filter(Boolean));
|
|
1979
|
+
// Simple text search across the repo for the old name (in files not already covered by graph)
|
|
1980
|
+
try {
|
|
1981
|
+
const { execFileSync } = await import('child_process');
|
|
1982
|
+
const rgArgs = [
|
|
1983
|
+
'-l',
|
|
1984
|
+
'--type-add',
|
|
1985
|
+
'code:*.{ts,tsx,js,jsx,py,go,rs,java,c,h,cpp,cc,cxx,hpp,hxx,hh,cs,php,swift}',
|
|
1986
|
+
'-t',
|
|
1987
|
+
'code',
|
|
1988
|
+
`\\b${oldName}\\b`,
|
|
1989
|
+
'.',
|
|
1990
|
+
];
|
|
1991
|
+
const output = execFileSync('rg', rgArgs, {
|
|
1992
|
+
cwd: repo.repoPath,
|
|
1993
|
+
encoding: 'utf-8',
|
|
1994
|
+
timeout: 5000,
|
|
1995
|
+
// Avoid ENOBUFS on large repos: rg -l can list many files.
|
|
1996
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
1997
|
+
});
|
|
1998
|
+
const files = output
|
|
1999
|
+
.trim()
|
|
2000
|
+
.split('\n')
|
|
2001
|
+
.filter((f) => f.length > 0);
|
|
2002
|
+
for (const file of files) {
|
|
2003
|
+
const normalizedFile = file.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
2004
|
+
if (graphFiles.has(normalizedFile))
|
|
2005
|
+
continue; // already covered by graph
|
|
2006
|
+
try {
|
|
2007
|
+
const content = await fs.readFile(assertSafePath(normalizedFile), 'utf-8');
|
|
2008
|
+
const lines = content.split('\n');
|
|
2009
|
+
const regex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
2010
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2011
|
+
regex.lastIndex = 0;
|
|
2012
|
+
if (regex.test(lines[i])) {
|
|
2013
|
+
regex.lastIndex = 0;
|
|
2014
|
+
addEdit(normalizedFile, i + 1, lines[i].trim(), lines[i].replace(regex, new_name).trim(), 'text_search');
|
|
2015
|
+
astSearchEdits++;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
catch (e) {
|
|
2020
|
+
logQueryError('rename:text-search-read', e);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
catch (e) {
|
|
2025
|
+
logQueryError('rename:ripgrep', e);
|
|
2026
|
+
}
|
|
2027
|
+
// Step 4: Apply or preview
|
|
2028
|
+
const allChanges = Array.from(changes.values());
|
|
2029
|
+
const totalEdits = allChanges.reduce((sum, c) => sum + c.edits.length, 0);
|
|
2030
|
+
if (!dry_run) {
|
|
2031
|
+
// Apply edits to files
|
|
2032
|
+
for (const change of allChanges) {
|
|
2033
|
+
try {
|
|
2034
|
+
const fullPath = assertSafePath(change.file_path);
|
|
2035
|
+
let content = await fs.readFile(fullPath, 'utf-8');
|
|
2036
|
+
const regex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
2037
|
+
content = content.replace(regex, new_name);
|
|
2038
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
2039
|
+
}
|
|
2040
|
+
catch (e) {
|
|
2041
|
+
logQueryError('rename:apply-edit', e);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
status: 'success',
|
|
2047
|
+
old_name: oldName,
|
|
2048
|
+
new_name,
|
|
2049
|
+
files_affected: allChanges.length,
|
|
2050
|
+
total_edits: totalEdits,
|
|
2051
|
+
graph_edits: graphEdits,
|
|
2052
|
+
text_search_edits: astSearchEdits,
|
|
2053
|
+
changes: allChanges,
|
|
2054
|
+
applied: !dry_run,
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
async impact(repo, params) {
|
|
2058
|
+
try {
|
|
2059
|
+
return await this._impactImpl(repo, params);
|
|
2060
|
+
}
|
|
2061
|
+
catch (err) {
|
|
2062
|
+
// Return structured error instead of crashing (#321)
|
|
2063
|
+
return {
|
|
2064
|
+
error: (err instanceof Error ? err.message : String(err)) || 'Impact analysis failed',
|
|
2065
|
+
target: { name: params.target },
|
|
2066
|
+
direction: params.direction,
|
|
2067
|
+
impactedCount: 0,
|
|
2068
|
+
risk: 'UNKNOWN',
|
|
2069
|
+
suggestion: 'The graph query failed — try arc context <symbol> as a fallback',
|
|
2070
|
+
...(isWalCorruptionError(err) ? { recoverySuggestion: WAL_RECOVERY_SUGGESTION } : {}),
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
async _impactImpl(repo, params) {
|
|
2075
|
+
await this.ensureInitialized(repo.id);
|
|
2076
|
+
const { target, direction } = params;
|
|
2077
|
+
const maxDepth = params.maxDepth || 3;
|
|
2078
|
+
// Map legacy relation type names before filtering (backward compat for OVERRIDES → METHOD_OVERRIDES)
|
|
2079
|
+
const mappedRelTypes = params.relationTypes?.flatMap((t) => t === 'OVERRIDES' ? ['OVERRIDES', 'METHOD_OVERRIDES'] : [t]);
|
|
2080
|
+
const hasExplicitRelationTypes = mappedRelTypes !== undefined && mappedRelTypes.length > 0;
|
|
2081
|
+
const rawRelTypes = mappedRelTypes && mappedRelTypes.length > 0
|
|
2082
|
+
? mappedRelTypes.filter((t) => VALID_RELATION_TYPES.has(t))
|
|
2083
|
+
: [
|
|
2084
|
+
'CALLS',
|
|
2085
|
+
'IMPORTS',
|
|
2086
|
+
'EXTENDS',
|
|
2087
|
+
'IMPLEMENTS',
|
|
2088
|
+
'USES',
|
|
2089
|
+
'METHOD_OVERRIDES',
|
|
2090
|
+
'OVERRIDES',
|
|
2091
|
+
'METHOD_IMPLEMENTS',
|
|
2092
|
+
];
|
|
2093
|
+
const relationTypes = rawRelTypes.length > 0
|
|
2094
|
+
? rawRelTypes
|
|
2095
|
+
: [
|
|
2096
|
+
'CALLS',
|
|
2097
|
+
'IMPORTS',
|
|
2098
|
+
'EXTENDS',
|
|
2099
|
+
'IMPLEMENTS',
|
|
2100
|
+
'USES',
|
|
2101
|
+
'METHOD_OVERRIDES',
|
|
2102
|
+
'OVERRIDES',
|
|
2103
|
+
'METHOD_IMPLEMENTS',
|
|
2104
|
+
];
|
|
2105
|
+
const includeTests = params.includeTests ?? false;
|
|
2106
|
+
const minConfidence = params.minConfidence ?? 0;
|
|
2107
|
+
// Resolve target via the shared symbol resolver. When the caller passes
|
|
2108
|
+
// target_uid we skip the name lookup entirely (zero-ambiguity). Otherwise
|
|
2109
|
+
// we rank candidates (#470) and either proceed with a confident single
|
|
2110
|
+
// match, or return a structured ambiguous response instead of silently
|
|
2111
|
+
// picking the wrong symbol.
|
|
2112
|
+
//
|
|
2113
|
+
// The resolver preserves the #480 Class/Constructor preference heuristic:
|
|
2114
|
+
// when a Class and its Constructor share name + filePath, the Class is
|
|
2115
|
+
// selected silently.
|
|
2116
|
+
const outcome = await this.resolveSymbolCandidates(repo, { uid: params.target_uid, name: target }, { file_path: params.file_path, kind: params.kind });
|
|
2117
|
+
if (outcome.kind === 'not_found') {
|
|
2118
|
+
const missing = params.target_uid ?? target;
|
|
2119
|
+
return {
|
|
2120
|
+
error: `Target '${missing}' not found`,
|
|
2121
|
+
target: { name: target },
|
|
2122
|
+
direction,
|
|
2123
|
+
impactedCount: 0,
|
|
2124
|
+
risk: 'UNKNOWN',
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
if (outcome.kind === 'ambiguous') {
|
|
2128
|
+
return {
|
|
2129
|
+
status: 'ambiguous',
|
|
2130
|
+
message: `Found ${outcome.candidates.length} symbols matching '${target}'. Use target_uid, file_path, or kind to disambiguate.`,
|
|
2131
|
+
target: { name: target },
|
|
2132
|
+
direction,
|
|
2133
|
+
impactedCount: 0,
|
|
2134
|
+
risk: 'UNKNOWN',
|
|
2135
|
+
candidates: outcome.candidates.map((c) => ({
|
|
2136
|
+
uid: c.id,
|
|
2137
|
+
name: c.name,
|
|
2138
|
+
kind: c.type,
|
|
2139
|
+
filePath: c.filePath,
|
|
2140
|
+
line: c.startLine,
|
|
2141
|
+
score: Number(c.score.toFixed(2)),
|
|
2142
|
+
})),
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
const sym = {
|
|
2146
|
+
id: outcome.symbol.id,
|
|
2147
|
+
name: outcome.symbol.name,
|
|
2148
|
+
filePath: outcome.symbol.filePath,
|
|
2149
|
+
};
|
|
2150
|
+
const symType = outcome.resolvedLabel || outcome.symbol.type || '';
|
|
2151
|
+
const effectiveRelationTypes = (symType === 'Class' || symType === 'Interface') &&
|
|
2152
|
+
!hasExplicitRelationTypes &&
|
|
2153
|
+
!relationTypes.includes('ACCESSES')
|
|
2154
|
+
? [...relationTypes, 'ACCESSES']
|
|
2155
|
+
: relationTypes;
|
|
2156
|
+
return this._runImpactBFS(repo, sym, symType, direction, {
|
|
2157
|
+
maxDepth,
|
|
2158
|
+
relationTypes: effectiveRelationTypes,
|
|
2159
|
+
includeTests,
|
|
2160
|
+
minConfidence,
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Shared BFS traversal for impact analysis (name-resolved or UID-resolved symbol).
|
|
2165
|
+
*/
|
|
2166
|
+
async _runImpactBFS(repo, sym, symType, direction, opts) {
|
|
2167
|
+
const { maxDepth, relationTypes, includeTests, minConfidence } = opts;
|
|
2168
|
+
const relTypeFilter = relationTypes.map((t) => `'${t}'`).join(', ');
|
|
2169
|
+
const confidenceFilter = minConfidence > 0 ? ` AND r.confidence >= ${minConfidence}` : '';
|
|
2170
|
+
const symId = sym.id || sym[0];
|
|
2171
|
+
const impacted = [];
|
|
2172
|
+
const visited = new Set([symId]);
|
|
2173
|
+
let frontier = [symId];
|
|
2174
|
+
let traversalComplete = true;
|
|
2175
|
+
// Fix #480: For Java (and other JVM) Class/Interface nodes, CALLS edges
|
|
2176
|
+
// point to Constructor nodes and IMPORTS edges point to File nodes — not
|
|
2177
|
+
// the Class/Interface itself. Seed the frontier with the Constructor(s)
|
|
2178
|
+
// and owning File so the BFS traversal finds those edges naturally.
|
|
2179
|
+
// The owning File is kept only as an internal seed (frontier/visited) and
|
|
2180
|
+
// is NOT added to impacted — it is the definition container, not an
|
|
2181
|
+
// upstream dependent. The BFS will discover IMPORTS edges on it naturally.
|
|
2182
|
+
if (symType === 'Class' || symType === 'Interface') {
|
|
2183
|
+
try {
|
|
2184
|
+
// Run both seed queries in parallel — they are independent.
|
|
2185
|
+
const [ctorRows, fileRows] = await Promise.all([
|
|
2186
|
+
executeParameterized(repo.id, `
|
|
2187
|
+
MATCH (n)-[hm:CodeRelation]->(c:Constructor)
|
|
2188
|
+
WHERE n.id = $symId AND hm.type = 'HAS_METHOD'
|
|
2189
|
+
RETURN c.id AS id, c.name AS name, labels(c)[0] AS type, c.filePath AS filePath
|
|
2190
|
+
`, { symId }),
|
|
2191
|
+
// Restrict to DEFINES edges only — other File->Class edge types (if
|
|
2192
|
+
// any) should not be treated as the owning file relationship.
|
|
2193
|
+
executeParameterized(repo.id, `
|
|
2194
|
+
MATCH (f:File)-[rel:CodeRelation]->(n)
|
|
2195
|
+
WHERE n.id = $symId AND rel.type = 'DEFINES'
|
|
2196
|
+
RETURN f.id AS id, f.name AS name, labels(f)[0] AS type, f.filePath AS filePath
|
|
2197
|
+
`, { symId }),
|
|
2198
|
+
]);
|
|
2199
|
+
for (const r of ctorRows) {
|
|
2200
|
+
const rid = r.id || r[0];
|
|
2201
|
+
if (rid && !visited.has(rid)) {
|
|
2202
|
+
visited.add(rid);
|
|
2203
|
+
frontier.push(rid);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
for (const r of fileRows) {
|
|
2207
|
+
const rid = r.id || r[0];
|
|
2208
|
+
if (rid && !visited.has(rid)) {
|
|
2209
|
+
visited.add(rid);
|
|
2210
|
+
frontier.push(rid);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
const typedPropertyRows = await executeParameterized(repo.id, `
|
|
2214
|
+
MATCH (p:\`Property\`)
|
|
2215
|
+
WHERE p.declaredType = $name
|
|
2216
|
+
OR p.declaredType STARTS WITH $genericPrefix
|
|
2217
|
+
OR p.declaredType CONTAINS $genericArg
|
|
2218
|
+
RETURN p.id AS id, p.name AS name, labels(p)[0] AS type, p.filePath AS filePath
|
|
2219
|
+
`, {
|
|
2220
|
+
name: sym.name,
|
|
2221
|
+
genericPrefix: `${sym.name}<`,
|
|
2222
|
+
genericArg: `<${sym.name}>`,
|
|
2223
|
+
});
|
|
2224
|
+
for (const r of typedPropertyRows) {
|
|
2225
|
+
const rid = r.id || r[0];
|
|
2226
|
+
if (rid && !visited.has(rid)) {
|
|
2227
|
+
visited.add(rid);
|
|
2228
|
+
frontier.push(rid);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
catch (e) {
|
|
2233
|
+
logQueryError('impact:class-node-expansion', e);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
|
|
2237
|
+
const nextFrontier = [];
|
|
2238
|
+
// Batch frontier nodes into a single Cypher query per depth level
|
|
2239
|
+
const idList = frontier.map((id) => `'${id.replace(/'/g, "''")}'`).join(', ');
|
|
2240
|
+
const query = direction === 'upstream'
|
|
2241
|
+
? `MATCH (caller)-[r:CodeRelation]->(n) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, caller.id AS id, caller.name AS name, labels(caller)[0] AS type, caller.filePath AS filePath, r.type AS relType, r.confidence AS confidence`
|
|
2242
|
+
: `MATCH (n)-[r:CodeRelation]->(callee) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, callee.id AS id, callee.name AS name, labels(callee)[0] AS type, callee.filePath AS filePath, r.type AS relType, r.confidence AS confidence`;
|
|
2243
|
+
try {
|
|
2244
|
+
const related = await executeQuery(repo.id, query);
|
|
2245
|
+
for (const rel of related) {
|
|
2246
|
+
const relId = rel.id || rel[1];
|
|
2247
|
+
const filePath = rel.filePath || rel[4] || '';
|
|
2248
|
+
if (!includeTests && isTestFilePath(filePath))
|
|
2249
|
+
continue;
|
|
2250
|
+
if (!visited.has(relId)) {
|
|
2251
|
+
visited.add(relId);
|
|
2252
|
+
nextFrontier.push(relId);
|
|
2253
|
+
const storedConfidence = rel.confidence ?? rel[6];
|
|
2254
|
+
const relationType = rel.relType || rel[5];
|
|
2255
|
+
// Prefer the stored confidence from the graph (set at analysis time);
|
|
2256
|
+
// fall back to the per-type floor for edges without a stored value.
|
|
2257
|
+
const effectiveConfidence = typeof storedConfidence === 'number' && storedConfidence > 0
|
|
2258
|
+
? storedConfidence
|
|
2259
|
+
: confidenceForRelType(relationType);
|
|
2260
|
+
impacted.push({
|
|
2261
|
+
depth,
|
|
2262
|
+
id: relId,
|
|
2263
|
+
name: rel.name || rel[2],
|
|
2264
|
+
type: rel.type || rel[3],
|
|
2265
|
+
filePath,
|
|
2266
|
+
relationType,
|
|
2267
|
+
confidence: effectiveConfidence,
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
catch (e) {
|
|
2273
|
+
logQueryError('impact:depth-traversal', e);
|
|
2274
|
+
// Break out of depth loop on query failure but return partial results
|
|
2275
|
+
// collected so far, rather than silently swallowing the error (#321)
|
|
2276
|
+
traversalComplete = false;
|
|
2277
|
+
break;
|
|
2278
|
+
}
|
|
2279
|
+
frontier = nextFrontier;
|
|
2280
|
+
}
|
|
2281
|
+
const grouped = {};
|
|
2282
|
+
for (const item of impacted) {
|
|
2283
|
+
if (!grouped[item.depth])
|
|
2284
|
+
grouped[item.depth] = [];
|
|
2285
|
+
grouped[item.depth].push(item);
|
|
2286
|
+
}
|
|
2287
|
+
// ── Enrichment: affected processes, modules, risk ──────────────
|
|
2288
|
+
const directCount = (grouped[1] || []).length;
|
|
2289
|
+
let affectedProcesses = [];
|
|
2290
|
+
let affectedModules = [];
|
|
2291
|
+
if (impacted.length > 0) {
|
|
2292
|
+
const CHUNK_SIZE = 100;
|
|
2293
|
+
// Max number of chunks to process to avoid unbounded DB round-trips.
|
|
2294
|
+
// Configurable via env IMPACT_MAX_CHUNKS, default 10 => max items = 1000
|
|
2295
|
+
const MAX_CHUNKS = parseInt(process.env.IMPACT_MAX_CHUNKS || '10', 10);
|
|
2296
|
+
// ── Process enrichment: batched chunking (bounded by MAX_CHUNKS) ─
|
|
2297
|
+
// Uses merged Cypher query (WITH + OPTIONAL MATCH) to fetch
|
|
2298
|
+
// process + entry point info in 1 round-trip per chunk. Converted to
|
|
2299
|
+
// parameterized queries to avoid manual string escaping and long query strings.
|
|
2300
|
+
const entryPointMap = new Map();
|
|
2301
|
+
// Map process id -> entryPointId to allow fixing missing minStep values later
|
|
2302
|
+
const processToEntryPoint = new Map();
|
|
2303
|
+
// Collect process ids where MIN(r.step) returned null so we can retry in batch
|
|
2304
|
+
const processesMissingMinStep = new Set();
|
|
2305
|
+
let chunksProcessed = 0;
|
|
2306
|
+
for (let i = 0; i < impacted.length && chunksProcessed < MAX_CHUNKS; i += CHUNK_SIZE, chunksProcessed++) {
|
|
2307
|
+
const chunk = impacted.slice(i, i + CHUNK_SIZE);
|
|
2308
|
+
const ids = chunk.map((item) => String(item.id ?? ''));
|
|
2309
|
+
try {
|
|
2310
|
+
// Use parameterized list to avoid building long query strings
|
|
2311
|
+
const rows = await executeParameterized(repo.id, `
|
|
2312
|
+
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
2313
|
+
WHERE s.id IN $ids
|
|
2314
|
+
WITH p, COUNT(DISTINCT s.id) AS hits, MIN(r.step) AS minStep
|
|
2315
|
+
OPTIONAL MATCH (ep {id: p.entryPointId})
|
|
2316
|
+
RETURN p.id AS pId, p.heuristicLabel AS name, p.processType AS processType,
|
|
2317
|
+
p.entryPointId AS entryPointId, hits, minStep, p.stepCount AS stepCount,
|
|
2318
|
+
ep.name AS epName, labels(ep)[0] AS epType, ep.filePath AS epFilePath
|
|
2319
|
+
`, { ids }).catch(() => []);
|
|
2320
|
+
for (const row of rows) {
|
|
2321
|
+
const pId = row.pId ?? row[0];
|
|
2322
|
+
const epId = row.entryPointId ?? row[3] ?? row.pId ?? row[0];
|
|
2323
|
+
// Track mapping from process -> entryPoint so we can backfill missing minStep
|
|
2324
|
+
if (pId)
|
|
2325
|
+
processToEntryPoint.set(String(pId), String(epId));
|
|
2326
|
+
// Normalize epName: prefer epName, fall back to other columns, and
|
|
2327
|
+
// ensure we don't keep an empty string (labels(...) can return "").
|
|
2328
|
+
const epNameRaw = row.epName ?? row[7] ?? row.name ?? row[1] ?? 'unknown';
|
|
2329
|
+
const epName = typeof epNameRaw === 'string' && epNameRaw.trim().length > 0
|
|
2330
|
+
? epNameRaw.trim()
|
|
2331
|
+
: 'unknown';
|
|
2332
|
+
// Normalize epType: labels(ep)[0] can return an empty string in
|
|
2333
|
+
// some DBs (LadybugDB). Using nullish coalescing (??) preserves
|
|
2334
|
+
// empty strings, which results in empty `type` values being
|
|
2335
|
+
// propagated. Treat empty-string labels as missing and fall back
|
|
2336
|
+
// to the next candidate or a sensible default.
|
|
2337
|
+
const epTypeRaw = row.epType ?? row[8] ?? '';
|
|
2338
|
+
const epType = typeof epTypeRaw === 'string' && epTypeRaw.trim().length > 0
|
|
2339
|
+
? epTypeRaw.trim()
|
|
2340
|
+
: 'Function';
|
|
2341
|
+
const epFilePath = row.epFilePath ?? row[9] ?? '';
|
|
2342
|
+
const hits = row.hits ?? row[4] ?? 0;
|
|
2343
|
+
const minStep = row.minStep ?? row[5];
|
|
2344
|
+
// If the DB returned null for minStep, note the process id so we
|
|
2345
|
+
// can run a follow-up query using a different aggregation strategy.
|
|
2346
|
+
if (minStep === null || minStep === undefined) {
|
|
2347
|
+
if (pId)
|
|
2348
|
+
processesMissingMinStep.add(String(pId));
|
|
2349
|
+
}
|
|
2350
|
+
if (!entryPointMap.has(epId)) {
|
|
2351
|
+
entryPointMap.set(epId, {
|
|
2352
|
+
name: epName,
|
|
2353
|
+
type: epType,
|
|
2354
|
+
filePath: epFilePath,
|
|
2355
|
+
affected_process_count: 0,
|
|
2356
|
+
total_hits: 0,
|
|
2357
|
+
earliest_broken_step: Infinity,
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
const ep = entryPointMap.get(epId);
|
|
2361
|
+
ep.affected_process_count += 1;
|
|
2362
|
+
ep.total_hits += hits;
|
|
2363
|
+
ep.earliest_broken_step = Math.min(ep.earliest_broken_step, minStep ?? Infinity);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
catch (e) {
|
|
2367
|
+
logQueryError('impact:process-chunk', e);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
// If some processes returned null minStep, try a batched follow-up query
|
|
2371
|
+
// using the full impacted id set. This handles older indexes or DBs
|
|
2372
|
+
// where MIN(r.step) can come back null even when step properties exist.
|
|
2373
|
+
if (processesMissingMinStep.size > 0) {
|
|
2374
|
+
try {
|
|
2375
|
+
const pIds = Array.from(processesMissingMinStep);
|
|
2376
|
+
const allImpactedIds = impacted.map((it) => String(it.id ?? ''));
|
|
2377
|
+
const missingRows = await executeParameterized(repo.id, `
|
|
2378
|
+
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
2379
|
+
WHERE p.id IN $pIds AND s.id IN $ids
|
|
2380
|
+
RETURN p.id AS pid, MIN(r.step) AS minStep
|
|
2381
|
+
`, { pIds, ids: allImpactedIds }).catch(() => []);
|
|
2382
|
+
for (const mr of missingRows) {
|
|
2383
|
+
const pid = mr.pid ?? mr[0];
|
|
2384
|
+
const minStep = mr.minStep ?? mr[1];
|
|
2385
|
+
const epId = processToEntryPoint.get(String(pid));
|
|
2386
|
+
if (!epId)
|
|
2387
|
+
continue;
|
|
2388
|
+
const ep = entryPointMap.get(epId);
|
|
2389
|
+
if (!ep)
|
|
2390
|
+
continue;
|
|
2391
|
+
if (typeof minStep === 'number') {
|
|
2392
|
+
ep.earliest_broken_step = Math.min(ep.earliest_broken_step, minStep);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
catch (e) {
|
|
2397
|
+
logQueryError('impact:process-chunk-backfill', e);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
// If we capped chunks, mark traversal incomplete so caller knows results are partial
|
|
2401
|
+
if (chunksProcessed * CHUNK_SIZE < impacted.length) {
|
|
2402
|
+
traversalComplete = false;
|
|
2403
|
+
}
|
|
2404
|
+
affectedProcesses = Array.from(entryPointMap.values())
|
|
2405
|
+
.map((ep) => ({
|
|
2406
|
+
...ep,
|
|
2407
|
+
earliest_broken_step: ep.earliest_broken_step === Infinity ? null : ep.earliest_broken_step,
|
|
2408
|
+
}))
|
|
2409
|
+
.sort((a, b) => b.total_hits - a.total_hits);
|
|
2410
|
+
// ── Module enrichment: use same cap as process enrichment and parameterized queries
|
|
2411
|
+
const maxItems = Math.min(impacted.length, MAX_CHUNKS * CHUNK_SIZE);
|
|
2412
|
+
const cappedImpacted = impacted.slice(0, maxItems);
|
|
2413
|
+
const allIdsArr = cappedImpacted.map((i) => String(i.id ?? ''));
|
|
2414
|
+
const d1Items = (grouped[1] || []).slice(0, maxItems);
|
|
2415
|
+
const d1IdsArr = d1Items.map((i) => String(i.id ?? ''));
|
|
2416
|
+
// Chunked module enrichment: run the MEMBER_OF queries in chunks
|
|
2417
|
+
// to avoid large single queries or concurrent Kuzu calls that can
|
|
2418
|
+
// crash (SIGSEGV) on arm64 macOS; behavior preserves existing maxItems cap and returns equivalent aggregated results.
|
|
2419
|
+
const moduleHitsMap = new Map();
|
|
2420
|
+
const directModuleSet = new Set();
|
|
2421
|
+
// Helper to run a single module chunk and accumulate hits by name
|
|
2422
|
+
const runModuleChunk = async (idsChunk) => {
|
|
2423
|
+
if (!idsChunk || idsChunk.length === 0)
|
|
2424
|
+
return;
|
|
2425
|
+
try {
|
|
2426
|
+
const rows = await executeParameterized(repo.id, `
|
|
2427
|
+
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
2428
|
+
WHERE s.id IN $ids
|
|
2429
|
+
RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits
|
|
2430
|
+
ORDER BY hits DESC
|
|
2431
|
+
LIMIT 20
|
|
2432
|
+
`, { ids: idsChunk }).catch(() => []);
|
|
2433
|
+
for (const r of rows) {
|
|
2434
|
+
const name = r.name ?? r[0] ?? null;
|
|
2435
|
+
const hits = (r.hits ?? r[1]) || 0;
|
|
2436
|
+
if (!name)
|
|
2437
|
+
continue;
|
|
2438
|
+
moduleHitsMap.set(name, (moduleHitsMap.get(name) || 0) + hits);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
catch (e) {
|
|
2442
|
+
logQueryError('impact:module-chunk', e);
|
|
2443
|
+
}
|
|
2444
|
+
};
|
|
2445
|
+
// Run module query chunks sequentially (safe on arm64 macOS)
|
|
2446
|
+
for (let i = 0; i < allIdsArr.length; i += CHUNK_SIZE) {
|
|
2447
|
+
const chunkIds = allIdsArr.slice(i, i + CHUNK_SIZE);
|
|
2448
|
+
await runModuleChunk(chunkIds);
|
|
2449
|
+
}
|
|
2450
|
+
// Run direct module query similarly (distinct heuristic labels for depth-1 items)
|
|
2451
|
+
const runDirectModuleChunk = async (idsChunk) => {
|
|
2452
|
+
if (!idsChunk || idsChunk.length === 0)
|
|
2453
|
+
return;
|
|
2454
|
+
try {
|
|
2455
|
+
const rows = await executeParameterized(repo.id, `
|
|
2456
|
+
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
2457
|
+
WHERE s.id IN $ids
|
|
2458
|
+
RETURN DISTINCT c.heuristicLabel AS name
|
|
2459
|
+
`, { ids: idsChunk }).catch(() => []);
|
|
2460
|
+
for (const r of rows) {
|
|
2461
|
+
const name = r.name ?? r[0] ?? null;
|
|
2462
|
+
if (name)
|
|
2463
|
+
directModuleSet.add(name);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
catch (e) {
|
|
2467
|
+
logQueryError('impact:direct-module-chunk', e);
|
|
2468
|
+
}
|
|
2469
|
+
};
|
|
2470
|
+
for (let i = 0; i < d1IdsArr.length; i += CHUNK_SIZE) {
|
|
2471
|
+
const chunkIds = d1IdsArr.slice(i, i + CHUNK_SIZE);
|
|
2472
|
+
await runDirectModuleChunk(chunkIds);
|
|
2473
|
+
}
|
|
2474
|
+
// Build final moduleRows array from aggregated hits map, sorted & limited
|
|
2475
|
+
const moduleRows = Array.from(moduleHitsMap.entries())
|
|
2476
|
+
.map(([name, hits]) => ({ name, hits }))
|
|
2477
|
+
.sort((a, b) => b.hits - a.hits)
|
|
2478
|
+
.slice(0, 20);
|
|
2479
|
+
const directModuleRows = Array.from(directModuleSet).map((name) => ({ name }));
|
|
2480
|
+
// Build affectedModules in the same shape as original implementation
|
|
2481
|
+
const directModuleNameSet = new Set(directModuleRows.map((r) => r.name || r[0]));
|
|
2482
|
+
affectedModules = moduleRows.map((r) => {
|
|
2483
|
+
const name = r.name ?? r[0];
|
|
2484
|
+
const hits = r.hits ?? r[1] ?? 0;
|
|
2485
|
+
return {
|
|
2486
|
+
name,
|
|
2487
|
+
hits,
|
|
2488
|
+
impact: directModuleNameSet.has(name) ? 'direct' : 'indirect',
|
|
2489
|
+
};
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
// Risk scoring
|
|
2493
|
+
const processCount = affectedProcesses.length;
|
|
2494
|
+
const moduleCount = affectedModules.length;
|
|
2495
|
+
let risk = 'LOW';
|
|
2496
|
+
if (directCount >= 30 || processCount >= 5 || moduleCount >= 5 || impacted.length >= 200) {
|
|
2497
|
+
risk = 'CRITICAL';
|
|
2498
|
+
}
|
|
2499
|
+
else if (directCount >= 15 ||
|
|
2500
|
+
processCount >= 3 ||
|
|
2501
|
+
moduleCount >= 3 ||
|
|
2502
|
+
impacted.length >= 100) {
|
|
2503
|
+
risk = 'HIGH';
|
|
2504
|
+
}
|
|
2505
|
+
else if (directCount >= 5 || impacted.length >= 30) {
|
|
2506
|
+
risk = 'MEDIUM';
|
|
2507
|
+
}
|
|
2508
|
+
return {
|
|
2509
|
+
target: {
|
|
2510
|
+
id: symId,
|
|
2511
|
+
name: sym.name || sym[1],
|
|
2512
|
+
type: symType,
|
|
2513
|
+
filePath: sym.filePath || sym[2],
|
|
2514
|
+
},
|
|
2515
|
+
direction,
|
|
2516
|
+
impactedCount: impacted.length,
|
|
2517
|
+
risk,
|
|
2518
|
+
...(!traversalComplete && { partial: true }),
|
|
2519
|
+
summary: {
|
|
2520
|
+
direct: directCount,
|
|
2521
|
+
processes_affected: processCount,
|
|
2522
|
+
modules_affected: moduleCount,
|
|
2523
|
+
},
|
|
2524
|
+
affected_processes: affectedProcesses,
|
|
2525
|
+
affected_modules: affectedModules,
|
|
2526
|
+
byDepth: grouped,
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* UID-based impact for cross-repo fan-out. Same result shape as `impact`.
|
|
2531
|
+
* Returns null if the repo is unknown, the UID is missing, or analysis fails.
|
|
2532
|
+
*/
|
|
2533
|
+
async impactByUid(repoId, uid, direction, opts) {
|
|
2534
|
+
// Honor an already-aborted signal at the entry boundary as a fast
|
|
2535
|
+
// path. Cooperative cancellation inside _runImpactBFS is out of
|
|
2536
|
+
// scope — the caller's Promise.race against the same signal
|
|
2537
|
+
// resolves the await regardless of how long this body runs.
|
|
2538
|
+
if (opts.signal?.aborted)
|
|
2539
|
+
return null;
|
|
2540
|
+
try {
|
|
2541
|
+
await this.refreshRepos();
|
|
2542
|
+
await this.ensureInitialized(repoId);
|
|
2543
|
+
}
|
|
2544
|
+
catch {
|
|
2545
|
+
return null;
|
|
2546
|
+
}
|
|
2547
|
+
const repo = this.repos.get(repoId);
|
|
2548
|
+
if (!repo)
|
|
2549
|
+
return null;
|
|
2550
|
+
const dir = direction === 'downstream' ? 'downstream' : 'upstream';
|
|
2551
|
+
let rows;
|
|
2552
|
+
try {
|
|
2553
|
+
rows = await executeParameterized(repoId, `MATCH (n) WHERE n.id = $uid
|
|
2554
|
+
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, labels(n)[0] AS type
|
|
2555
|
+
LIMIT 1`, { uid });
|
|
2556
|
+
}
|
|
2557
|
+
catch {
|
|
2558
|
+
return null;
|
|
2559
|
+
}
|
|
2560
|
+
if (!rows?.length)
|
|
2561
|
+
return null;
|
|
2562
|
+
const sym = rows[0];
|
|
2563
|
+
const labelRaw = sym.type ?? sym[3];
|
|
2564
|
+
const symType = typeof labelRaw === 'string' && labelRaw.trim().length > 0 ? labelRaw.trim() : '';
|
|
2565
|
+
// Map legacy relation type names (backward compat for OVERRIDES → METHOD_OVERRIDES)
|
|
2566
|
+
const mappedRelTypes = opts.relationTypes?.flatMap((t) => t === 'OVERRIDES' ? ['OVERRIDES', 'METHOD_OVERRIDES'] : [t]);
|
|
2567
|
+
const rawRelTypes = mappedRelTypes && mappedRelTypes.length > 0
|
|
2568
|
+
? mappedRelTypes.filter((t) => VALID_RELATION_TYPES.has(t))
|
|
2569
|
+
: [
|
|
2570
|
+
'CALLS',
|
|
2571
|
+
'IMPORTS',
|
|
2572
|
+
'EXTENDS',
|
|
2573
|
+
'IMPLEMENTS',
|
|
2574
|
+
'METHOD_OVERRIDES',
|
|
2575
|
+
'OVERRIDES',
|
|
2576
|
+
'METHOD_IMPLEMENTS',
|
|
2577
|
+
];
|
|
2578
|
+
const relationTypes = rawRelTypes.length > 0
|
|
2579
|
+
? rawRelTypes
|
|
2580
|
+
: [
|
|
2581
|
+
'CALLS',
|
|
2582
|
+
'IMPORTS',
|
|
2583
|
+
'EXTENDS',
|
|
2584
|
+
'IMPLEMENTS',
|
|
2585
|
+
'METHOD_OVERRIDES',
|
|
2586
|
+
'OVERRIDES',
|
|
2587
|
+
'METHOD_IMPLEMENTS',
|
|
2588
|
+
];
|
|
2589
|
+
try {
|
|
2590
|
+
return await this._runImpactBFS(repo, sym, symType, dir, {
|
|
2591
|
+
maxDepth: opts.maxDepth,
|
|
2592
|
+
relationTypes,
|
|
2593
|
+
includeTests: opts.includeTests,
|
|
2594
|
+
minConfidence: opts.minConfidence,
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
catch {
|
|
2598
|
+
return null;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
handleGroupTool(method, params) {
|
|
2602
|
+
switch (method) {
|
|
2603
|
+
case 'group_list':
|
|
2604
|
+
return this.groupList(params);
|
|
2605
|
+
case 'group_sync':
|
|
2606
|
+
return this.groupSync(params);
|
|
2607
|
+
default:
|
|
2608
|
+
throw new Error(`Unknown group tool: ${method}. Removed tools: use repo "@<groupName>" on impact, query, or context (optional "/<memberPath>"), or MCP resources.`);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Dispatch impact/query/context when `repo` is `@groupName` or `@groupName/memberPath`
|
|
2613
|
+
* (group mode — not the global indexed-repo `repo` parameter).
|
|
2614
|
+
*/
|
|
2615
|
+
async callToolAtGroupRepo(method, params) {
|
|
2616
|
+
await this.refreshRepos();
|
|
2617
|
+
if (params.service !== undefined &&
|
|
2618
|
+
params.service !== null &&
|
|
2619
|
+
String(params.service).trim() === '') {
|
|
2620
|
+
return { error: 'service must not be an empty string' };
|
|
2621
|
+
}
|
|
2622
|
+
const raw = String(params.repo).slice(1);
|
|
2623
|
+
const slash = raw.indexOf('/');
|
|
2624
|
+
const groupName = (slash === -1 ? raw : raw.slice(0, slash)).trim();
|
|
2625
|
+
const memberRest = slash === -1 ? undefined : raw.slice(slash + 1).trim() || undefined;
|
|
2626
|
+
const resolved = await resolveAtGroupMemberRepoPath(groupName, memberRest);
|
|
2627
|
+
if (resolved.ok === false)
|
|
2628
|
+
return { error: resolved.error };
|
|
2629
|
+
const svc = this.getGroupService();
|
|
2630
|
+
if (method === 'impact') {
|
|
2631
|
+
const impactArgs = {
|
|
2632
|
+
name: groupName,
|
|
2633
|
+
repo: resolved.repoPath,
|
|
2634
|
+
target: params.target,
|
|
2635
|
+
direction: params.direction,
|
|
2636
|
+
};
|
|
2637
|
+
if (params.maxDepth !== undefined)
|
|
2638
|
+
impactArgs.maxDepth = params.maxDepth;
|
|
2639
|
+
if (params.crossDepth !== undefined)
|
|
2640
|
+
impactArgs.crossDepth = params.crossDepth;
|
|
2641
|
+
if (params.relationTypes !== undefined)
|
|
2642
|
+
impactArgs.relationTypes = params.relationTypes;
|
|
2643
|
+
if (params.includeTests !== undefined)
|
|
2644
|
+
impactArgs.includeTests = params.includeTests;
|
|
2645
|
+
if (params.minConfidence !== undefined)
|
|
2646
|
+
impactArgs.minConfidence = params.minConfidence;
|
|
2647
|
+
if (params.service !== undefined && params.service !== null)
|
|
2648
|
+
impactArgs.service = params.service;
|
|
2649
|
+
if (typeof params.subgroup === 'string')
|
|
2650
|
+
impactArgs.subgroup = params.subgroup;
|
|
2651
|
+
if (params.timeoutMs !== undefined)
|
|
2652
|
+
impactArgs.timeoutMs = params.timeoutMs;
|
|
2653
|
+
if (params.timeout !== undefined)
|
|
2654
|
+
impactArgs.timeout = params.timeout;
|
|
2655
|
+
return svc.groupImpact(impactArgs);
|
|
2656
|
+
}
|
|
2657
|
+
if (method === 'query') {
|
|
2658
|
+
const queryArgs = {
|
|
2659
|
+
name: groupName,
|
|
2660
|
+
query: params.query,
|
|
2661
|
+
};
|
|
2662
|
+
if (typeof params.task_context === 'string')
|
|
2663
|
+
queryArgs.task_context = params.task_context;
|
|
2664
|
+
if (typeof params.goal === 'string')
|
|
2665
|
+
queryArgs.goal = params.goal;
|
|
2666
|
+
if (typeof params.limit === 'number')
|
|
2667
|
+
queryArgs.limit = params.limit;
|
|
2668
|
+
if (typeof params.max_symbols === 'number')
|
|
2669
|
+
queryArgs.max_symbols = params.max_symbols;
|
|
2670
|
+
if (params.include_content !== undefined)
|
|
2671
|
+
queryArgs.include_content = params.include_content;
|
|
2672
|
+
if (params.service !== undefined && params.service !== null)
|
|
2673
|
+
queryArgs.service = params.service;
|
|
2674
|
+
if (memberRest !== undefined) {
|
|
2675
|
+
queryArgs.subgroup = memberRest;
|
|
2676
|
+
queryArgs.subgroupExact = true;
|
|
2677
|
+
}
|
|
2678
|
+
return svc.groupQuery(queryArgs);
|
|
2679
|
+
}
|
|
2680
|
+
if (method === 'context') {
|
|
2681
|
+
const targetSym = typeof params.target === 'string' && params.target.trim() !== ''
|
|
2682
|
+
? params.target.trim()
|
|
2683
|
+
: typeof params.name === 'string' && params.name.trim() !== ''
|
|
2684
|
+
? params.name.trim()
|
|
2685
|
+
: undefined;
|
|
2686
|
+
const contextArgs = {
|
|
2687
|
+
name: groupName,
|
|
2688
|
+
target: targetSym,
|
|
2689
|
+
};
|
|
2690
|
+
if (typeof params.uid === 'string')
|
|
2691
|
+
contextArgs.uid = params.uid;
|
|
2692
|
+
if (typeof params.file_path === 'string')
|
|
2693
|
+
contextArgs.file_path = params.file_path;
|
|
2694
|
+
if (params.include_content !== undefined)
|
|
2695
|
+
contextArgs.include_content = params.include_content;
|
|
2696
|
+
if (params.service !== undefined && params.service !== null)
|
|
2697
|
+
contextArgs.service = params.service;
|
|
2698
|
+
if (memberRest !== undefined) {
|
|
2699
|
+
contextArgs.subgroup = memberRest;
|
|
2700
|
+
contextArgs.subgroupExact = true;
|
|
2701
|
+
}
|
|
2702
|
+
return svc.groupContext(contextArgs);
|
|
2703
|
+
}
|
|
2704
|
+
throw new Error(`Internal: unsupported group-repo tool ${method}`);
|
|
2705
|
+
}
|
|
2706
|
+
async groupList(params) {
|
|
2707
|
+
return this.getGroupService().groupList(params);
|
|
2708
|
+
}
|
|
2709
|
+
async groupSync(params) {
|
|
2710
|
+
return this.getGroupService().groupSync(params);
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* MCP resource body for `arc://group/{name}/contracts` (Issue #794).
|
|
2714
|
+
*/
|
|
2715
|
+
async readGroupContractsResource(groupName, filter) {
|
|
2716
|
+
try {
|
|
2717
|
+
const params = { name: groupName };
|
|
2718
|
+
if (filter.type !== undefined)
|
|
2719
|
+
params.type = filter.type;
|
|
2720
|
+
if (filter.repo !== undefined)
|
|
2721
|
+
params.repo = filter.repo;
|
|
2722
|
+
if (filter.unmatchedOnly === true)
|
|
2723
|
+
params.unmatchedOnly = true;
|
|
2724
|
+
const raw = await this.getGroupService().groupContracts(params);
|
|
2725
|
+
return LocalBackend.formatGroupResourcePayload(raw);
|
|
2726
|
+
}
|
|
2727
|
+
catch (e) {
|
|
2728
|
+
return `error: ${e instanceof Error ? e.message : String(e)}`;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
/**
|
|
2732
|
+
* MCP resource body for `arc://group/{name}/status` (Issue #794).
|
|
2733
|
+
*/
|
|
2734
|
+
async readGroupStatusResource(groupName) {
|
|
2735
|
+
try {
|
|
2736
|
+
const raw = await this.getGroupService().groupStatus({ name: groupName });
|
|
2737
|
+
return LocalBackend.formatGroupResourcePayload(raw);
|
|
2738
|
+
}
|
|
2739
|
+
catch (e) {
|
|
2740
|
+
return `error: ${e instanceof Error ? e.message : String(e)}`;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
static formatGroupResourcePayload(raw) {
|
|
2744
|
+
if (raw && typeof raw === 'object' && 'error' in raw) {
|
|
2745
|
+
const err = raw.error;
|
|
2746
|
+
if (typeof err === 'string' && err.length > 0) {
|
|
2747
|
+
return `error: ${err}`;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
return JSON.stringify(raw, null, 2);
|
|
2751
|
+
}
|
|
2752
|
+
/**
|
|
2753
|
+
* Fetch Route nodes with their consumers in a single query.
|
|
2754
|
+
* Shared by routeMap and shapeCheck to avoid N+1 query patterns.
|
|
2755
|
+
*/
|
|
2756
|
+
async fetchRoutesWithConsumers(repoId, routeFilter, params) {
|
|
2757
|
+
const rows = await executeParameterized(repoId, `
|
|
2758
|
+
MATCH (n:Route)
|
|
2759
|
+
WHERE n.id STARTS WITH 'Route:' ${routeFilter}
|
|
2760
|
+
OPTIONAL MATCH (consumer)-[r:CodeRelation]->(n)
|
|
2761
|
+
WHERE r.type = 'FETCHES'
|
|
2762
|
+
RETURN n.id AS routeId, n.name AS routeName, n.filePath AS handlerFile,
|
|
2763
|
+
n.responseKeys AS responseKeys, n.errorKeys AS errorKeys, n.middleware AS middleware,
|
|
2764
|
+
consumer.name AS consumerName, consumer.filePath AS consumerFile,
|
|
2765
|
+
r.reason AS fetchReason
|
|
2766
|
+
`, params);
|
|
2767
|
+
// Strip wrapping quotes from DB array elements — CSV COPY stores ['key'] which
|
|
2768
|
+
// LadybugDB may return as "'key'" rather than "key"
|
|
2769
|
+
const stripQuotes = (keys) => keys ? keys.map((k) => k.replace(/^['"]|['"]$/g, '')) : null;
|
|
2770
|
+
const routeMap = new Map();
|
|
2771
|
+
for (const row of rows) {
|
|
2772
|
+
const id = row.routeId ?? row[0];
|
|
2773
|
+
const name = row.routeName ?? row[1];
|
|
2774
|
+
const filePath = row.handlerFile ?? row[2];
|
|
2775
|
+
const responseKeys = stripQuotes(row.responseKeys ?? row[3] ?? null);
|
|
2776
|
+
const errorKeys = stripQuotes(row.errorKeys ?? row[4] ?? null);
|
|
2777
|
+
const middleware = stripQuotes(row.middleware ?? row[5] ?? null);
|
|
2778
|
+
const consumerName = row.consumerName ?? row[6];
|
|
2779
|
+
const consumerFile = row.consumerFile ?? row[7];
|
|
2780
|
+
const fetchReason = row.fetchReason ?? row[8] ?? null;
|
|
2781
|
+
if (!routeMap.has(id)) {
|
|
2782
|
+
routeMap.set(id, {
|
|
2783
|
+
id,
|
|
2784
|
+
name,
|
|
2785
|
+
filePath,
|
|
2786
|
+
responseKeys,
|
|
2787
|
+
errorKeys,
|
|
2788
|
+
middleware,
|
|
2789
|
+
consumers: [],
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
if (consumerName && consumerFile) {
|
|
2793
|
+
// Parse accessed keys from reason field: "fetch-url-match|keys:data,pagination|fetches:3"
|
|
2794
|
+
let accessedKeys;
|
|
2795
|
+
let fetchCount;
|
|
2796
|
+
if (fetchReason) {
|
|
2797
|
+
const keysMatch = fetchReason.match(/\|keys:([^|]+)/);
|
|
2798
|
+
if (keysMatch) {
|
|
2799
|
+
accessedKeys = keysMatch[1].split(',').filter((k) => k.length > 0);
|
|
2800
|
+
}
|
|
2801
|
+
const fetchesMatch = fetchReason.match(/\|fetches:(\d+)/);
|
|
2802
|
+
if (fetchesMatch) {
|
|
2803
|
+
fetchCount = parseInt(fetchesMatch[1], 10);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
routeMap.get(id).consumers.push({
|
|
2807
|
+
name: consumerName,
|
|
2808
|
+
filePath: consumerFile,
|
|
2809
|
+
...(accessedKeys ? { accessedKeys } : {}),
|
|
2810
|
+
...(fetchCount && fetchCount > 1 ? { fetchCount } : {}),
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
return [...routeMap.values()];
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Batch-fetch execution flows linked to a set of Route or Tool nodes.
|
|
2818
|
+
* Single query instead of N+1.
|
|
2819
|
+
*/
|
|
2820
|
+
async fetchLinkedFlowsBatch(repoId, nodeIds) {
|
|
2821
|
+
const result = new Map();
|
|
2822
|
+
if (nodeIds.length === 0)
|
|
2823
|
+
return result;
|
|
2824
|
+
try {
|
|
2825
|
+
// Use list_contains to filter at DB level instead of fetching all and filtering in memory
|
|
2826
|
+
const rows = await executeParameterized(repoId, `
|
|
2827
|
+
MATCH (source)-[r:CodeRelation]->(proc:Process)
|
|
2828
|
+
WHERE r.type = 'ENTRY_POINT_OF'
|
|
2829
|
+
AND list_contains($nodeIds, source.id)
|
|
2830
|
+
RETURN source.id AS sourceId, proc.label AS name
|
|
2831
|
+
`, { nodeIds });
|
|
2832
|
+
for (const row of rows) {
|
|
2833
|
+
const sourceId = row.sourceId ?? row[0];
|
|
2834
|
+
const name = row.name ?? row[1];
|
|
2835
|
+
if (!name)
|
|
2836
|
+
continue;
|
|
2837
|
+
let list = result.get(sourceId);
|
|
2838
|
+
if (!list) {
|
|
2839
|
+
list = [];
|
|
2840
|
+
result.set(sourceId, list);
|
|
2841
|
+
}
|
|
2842
|
+
list.push(name);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
catch {
|
|
2846
|
+
/* no ENTRY_POINT_OF edges yet */
|
|
2847
|
+
}
|
|
2848
|
+
return result;
|
|
2849
|
+
}
|
|
2850
|
+
async routeMap(repo, params) {
|
|
2851
|
+
await this.ensureInitialized(repo.id);
|
|
2852
|
+
const routeFilter = params.route ? `AND n.name CONTAINS $route` : '';
|
|
2853
|
+
const queryParams = params.route ? { route: params.route } : {};
|
|
2854
|
+
const routes = await this.fetchRoutesWithConsumers(repo.id, routeFilter, queryParams);
|
|
2855
|
+
if (routes.length === 0) {
|
|
2856
|
+
return {
|
|
2857
|
+
routes: [],
|
|
2858
|
+
total: 0,
|
|
2859
|
+
message: params.route
|
|
2860
|
+
? `No routes matching "${params.route}"`
|
|
2861
|
+
: 'No routes found in this project.',
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
const flowMap = await this.fetchLinkedFlowsBatch(repo.id, routes.map((r) => r.id));
|
|
2865
|
+
return {
|
|
2866
|
+
routes: routes.map((r) => ({
|
|
2867
|
+
route: r.name,
|
|
2868
|
+
handler: r.filePath,
|
|
2869
|
+
middleware: r.middleware || [],
|
|
2870
|
+
consumers: r.consumers,
|
|
2871
|
+
flows: flowMap.get(r.id) || [],
|
|
2872
|
+
})),
|
|
2873
|
+
total: routes.length,
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
2876
|
+
async shapeCheck(repo, params) {
|
|
2877
|
+
await this.ensureInitialized(repo.id);
|
|
2878
|
+
const routeFilter = params.route ? `AND n.name CONTAINS $route` : '';
|
|
2879
|
+
const queryParams = params.route ? { route: params.route } : {};
|
|
2880
|
+
const allRoutes = await this.fetchRoutesWithConsumers(repo.id, routeFilter, queryParams);
|
|
2881
|
+
const results = allRoutes
|
|
2882
|
+
.filter((r) => ((r.responseKeys && r.responseKeys.length > 0) ||
|
|
2883
|
+
(r.errorKeys && r.errorKeys.length > 0)) &&
|
|
2884
|
+
r.consumers.length > 0)
|
|
2885
|
+
.map((r) => {
|
|
2886
|
+
// Keys already normalized by fetchRoutesWithConsumers (quotes stripped)
|
|
2887
|
+
const responseKeys = r.responseKeys ?? [];
|
|
2888
|
+
const errorKeys = r.errorKeys ?? [];
|
|
2889
|
+
// Combined set: consumer accessing either success or error keys is valid
|
|
2890
|
+
const allKnownKeys = new Set([...responseKeys, ...errorKeys]);
|
|
2891
|
+
// Check each consumer's accessed keys against the route's response shape
|
|
2892
|
+
const responseKeySet = new Set(responseKeys);
|
|
2893
|
+
const consumers = r.consumers.map((c) => {
|
|
2894
|
+
if (!c.accessedKeys || c.accessedKeys.length === 0) {
|
|
2895
|
+
return { name: c.name, filePath: c.filePath };
|
|
2896
|
+
}
|
|
2897
|
+
const mismatched = c.accessedKeys.filter((k) => !allKnownKeys.has(k));
|
|
2898
|
+
// Keys in allKnownKeys but not in responseKeys — error-path access (e.g., .error from errorKeys)
|
|
2899
|
+
const errorPathKeys = c.accessedKeys.filter((k) => allKnownKeys.has(k) && !responseKeySet.has(k));
|
|
2900
|
+
const isMultiFetch = (c.fetchCount ?? 1) > 1;
|
|
2901
|
+
return {
|
|
2902
|
+
name: c.name,
|
|
2903
|
+
filePath: c.filePath,
|
|
2904
|
+
accessedKeys: c.accessedKeys,
|
|
2905
|
+
...(mismatched.length > 0
|
|
2906
|
+
? {
|
|
2907
|
+
mismatched,
|
|
2908
|
+
mismatchConfidence: isMultiFetch ? 'low' : 'high',
|
|
2909
|
+
}
|
|
2910
|
+
: {}),
|
|
2911
|
+
...(errorPathKeys.length > 0 ? { errorPathKeys } : {}),
|
|
2912
|
+
...(isMultiFetch
|
|
2913
|
+
? {
|
|
2914
|
+
attributionNote: `This file fetches ${c.fetchCount} routes — accessed keys may belong to a different route.`,
|
|
2915
|
+
}
|
|
2916
|
+
: {}),
|
|
2917
|
+
};
|
|
2918
|
+
});
|
|
2919
|
+
const hasMismatches = consumers.some((c) => 'mismatched' in c && c.mismatched.length > 0);
|
|
2920
|
+
return {
|
|
2921
|
+
route: r.name,
|
|
2922
|
+
handler: r.filePath,
|
|
2923
|
+
...(responseKeys.length > 0 ? { responseKeys } : {}),
|
|
2924
|
+
...(errorKeys.length > 0 ? { errorKeys } : {}),
|
|
2925
|
+
consumers,
|
|
2926
|
+
...(hasMismatches ? { status: 'MISMATCH' } : {}),
|
|
2927
|
+
};
|
|
2928
|
+
});
|
|
2929
|
+
const mismatchCount = results.filter((r) => r.status === 'MISMATCH').length;
|
|
2930
|
+
return {
|
|
2931
|
+
routes: results,
|
|
2932
|
+
total: results.length,
|
|
2933
|
+
routesWithShapes: results.length,
|
|
2934
|
+
...(mismatchCount > 0 ? { mismatches: mismatchCount } : {}),
|
|
2935
|
+
message: results.length === 0
|
|
2936
|
+
? 'No routes with both response shapes and consumers found.'
|
|
2937
|
+
: mismatchCount > 0
|
|
2938
|
+
? `Found ${results.length} route(s) with response shape data. ${mismatchCount} route(s) have consumer/shape mismatches.`
|
|
2939
|
+
: `Found ${results.length} route(s) with response shape data and consumers.`,
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
async toolMap(repo, params) {
|
|
2943
|
+
await this.ensureInitialized(repo.id);
|
|
2944
|
+
const toolFilter = params.tool ? `AND n.name CONTAINS $tool` : '';
|
|
2945
|
+
const queryParams = params.tool ? { tool: params.tool } : {};
|
|
2946
|
+
const rows = await executeParameterized(repo.id, `
|
|
2947
|
+
MATCH (n:Tool)
|
|
2948
|
+
WHERE n.id STARTS WITH 'Tool:' ${toolFilter}
|
|
2949
|
+
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.description AS description
|
|
2950
|
+
`, queryParams);
|
|
2951
|
+
if (rows.length === 0) {
|
|
2952
|
+
return {
|
|
2953
|
+
tools: [],
|
|
2954
|
+
total: 0,
|
|
2955
|
+
message: params.tool ? `No tools matching "${params.tool}"` : 'No tool definitions found.',
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
const toolIds = rows.map((r) => r.id ?? r[0]);
|
|
2959
|
+
const flowMap = await this.fetchLinkedFlowsBatch(repo.id, toolIds);
|
|
2960
|
+
return {
|
|
2961
|
+
tools: rows.map((r) => {
|
|
2962
|
+
const id = r.id ?? r[0];
|
|
2963
|
+
return {
|
|
2964
|
+
name: r.name ?? r[1],
|
|
2965
|
+
filePath: r.filePath ?? r[2],
|
|
2966
|
+
description: (r.description ?? r[3] ?? '').slice(0, 200),
|
|
2967
|
+
flows: flowMap.get(id) || [],
|
|
2968
|
+
};
|
|
2969
|
+
}),
|
|
2970
|
+
total: rows.length,
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
async apiImpact(repo, params) {
|
|
2974
|
+
await this.ensureInitialized(repo.id);
|
|
2975
|
+
if (!params.route && !params.file) {
|
|
2976
|
+
return { error: 'Either "route" or "file" parameter is required.' };
|
|
2977
|
+
}
|
|
2978
|
+
// If file is provided but route is not, look up the route by file path
|
|
2979
|
+
let routeFilter = '';
|
|
2980
|
+
const queryParams = {};
|
|
2981
|
+
if (params.route) {
|
|
2982
|
+
routeFilter = `AND n.name CONTAINS $route`;
|
|
2983
|
+
queryParams.route = params.route;
|
|
2984
|
+
}
|
|
2985
|
+
else if (params.file) {
|
|
2986
|
+
routeFilter = `AND n.filePath CONTAINS $file`;
|
|
2987
|
+
queryParams.file = params.file;
|
|
2988
|
+
}
|
|
2989
|
+
const routes = await this.fetchRoutesWithConsumers(repo.id, routeFilter, queryParams);
|
|
2990
|
+
if (routes.length === 0) {
|
|
2991
|
+
const target = params.route || params.file;
|
|
2992
|
+
return { error: `No routes found matching "${target}".` };
|
|
2993
|
+
}
|
|
2994
|
+
const flowMap = await this.fetchLinkedFlowsBatch(repo.id, routes.map((r) => r.id));
|
|
2995
|
+
// Count how many routes share the same handler file (for middleware partial detection)
|
|
2996
|
+
const routeCountByHandler = new Map();
|
|
2997
|
+
for (const r of routes) {
|
|
2998
|
+
if (r.filePath) {
|
|
2999
|
+
routeCountByHandler.set(r.filePath, (routeCountByHandler.get(r.filePath) ?? 0) + 1);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
const results = routes.map((r) => {
|
|
3003
|
+
// Keys already normalized by fetchRoutesWithConsumers (quotes stripped)
|
|
3004
|
+
const responseKeys = r.responseKeys ?? [];
|
|
3005
|
+
const errorKeys = r.errorKeys ?? [];
|
|
3006
|
+
const allKnownKeys = new Set([...responseKeys, ...errorKeys]);
|
|
3007
|
+
// Build consumer list with mismatch detection
|
|
3008
|
+
const consumers = r.consumers.map((c) => ({
|
|
3009
|
+
name: c.name,
|
|
3010
|
+
file: c.filePath,
|
|
3011
|
+
accesses: c.accessedKeys ?? [],
|
|
3012
|
+
...(c.fetchCount && c.fetchCount > 1
|
|
3013
|
+
? {
|
|
3014
|
+
attributionNote: `This file fetches ${c.fetchCount} routes — accessed keys may belong to a different route.`,
|
|
3015
|
+
}
|
|
3016
|
+
: {}),
|
|
3017
|
+
}));
|
|
3018
|
+
// Detect mismatches: consumer accesses keys not in response shape
|
|
3019
|
+
const mismatches = [];
|
|
3020
|
+
if (allKnownKeys.size > 0) {
|
|
3021
|
+
for (const c of r.consumers) {
|
|
3022
|
+
if (!c.accessedKeys)
|
|
3023
|
+
continue;
|
|
3024
|
+
const isMultiFetch = (c.fetchCount ?? 1) > 1;
|
|
3025
|
+
for (const key of c.accessedKeys) {
|
|
3026
|
+
if (!allKnownKeys.has(key)) {
|
|
3027
|
+
mismatches.push({
|
|
3028
|
+
consumer: c.filePath,
|
|
3029
|
+
field: key,
|
|
3030
|
+
reason: 'accessed but not in response shape',
|
|
3031
|
+
confidence: isMultiFetch ? 'low' : 'high',
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
const flows = flowMap.get(r.id) || [];
|
|
3038
|
+
const consumerCount = r.consumers.length;
|
|
3039
|
+
// Risk level heuristic
|
|
3040
|
+
let riskLevel;
|
|
3041
|
+
if (consumerCount >= 10) {
|
|
3042
|
+
riskLevel = 'HIGH';
|
|
3043
|
+
}
|
|
3044
|
+
else if (consumerCount >= 4) {
|
|
3045
|
+
riskLevel = 'MEDIUM';
|
|
3046
|
+
}
|
|
3047
|
+
else {
|
|
3048
|
+
riskLevel = 'LOW';
|
|
3049
|
+
}
|
|
3050
|
+
// Bump up one level if mismatches exist
|
|
3051
|
+
if (mismatches.length > 0) {
|
|
3052
|
+
if (riskLevel === 'LOW')
|
|
3053
|
+
riskLevel = 'MEDIUM';
|
|
3054
|
+
else if (riskLevel === 'MEDIUM')
|
|
3055
|
+
riskLevel = 'HIGH';
|
|
3056
|
+
}
|
|
3057
|
+
const warning = consumerCount > 0
|
|
3058
|
+
? `Changing response shape will affect ${consumerCount} component${consumerCount === 1 ? '' : 's'}`
|
|
3059
|
+
: undefined;
|
|
3060
|
+
// Flag when middleware was detected but handler exports multiple HTTP methods
|
|
3061
|
+
// (middleware chain may only reflect one export)
|
|
3062
|
+
const middlewareArr = r.middleware || [];
|
|
3063
|
+
const handlerRouteCount = r.filePath ? (routeCountByHandler.get(r.filePath) ?? 1) : 1;
|
|
3064
|
+
const middlewarePartial = middlewareArr.length > 0 && handlerRouteCount > 1;
|
|
3065
|
+
return {
|
|
3066
|
+
route: r.name,
|
|
3067
|
+
handler: r.filePath,
|
|
3068
|
+
responseShape: {
|
|
3069
|
+
success: responseKeys,
|
|
3070
|
+
error: errorKeys,
|
|
3071
|
+
},
|
|
3072
|
+
middleware: middlewareArr,
|
|
3073
|
+
...(middlewarePartial
|
|
3074
|
+
? {
|
|
3075
|
+
middlewareDetection: 'partial',
|
|
3076
|
+
middlewareNote: 'Middleware captured from first HTTP method export only — other methods in this handler may use different middleware chains.',
|
|
3077
|
+
}
|
|
3078
|
+
: {}),
|
|
3079
|
+
consumers,
|
|
3080
|
+
...(mismatches.length > 0 ? { mismatches } : {}),
|
|
3081
|
+
executionFlows: flows,
|
|
3082
|
+
impactSummary: {
|
|
3083
|
+
directConsumers: consumerCount,
|
|
3084
|
+
affectedFlows: flows.length,
|
|
3085
|
+
riskLevel,
|
|
3086
|
+
...(warning ? { warning } : {}),
|
|
3087
|
+
},
|
|
3088
|
+
};
|
|
3089
|
+
});
|
|
3090
|
+
// If a single route was targeted, return it directly (not wrapped in array)
|
|
3091
|
+
if (results.length === 1) {
|
|
3092
|
+
return results[0];
|
|
3093
|
+
}
|
|
3094
|
+
return { routes: results, total: results.length };
|
|
3095
|
+
}
|
|
3096
|
+
// ─── Direct Graph Queries (for resources.ts) ────────────────────
|
|
3097
|
+
/**
|
|
3098
|
+
* Query clusters (communities) directly from graph.
|
|
3099
|
+
* Used by getClustersResource — avoids legacy overview() dispatch.
|
|
3100
|
+
*/
|
|
3101
|
+
async queryClusters(repoName, limit = 100) {
|
|
3102
|
+
const repo = await this.resolveRepo(repoName);
|
|
3103
|
+
await this.ensureInitialized(repo.id);
|
|
3104
|
+
try {
|
|
3105
|
+
const rawLimit = Math.max(limit * 5, 200);
|
|
3106
|
+
const clusters = await executeQuery(repo.id, `
|
|
3107
|
+
MATCH (c:Community)
|
|
3108
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
3109
|
+
ORDER BY c.symbolCount DESC
|
|
3110
|
+
LIMIT ${rawLimit}
|
|
3111
|
+
`);
|
|
3112
|
+
const rawClusters = clusters.map((c) => ({
|
|
3113
|
+
id: c.id || c[0],
|
|
3114
|
+
label: c.label || c[1],
|
|
3115
|
+
heuristicLabel: c.heuristicLabel || c[2],
|
|
3116
|
+
cohesion: c.cohesion || c[3],
|
|
3117
|
+
symbolCount: c.symbolCount || c[4],
|
|
3118
|
+
}));
|
|
3119
|
+
return { clusters: this.aggregateClusters(rawClusters).slice(0, limit) };
|
|
3120
|
+
}
|
|
3121
|
+
catch {
|
|
3122
|
+
return { clusters: [] };
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* Query processes directly from graph.
|
|
3127
|
+
* Used by getProcessesResource — avoids legacy overview() dispatch.
|
|
3128
|
+
*/
|
|
3129
|
+
async queryProcesses(repoName, limit = 50) {
|
|
3130
|
+
const repo = await this.resolveRepo(repoName);
|
|
3131
|
+
await this.ensureInitialized(repo.id);
|
|
3132
|
+
try {
|
|
3133
|
+
const processes = await executeQuery(repo.id, `
|
|
3134
|
+
MATCH (p:Process)
|
|
3135
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
3136
|
+
ORDER BY p.stepCount DESC
|
|
3137
|
+
LIMIT ${limit}
|
|
3138
|
+
`);
|
|
3139
|
+
return {
|
|
3140
|
+
processes: processes.map((p) => ({
|
|
3141
|
+
id: p.id || p[0],
|
|
3142
|
+
label: p.label || p[1],
|
|
3143
|
+
heuristicLabel: p.heuristicLabel || p[2],
|
|
3144
|
+
processType: p.processType || p[3],
|
|
3145
|
+
stepCount: p.stepCount || p[4],
|
|
3146
|
+
})),
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
catch {
|
|
3150
|
+
return { processes: [] };
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
/**
|
|
3154
|
+
* Query cluster detail (members) directly from graph.
|
|
3155
|
+
* Used by getClusterDetailResource.
|
|
3156
|
+
*/
|
|
3157
|
+
async queryClusterDetail(name, repoName) {
|
|
3158
|
+
const repo = await this.resolveRepo(repoName);
|
|
3159
|
+
await this.ensureInitialized(repo.id);
|
|
3160
|
+
const clusters = await executeParameterized(repo.id, `
|
|
3161
|
+
MATCH (c:Community)
|
|
3162
|
+
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
|
|
3163
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
3164
|
+
`, { clusterName: name });
|
|
3165
|
+
if (clusters.length === 0)
|
|
3166
|
+
return { error: `Cluster '${name}' not found` };
|
|
3167
|
+
const rawClusters = clusters.map((c) => ({
|
|
3168
|
+
id: c.id || c[0],
|
|
3169
|
+
label: c.label || c[1],
|
|
3170
|
+
heuristicLabel: c.heuristicLabel || c[2],
|
|
3171
|
+
cohesion: c.cohesion || c[3],
|
|
3172
|
+
symbolCount: c.symbolCount || c[4],
|
|
3173
|
+
}));
|
|
3174
|
+
let totalSymbols = 0, weightedCohesion = 0;
|
|
3175
|
+
for (const c of rawClusters) {
|
|
3176
|
+
const s = c.symbolCount || 0;
|
|
3177
|
+
totalSymbols += s;
|
|
3178
|
+
weightedCohesion += (c.cohesion || 0) * s;
|
|
3179
|
+
}
|
|
3180
|
+
const members = await executeParameterized(repo.id, `
|
|
3181
|
+
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
3182
|
+
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
|
|
3183
|
+
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
3184
|
+
LIMIT 30
|
|
3185
|
+
`, { clusterName: name });
|
|
3186
|
+
return {
|
|
3187
|
+
cluster: {
|
|
3188
|
+
id: rawClusters[0].id,
|
|
3189
|
+
label: rawClusters[0].heuristicLabel || rawClusters[0].label,
|
|
3190
|
+
heuristicLabel: rawClusters[0].heuristicLabel || rawClusters[0].label,
|
|
3191
|
+
cohesion: totalSymbols > 0 ? weightedCohesion / totalSymbols : 0,
|
|
3192
|
+
symbolCount: totalSymbols,
|
|
3193
|
+
subCommunities: rawClusters.length,
|
|
3194
|
+
},
|
|
3195
|
+
members: members.map((m) => ({
|
|
3196
|
+
name: m.name || m[0],
|
|
3197
|
+
type: m.type || m[1],
|
|
3198
|
+
filePath: m.filePath || m[2],
|
|
3199
|
+
})),
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Query process detail (steps) directly from graph.
|
|
3204
|
+
* Used by getProcessDetailResource.
|
|
3205
|
+
*/
|
|
3206
|
+
async queryProcessDetail(name, repoName) {
|
|
3207
|
+
const repo = await this.resolveRepo(repoName);
|
|
3208
|
+
await this.ensureInitialized(repo.id);
|
|
3209
|
+
const processes = await executeParameterized(repo.id, `
|
|
3210
|
+
MATCH (p:Process)
|
|
3211
|
+
WHERE p.label = $processName OR p.heuristicLabel = $processName
|
|
3212
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
3213
|
+
LIMIT 1
|
|
3214
|
+
`, { processName: name });
|
|
3215
|
+
if (processes.length === 0)
|
|
3216
|
+
return { error: `Process '${name}' not found` };
|
|
3217
|
+
const proc = processes[0];
|
|
3218
|
+
const procId = proc.id || proc[0];
|
|
3219
|
+
const steps = await executeParameterized(repo.id, `
|
|
3220
|
+
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: $procId})
|
|
3221
|
+
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
3222
|
+
ORDER BY r.step
|
|
3223
|
+
`, { procId });
|
|
3224
|
+
return {
|
|
3225
|
+
process: {
|
|
3226
|
+
id: procId,
|
|
3227
|
+
label: proc.label || proc[1],
|
|
3228
|
+
heuristicLabel: proc.heuristicLabel || proc[2],
|
|
3229
|
+
processType: proc.processType || proc[3],
|
|
3230
|
+
stepCount: proc.stepCount || proc[4],
|
|
3231
|
+
},
|
|
3232
|
+
steps: steps.map((s) => ({
|
|
3233
|
+
step: s.step || s[3],
|
|
3234
|
+
name: s.name || s[0],
|
|
3235
|
+
type: s.type || s[1],
|
|
3236
|
+
filePath: s.filePath || s[2],
|
|
3237
|
+
})),
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
async disconnect() {
|
|
3241
|
+
await closeLbug(); // close all connections
|
|
3242
|
+
// Note: we intentionally do NOT call disposeEmbedder() here.
|
|
3243
|
+
// ONNX Runtime's native cleanup segfaults on macOS and some Linux configs,
|
|
3244
|
+
// and importing the embedder module on Node v24+ crashes if onnxruntime
|
|
3245
|
+
// was never loaded during the session. Since process.exit(0) follows
|
|
3246
|
+
// immediately after disconnect(), the OS reclaims everything. See #38, #89.
|
|
3247
|
+
this.repos.clear();
|
|
3248
|
+
this.contextCache.clear();
|
|
3249
|
+
this.initializedRepos.clear();
|
|
3250
|
+
}
|
|
3251
|
+
}
|