cgraphx 2.0.2 → 2.0.6
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/dist/.claude-template/FLOW-MAP.md +13 -3
- package/dist/.claude-template/agents/task-history.md +186 -0
- package/dist/.claude-template/commands/end.md +1 -0
- package/dist/.claude-template/skills/cgraphx-guide/SKILL.md +5 -3
- package/dist/.claude-template/skills/cgraphx-guide/how-to-use.html +8 -8
- package/dist/.claude-template/skills/cgraphx-guide/how-to-use.md +8 -8
- package/dist/.claude-template/skills/clarify-requirements/SKILL.md +13 -9
- package/dist/.claude-template/skills/db-query/SKILL.md +19 -2
- package/dist/.claude-template/skills/iboc-check/SKILL.md +51 -23
- package/dist/.claude-template/skills/iboc-check/filler-prompt.md +3 -4
- package/dist/.claude-template/skills/run-api-test/SKILL.md +15 -15
- package/dist/.claude-template/skills/run-api-test/assets/{template-test-report.md → template-/346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212.md} +4 -4
- package/dist/.claude-template/skills/run-api-test/references/bru-run.md +1 -1
- package/dist/.claude-template/skills/run-api-test/references/db-verification.md +2 -2
- package/dist/.claude-template/skills/run-api-test/references/report-format.md +6 -6
- package/dist/.claude-template/skills/run-api-test/references/service-readiness.md +1 -1
- package/dist/.claude-template/skills/run-api-test/references/test-scope.md +6 -6
- package/dist/.claude-template/skills/run-ui-test/SKILL.md +186 -0
- package/dist/.claude-template/skills/run-ui-test/references/report-format.md +77 -0
- package/dist/.claude-template/skills/subagent-implement/SKILL.md +3 -2
- package/dist/.claude-template/skills/write-api/SKILL.md +18 -18
- package/dist/.claude-template/skills/write-api/assets/{template-api-spec.md → template-/346/216/245/345/217/243/346/265/213/350/257/225-spec.md} +1 -1
- package/dist/.claude-template/skills/write-api/references/{api-spec-format.md → /346/216/245/345/217/243/346/265/213/350/257/225-spec-format.md} +3 -3
- package/dist/.claude-template/skills/write-plan/SKILL.md +4 -2
- package/dist/.claude-template/skills/write-ui-test/SKILL.md +125 -0
- package/dist/.claude-template/skills/write-ui-test/references//347/224/250/344/276/213/350/247/204/346/240/274-format.md +94 -0
- package/dist/core/code/engine/cli/analyze-config.js +320 -0
- package/dist/core/code/engine/cli/analyze.js +1086 -0
- package/dist/core/code/engine/cli/cli-message.js +83 -0
- package/dist/core/code/engine/cli/detect-changes-format.js +58 -0
- package/dist/core/code/engine/cli/embedding-dims.js +43 -0
- package/dist/core/code/engine/cli/format-elapsed.js +12 -0
- package/dist/core/code/engine/cli/help-i18n.js +144 -0
- package/dist/core/code/engine/cli/i18n/en.js +110 -0
- package/dist/core/code/engine/cli/i18n/index.js +47 -0
- package/dist/core/code/engine/cli/i18n/resources.js +9 -0
- package/dist/core/code/engine/cli/i18n/zh-CN.js +110 -0
- package/dist/core/code/engine/cli/lazy-action.js +67 -0
- package/dist/core/code/engine/cli/optional-grammars.js +136 -0
- package/dist/core/code/engine/cli/resolve-invocation.js +76 -0
- package/dist/core/code/engine/cli/status.js +156 -0
- package/dist/core/code/engine/cli/tool.js +384 -0
- package/dist/core/code/engine/config/ignore-service.js +511 -0
- package/dist/core/code/engine/config/supported-languages.js +17 -0
- package/dist/core/code/engine/core/analysis-features.js +64 -0
- package/dist/core/code/engine/core/analyzer-identity.js +2171 -0
- package/dist/core/code/engine/core/git-staleness.js +180 -0
- package/dist/core/code/engine/core/graph/graph.js +180 -0
- package/dist/core/code/engine/core/graph/import-cycles.js +106 -0
- package/dist/core/code/engine/core/graph/types.js +2 -0
- package/dist/core/code/engine/core/index-freshness.js +12 -0
- package/dist/core/code/engine/core/ingestion/binding-accumulator.js +341 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/configs/c-cpp.js +168 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/configs/csharp.js +9 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/configs/dart.js +8 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/configs/go.js +8 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/configs/jvm.js +54 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/configs/php.js +8 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/configs/python.js +8 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/configs/ruby.js +8 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/configs/rust.js +8 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/configs/swift.js +8 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/configs/typescript-javascript.js +11 -0
- package/dist/core/code/engine/core/ingestion/call-extractors/generic.js +62 -0
- package/dist/core/code/engine/core/ingestion/call-processor.js +503 -0
- package/dist/core/code/engine/core/ingestion/call-routing.js +98 -0
- package/dist/core/code/engine/core/ingestion/call-types.js +3 -0
- package/dist/core/code/engine/core/ingestion/cfg/callee-cell-format.js +45 -0
- package/dist/core/code/engine/core/ingestion/cfg/cfg-builder.js +202 -0
- package/dist/core/code/engine/core/ingestion/cfg/collect.js +81 -0
- package/dist/core/code/engine/core/ingestion/cfg/control-dependence.js +185 -0
- package/dist/core/code/engine/core/ingestion/cfg/control-flow-context.js +130 -0
- package/dist/core/code/engine/core/ingestion/cfg/emit.js +646 -0
- package/dist/core/code/engine/core/ingestion/cfg/post-dominators.js +182 -0
- package/dist/core/code/engine/core/ingestion/cfg/reaching-def-reason-codec.js +139 -0
- package/dist/core/code/engine/core/ingestion/cfg/reaching-defs-graph.js +322 -0
- package/dist/core/code/engine/core/ingestion/cfg/reaching-defs.js +792 -0
- package/dist/core/code/engine/core/ingestion/cfg/synthetic-escape.js +305 -0
- package/dist/core/code/engine/core/ingestion/cfg/traversal-result.js +6 -0
- package/dist/core/code/engine/core/ingestion/cfg/types.js +14 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/c-cpp-harvest.js +545 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/c-cpp.js +590 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/call-site-harvest.js +356 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/csharp-harvest.js +593 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/csharp.js +871 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/dart-harvest.js +874 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/dart.js +840 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/go-harvest.js +625 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/go.js +642 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/java-harvest.js +517 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/java.js +816 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/kotlin-harvest.js +723 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/kotlin.js +813 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/php-harvest.js +630 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/php.js +725 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/python-harvest.js +776 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/python.js +562 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/ruby-harvest.js +591 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/ruby.js +760 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/rust-harvest.js +877 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/rust.js +562 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/scope-tree-harvest.js +120 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/swift-harvest.js +683 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/swift.js +791 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/typescript-harvest.js +1060 -0
- package/dist/core/code/engine/core/ingestion/cfg/visitors/typescript.js +587 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/configs/c-cpp.js +77 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/configs/csharp.js +24 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/configs/dart.js +10 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/configs/go.js +28 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/configs/jvm.js +67 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/configs/php.js +10 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/configs/python.js +10 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/configs/ruby.js +13 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/configs/rust.js +10 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/configs/swift.js +21 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/configs/typescript-javascript.js +31 -0
- package/dist/core/code/engine/core/ingestion/class-extractors/generic.js +144 -0
- package/dist/core/code/engine/core/ingestion/class-types.js +2 -0
- package/dist/core/code/engine/core/ingestion/cluster-enricher.js +174 -0
- package/dist/core/code/engine/core/ingestion/community-processor.js +604 -0
- package/dist/core/code/engine/core/ingestion/constants.js +26 -0
- package/dist/core/code/engine/core/ingestion/cpp-ue-preprocessor.js +263 -0
- package/dist/core/code/engine/core/ingestion/csharp-namespace-gate.js +133 -0
- package/dist/core/code/engine/core/ingestion/di-extractors/index.js +38 -0
- package/dist/core/code/engine/core/ingestion/di-extractors/spring.js +310 -0
- package/dist/core/code/engine/core/ingestion/emit-references.js +244 -0
- package/dist/core/code/engine/core/ingestion/entry-point-scoring.js +201 -0
- package/dist/core/code/engine/core/ingestion/export-detection.js +244 -0
- package/dist/core/code/engine/core/ingestion/field-extractor.js +29 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/c-cpp.js +107 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/csharp.js +124 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/dart.js +99 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/go.js +102 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/helpers.js +198 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/jvm.js +172 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/php.js +67 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/python.js +94 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/ruby.js +79 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/rust.js +55 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/swift.js +93 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/configs/typescript-javascript.js +59 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/generic.js +147 -0
- package/dist/core/code/engine/core/ingestion/field-extractors/typescript.js +266 -0
- package/dist/core/code/engine/core/ingestion/field-types.js +3 -0
- package/dist/core/code/engine/core/ingestion/filesystem-walker.js +136 -0
- package/dist/core/code/engine/core/ingestion/finalize-orchestrator.js +159 -0
- package/dist/core/code/engine/core/ingestion/framework-detection.js +432 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/analysis-features.js +38 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/annotation-arguments.js +234 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/aop-candidates.js +88 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/aop.js +487 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/auto-configuration.js +21 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/bean-candidates.js +189 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/bean-catalog.js +32 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/bean-factories.js +73 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/conditionals.js +323 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/config-bindings.js +119 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/di-metadata.js +385 -0
- package/dist/core/code/engine/core/ingestion/frameworks/spring/resource-injection.js +96 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/configs/c-cpp.js +17 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/configs/csharp.js +46 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/configs/dart.js +59 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/configs/go.js +30 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/configs/jvm.js +73 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/configs/php.js +19 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/configs/python.js +45 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/configs/ruby.js +20 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/configs/rust.js +58 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/configs/swift.js +94 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/configs/typescript-javascript.js +26 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/csharp.js +128 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/go.js +50 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/jvm.js +112 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/php.js +80 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/python.js +75 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/resolver-factory.js +36 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/ruby.js +20 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/rust.js +79 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/standard.js +180 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/types.js +7 -0
- package/dist/core/code/engine/core/ingestion/import-resolvers/utils.js +153 -0
- package/dist/core/code/engine/core/ingestion/import-target-adapter.js +99 -0
- package/dist/core/code/engine/core/ingestion/language-config.js +391 -0
- package/dist/core/code/engine/core/ingestion/language-provider.js +25 -0
- package/dist/core/code/engine/core/ingestion/languages/c/arity-metadata.js +98 -0
- package/dist/core/code/engine/core/ingestion/languages/c/arity.js +21 -0
- package/dist/core/code/engine/core/ingestion/languages/c/capture-side-channel.js +69 -0
- package/dist/core/code/engine/core/ingestion/languages/c/captures.js +189 -0
- package/dist/core/code/engine/core/ingestion/languages/c/header-scan.js +58 -0
- package/dist/core/code/engine/core/ingestion/languages/c/import-decomposer.js +68 -0
- package/dist/core/code/engine/core/ingestion/languages/c/import-target.js +103 -0
- package/dist/core/code/engine/core/ingestion/languages/c/index.js +33 -0
- package/dist/core/code/engine/core/ingestion/languages/c/interpret.js +53 -0
- package/dist/core/code/engine/core/ingestion/languages/c/merge-bindings.js +26 -0
- package/dist/core/code/engine/core/ingestion/languages/c/query.js +210 -0
- package/dist/core/code/engine/core/ingestion/languages/c/scope-resolver.js +112 -0
- package/dist/core/code/engine/core/ingestion/languages/c/simple-hooks.js +24 -0
- package/dist/core/code/engine/core/ingestion/languages/c/static-linkage.js +109 -0
- package/dist/core/code/engine/core/ingestion/languages/c-cpp.js +506 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/adl.js +804 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/arity-metadata.js +258 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/arity.js +37 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/capture-side-channel.js +89 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/captures.js +1975 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/constraint-extractor.js +311 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/constraint-filter.js +210 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/conversion-rank.js +163 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/file-local-linkage.js +327 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/header-scan.js +53 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/import-decomposer.js +134 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/import-target.js +16 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/index.js +33 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/inline-namespaces.js +379 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/interpret.js +239 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/member-lookup.js +470 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/merge-bindings.js +32 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/query.js +763 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/range-bindings.js +230 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/scope-resolver.js +354 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/simple-hooks.js +67 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/two-phase-lookup.js +348 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/type-classifier.js +56 -0
- package/dist/core/code/engine/core/ingestion/languages/cpp/user-defined-conversions.js +128 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/accessor-unwrap.js +67 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/arity-metadata.js +49 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/arity.js +40 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/cache-stats.js +32 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/captures.js +557 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/import-decomposer.js +96 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/import-target.js +176 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/index.js +95 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/interpret.js +150 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/merge-bindings.js +58 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/namespace-siblings.js +708 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/qualified-type-names.js +62 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/query.js +578 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/receiver-binding.js +142 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/resolution-config.js +18 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/scope-resolver.js +84 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp/simple-hooks.js +81 -0
- package/dist/core/code/engine/core/ingestion/languages/csharp.js +204 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/arity-metadata.js +38 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/arity.js +34 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/built-ins.js +37 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/cache-stats.js +30 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/captures.js +1096 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/expand-wildcards.js +34 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/extension-type-preprocess.js +33 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/import-target.js +68 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/index.js +45 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/interpret.js +101 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/merge-bindings.js +42 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/query.js +246 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/receiver-binding.js +90 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/scope-resolver.js +197 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/signature-bindings.js +54 -0
- package/dist/core/code/engine/core/ingestion/languages/dart/simple-hooks.js +61 -0
- package/dist/core/code/engine/core/ingestion/languages/dart.js +138 -0
- package/dist/core/code/engine/core/ingestion/languages/go/arity-metadata.js +71 -0
- package/dist/core/code/engine/core/ingestion/languages/go/arity.js +17 -0
- package/dist/core/code/engine/core/ingestion/languages/go/cache-stats.js +21 -0
- package/dist/core/code/engine/core/ingestion/languages/go/captures.js +492 -0
- package/dist/core/code/engine/core/ingestion/languages/go/expand-wildcards.js +97 -0
- package/dist/core/code/engine/core/ingestion/languages/go/generic-type-parameters.js +146 -0
- package/dist/core/code/engine/core/ingestion/languages/go/import-decomposer.js +47 -0
- package/dist/core/code/engine/core/ingestion/languages/go/import-target.js +70 -0
- package/dist/core/code/engine/core/ingestion/languages/go/index.js +39 -0
- package/dist/core/code/engine/core/ingestion/languages/go/interface-impls.js +955 -0
- package/dist/core/code/engine/core/ingestion/languages/go/interpret.js +177 -0
- package/dist/core/code/engine/core/ingestion/languages/go/merge-bindings.js +21 -0
- package/dist/core/code/engine/core/ingestion/languages/go/method-owners.js +131 -0
- package/dist/core/code/engine/core/ingestion/languages/go/namespace-mirror.js +56 -0
- package/dist/core/code/engine/core/ingestion/languages/go/package-clause.js +79 -0
- package/dist/core/code/engine/core/ingestion/languages/go/package-siblings.js +83 -0
- package/dist/core/code/engine/core/ingestion/languages/go/query.js +298 -0
- package/dist/core/code/engine/core/ingestion/languages/go/range-binding.js +127 -0
- package/dist/core/code/engine/core/ingestion/languages/go/receiver-binding.js +24 -0
- package/dist/core/code/engine/core/ingestion/languages/go/scope-resolver.js +75 -0
- package/dist/core/code/engine/core/ingestion/languages/go/simple-hooks.js +31 -0
- package/dist/core/code/engine/core/ingestion/languages/go/type-binding.js +279 -0
- package/dist/core/code/engine/core/ingestion/languages/go.js +160 -0
- package/dist/core/code/engine/core/ingestion/languages/index.js +66 -0
- package/dist/core/code/engine/core/ingestion/languages/java/analysis-features.js +16 -0
- package/dist/core/code/engine/core/ingestion/languages/java/arity-metadata.js +43 -0
- package/dist/core/code/engine/core/ingestion/languages/java/arity.js +27 -0
- package/dist/core/code/engine/core/ingestion/languages/java/cache-stats.js +32 -0
- package/dist/core/code/engine/core/ingestion/languages/java/capture-side-channel.js +123 -0
- package/dist/core/code/engine/core/ingestion/languages/java/captures.js +791 -0
- package/dist/core/code/engine/core/ingestion/languages/java/import-decomposer.js +88 -0
- package/dist/core/code/engine/core/ingestion/languages/java/import-target.js +103 -0
- package/dist/core/code/engine/core/ingestion/languages/java/index.js +43 -0
- package/dist/core/code/engine/core/ingestion/languages/java/interpret.js +146 -0
- package/dist/core/code/engine/core/ingestion/languages/java/merge-bindings.js +43 -0
- package/dist/core/code/engine/core/ingestion/languages/java/package-facts.js +16 -0
- package/dist/core/code/engine/core/ingestion/languages/java/package-siblings.js +11 -0
- package/dist/core/code/engine/core/ingestion/languages/java/query.js +319 -0
- package/dist/core/code/engine/core/ingestion/languages/java/receiver-binding.js +98 -0
- package/dist/core/code/engine/core/ingestion/languages/java/scope-resolver.js +213 -0
- package/dist/core/code/engine/core/ingestion/languages/java/simple-hooks.js +39 -0
- package/dist/core/code/engine/core/ingestion/languages/java/spring-aop.js +53 -0
- package/dist/core/code/engine/core/ingestion/languages/java/spring-bean-metadata.js +11 -0
- package/dist/core/code/engine/core/ingestion/languages/java/spring-conditionals.js +52 -0
- package/dist/core/code/engine/core/ingestion/languages/java/spring-config-bindings.js +222 -0
- package/dist/core/code/engine/core/ingestion/languages/java/spring-di.js +165 -0
- package/dist/core/code/engine/core/ingestion/languages/java.js +192 -0
- package/dist/core/code/engine/core/ingestion/languages/javascript/arity.js +15 -0
- package/dist/core/code/engine/core/ingestion/languages/javascript/captures.js +1122 -0
- package/dist/core/code/engine/core/ingestion/languages/javascript/import-target.js +56 -0
- package/dist/core/code/engine/core/ingestion/languages/javascript/index.js +109 -0
- package/dist/core/code/engine/core/ingestion/languages/javascript/interpret.js +45 -0
- package/dist/core/code/engine/core/ingestion/languages/javascript/merge-bindings.js +21 -0
- package/dist/core/code/engine/core/ingestion/languages/javascript/query.js +659 -0
- package/dist/core/code/engine/core/ingestion/languages/javascript/scope-resolver.js +78 -0
- package/dist/core/code/engine/core/ingestion/languages/javascript/simple-hooks.js +44 -0
- package/dist/core/code/engine/core/ingestion/languages/jvm/package-facts.js +46 -0
- package/dist/core/code/engine/core/ingestion/languages/jvm/package-siblings.js +200 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/arity-metadata.js +23 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/arity.js +18 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/cache-stats.js +21 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/capture-side-channel.js +160 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/captures.js +1262 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/companion-scopes.js +72 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/import-decomposer.js +40 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/import-target.js +135 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/index.js +26 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/interpret.js +75 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/merge-bindings.js +28 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/owners.js +134 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/package-facts.js +16 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/package-siblings.js +11 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/query.js +243 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/receiver-binding.js +103 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/scope-resolver.js +207 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/simple-hooks.js +42 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/spring-aop.js +68 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/spring-bean-metadata.js +11 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/spring-conditionals.js +53 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin/spring-di.js +304 -0
- package/dist/core/code/engine/core/ingestion/languages/kotlin.js +189 -0
- package/dist/core/code/engine/core/ingestion/languages/php/arity-metadata.js +66 -0
- package/dist/core/code/engine/core/ingestion/languages/php/arity.js +43 -0
- package/dist/core/code/engine/core/ingestion/languages/php/cache-stats.js +32 -0
- package/dist/core/code/engine/core/ingestion/languages/php/captures.js +1166 -0
- package/dist/core/code/engine/core/ingestion/languages/php/import-decomposer.js +238 -0
- package/dist/core/code/engine/core/ingestion/languages/php/import-target.js +213 -0
- package/dist/core/code/engine/core/ingestion/languages/php/index.js +85 -0
- package/dist/core/code/engine/core/ingestion/languages/php/interpret.js +256 -0
- package/dist/core/code/engine/core/ingestion/languages/php/merge-bindings.js +50 -0
- package/dist/core/code/engine/core/ingestion/languages/php/namespace-siblings.js +353 -0
- package/dist/core/code/engine/core/ingestion/languages/php/query.js +391 -0
- package/dist/core/code/engine/core/ingestion/languages/php/receiver-binding.js +135 -0
- package/dist/core/code/engine/core/ingestion/languages/php/scope-resolver.js +361 -0
- package/dist/core/code/engine/core/ingestion/languages/php/simple-hooks.js +116 -0
- package/dist/core/code/engine/core/ingestion/languages/php.js +302 -0
- package/dist/core/code/engine/core/ingestion/languages/python/arity-metadata.js +49 -0
- package/dist/core/code/engine/core/ingestion/languages/python/arity.js +41 -0
- package/dist/core/code/engine/core/ingestion/languages/python/cache-stats.js +34 -0
- package/dist/core/code/engine/core/ingestion/languages/python/captures.js +299 -0
- package/dist/core/code/engine/core/ingestion/languages/python/depends-references.js +68 -0
- package/dist/core/code/engine/core/ingestion/languages/python/import-decomposer.js +115 -0
- package/dist/core/code/engine/core/ingestion/languages/python/import-target.js +440 -0
- package/dist/core/code/engine/core/ingestion/languages/python/index-stats.js +30 -0
- package/dist/core/code/engine/core/ingestion/languages/python/index.js +96 -0
- package/dist/core/code/engine/core/ingestion/languages/python/interpret.js +430 -0
- package/dist/core/code/engine/core/ingestion/languages/python/merge-bindings.js +47 -0
- package/dist/core/code/engine/core/ingestion/languages/python/query.js +323 -0
- package/dist/core/code/engine/core/ingestion/languages/python/receiver-binding.js +310 -0
- package/dist/core/code/engine/core/ingestion/languages/python/scope-resolver.js +80 -0
- package/dist/core/code/engine/core/ingestion/languages/python/simple-hooks.js +53 -0
- package/dist/core/code/engine/core/ingestion/languages/python.js +140 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby/arity.js +41 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby/cache-stats.js +21 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby/captures.js +864 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby/import-target.js +88 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby/index.js +29 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby/interpret.js +115 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby/merge-bindings.js +21 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby/query.js +349 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby/receiver-binding.js +70 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby/scope-resolver.js +263 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby/simple-hooks.js +68 -0
- package/dist/core/code/engine/core/ingestion/languages/ruby.js +215 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/arity.js +16 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/cache-stats.js +21 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/captures.js +304 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/import-decomposer.js +167 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/import-target.js +108 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/index.js +29 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/interpret.js +201 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/merge-bindings.js +21 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/method-owners.js +76 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/module-path.js +222 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/qualified-call.js +482 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/query.js +280 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/range-binding.js +687 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/receiver-binding.js +148 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/scope-resolver.js +151 -0
- package/dist/core/code/engine/core/ingestion/languages/rust/simple-hooks.js +32 -0
- package/dist/core/code/engine/core/ingestion/languages/rust.js +183 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/arity-metadata.js +44 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/arity.js +45 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/base-type.js +30 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/cache-stats.js +32 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/captures.js +594 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/conditional-directive-preprocess.js +256 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/implicit-imports.js +60 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/import-decomposer.js +87 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/import-target.js +84 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/index.js +56 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/interpret.js +93 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/merge-bindings.js +51 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/query.js +226 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/receiver-binding.js +169 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/scope-resolver.js +192 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/sibling-type-bindings.js +68 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/signature-bindings.js +69 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/simple-hooks.js +65 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/target-grouping.js +97 -0
- package/dist/core/code/engine/core/ingestion/languages/swift/target-siblings.js +74 -0
- package/dist/core/code/engine/core/ingestion/languages/swift.js +246 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/arity-metadata.js +106 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/arity.js +57 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/array-callback.js +58 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/cache-stats.js +34 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/captures.js +956 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/cjs-export-assignment.js +535 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/cjs-module-exports.js +196 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/import-decomposer.js +374 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/import-target.js +65 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/index.js +108 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/interpret.js +344 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/merge-bindings.js +161 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/nuxt-auto-imports.js +325 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/query.js +1328 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/receiver-binding.js +201 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/scope-resolver.js +293 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript/simple-hooks.js +139 -0
- package/dist/core/code/engine/core/ingestion/languages/typescript.js +455 -0
- package/dist/core/code/engine/core/ingestion/languages/vue/captures.js +70 -0
- package/dist/core/code/engine/core/ingestion/languages/vue/import-target.js +61 -0
- package/dist/core/code/engine/core/ingestion/languages/vue/index.js +55 -0
- package/dist/core/code/engine/core/ingestion/languages/vue/scope-resolver.js +295 -0
- package/dist/core/code/engine/core/ingestion/languages/vue.js +96 -0
- package/dist/core/code/engine/core/ingestion/local-symbol-pruner.js +68 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/configs/csharp.js +290 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/configs/dart.js +392 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/configs/go.js +179 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/configs/jvm.js +350 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/configs/php.js +306 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/configs/python.js +312 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/configs/ruby.js +289 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/configs/rust.js +198 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/configs/swift.js +286 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/configs/typescript-javascript.js +341 -0
- package/dist/core/code/engine/core/ingestion/method-extractors/generic.js +209 -0
- package/dist/core/code/engine/core/ingestion/method-types.js +3 -0
- package/dist/core/code/engine/core/ingestion/model/field-registry.js +41 -0
- package/dist/core/code/engine/core/ingestion/model/index.js +52 -0
- package/dist/core/code/engine/core/ingestion/model/method-registry.js +138 -0
- package/dist/core/code/engine/core/ingestion/model/owned-members-lookup.js +46 -0
- package/dist/core/code/engine/core/ingestion/model/registration-table.js +234 -0
- package/dist/core/code/engine/core/ingestion/model/resolve.js +183 -0
- package/dist/core/code/engine/core/ingestion/model/scope-resolution-indexes.js +43 -0
- package/dist/core/code/engine/core/ingestion/model/semantic-model.js +179 -0
- package/dist/core/code/engine/core/ingestion/model/symbol-table.js +216 -0
- package/dist/core/code/engine/core/ingestion/model/type-registry.js +84 -0
- package/dist/core/code/engine/core/ingestion/mro-processor.js +709 -0
- package/dist/core/code/engine/core/ingestion/parsing-processor.js +312 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/communities.js +69 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/cross-file.js +71 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/di.js +338 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/http-api-calls.js +312 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/index.js +54 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/mro.js +40 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/orm.js +78 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/parse-impl.js +1286 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/parse.js +41 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/processes.js +193 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/prune-local-symbols.js +29 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/registry.js +52 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/routes.js +409 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/rpc-edges.js +301 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/runner.js +207 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/scan.js +49 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/spring-aop.js +442 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/spring-auto-configuration.js +264 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/spring-config.js +440 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/structure.js +38 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/tools.js +89 -0
- package/dist/core/code/engine/core/ingestion/pipeline-phases/types.js +40 -0
- package/dist/core/code/engine/core/ingestion/pipeline.js +152 -0
- package/dist/core/code/engine/core/ingestion/process-processor.js +325 -0
- package/dist/core/code/engine/core/ingestion/resolve-references.js +205 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/constant-resolver.js +135 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/django-root-discovery.js +221 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/django.js +428 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/expo.js +39 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/fastapi-router-bindings.js +264 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/laravel.js +501 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/middleware.js +175 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/nextjs.js +81 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/php.js +25 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/python-const-resolver.js +307 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/response-shapes.js +299 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/route-path.js +71 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/spring-shared.js +310 -0
- package/dist/core/code/engine/core/ingestion/route-extractors/spring.js +441 -0
- package/dist/core/code/engine/core/ingestion/scope-extractor-bridge.js +60 -0
- package/dist/core/code/engine/core/ingestion/scope-extractor.js +1373 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/contract/scope-resolver.js +282 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js +72 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/graph-bridge/edges.js +194 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/graph-bridge/ids.js +472 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +49 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +43 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +274 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +93 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/passes/callable-value-flow.js +1240 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/passes/compound-receiver.js +1174 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/passes/free-call-fallback.js +873 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/passes/imported-return-types.js +226 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/passes/mro.js +107 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/passes/overload-narrowing.js +441 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/passes/property-dispatch.js +122 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +1720 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/pipeline/phase.js +395 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +208 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/pipeline/registry.js +49 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/pipeline/run.js +632 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/pipeline/validate-bindings-immutability.js +112 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/resolution-outcome.js +41 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/scope/namespace-targets.js +81 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/scope/walkers.js +1835 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/unresolved-receivers.js +242 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/utils/definition-id.js +22 -0
- package/dist/core/code/engine/core/ingestion/scope-resolution/workspace-index.js +151 -0
- package/dist/core/code/engine/core/ingestion/structure-processor.js +40 -0
- package/dist/core/code/engine/core/ingestion/tree-sitter-queries.js +2244 -0
- package/dist/core/code/engine/core/ingestion/ts-js-hoc-utils.js +115 -0
- package/dist/core/code/engine/core/ingestion/type-env.js +1136 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/c-cpp.js +555 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/csharp.js +570 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/dart.js +372 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/go.js +508 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/jvm.js +875 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/php.js +537 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/python.js +477 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/ruby.js +380 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/rust.js +502 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/shared.js +843 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/swift.js +490 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/types.js +2 -0
- package/dist/core/code/engine/core/ingestion/type-extractors/typescript.js +690 -0
- package/dist/core/code/engine/core/ingestion/utils/ast-helpers.js +1693 -0
- package/dist/core/code/engine/core/ingestion/utils/call-analysis.js +779 -0
- package/dist/core/code/engine/core/ingestion/utils/callable-flow-captures.js +932 -0
- package/dist/core/code/engine/core/ingestion/utils/callable-labels.js +49 -0
- package/dist/core/code/engine/core/ingestion/utils/deferred-resolution-profile.js +151 -0
- package/dist/core/code/engine/core/ingestion/utils/effective-ram.js +67 -0
- package/dist/core/code/engine/core/ingestion/utils/env.js +60 -0
- package/dist/core/code/engine/core/ingestion/utils/event-loop.js +9 -0
- package/dist/core/code/engine/core/ingestion/utils/graph-sort.js +103 -0
- package/dist/core/code/engine/core/ingestion/utils/heap-probe.js +45 -0
- package/dist/core/code/engine/core/ingestion/utils/heritage-marker.js +47 -0
- package/dist/core/code/engine/core/ingestion/utils/line-base.js +24 -0
- package/dist/core/code/engine/core/ingestion/utils/max-file-size.js +59 -0
- package/dist/core/code/engine/core/ingestion/utils/method-props.js +198 -0
- package/dist/core/code/engine/core/ingestion/utils/qualified-name.js +73 -0
- package/dist/core/code/engine/core/ingestion/utils/receiver-chain-captures.js +60 -0
- package/dist/core/code/engine/core/ingestion/utils/receiver-chain-codec.js +188 -0
- package/dist/core/code/engine/core/ingestion/utils/scope-tree-walk.js +36 -0
- package/dist/core/code/engine/core/ingestion/utils/symbol-labels.js +48 -0
- package/dist/core/code/engine/core/ingestion/utils/template-arguments.js +187 -0
- package/dist/core/code/engine/core/ingestion/utils/type-parameters.js +209 -0
- package/dist/core/code/engine/core/ingestion/utils/verbose.js +6 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/configs/c-cpp.js +133 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/configs/csharp.js +66 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/configs/dart.js +111 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/configs/go.js +153 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/configs/jvm.js +145 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/configs/php.js +61 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/configs/python.js +104 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/configs/ruby.js +55 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/configs/rust.js +79 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/configs/swift.js +91 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/configs/typescript-javascript.js +86 -0
- package/dist/core/code/engine/core/ingestion/variable-extractors/generic.js +111 -0
- package/dist/core/code/engine/core/ingestion/variable-types.js +3 -0
- package/dist/core/code/engine/core/ingestion/vue-sfc-extractor.js +544 -0
- package/dist/core/code/engine/core/ingestion/workers/callable-id.js +122 -0
- package/dist/core/code/engine/core/ingestion/workers/clone-safety.js +470 -0
- package/dist/core/code/engine/core/ingestion/workers/parse-worker.js +2456 -0
- package/dist/core/code/engine/core/ingestion/workers/post-result.js +90 -0
- package/dist/core/code/engine/core/ingestion/workers/quarantine.js +41 -0
- package/dist/core/code/engine/core/ingestion/workers/result-merge.js +59 -0
- package/dist/core/code/engine/core/ingestion/workers/worker-pool.js +1725 -0
- package/dist/core/code/engine/core/ingestion/workspace-config.js +139 -0
- package/dist/core/code/engine/core/lbug/conn-lock.js +72 -0
- package/dist/core/code/engine/core/lbug/csv-generator.js +710 -0
- package/dist/core/code/engine/core/lbug/cypher-escape.js +24 -0
- package/dist/core/code/engine/core/lbug/extension-load-error.js +339 -0
- package/dist/core/code/engine/core/lbug/extension-loader.js +261 -0
- package/dist/core/code/engine/core/lbug/graph-emit-sink.js +584 -0
- package/dist/core/code/engine/core/lbug/lbug-adapter.js +2703 -0
- package/dist/core/code/engine/core/lbug/lbug-config.js +1029 -0
- package/dist/core/code/engine/core/lbug/native-check.js +435 -0
- package/dist/core/code/engine/core/lbug/pool-adapter.js +973 -0
- package/dist/core/code/engine/core/lbug/query-params.js +25 -0
- package/dist/core/code/engine/core/lbug/query-result-utils.js +31 -0
- package/dist/core/code/engine/core/lbug/rel-pair-routing.js +373 -0
- package/dist/core/code/engine/core/lbug/schema.js +771 -0
- package/dist/core/code/engine/core/lbug/shutdown-helpers.js +40 -0
- package/dist/core/code/engine/core/lbug/sidecar-recovery.js +716 -0
- package/dist/core/code/engine/core/lbug/stdio-capture.js +49 -0
- package/dist/core/code/engine/core/lbug/sync-csv-writer.js +115 -0
- package/dist/core/code/engine/core/lbug/wal-checkpoint-driver.js +215 -0
- package/dist/core/code/engine/core/lbug/wal-driver-state.js +28 -0
- package/dist/core/code/engine/core/logger.js +339 -0
- package/dist/core/code/engine/core/platform/capabilities.js +91 -0
- package/dist/core/code/engine/core/run-analyze.js +1098 -0
- package/dist/core/code/engine/core/tree-sitter/parser-loader.js +281 -0
- package/dist/core/code/engine/core/tree-sitter/safe-parse.js +258 -0
- package/dist/core/code/engine/core/tree-sitter/vendored-grammars.js +64 -0
- package/dist/core/code/engine/lib/utils.js +121 -0
- package/dist/core/code/engine/mcp/core/lbug-adapter.js +27 -0
- package/dist/core/code/engine/mcp/local/aop-metadata.js +230 -0
- package/dist/core/code/engine/mcp/local/bean-metadata.js +49 -0
- package/dist/core/code/engine/mcp/local/limits.js +15 -0
- package/dist/core/code/engine/mcp/local/line-display.js +6 -0
- package/dist/core/code/engine/mcp/local/local-backend.js +4142 -0
- package/dist/core/code/engine/storage/branch-index.js +72 -0
- package/dist/core/code/engine/storage/file-hash.js +95 -0
- package/dist/core/code/engine/storage/fs-atomic.js +34 -0
- package/dist/core/code/engine/storage/git.js +555 -0
- package/dist/core/code/engine/storage/index-lock.js +664 -0
- package/dist/core/code/engine/storage/parse-cache.js +650 -0
- package/dist/core/code/engine/storage/parsedfile-store.js +620 -0
- package/dist/core/code/engine/storage/repo-manager.js +1061 -0
- package/dist/core/code/engine/storage/scope-index-store.js +247 -0
- package/dist/core/code/engine/types/pipeline.js +2 -0
- package/dist/core/code/scripts/install-duckdb-extension.mjs +125 -0
- package/dist/core/code/scripts/resolve-analyze-cmd.cjs +346 -0
- package/dist/core/code/shared/graph/types.js +8 -0
- package/dist/core/code/shared/index.js +105 -0
- package/dist/core/code/shared/integrations/circuit-breaker.js +242 -0
- package/dist/core/code/shared/integrations/resilient-fetch.js +224 -0
- package/dist/core/code/shared/integrations/retry.js +70 -0
- package/dist/core/code/shared/integrations/understand-quickly.js +145 -0
- package/dist/core/code/shared/language-detection.js +162 -0
- package/dist/core/code/shared/languages.js +27 -0
- package/dist/core/code/shared/lbug/schema-constants.js +98 -0
- package/dist/core/code/shared/mro-strategy.js +2 -0
- package/dist/core/code/shared/pipeline.js +5 -0
- package/dist/core/code/shared/scope-resolution/callable-flow-site.js +11 -0
- package/dist/core/code/shared/scope-resolution/def-index.js +53 -0
- package/dist/core/code/shared/scope-resolution/evidence-weights.js +87 -0
- package/dist/core/code/shared/scope-resolution/finalize-algorithm.js +807 -0
- package/dist/core/code/shared/scope-resolution/language-classification.js +46 -0
- package/dist/core/code/shared/scope-resolution/method-dispatch-index.js +100 -0
- package/dist/core/code/shared/scope-resolution/module-scope-index.js +59 -0
- package/dist/core/code/shared/scope-resolution/origin-priority.js +23 -0
- package/dist/core/code/shared/scope-resolution/parsed-file.js +54 -0
- package/dist/core/code/shared/scope-resolution/position-index.js +136 -0
- package/dist/core/code/shared/scope-resolution/qualified-name-index.js +77 -0
- package/dist/core/code/shared/scope-resolution/reference-site.js +24 -0
- package/dist/core/code/shared/scope-resolution/registries/class-registry.js +32 -0
- package/dist/core/code/shared/scope-resolution/registries/context.js +52 -0
- package/dist/core/code/shared/scope-resolution/registries/evidence.js +152 -0
- package/dist/core/code/shared/scope-resolution/registries/field-registry.js +33 -0
- package/dist/core/code/shared/scope-resolution/registries/lookup-core.js +392 -0
- package/dist/core/code/shared/scope-resolution/registries/lookup-qualified.js +58 -0
- package/dist/core/code/shared/scope-resolution/registries/macro-registry.js +34 -0
- package/dist/core/code/shared/scope-resolution/registries/method-registry.js +34 -0
- package/dist/core/code/shared/scope-resolution/registries/tie-breaks.js +63 -0
- package/dist/core/code/shared/scope-resolution/resolve-type-ref.js +128 -0
- package/dist/core/code/shared/scope-resolution/scope-id.js +49 -0
- package/dist/core/code/shared/scope-resolution/scope-tree.js +225 -0
- package/dist/core/code/shared/scope-resolution/symbol-definition.js +12 -0
- package/dist/core/code/shared/scope-resolution/types.js +25 -0
- package/dist/core/code/shared/test-helpers.js +17 -0
- package/dist/core/code/vendor/leiden/index.cjs +355 -0
- package/dist/core/code/vendor/leiden/utils.cjs +419 -0
- package/dist/core/dbquery/cli.d.ts.map +1 -1
- package/dist/core/dbquery/cli.js +5 -4
- package/dist/core/dbquery/cli.js.map +1 -1
- package/dist/core/dbquery/config.d.ts.map +1 -1
- package/dist/core/dbquery/config.js +39 -3
- package/dist/core/dbquery/config.js.map +1 -1
- package/dist/core/dbquery/drivers/http.d.ts +23 -0
- package/dist/core/dbquery/drivers/http.d.ts.map +1 -0
- package/dist/core/dbquery/drivers/http.js +125 -0
- package/dist/core/dbquery/drivers/http.js.map +1 -0
- package/dist/core/dbquery/drivers/mysql.js +1 -1
- package/dist/core/dbquery/drivers/mysql.js.map +1 -1
- package/dist/core/dbquery/drivers/postgres.js +1 -1
- package/dist/core/dbquery/drivers/postgres.js.map +1 -1
- package/dist/core/dbquery/executor.d.ts.map +1 -1
- package/dist/core/dbquery/executor.js +46 -12
- package/dist/core/dbquery/executor.js.map +1 -1
- package/dist/core/dbquery/format.d.ts.map +1 -1
- package/dist/core/dbquery/format.js +2 -1
- package/dist/core/dbquery/format.js.map +1 -1
- package/dist/core/dbquery/init.d.ts.map +1 -1
- package/dist/core/dbquery/init.js +7 -0
- package/dist/core/dbquery/init.js.map +1 -1
- package/dist/core/dbquery/types.d.ts +55 -7
- package/dist/core/dbquery/types.d.ts.map +1 -1
- package/dist/core/directory.d.ts +17 -0
- package/dist/core/directory.d.ts.map +1 -1
- package/dist/core/directory.js +37 -0
- package/dist/core/directory.js.map +1 -1
- package/dist/core/features/cli.d.ts.map +1 -1
- package/dist/core/features/cli.js +3 -2
- package/dist/core/features/cli.js.map +1 -1
- package/dist/core/features/scanner.d.ts +1 -1
- package/dist/core/features/scanner.d.ts.map +1 -1
- package/dist/core/features/scanner.js +20 -4
- package/dist/core/features/scanner.js.map +1 -1
- package/dist/core/markdown/cli.d.ts.map +1 -1
- package/dist/core/markdown/cli.js +9 -8
- package/dist/core/markdown/cli.js.map +1 -1
- package/dist/core/timeline/cli.d.ts.map +1 -1
- package/dist/core/timeline/cli.js +13 -6
- package/dist/core/timeline/cli.js.map +1 -1
- package/dist/core/timeline/debris.d.ts +20 -0
- package/dist/core/timeline/debris.d.ts.map +1 -0
- package/dist/core/timeline/debris.js +124 -0
- package/dist/core/timeline/debris.js.map +1 -0
- package/dist/core/timeline/hook-runner.d.ts.map +1 -1
- package/dist/core/timeline/hook-runner.js +3 -1
- package/dist/core/timeline/hook-runner.js.map +1 -1
- package/dist/core/timeline/hooks.d.ts.map +1 -1
- package/dist/core/timeline/hooks.js +13 -5
- package/dist/core/timeline/hooks.js.map +1 -1
- package/dist/core/timeline/installer.d.ts +2 -0
- package/dist/core/timeline/installer.d.ts.map +1 -1
- package/dist/core/timeline/installer.js +12 -0
- package/dist/core/timeline/installer.js.map +1 -1
- package/dist/core/timeline/project-root.d.ts +21 -0
- package/dist/core/timeline/project-root.d.ts.map +1 -0
- package/dist/core/timeline/project-root.js +83 -0
- package/dist/core/timeline/project-root.js.map +1 -0
- package/dist/web/register.d.ts.map +1 -1
- package/dist/web/register.js +2 -1
- package/dist/web/register.js.map +1 -1
- package/package.json +1 -1
- package/dist/.claude-template/.cgraphx/timeline-current-turn.json +0 -1
- package/dist/.claude-template/skills/.cgraphx/timeline-current-turn.json +0 -1
- package/dist/.claude-template/skills/handdrawn-deck/.cgraphx/timeline-current-turn.json +0 -1
- package/dist/.claude-template/skills/handdrawn-deck/references/.cgraphx/timeline-current-turn.json +0 -1
- package/dist/.claude-template/skills/handdrawn-sketch-engine/.cgraphx/timeline-current-turn.json +0 -1
- /package/dist/.claude-template/skills/run-api-test/assets/{template-test-verify.jsonl → template-/346/216/245/345/217/243/346/265/213/350/257/225/351/252/214/350/257/201.jsonl"} +0 -0
|
@@ -0,0 +1,2703 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.resolveGateRows = exports.readIndexCatalogSnapshot = exports.INDEX_CATALOG_UNREADABLE = exports.readIndexCatalogRows = exports.indexRowType = exports.indexRowTable = exports.createVectorIndex = exports.loadVectorExtension = exports.deleteSpringAutoConfigurationSyntheticClasses = exports.deleteSpringAutoConfigurationDeclarations = exports.deleteSpringAopEvidenceNodes = exports.deleteAllAdvisedBy = exports.deleteAllInjects = exports.deleteAllCallSummaries = exports.deleteAllInterprocTaintPaths = exports.deleteAllCommunitiesAndProcesses = exports.queryImportersBatch = exports.queryImporters = exports.getEmbeddingTableName = exports.deleteNodesForFiles = exports.DELETE_FILES_CHUNK_SIZE = exports.deleteNodesForFile = exports.isLbugReady = exports.wipeLbugDbFiles = exports.LbugWipeError = exports.closeLbug = exports.closeLbugBeforeExit = exports.safeClose = exports.tryFlushWAL = exports.flushWAL = exports.fetchExistingEmbeddingHashes = exports.loadCachedEmbeddings = exports.getLbugStats = exports.executeWithReusedStatement = exports.executePrepared = exports.streamQuery = exports.executeQuery = exports.batchInsertNodesToLbug = exports.insertNodeToLbug = exports.getCopyQuery = exports.fallbackRelationshipInserts = exports.COPY_CSV_OPTS = exports.loadGraphToLbug = exports.withLbugDb = exports.initLbug = exports._initLockPathForTest = exports.acquireInitLock = exports.isReadOnlyDbError = exports.getDatabase = exports.splitRelCsvByLabelPair = void 0;
|
|
40
|
+
exports.ensureEmbeddingRowDmlSafe = void 0;
|
|
41
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
42
|
+
const fs_1 = require("fs");
|
|
43
|
+
const readline_1 = require("readline");
|
|
44
|
+
const events_1 = require("events");
|
|
45
|
+
const promises_2 = require("stream/promises");
|
|
46
|
+
const path_1 = __importDefault(require("path"));
|
|
47
|
+
const lbug = __importStar(require("@ladybugdb/core"));
|
|
48
|
+
const query_result_utils_js_1 = require("./query-result-utils.js");
|
|
49
|
+
const cypher_escape_js_1 = require("./cypher-escape.js");
|
|
50
|
+
const conn_lock_js_1 = require("./conn-lock.js");
|
|
51
|
+
const wal_driver_state_js_1 = require("./wal-driver-state.js");
|
|
52
|
+
const schema_js_1 = require("./schema.js");
|
|
53
|
+
// Analyze-only, but reached from backend startup via `pool-adapter.js`. #2802
|
|
54
|
+
// proposed lazy-importing it; rejected — deferring here relocates the startup
|
|
55
|
+
// cost to first query rather than removing it. The measured figures live in
|
|
56
|
+
// #2802; they were environment-bound, this is not.
|
|
57
|
+
const csv_generator_js_1 = require("./csv-generator.js");
|
|
58
|
+
const rel_pair_routing_js_1 = require("./rel-pair-routing.js");
|
|
59
|
+
// embeddings 子系统已删除——EMBEDDABLE_LABELS 在此局部定义以维持文件路径节点删除
|
|
60
|
+
// 查询的标签匹配(增量写回已删,但全量重建的清表路径仍需枚举可嵌入标签)。
|
|
61
|
+
const EMBEDDABLE_LABELS = [
|
|
62
|
+
'Function', 'Method', 'Constructor', 'Class', 'Interface', 'Struct',
|
|
63
|
+
'Enum', 'Trait', 'Impl', 'Macro',
|
|
64
|
+
'TypeAlias', 'TypeDef', 'Const', 'Property', 'Record', 'Union', 'Static', 'Variable',
|
|
65
|
+
// Kept in sync with DOC_BEARING_LABELS (ast-helpers.ts) so every doc-bearing
|
|
66
|
+
// label's leading-doc description remains semantically matchable (#2270).
|
|
67
|
+
'Namespace',
|
|
68
|
+
];
|
|
69
|
+
const extension_loader_js_1 = require("./extension-loader.js");
|
|
70
|
+
const lbug_config_js_1 = require("./lbug-config.js");
|
|
71
|
+
const sidecar_recovery_js_1 = require("./sidecar-recovery.js");
|
|
72
|
+
const logger_js_1 = require("../logger.js");
|
|
73
|
+
const auto_configuration_js_1 = require("../ingestion/frameworks/spring/auto-configuration.js");
|
|
74
|
+
const aop_js_1 = require("../ingestion/frameworks/spring/aop.js");
|
|
75
|
+
/**
|
|
76
|
+
* Split a relationship CSV into per-label-pair files on disk.
|
|
77
|
+
*
|
|
78
|
+
* @internal RETAINED AS A DIFFERENTIAL ORACLE. As of #2203 U2, production emit
|
|
79
|
+
* routes relationships to per-pair files directly during the single pass (see
|
|
80
|
+
* RelPairRouter in `rel-pair-routing.ts`), so this function has NO production
|
|
81
|
+
* callers — it is kept ONLY so the byte-identity test in
|
|
82
|
+
* `test/integration/csv-pipeline.test.ts` ("direct per-pair emit matches the
|
|
83
|
+
* split oracle") can diff the direct-emit output against this proven path. Do
|
|
84
|
+
* NOT delete it as dead code without also removing that test and accepting the
|
|
85
|
+
* loss of the byte-identity guard (and likewise `test/unit/rel-csv-split.test.ts`).
|
|
86
|
+
*
|
|
87
|
+
* Streams the CSV line-by-line, routing each relationship to a file named
|
|
88
|
+
* `rel_{fromLabel}_{toLabel}.csv`. Handles backpressure correctly: only one
|
|
89
|
+
* drain listener per stream at a time, and readline resumes only when ALL
|
|
90
|
+
* backpressured streams have drained.
|
|
91
|
+
*
|
|
92
|
+
* @param csvPath Path to the combined relationship CSV
|
|
93
|
+
* @param csvDir Directory to write per-pair CSV files
|
|
94
|
+
* @param validTables Set of valid node table names
|
|
95
|
+
* @param getNodeLabel Function to extract the label from a node ID
|
|
96
|
+
* @param wsFactory Optional WriteStream factory (defaults to fs.createWriteStream)
|
|
97
|
+
*/
|
|
98
|
+
const splitRelCsvByLabelPair = async (csvPath, csvDir, validTables, getNodeLabel, wsFactory = (p) => (0, fs_1.createWriteStream)(p, 'utf-8')) => {
|
|
99
|
+
let relHeader = '';
|
|
100
|
+
const relsByPairMeta = new Map();
|
|
101
|
+
const pairWriteStreams = new Map();
|
|
102
|
+
let skippedRels = 0;
|
|
103
|
+
let totalValidRels = 0;
|
|
104
|
+
const inputStream = (0, fs_1.createReadStream)(csvPath, 'utf-8');
|
|
105
|
+
const rl = (0, readline_1.createInterface)({ input: inputStream, crlfDelay: Infinity });
|
|
106
|
+
// If any pair WriteStream errors (disk full, EMFILE, etc.) or the input
|
|
107
|
+
// stream fails, we need to abort the pending `once(ws, 'drain')` await.
|
|
108
|
+
// An AbortController gives us one signal to cancel all pending waits
|
|
109
|
+
// without a custom state machine.
|
|
110
|
+
const abortOnError = new AbortController();
|
|
111
|
+
let streamError = null;
|
|
112
|
+
const markStreamError = (err) => {
|
|
113
|
+
streamError ??= err;
|
|
114
|
+
abortOnError.abort(err);
|
|
115
|
+
};
|
|
116
|
+
try {
|
|
117
|
+
// `for await (const line of rl)` replaces the old manual
|
|
118
|
+
// on('line')/pause()/resume()/waitingForDrain state machine: readline's
|
|
119
|
+
// async iterator naturally serializes line delivery with our awaits, so
|
|
120
|
+
// at most one ws can be in backpressure at a time and we just await its
|
|
121
|
+
// 'drain' event.
|
|
122
|
+
let isFirst = true;
|
|
123
|
+
for await (const line of rl) {
|
|
124
|
+
if (streamError)
|
|
125
|
+
throw streamError;
|
|
126
|
+
if (isFirst) {
|
|
127
|
+
relHeader = line;
|
|
128
|
+
isFirst = false;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (!line.trim())
|
|
132
|
+
continue;
|
|
133
|
+
const match = line.match(/"([^"]*)","([^"]*)"/);
|
|
134
|
+
if (!match) {
|
|
135
|
+
skippedRels++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const fromLabel = getNodeLabel(match[1]);
|
|
139
|
+
const toLabel = getNodeLabel(match[2]);
|
|
140
|
+
if (!validTables.has(fromLabel) || !validTables.has(toLabel)) {
|
|
141
|
+
skippedRels++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const pairKey = `${fromLabel}|${toLabel}`;
|
|
145
|
+
let ws = pairWriteStreams.get(pairKey);
|
|
146
|
+
if (!ws) {
|
|
147
|
+
const pairCsvPath = path_1.default.join(csvDir, `rel_${fromLabel}_${toLabel}.csv`);
|
|
148
|
+
ws = wsFactory(pairCsvPath);
|
|
149
|
+
ws.on('error', markStreamError);
|
|
150
|
+
pairWriteStreams.set(pairKey, ws);
|
|
151
|
+
relsByPairMeta.set(pairKey, { csvPath: pairCsvPath, rows: 0 });
|
|
152
|
+
if (!ws.write(relHeader + '\n')) {
|
|
153
|
+
await (0, events_1.once)(ws, 'drain', { signal: abortOnError.signal });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!ws.write(line + '\n')) {
|
|
157
|
+
await (0, events_1.once)(ws, 'drain', { signal: abortOnError.signal });
|
|
158
|
+
}
|
|
159
|
+
relsByPairMeta.get(pairKey).rows++;
|
|
160
|
+
totalValidRels++;
|
|
161
|
+
}
|
|
162
|
+
if (streamError)
|
|
163
|
+
throw streamError;
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
// Tear down everything so no fd is left dangling. If the abort was caused
|
|
167
|
+
// by a stream error, rethrow that error (more actionable than AbortError).
|
|
168
|
+
for (const ws of pairWriteStreams.values())
|
|
169
|
+
ws.destroy();
|
|
170
|
+
inputStream.destroy();
|
|
171
|
+
throw streamError ?? err;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
// Readline 'close' fires before the underlying fs.ReadStream releases its
|
|
175
|
+
// fd — on Windows that race caused ENOTEMPTY on the parent dir.
|
|
176
|
+
// stream/promises.finished is the stdlib "wait until this stream is fully
|
|
177
|
+
// closed" primitive and handles both success and error paths.
|
|
178
|
+
await (0, promises_2.finished)(inputStream).catch(() => { });
|
|
179
|
+
}
|
|
180
|
+
return { relHeader, relsByPairMeta, pairWriteStreams, skippedRels, totalValidRels };
|
|
181
|
+
};
|
|
182
|
+
exports.splitRelCsvByLabelPair = splitRelCsvByLabelPair;
|
|
183
|
+
let db = null;
|
|
184
|
+
let conn = null;
|
|
185
|
+
// Serialize every operation on the shared singleton `conn`. LadybugDB's
|
|
186
|
+
// Connection is single-writer and is NOT safe for concurrent query execution;
|
|
187
|
+
// the periodic WAL-checkpoint driver overlapping a long `--pdg` COPY on this
|
|
188
|
+
// connection corrupted native state (`double free or corruption`). Each
|
|
189
|
+
// singleton-`conn` helper below runs its full query + drain inside withConnLock.
|
|
190
|
+
// Invariant: a wrapped helper MUST NOT call another wrapped helper (re-entry
|
|
191
|
+
// self-deadlocks); all current holders are leaf-level. `streamQuery` is
|
|
192
|
+
// deliberately NOT wrapped — its per-row callback can re-enter the adapter and
|
|
193
|
+
// it only runs on the read path where the checkpoint driver is inactive.
|
|
194
|
+
// See conn-lock.ts for the full rationale.
|
|
195
|
+
//
|
|
196
|
+
// The gate that decides whether an op must take withConnLock: only operations on
|
|
197
|
+
// the shared singleton `conn` serialize. Per-file / temp connections (distinct
|
|
198
|
+
// native objects with no shared engine state) must NOT block on — or be blocked
|
|
199
|
+
// by — the singleton's lock. Reads the live `conn` binding at call time (it's
|
|
200
|
+
// reassigned only at open/close, never mid-load).
|
|
201
|
+
const isSharedSingletonConn = (c) => c === conn;
|
|
202
|
+
let currentDbPath = null;
|
|
203
|
+
let currentDbReadOnly = false;
|
|
204
|
+
let vectorExtensionLoaded = false;
|
|
205
|
+
// In-process guard so a repeated createVectorIndex() within one connection
|
|
206
|
+
// lifetime skips the DB round-trip. Reset wherever vectorExtensionLoaded
|
|
207
|
+
// resets, so it can never stay true against a swapped or closed connection.
|
|
208
|
+
let vectorIndexEnsured = false;
|
|
209
|
+
/**
|
|
210
|
+
* Check if an error indicates a missing column or table (schema-level problem)
|
|
211
|
+
* rather than a transient/connection error. Used for legacy DB fallback logic.
|
|
212
|
+
*/
|
|
213
|
+
const isMissingColumnOrTableError = (msg) => msg.includes('does not exist') ||
|
|
214
|
+
// Kuzu-specific: "(table|column|property) ... not found" — narrow enough to avoid
|
|
215
|
+
// matching transient errors like "connection not found" or "key not found".
|
|
216
|
+
/(table|column|property).*not found/i.test(msg);
|
|
217
|
+
/** Expose the current Database for pool adapter reuse in tests. */
|
|
218
|
+
const getDatabase = () => db;
|
|
219
|
+
exports.getDatabase = getDatabase;
|
|
220
|
+
// Global session lock for operations that touch module-level lbug globals.
|
|
221
|
+
// This guarantees no DB switch can happen while an operation is running.
|
|
222
|
+
let sessionLock = Promise.resolve();
|
|
223
|
+
/** Number of times to retry on a BUSY / lock-held error before giving up. */
|
|
224
|
+
const DB_LOCK_RETRY_ATTEMPTS = 3;
|
|
225
|
+
/** Base back-off in ms between BUSY retries (multiplied by attempt number). */
|
|
226
|
+
const DB_LOCK_RETRY_DELAY_MS = 500;
|
|
227
|
+
/**
|
|
228
|
+
* Return true when the error message indicates a write was attempted against
|
|
229
|
+
* a read-only LadybugDB connection. The read pool opens DBs read-only, so any
|
|
230
|
+
* path that calls a `CREATE_*` procedure there will surface this. Owners of
|
|
231
|
+
* the writable analyze path should ignore this error — index creation is owned
|
|
232
|
+
* by `cgraph analyze` and either already happened or will happen on the next run.
|
|
233
|
+
*/
|
|
234
|
+
const isReadOnlyDbError = (err) => {
|
|
235
|
+
// Walk the `cause` chain (bounded) so a wrapped read-only error — e.g. the
|
|
236
|
+
// pool adapter's `new Error('…read-only.', { cause: nativeReadOnlyErr })` —
|
|
237
|
+
// is still detected by callers that only see the wrapper (#2068 follow-up).
|
|
238
|
+
// The same strict regex is re-applied at each level, so a non-read-only
|
|
239
|
+
// chain stays false; the depth bound guards a cyclic `cause`.
|
|
240
|
+
let cur = err;
|
|
241
|
+
for (let depth = 0; depth < 5 && cur != null; depth++) {
|
|
242
|
+
const msg = cur instanceof Error ? cur.message : String(cur);
|
|
243
|
+
if (/read-only database/i.test(msg))
|
|
244
|
+
return true;
|
|
245
|
+
cur = cur instanceof Error ? cur.cause : undefined;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
};
|
|
249
|
+
exports.isReadOnlyDbError = isReadOnlyDbError;
|
|
250
|
+
const isMissingFileError = (err) => {
|
|
251
|
+
const errno = err;
|
|
252
|
+
return errno?.code === 'ENOENT';
|
|
253
|
+
};
|
|
254
|
+
const extractErrnoCode = (err) => {
|
|
255
|
+
const errno = err;
|
|
256
|
+
return errno?.code;
|
|
257
|
+
};
|
|
258
|
+
const MAX_LOGGED_ERROR_MESSAGE_LENGTH = 160;
|
|
259
|
+
const summarizeError = (err) => (err instanceof Error ? err.message : String(err)).slice(0, MAX_LOGGED_ERROR_MESSAGE_LENGTH);
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Cross-process init lock
|
|
262
|
+
//
|
|
263
|
+
// Prevents a TOCTOU race in orphan sidecar cleanup: between checking that
|
|
264
|
+
// the main DB file is missing and unlinking sidecars, another process could
|
|
265
|
+
// create a fresh DB. The lock file (`${dbPath}.init.lock`) is created with
|
|
266
|
+
// O_CREAT | O_EXCL (atomic create-or-fail) and contains the owning PID +
|
|
267
|
+
// timestamp so stale locks from crashed processes can be reclaimed.
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
/** Maximum age (ms) before an init lock is considered stale. */
|
|
270
|
+
const INIT_LOCK_STALE_MS = 30_000;
|
|
271
|
+
/** Maximum attempts to acquire the init lock before giving up. */
|
|
272
|
+
const INIT_LOCK_MAX_ATTEMPTS = 6;
|
|
273
|
+
/** Delay between lock-acquisition retries (ms). */
|
|
274
|
+
const INIT_LOCK_RETRY_DELAY_MS = 500;
|
|
275
|
+
const initLockPath = (dbPath) => `${dbPath}.init.lock`;
|
|
276
|
+
/**
|
|
277
|
+
* Returns true when the process identified by `pid` is still running.
|
|
278
|
+
* Uses `process.kill(pid, 0)` which sends signal 0 (a no-op probe) —
|
|
279
|
+
* it throws ESRCH when the process does not exist.
|
|
280
|
+
*/
|
|
281
|
+
const isProcessAlive = (pid) => {
|
|
282
|
+
try {
|
|
283
|
+
process.kill(pid, 0);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Try to break a stale lock whose owning process has exited.
|
|
292
|
+
* Returns `true` if the stale lock was removed (caller should retry acquire).
|
|
293
|
+
* Returns `false` if the lock is still valid (another live process owns it).
|
|
294
|
+
*/
|
|
295
|
+
const tryBreakStaleLock = async (lockPath) => {
|
|
296
|
+
try {
|
|
297
|
+
const content = await promises_1.default.readFile(lockPath, 'utf-8');
|
|
298
|
+
const parsed = JSON.parse(content);
|
|
299
|
+
// If the owning process is still alive AND the lock is not stale, don't break.
|
|
300
|
+
if (typeof parsed.pid === 'number' && isProcessAlive(parsed.pid)) {
|
|
301
|
+
// Even a live process's lock can be stale if it's been held too long
|
|
302
|
+
// (e.g. the process is hung). Check the timestamp.
|
|
303
|
+
if (typeof parsed.ts === 'number' && Date.now() - parsed.ts < INIT_LOCK_STALE_MS) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// PID is gone or lock exceeded INIT_LOCK_STALE_MS — reclaim it.
|
|
308
|
+
await promises_1.default.unlink(lockPath);
|
|
309
|
+
logger_js_1.logger.warn(`GitNexus: removed stale init lock (pid=${parsed.pid ?? '?'}, age=${typeof parsed.ts === 'number' ? `${Date.now() - parsed.ts}ms` : '?'})`);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
// Lock file disappeared between our read and unlink, or is unreadable.
|
|
314
|
+
// Either way, let the caller retry the acquire.
|
|
315
|
+
if (isMissingFileError(err))
|
|
316
|
+
return true;
|
|
317
|
+
// Permission error or corrupt content — log and let caller retry.
|
|
318
|
+
const code = extractErrnoCode(err);
|
|
319
|
+
logger_js_1.logger.warn(`GitNexus: unable to inspect init lock (${code ?? 'UNKNOWN'}): ${summarizeError(err)}`);
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* Acquire a cross-process init lock for `dbPath`.
|
|
325
|
+
* Uses `O_CREAT | O_EXCL` for atomic create-or-fail semantics.
|
|
326
|
+
*
|
|
327
|
+
* Returns a release function that removes the lock file. The release
|
|
328
|
+
* function is idempotent and safe to call even if the lock was already
|
|
329
|
+
* cleaned up externally.
|
|
330
|
+
*
|
|
331
|
+
* Throws if the lock cannot be acquired after `INIT_LOCK_MAX_ATTEMPTS`.
|
|
332
|
+
*/
|
|
333
|
+
const acquireInitLock = async (dbPath) => {
|
|
334
|
+
const lockPath = initLockPath(dbPath);
|
|
335
|
+
const payload = JSON.stringify({ pid: process.pid, ts: Date.now() });
|
|
336
|
+
// Ensure the parent directory exists before creating the lock file.
|
|
337
|
+
// On a fresh repo the `.cgraphx/code/` directory may not exist yet, and
|
|
338
|
+
// fs.open with O_CREAT | O_EXCL would fail with ENOENT.
|
|
339
|
+
await promises_1.default.mkdir(path_1.default.dirname(lockPath), { recursive: true });
|
|
340
|
+
for (let attempt = 1; attempt <= INIT_LOCK_MAX_ATTEMPTS; attempt++) {
|
|
341
|
+
try {
|
|
342
|
+
const handle = await promises_1.default.open(lockPath, fs_1.constants.O_CREAT | fs_1.constants.O_EXCL | fs_1.constants.O_WRONLY);
|
|
343
|
+
await handle.writeFile(payload);
|
|
344
|
+
await handle.close();
|
|
345
|
+
// Return the idempotent release function
|
|
346
|
+
return async () => {
|
|
347
|
+
try {
|
|
348
|
+
await promises_1.default.unlink(lockPath);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
if (!isMissingFileError(err)) {
|
|
352
|
+
const code = extractErrnoCode(err);
|
|
353
|
+
logger_js_1.logger.warn(`GitNexus: failed to release init lock (${code ?? 'UNKNOWN'}): ${summarizeError(err)}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
if (err?.code !== 'EEXIST') {
|
|
360
|
+
throw err; // Unexpected error — propagate immediately
|
|
361
|
+
}
|
|
362
|
+
// Lock file exists — check if it's stale
|
|
363
|
+
const broken = await tryBreakStaleLock(lockPath);
|
|
364
|
+
if (broken && attempt < INIT_LOCK_MAX_ATTEMPTS) {
|
|
365
|
+
continue; // Stale lock removed — retry immediately
|
|
366
|
+
}
|
|
367
|
+
if (attempt === INIT_LOCK_MAX_ATTEMPTS) {
|
|
368
|
+
throw new Error(`GitNexus: unable to acquire init lock after ${INIT_LOCK_MAX_ATTEMPTS} attempts — ` +
|
|
369
|
+
`another gitnexus process may be initializing the same database (${lockPath})`);
|
|
370
|
+
}
|
|
371
|
+
// Live process holds the lock — wait and retry
|
|
372
|
+
await new Promise((resolve) => setTimeout(resolve, INIT_LOCK_RETRY_DELAY_MS));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Unreachable — loop always throws or returns
|
|
376
|
+
throw new Error('GitNexus: init lock acquisition failed unexpectedly');
|
|
377
|
+
};
|
|
378
|
+
exports.acquireInitLock = acquireInitLock;
|
|
379
|
+
/** Exported for testing — returns the lock file path for a given dbPath. */
|
|
380
|
+
exports._initLockPathForTest = initLockPath;
|
|
381
|
+
const runWithSessionLock = async (operation) => {
|
|
382
|
+
const previous = sessionLock;
|
|
383
|
+
let release = null;
|
|
384
|
+
sessionLock = new Promise((resolve) => {
|
|
385
|
+
release = resolve;
|
|
386
|
+
});
|
|
387
|
+
await previous;
|
|
388
|
+
try {
|
|
389
|
+
return await operation();
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
release?.();
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
const normalizeCopyPath = (filePath) => (0, lbug_config_js_1.toNativeSafePath)(filePath).replace(/\\/g, '/');
|
|
396
|
+
// Single-result convenience wrapper over the shared best-effort closer
|
|
397
|
+
// (drainQueryResult / readQueryRows close one cursor at a time).
|
|
398
|
+
const closeQueryResult = async (result) => {
|
|
399
|
+
await (0, query_result_utils_js_1.closeQueryResults)(result);
|
|
400
|
+
};
|
|
401
|
+
const drainQueryResult = async (queryResult) => {
|
|
402
|
+
const results = Array.isArray(queryResult) ? queryResult : [queryResult];
|
|
403
|
+
let firstError;
|
|
404
|
+
let hasError = false;
|
|
405
|
+
for (const result of results) {
|
|
406
|
+
try {
|
|
407
|
+
await result.getAll();
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
if (!hasError) {
|
|
411
|
+
firstError = err;
|
|
412
|
+
hasError = true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
await closeQueryResult(result);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (hasError)
|
|
420
|
+
throw firstError;
|
|
421
|
+
};
|
|
422
|
+
const readQueryRows = async (queryResult) => {
|
|
423
|
+
const results = Array.isArray(queryResult) ? queryResult : [queryResult];
|
|
424
|
+
let rows = [];
|
|
425
|
+
let firstError;
|
|
426
|
+
let hasError = false;
|
|
427
|
+
for (let i = 0; i < results.length; i++) {
|
|
428
|
+
const result = results[i];
|
|
429
|
+
try {
|
|
430
|
+
const resultRows = await result.getAll();
|
|
431
|
+
if (i === 0)
|
|
432
|
+
rows = resultRows;
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
if (!hasError) {
|
|
436
|
+
firstError = err;
|
|
437
|
+
hasError = true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
await closeQueryResult(result);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (hasError)
|
|
445
|
+
throw firstError;
|
|
446
|
+
return rows;
|
|
447
|
+
};
|
|
448
|
+
const queryAndDrain = async (targetConn, cypher) => {
|
|
449
|
+
const run = async () => {
|
|
450
|
+
const queryResult = await targetConn.query(cypher);
|
|
451
|
+
await drainQueryResult(queryResult);
|
|
452
|
+
};
|
|
453
|
+
// Serialize only when this runs on the shared singleton connection (the bulk
|
|
454
|
+
// node/relationship COPY captures `writeConn = conn`); per-file / temp
|
|
455
|
+
// connections skip the lock — see isSharedSingletonConn.
|
|
456
|
+
return isSharedSingletonConn(targetConn) ? (0, conn_lock_js_1.withConnLock)(run) : run();
|
|
457
|
+
};
|
|
458
|
+
// determinism: probe — existence only. Every call site runs this through
|
|
459
|
+
// `queryAndDrain`, which drains and discards the rows; the ONLY observable is
|
|
460
|
+
// whether the read-only shadow replay throws, so no row identity is read.
|
|
461
|
+
const READ_ONLY_SHADOW_REPLAY_PROBE = 'MATCH (n) RETURN n LIMIT 1';
|
|
462
|
+
/**
|
|
463
|
+
* Serve-side entry to the shared WAL-quarantine safety gate. Refuses (throws)
|
|
464
|
+
* when the `.shadow` is present on disk or the orphan WAL is too large to
|
|
465
|
+
* safely discard; returns silently otherwise. The policy itself lives in
|
|
466
|
+
* `guardWalQuarantine` (sidecar-recovery.ts) so serve and the MCP pool share
|
|
467
|
+
* one source of truth (PR #1747 review D2; issue #2382 review, Finding B).
|
|
468
|
+
*/
|
|
469
|
+
const refuseLargeWalQuarantine = async (dbPath, mode, triggeringErr) => {
|
|
470
|
+
await (0, sidecar_recovery_js_1.guardWalQuarantine)(dbPath, mode, triggeringErr, logger_js_1.logger);
|
|
471
|
+
};
|
|
472
|
+
const reopenReadOnlyAfterMissingShadow = async (dbPath, err) => {
|
|
473
|
+
await refuseLargeWalQuarantine(dbPath, 'read-only', err);
|
|
474
|
+
try {
|
|
475
|
+
await (0, sidecar_recovery_js_1.quarantineWalForMissingShadow)(dbPath, {
|
|
476
|
+
logger: logger_js_1.logger,
|
|
477
|
+
level: 'warn',
|
|
478
|
+
reason: 'read-only recovery',
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
catch (renameErr) {
|
|
482
|
+
throw new Error((0, sidecar_recovery_js_1.renameFailureMessage)(dbPath, renameErr));
|
|
483
|
+
}
|
|
484
|
+
const reopened = await (0, lbug_config_js_1.openLbugConnection)(lbug, dbPath, { readOnly: true });
|
|
485
|
+
try {
|
|
486
|
+
await queryAndDrain(reopened.conn, READ_ONLY_SHADOW_REPLAY_PROBE);
|
|
487
|
+
return reopened;
|
|
488
|
+
}
|
|
489
|
+
catch (retryErr) {
|
|
490
|
+
await (0, lbug_config_js_1.closeLbugConnection)(reopened);
|
|
491
|
+
if ((0, sidecar_recovery_js_1.isMissingShadowSidecarError)(retryErr) || (0, sidecar_recovery_js_1.isReadOnlyShadowReplayError)(retryErr)) {
|
|
492
|
+
throw new Error((0, sidecar_recovery_js_1.shadowSidecarRecoveryMessage)(dbPath, retryErr));
|
|
493
|
+
}
|
|
494
|
+
throw retryErr;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
const reopenWritableAfterMissingShadow = async (dbPath, err) => {
|
|
498
|
+
await refuseLargeWalQuarantine(dbPath, 'writable', err);
|
|
499
|
+
try {
|
|
500
|
+
await (0, sidecar_recovery_js_1.quarantineWalForMissingShadow)(dbPath, {
|
|
501
|
+
logger: logger_js_1.logger,
|
|
502
|
+
level: 'warn',
|
|
503
|
+
reason: 'writable recovery',
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
catch (renameErr) {
|
|
507
|
+
throw new Error((0, sidecar_recovery_js_1.renameFailureMessage)(dbPath, renameErr));
|
|
508
|
+
}
|
|
509
|
+
return await (0, lbug_config_js_1.openLbugConnection)(lbug, dbPath);
|
|
510
|
+
};
|
|
511
|
+
const ensureReadOnlyConnectionUsable = async (dbPath, handle) => {
|
|
512
|
+
let shadowReplayErr;
|
|
513
|
+
try {
|
|
514
|
+
await queryAndDrain(handle.conn, READ_ONLY_SHADOW_REPLAY_PROBE);
|
|
515
|
+
return handle;
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
if ((0, sidecar_recovery_js_1.isMissingShadowSidecarError)(err)) {
|
|
519
|
+
await (0, lbug_config_js_1.closeLbugConnection)(handle);
|
|
520
|
+
return await reopenReadOnlyAfterMissingShadow(dbPath, err);
|
|
521
|
+
}
|
|
522
|
+
if (!(0, sidecar_recovery_js_1.isReadOnlyShadowReplayError)(err)) {
|
|
523
|
+
await (0, lbug_config_js_1.closeLbugConnection)(handle);
|
|
524
|
+
throw err;
|
|
525
|
+
}
|
|
526
|
+
shadowReplayErr = err;
|
|
527
|
+
}
|
|
528
|
+
await (0, lbug_config_js_1.closeLbugConnection)(handle);
|
|
529
|
+
let writable;
|
|
530
|
+
try {
|
|
531
|
+
writable = await (0, lbug_config_js_1.openLbugConnection)(lbug, dbPath);
|
|
532
|
+
}
|
|
533
|
+
catch (openErr) {
|
|
534
|
+
const code = extractErrnoCode(openErr);
|
|
535
|
+
if (code === 'EROFS' || code === 'EACCES' || code === 'EPERM') {
|
|
536
|
+
throw new Error((0, sidecar_recovery_js_1.shadowSidecarRecoveryMessage)(dbPath, shadowReplayErr) +
|
|
537
|
+
'\n The workspace appears to be read-only — mount it read-write to perform shadow replay recovery,' +
|
|
538
|
+
' or re-run `cgraph analyze` on a writable filesystem to rebuild the index.');
|
|
539
|
+
}
|
|
540
|
+
throw openErr;
|
|
541
|
+
}
|
|
542
|
+
let missingShadowError;
|
|
543
|
+
try {
|
|
544
|
+
await queryAndDrain(writable.conn, READ_ONLY_SHADOW_REPLAY_PROBE);
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
if ((0, sidecar_recovery_js_1.isMissingShadowSidecarError)(err)) {
|
|
548
|
+
missingShadowError = err;
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
throw err;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
finally {
|
|
555
|
+
await (0, lbug_config_js_1.closeLbugConnection)(writable);
|
|
556
|
+
}
|
|
557
|
+
if (missingShadowError) {
|
|
558
|
+
return await reopenReadOnlyAfterMissingShadow(dbPath, missingShadowError);
|
|
559
|
+
}
|
|
560
|
+
const reopened = await (0, lbug_config_js_1.openLbugConnection)(lbug, dbPath, { readOnly: true });
|
|
561
|
+
try {
|
|
562
|
+
await queryAndDrain(reopened.conn, READ_ONLY_SHADOW_REPLAY_PROBE);
|
|
563
|
+
return reopened;
|
|
564
|
+
}
|
|
565
|
+
catch (err) {
|
|
566
|
+
await (0, lbug_config_js_1.closeLbugConnection)(reopened);
|
|
567
|
+
if ((0, sidecar_recovery_js_1.isMissingShadowSidecarError)(err)) {
|
|
568
|
+
throw new Error((0, sidecar_recovery_js_1.shadowSidecarRecoveryMessage)(dbPath, err));
|
|
569
|
+
}
|
|
570
|
+
throw err;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
const resetOpenConnectionState = () => {
|
|
574
|
+
currentDbPath = null;
|
|
575
|
+
vectorExtensionLoaded = false;
|
|
576
|
+
vectorIndexEnsured = false;
|
|
577
|
+
};
|
|
578
|
+
const runSchemaCreationQueries = async (dbPath) => {
|
|
579
|
+
for (const schemaQuery of schema_js_1.SCHEMA_QUERIES) {
|
|
580
|
+
try {
|
|
581
|
+
await queryAndDrain(conn, schemaQuery);
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
if ((0, sidecar_recovery_js_1.isMissingShadowSidecarError)(err)) {
|
|
585
|
+
return err;
|
|
586
|
+
}
|
|
587
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
588
|
+
// Suppression list:
|
|
589
|
+
// - "already exists": expected idempotent re-create on existing DBs
|
|
590
|
+
// - "could not set lock on file": LadybugDB v0.18.0 emits this on
|
|
591
|
+
// Windows when CREATE NODE TABLE runs against a path that was
|
|
592
|
+
// just opened (the WAL handle from a fresh Database briefly
|
|
593
|
+
// contests the table's first-write lock). The table is created
|
|
594
|
+
// anyway and any genuine cross-process lock contention surfaces
|
|
595
|
+
// on the next operation via withLbugDb's retry. Logging it here
|
|
596
|
+
// would just be noise in CI.
|
|
597
|
+
//
|
|
598
|
+
// WAL corruption: the first DDL write after DB open triggers WAL
|
|
599
|
+
// replay — if the WAL file was left in a corrupt state by an
|
|
600
|
+
// interrupted previous run, the native engine throws here. Rather
|
|
601
|
+
// than logging a WARN and continuing in a broken state, close the
|
|
602
|
+
// DB cleanly and surface an actionable error so the caller (serve,
|
|
603
|
+
// MCP, analyze) can exit with a clear recovery message.
|
|
604
|
+
if ((0, lbug_config_js_1.isWalCorruptionError)(err)) {
|
|
605
|
+
await (0, exports.safeClose)();
|
|
606
|
+
resetOpenConnectionState();
|
|
607
|
+
throw new Error(`LadybugDB WAL corruption detected at ${dbPath}. ${lbug_config_js_1.WAL_RECOVERY_SUGGESTION}\n` +
|
|
608
|
+
` Original error: ${msg.slice(0, 200)}`);
|
|
609
|
+
}
|
|
610
|
+
if (!msg.includes('already exists') && !(0, lbug_config_js_1.isDbBusyError)(err) && !(0, exports.isReadOnlyDbError)(err)) {
|
|
611
|
+
logger_js_1.logger.warn(`⚠️ Schema creation warning: ${msg.slice(0, 120)}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
};
|
|
617
|
+
const initLbug = async (dbPath) => {
|
|
618
|
+
return runWithSessionLock(() => ensureLbugInitialized(dbPath));
|
|
619
|
+
};
|
|
620
|
+
exports.initLbug = initLbug;
|
|
621
|
+
/**
|
|
622
|
+
* Execute multiple queries against one repo DB atomically.
|
|
623
|
+
* While the callback runs, no other request can switch the active DB.
|
|
624
|
+
*
|
|
625
|
+
* Automatically retries up to DB_LOCK_RETRY_ATTEMPTS times when the
|
|
626
|
+
* database is busy (e.g. `cgraph analyze` holds the write lock).
|
|
627
|
+
* Each retry waits DB_LOCK_RETRY_DELAY_MS * attempt milliseconds.
|
|
628
|
+
*/
|
|
629
|
+
const withLbugDb = async (dbPath, operation, options = {}) => {
|
|
630
|
+
let lastError;
|
|
631
|
+
const readOnly = options.readOnly === true;
|
|
632
|
+
for (let attempt = 1; attempt <= DB_LOCK_RETRY_ATTEMPTS; attempt++) {
|
|
633
|
+
try {
|
|
634
|
+
return await runWithSessionLock(async () => {
|
|
635
|
+
await ensureLbugInitialized(dbPath, readOnly);
|
|
636
|
+
return operation();
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
lastError = err;
|
|
641
|
+
// Skip outer retry when the inner open-retry already exhausted: the
|
|
642
|
+
// ~1.5s open-time budget was just spent, repeating the full reset+
|
|
643
|
+
// reopen cycle would only add 4-5s of tail latency without changing
|
|
644
|
+
// the outcome (both layers consult the same isDbBusyError matcher).
|
|
645
|
+
if (!(0, lbug_config_js_1.isDbBusyError)(err) || (0, lbug_config_js_1.isOpenRetryExhausted)(err) || attempt === DB_LOCK_RETRY_ATTEMPTS) {
|
|
646
|
+
throw err;
|
|
647
|
+
}
|
|
648
|
+
// Close stale connection inside the session lock to prevent race conditions
|
|
649
|
+
// with concurrent operations that might acquire the lock between cleanup steps
|
|
650
|
+
await runWithSessionLock(async () => {
|
|
651
|
+
await (0, exports.safeClose)();
|
|
652
|
+
currentDbPath = null;
|
|
653
|
+
vectorExtensionLoaded = false;
|
|
654
|
+
vectorIndexEnsured = false;
|
|
655
|
+
});
|
|
656
|
+
// Sleep outside the lock — no need to block others while waiting
|
|
657
|
+
await new Promise((resolve) => setTimeout(resolve, DB_LOCK_RETRY_DELAY_MS * attempt));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// This line is unreachable — the loop either returns or throws inside,
|
|
661
|
+
// but TypeScript needs an explicit throw to satisfy the return type.
|
|
662
|
+
throw lastError;
|
|
663
|
+
};
|
|
664
|
+
exports.withLbugDb = withLbugDb;
|
|
665
|
+
const ensureLbugInitialized = async (dbPath, readOnly = false) => {
|
|
666
|
+
if (conn && currentDbPath === dbPath && currentDbReadOnly === readOnly) {
|
|
667
|
+
return { db, conn };
|
|
668
|
+
}
|
|
669
|
+
await doInitLbug(dbPath, readOnly);
|
|
670
|
+
return { db, conn };
|
|
671
|
+
};
|
|
672
|
+
const doInitLbug = async (dbPath, readOnly = false) => {
|
|
673
|
+
// Different database requested — close the old one first
|
|
674
|
+
if (conn || db) {
|
|
675
|
+
await (0, exports.safeClose)();
|
|
676
|
+
currentDbPath = null;
|
|
677
|
+
vectorExtensionLoaded = false;
|
|
678
|
+
vectorIndexEnsured = false;
|
|
679
|
+
}
|
|
680
|
+
// ---------------------------------------------------------------------------
|
|
681
|
+
// Read-only fast path: skip all filesystem mutations (path cleanup, init
|
|
682
|
+
// lock, orphan sidecar removal, mkdir) so the open succeeds on read-only
|
|
683
|
+
// filesystems such as Docker `:ro` bind mounts. The init lock exists to
|
|
684
|
+
// prevent a TOCTOU race during DB *creation* — read-only opens never
|
|
685
|
+
// create databases and don't need the lock.
|
|
686
|
+
// ---------------------------------------------------------------------------
|
|
687
|
+
if (readOnly) {
|
|
688
|
+
await (0, sidecar_recovery_js_1.preflightLbugSidecars)(dbPath, {
|
|
689
|
+
mode: 'read-only',
|
|
690
|
+
logger: logger_js_1.logger,
|
|
691
|
+
allowQuarantine: false,
|
|
692
|
+
});
|
|
693
|
+
const opened = await (0, lbug_config_js_1.openLbugConnection)(lbug, dbPath, { readOnly: true });
|
|
694
|
+
const usable = await ensureReadOnlyConnectionUsable(dbPath, opened);
|
|
695
|
+
db = usable.db;
|
|
696
|
+
conn = usable.conn;
|
|
697
|
+
currentDbReadOnly = true;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
// LadybugDB stores the database as a single file (not a directory).
|
|
701
|
+
// If the path already exists, it must be a valid LadybugDB database file.
|
|
702
|
+
// Remove stale empty directories or files from older versions.
|
|
703
|
+
try {
|
|
704
|
+
const stat = await promises_1.default.lstat(dbPath);
|
|
705
|
+
if (stat.isSymbolicLink()) {
|
|
706
|
+
// Never follow symlinks — just remove the link itself
|
|
707
|
+
await promises_1.default.unlink(dbPath);
|
|
708
|
+
}
|
|
709
|
+
else if (stat.isDirectory()) {
|
|
710
|
+
// Verify path is within expected storage directory before deleting
|
|
711
|
+
const realPath = await promises_1.default.realpath(dbPath);
|
|
712
|
+
const parentDir = path_1.default.dirname(dbPath);
|
|
713
|
+
const realParent = await promises_1.default.realpath(parentDir);
|
|
714
|
+
const safePrefix = realParent.endsWith(path_1.default.sep) ? realParent : realParent + path_1.default.sep;
|
|
715
|
+
if (!realPath.startsWith(safePrefix) && realPath !== realParent) {
|
|
716
|
+
throw new Error(`Refusing to delete ${dbPath}: resolved path ${realPath} is outside storage directory`);
|
|
717
|
+
}
|
|
718
|
+
// Old-style directory database or empty leftover - remove it
|
|
719
|
+
await promises_1.default.rm(dbPath, { recursive: true, force: true });
|
|
720
|
+
}
|
|
721
|
+
// If it's a file, assume it's an existing LadybugDB database - LadybugDB will open it
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
if (!isMissingFileError(err)) {
|
|
725
|
+
throw err;
|
|
726
|
+
}
|
|
727
|
+
// Path doesn't exist, which is what LadybugDB wants for a new database
|
|
728
|
+
}
|
|
729
|
+
// -------------------------------------------------------------------------
|
|
730
|
+
// Cross-process critical section: acquire init lock, clean orphan sidecars,
|
|
731
|
+
// and open the database. The lock prevents a TOCTOU race where another
|
|
732
|
+
// process could create a fresh DB between our access() check and the
|
|
733
|
+
// unlink() of stale sidecars.
|
|
734
|
+
// -------------------------------------------------------------------------
|
|
735
|
+
const releaseInitLock = await (0, exports.acquireInitLock)(dbPath);
|
|
736
|
+
try {
|
|
737
|
+
// Reclaim missing-shadow WAL quarantines from a PRIOR crash (#2637).
|
|
738
|
+
// LadybugDB renames an unrecoverable WAL aside as
|
|
739
|
+
// `${dbPath}.wal.missing-shadow.<ts>-<rand>` (quarantineWalForMissingShadow)
|
|
740
|
+
// instead of deleting it. Once quarantined it is permanently detached from
|
|
741
|
+
// the live store and never reopened, so reclaiming it is safe regardless of
|
|
742
|
+
// whether the main DB file exists this run — unlike the orphan-sidecar
|
|
743
|
+
// cleanup below, this must NOT be gated on "main DB missing": a quarantine
|
|
744
|
+
// event and a healthy main DB are independent facts. Never let a reclaim
|
|
745
|
+
// failure (e.g. a transient EBUSY from an antivirus scan) block DB startup.
|
|
746
|
+
if (!(0, sidecar_recovery_js_1.sidecarPreflightDisabled)()) {
|
|
747
|
+
try {
|
|
748
|
+
const reclaimed = await (0, sidecar_recovery_js_1.cleanQuarantinedMissingShadowWals)(dbPath);
|
|
749
|
+
for (const file of reclaimed) {
|
|
750
|
+
logger_js_1.logger.warn(`GitNexus: reclaimed quarantined WAL ${path_1.default.basename(file)} from a prior crash`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
catch (err) {
|
|
754
|
+
logger_js_1.logger.warn(`GitNexus: failed to reclaim missing-shadow WAL quarantines: ${summarizeError(err)}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// Crash-recovery cleanup: if the main DB file is missing, stale sidecars
|
|
758
|
+
// from an interrupted run can block fresh opens indefinitely.
|
|
759
|
+
try {
|
|
760
|
+
await promises_1.default.access(dbPath);
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
if (isMissingFileError(err)) {
|
|
764
|
+
// `.shadow` is documented by LadybugDB checkpointing and `.wal.checkpoint`
|
|
765
|
+
// was observed in the #1618 crash loop that motivated this recovery path.
|
|
766
|
+
const orphanSidecars = [`${dbPath}.shadow`, `${dbPath}.wal.checkpoint`];
|
|
767
|
+
for (const sidecar of orphanSidecars) {
|
|
768
|
+
try {
|
|
769
|
+
await promises_1.default.unlink(sidecar);
|
|
770
|
+
logger_js_1.logger.warn(`GitNexus: removed orphan sidecar ${path_1.default.basename(sidecar)} (no main DB file present)`);
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
if (isMissingFileError(err)) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const code = extractErrnoCode(err);
|
|
777
|
+
logger_js_1.logger.warn(`GitNexus: failed to remove orphan sidecar ${path_1.default.basename(sidecar)} (${code ?? 'UNKNOWN'}) while main DB file is missing; LadybugDB open may still fail: ${summarizeError(err)}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
const code = extractErrnoCode(err);
|
|
783
|
+
logger_js_1.logger.warn(`GitNexus: unable to verify main DB file before orphan sidecar cleanup (${code ?? 'UNKNOWN'}); skipping cleanup: ${summarizeError(err)}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// Ensure parent directory exists
|
|
787
|
+
const parentDir = path_1.default.dirname(dbPath);
|
|
788
|
+
await promises_1.default.mkdir(parentDir, { recursive: true });
|
|
789
|
+
await (0, sidecar_recovery_js_1.preflightLbugSidecars)(dbPath, {
|
|
790
|
+
mode: 'write',
|
|
791
|
+
logger: logger_js_1.logger,
|
|
792
|
+
allowQuarantine: true,
|
|
793
|
+
});
|
|
794
|
+
const opened = await (0, lbug_config_js_1.openLbugConnection)(lbug, dbPath);
|
|
795
|
+
db = opened.db;
|
|
796
|
+
conn = opened.conn;
|
|
797
|
+
currentDbReadOnly = false;
|
|
798
|
+
}
|
|
799
|
+
finally {
|
|
800
|
+
await releaseInitLock();
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (!readOnly) {
|
|
804
|
+
const missingShadowError = await runSchemaCreationQueries(dbPath);
|
|
805
|
+
if (missingShadowError) {
|
|
806
|
+
await (0, exports.safeClose)();
|
|
807
|
+
resetOpenConnectionState();
|
|
808
|
+
const reopened = await reopenWritableAfterMissingShadow(dbPath, missingShadowError);
|
|
809
|
+
db = reopened.db;
|
|
810
|
+
conn = reopened.conn;
|
|
811
|
+
currentDbReadOnly = false;
|
|
812
|
+
const retryMissingShadowError = await runSchemaCreationQueries(dbPath);
|
|
813
|
+
if (retryMissingShadowError) {
|
|
814
|
+
await (0, exports.safeClose)();
|
|
815
|
+
resetOpenConnectionState();
|
|
816
|
+
throw new Error((0, sidecar_recovery_js_1.shadowSidecarRecoveryMessage)(dbPath, retryMissingShadowError));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
currentDbPath = dbPath;
|
|
821
|
+
return { db, conn };
|
|
822
|
+
};
|
|
823
|
+
/**
|
|
824
|
+
* Run a COPY, retrying once with IGNORE_ERRORS=true (which skips row-level
|
|
825
|
+
* errors) on first failure. On a second failure, hand the RAW retry error to
|
|
826
|
+
* `onError` — each call site formats + slices its own message (#2226 F5: node
|
|
827
|
+
* COPY slices to 200 chars and throws; relationship COPY slices to 80 and warns,
|
|
828
|
+
* so the helper must not pre-format and lose that distinction). `onError` may
|
|
829
|
+
* throw to propagate the failure.
|
|
830
|
+
*/
|
|
831
|
+
const copyCsvWithRetry = async (targetConn, copyQuery, onError) => {
|
|
832
|
+
try {
|
|
833
|
+
await queryAndDrain(targetConn, copyQuery);
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
try {
|
|
837
|
+
const retryQuery = copyQuery.replace('auto_detect=false)', 'auto_detect=false, IGNORE_ERRORS=true)');
|
|
838
|
+
await queryAndDrain(targetConn, retryQuery);
|
|
839
|
+
}
|
|
840
|
+
catch (retryErr) {
|
|
841
|
+
onError(retryErr);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
/**
|
|
846
|
+
* Bulk-COPY every node CSV sequentially on the single writable connection
|
|
847
|
+
* (LadybugDB allows one write txn at a time). Extracted from loadGraphToLbug so
|
|
848
|
+
* it can run either at the node-phase boundary — overlapping the relationship
|
|
849
|
+
* emit pass (#2203) — or after emit in the serial escape-hatch path. Each COPY
|
|
850
|
+
* keeps the IGNORE_ERRORS=true retry; a hard failure throws (no node rows ⇒ the
|
|
851
|
+
* relationship COPY would dangle on missing endpoints).
|
|
852
|
+
*/
|
|
853
|
+
const copyNodeCSVs = async (targetConn, nodeFileEntries, log, totalSteps) => {
|
|
854
|
+
let stepsDone = 0;
|
|
855
|
+
for (const [table, { csvPath, rows }] of nodeFileEntries) {
|
|
856
|
+
stepsDone++;
|
|
857
|
+
log(`Loading nodes ${stepsDone}/${totalSteps}: ${table} (${rows.toLocaleString()} rows)`);
|
|
858
|
+
const copyQuery = (0, exports.getCopyQuery)(table, normalizeCopyPath(csvPath));
|
|
859
|
+
await copyCsvWithRetry(targetConn, copyQuery, (retryErr) => {
|
|
860
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
861
|
+
// Pool exhaustion gets a remedy (#2631): the raw binder text gives the
|
|
862
|
+
// operator nothing to act on, and on non-4K-page hosts (Ascend aarch64,
|
|
863
|
+
// Apple Silicon) the pool bills up to pageSize/4KiB x faster than the
|
|
864
|
+
// sizing was calibrated for — name the knob and the mechanism.
|
|
865
|
+
const remedy = (0, lbug_config_js_1.bufferPoolExhaustionRemedy)(retryMsg);
|
|
866
|
+
throw new Error(`COPY failed for ${table}: ${retryMsg.slice(0, 200)}${remedy ? ` ${remedy}` : ''}`);
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
/**
|
|
871
|
+
* Persist a KnowledgeGraph: stream CSVs, then bulk-COPY nodes (overlapped with
|
|
872
|
+
* relationship emit — see the body) and relationships.
|
|
873
|
+
*
|
|
874
|
+
* NOT TRANSACTIONAL (#2226). Each `COPY` commits independently and there is no
|
|
875
|
+
* surrounding transaction, so a failure partway through — a node `COPY` that
|
|
876
|
+
* throws at the FK barrier, or a relationship `COPY` failure —
|
|
877
|
+
* leaves a partially-loaded DB. The caller surfaces the error; recovery is a
|
|
878
|
+
* re-analyze (a full rebuild), not a partial retry. Callers must not
|
|
879
|
+
* assume the DB is either fully loaded or untouched after a rejection.
|
|
880
|
+
*/
|
|
881
|
+
const loadGraphToLbug = async (graph, repoPath, storagePath, onProgress,
|
|
882
|
+
/**
|
|
883
|
+
* Streamed structural-emit manifest (#2680). These pair keys are NOT disjoint
|
|
884
|
+
* from the whole-graph emit's: a streamed `CALLS` edge is `Function|Function`,
|
|
885
|
+
* exactly like the retained edges `streamAllCSVsToDisk` just wrote. So these
|
|
886
|
+
* files are APPENDED as additional COPY jobs for the same pair rather than
|
|
887
|
+
* merged into `relsByPair` (a Map, which holds one CSV per pair and would
|
|
888
|
+
* silently drop one of them).
|
|
889
|
+
*/
|
|
890
|
+
graphEmitManifest) => {
|
|
891
|
+
if (!conn) {
|
|
892
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
893
|
+
}
|
|
894
|
+
const log = onProgress || (() => { });
|
|
895
|
+
// ── #2203 persistence-path profiling ──────────────────────────────────
|
|
896
|
+
// Mirrors the PROF_SCOPE_RESOLUTION pattern (scope-resolution/pipeline/
|
|
897
|
+
// run.ts): zero-cost when off — process.hrtime.bigint() is only read under
|
|
898
|
+
// PROF_LBUG_LOAD=1, and the summary is logged behind the same gate. Fills
|
|
899
|
+
// the gap that the DB-persistence path is un-timed today (the analyze
|
|
900
|
+
// "emit" number is the scope-resolution emit bucket, not this COPY path).
|
|
901
|
+
const PROF = process.env.PROF_LBUG_LOAD === '1';
|
|
902
|
+
// Escape hatch / differential oracle (#2203): force the legacy strictly-serial
|
|
903
|
+
// load order (emit everything, THEN COPY nodes, THEN COPY rels) instead of the
|
|
904
|
+
// default node-COPY ‖ rel-emit overlap. Lets an operator revert the behavior at
|
|
905
|
+
// runtime, and lets a test load the same graph both ways and assert identical
|
|
906
|
+
// persisted content.
|
|
907
|
+
const SERIAL = process.env.CGRAPH_SERIAL_LBUG_LOAD === '1';
|
|
908
|
+
const mark = () => (PROF ? process.hrtime.bigint() : 0n);
|
|
909
|
+
const span = (a, b) => (Number(b - a) / 1e6).toFixed(1);
|
|
910
|
+
const tStart = mark();
|
|
911
|
+
const csvDir = (0, lbug_config_js_1.resolveNativeSafeStorageDir)(storagePath, 'csv');
|
|
912
|
+
// The single writable connection (LadybugDB is single-writer). Captured as a
|
|
913
|
+
// const so the node-COPY closure has a non-null reference — TS cannot narrow
|
|
914
|
+
// the reassignable module-level `conn` across the callback boundary.
|
|
915
|
+
const writeConn = conn;
|
|
916
|
+
const validTables = new Set(schema_js_1.NODE_TABLES);
|
|
917
|
+
// Node COPY is the only DB write that can overlap relationship CSV emit: the
|
|
918
|
+
// rel pass writes new rel_*.csv files and never touches `conn`, while node COPY
|
|
919
|
+
// uses `conn` and never touches the rel files. We start node COPY at the
|
|
920
|
+
// node-phase boundary and let the rel pass run concurrently — the only
|
|
921
|
+
// single-writer-safe parallelism (#2203). The rel COPY still waits for node
|
|
922
|
+
// COPY (FK precondition), so the DB load order is unchanged.
|
|
923
|
+
let nodeCopyPromise;
|
|
924
|
+
let nodeCopyError;
|
|
925
|
+
const beginNodeCopy = (nodeFilesMap) => {
|
|
926
|
+
const entries = [...nodeFilesMap.entries()];
|
|
927
|
+
// copyNodeCSVs logs node progress as step/total; it processes only node
|
|
928
|
+
// tables (the rel COPY has its own "Loading edges" progress line), so the
|
|
929
|
+
// denominator is the node-table count — not +1 reserving a rel step.
|
|
930
|
+
// .catch captures the failure so an overlapped (mid-emit) rejection cannot
|
|
931
|
+
// surface as an unhandled rejection; it is rethrown at the FK barrier below.
|
|
932
|
+
nodeCopyPromise = copyNodeCSVs(writeConn, entries, log, entries.length).catch((e) => {
|
|
933
|
+
nodeCopyError = e;
|
|
934
|
+
});
|
|
935
|
+
};
|
|
936
|
+
log('Streaming CSVs to disk...');
|
|
937
|
+
let csvResult;
|
|
938
|
+
try {
|
|
939
|
+
csvResult = SERIAL
|
|
940
|
+
? await (0, csv_generator_js_1.streamAllCSVsToDisk)(graph, repoPath, csvDir)
|
|
941
|
+
: await (0, csv_generator_js_1.streamAllCSVsToDisk)(graph, repoPath, csvDir, beginNodeCopy);
|
|
942
|
+
}
|
|
943
|
+
catch (emitErr) {
|
|
944
|
+
// Relationship emit failed. In overlap mode a node COPY may be in flight —
|
|
945
|
+
// settle it (the .catch above means this never rejects) before rethrowing so
|
|
946
|
+
// it cannot leak as an unhandled rejection.
|
|
947
|
+
if (nodeCopyPromise)
|
|
948
|
+
await nodeCopyPromise;
|
|
949
|
+
// If node COPY ALSO failed, emitErr wins the throw — log the swallowed node
|
|
950
|
+
// error so a half-loaded DB isn't misattributed to the emit failure alone.
|
|
951
|
+
if (nodeCopyError) {
|
|
952
|
+
logger_js_1.logger.warn({ err: nodeCopyError }, '[lbug-load] node COPY also failed while relationship emit was failing');
|
|
953
|
+
}
|
|
954
|
+
throw emitErr;
|
|
955
|
+
}
|
|
956
|
+
const tCsv = mark();
|
|
957
|
+
// Serial path: all CSVs are on disk and node COPY has not started — start it
|
|
958
|
+
// here so the barrier below blocks on it exactly as the legacy path did.
|
|
959
|
+
if (SERIAL)
|
|
960
|
+
beginNodeCopy(csvResult.nodeFiles);
|
|
961
|
+
// FK barrier: node rows must exist before the relationship COPY resolves their
|
|
962
|
+
// endpoints. In overlap mode most of node COPY was hidden behind rel emit, so
|
|
963
|
+
// this await is the *residual* node-COPY time (≈0 when fully overlapped).
|
|
964
|
+
if (nodeCopyPromise)
|
|
965
|
+
await nodeCopyPromise;
|
|
966
|
+
if (nodeCopyError) {
|
|
967
|
+
throw nodeCopyError instanceof Error ? nodeCopyError : new Error(String(nodeCopyError));
|
|
968
|
+
}
|
|
969
|
+
const tCopyNodes = mark();
|
|
970
|
+
// Bulk COPY relationships. They were already routed to per-FROM→TO-label-pair
|
|
971
|
+
// files during the emit pass (#2203 U2) — there is no monolithic relations.csv
|
|
972
|
+
// to re-read/re-split here; we COPY each pair file directly.
|
|
973
|
+
const { relsByPair, relHeader, skippedRels, totalValidRels } = csvResult;
|
|
974
|
+
let tCopyRels = tCopyNodes;
|
|
975
|
+
let tFallback = tCopyNodes;
|
|
976
|
+
// One COPY job per CSV FILE, not per label pair. The whole-graph emit writes
|
|
977
|
+
// at most one file per pair, but the streamed structural manifest (#2680) can
|
|
978
|
+
// contribute a second file for a pair the whole-graph emit also wrote — both
|
|
979
|
+
// must load. `relsByPair` stays a one-file-per-pair Map so the PDG merge above
|
|
980
|
+
// and every other consumer are untouched.
|
|
981
|
+
const copyJobs = [];
|
|
982
|
+
for (const [pairKey, meta] of relsByPair) {
|
|
983
|
+
copyJobs.push({ pairKey, csvPath: meta.csvPath, rows: meta.rows });
|
|
984
|
+
}
|
|
985
|
+
if (graphEmitManifest) {
|
|
986
|
+
for (const [pairKey, meta] of graphEmitManifest.relsByPair) {
|
|
987
|
+
copyJobs.push({ pairKey, csvPath: meta.csvPath, rows: meta.rows });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const insertedRels = totalValidRels + (graphEmitManifest?.totalRows ?? 0);
|
|
991
|
+
const warnings = [];
|
|
992
|
+
let poolRemedyIssued = false;
|
|
993
|
+
if (insertedRels > 0) {
|
|
994
|
+
log(`Loading edges: ${insertedRels.toLocaleString()} across ${copyJobs.length} CSV files`);
|
|
995
|
+
let pairIdx = 0;
|
|
996
|
+
let failedPairEdges = 0;
|
|
997
|
+
const failedPairCsvPaths = new Set();
|
|
998
|
+
for (const { pairKey, csvPath: pairCsvPath, rows } of copyJobs) {
|
|
999
|
+
pairIdx++;
|
|
1000
|
+
const [fromLabel, toLabel] = pairKey.split('|');
|
|
1001
|
+
const normalizedPath = normalizeCopyPath(pairCsvPath);
|
|
1002
|
+
// PARALLEL=false is load-bearing here too — see COPY_CSV_OPTS (#2203 / kuzudb/kuzu#5778).
|
|
1003
|
+
const copyQuery = `COPY ${schema_js_1.REL_TABLE_NAME} FROM "${normalizedPath}" (from="${fromLabel}", to="${toLabel}", HEADER=true, ESCAPE='"', DELIM=',', QUOTE='"', PARALLEL=false, auto_detect=false)`;
|
|
1004
|
+
if (pairIdx % 5 === 0 || rows > 1000) {
|
|
1005
|
+
log(`Loading edges: ${pairIdx}/${copyJobs.length} files (${fromLabel} -> ${toLabel})`);
|
|
1006
|
+
}
|
|
1007
|
+
// Use the captured `writeConn` (not the module-level `conn`) for the rel
|
|
1008
|
+
// COPY, matching the node COPY above — one captured reference for the whole
|
|
1009
|
+
// bulk load (#2264 review P3). Same object during analyze (`conn` is only
|
|
1010
|
+
// reassigned at open/close under the session lock, never mid-load), so the
|
|
1011
|
+
// queryAndDrain `targetConn === conn` lock gate still engages.
|
|
1012
|
+
await copyCsvWithRetry(writeConn, copyQuery, (retryErr) => {
|
|
1013
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
1014
|
+
warnings.push(`${fromLabel}->${toLabel} (${rows} edges): ${retryMsg.slice(0, 80)}`);
|
|
1015
|
+
// One remedy per bulk load, not per pair (#2631): pool exhaustion
|
|
1016
|
+
// repeats for every remaining pair once it starts. logger.warn, not
|
|
1017
|
+
// just warnings.push — the returned warnings array has no consumer at
|
|
1018
|
+
// any call site, so a push alone would leave the remedy invisible
|
|
1019
|
+
// while the row-by-row fallback quietly degrades the load.
|
|
1020
|
+
const remedy = poolRemedyIssued ? undefined : (0, lbug_config_js_1.bufferPoolExhaustionRemedy)(retryMsg);
|
|
1021
|
+
if (remedy) {
|
|
1022
|
+
poolRemedyIssued = true;
|
|
1023
|
+
warnings.push(remedy);
|
|
1024
|
+
logger_js_1.logger.warn(remedy);
|
|
1025
|
+
}
|
|
1026
|
+
failedPairEdges += rows;
|
|
1027
|
+
failedPairCsvPaths.add(pairCsvPath);
|
|
1028
|
+
});
|
|
1029
|
+
// Only delete if not in failedPairCsvPaths (needed for fallback)
|
|
1030
|
+
if (!failedPairCsvPaths.has(pairCsvPath)) {
|
|
1031
|
+
try {
|
|
1032
|
+
await promises_1.default.unlink(pairCsvPath);
|
|
1033
|
+
}
|
|
1034
|
+
catch { }
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
tCopyRels = mark();
|
|
1038
|
+
if (failedPairCsvPaths.size > 0) {
|
|
1039
|
+
log(`Inserting ${failedPairEdges} edges individually (missing schema pairs)`);
|
|
1040
|
+
// Read failed pair files and merge for fallback inserts
|
|
1041
|
+
const allLines = [relHeader];
|
|
1042
|
+
for (const failedPath of failedPairCsvPaths) {
|
|
1043
|
+
try {
|
|
1044
|
+
const content = await promises_1.default.readFile(failedPath, 'utf-8');
|
|
1045
|
+
const lines = content.split('\n');
|
|
1046
|
+
// Skip header line (first) and empty lines
|
|
1047
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1048
|
+
if (lines[i].trim())
|
|
1049
|
+
allLines.push(lines[i]);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
catch { }
|
|
1053
|
+
try {
|
|
1054
|
+
await promises_1.default.unlink(failedPath);
|
|
1055
|
+
}
|
|
1056
|
+
catch { }
|
|
1057
|
+
}
|
|
1058
|
+
if (allLines.length > 1) {
|
|
1059
|
+
await (0, exports.fallbackRelationshipInserts)(allLines, validTables, rel_pair_routing_js_1.getNodeLabel);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
tFallback = mark();
|
|
1063
|
+
}
|
|
1064
|
+
// Cleanup all CSVs (per-pair rel files are unlinked in the COPY loop above;
|
|
1065
|
+
// the remaining sweep below catches node CSVs + any leftover pair files).
|
|
1066
|
+
for (const [, { csvPath }] of csvResult.nodeFiles) {
|
|
1067
|
+
try {
|
|
1068
|
+
await promises_1.default.unlink(csvPath);
|
|
1069
|
+
}
|
|
1070
|
+
catch { }
|
|
1071
|
+
}
|
|
1072
|
+
try {
|
|
1073
|
+
const remaining = await promises_1.default.readdir(csvDir);
|
|
1074
|
+
for (const f of remaining) {
|
|
1075
|
+
try {
|
|
1076
|
+
await promises_1.default.unlink(path_1.default.join(csvDir, f));
|
|
1077
|
+
}
|
|
1078
|
+
catch { }
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
catch { }
|
|
1082
|
+
try {
|
|
1083
|
+
await promises_1.default.rmdir(csvDir);
|
|
1084
|
+
}
|
|
1085
|
+
catch { }
|
|
1086
|
+
if (PROF) {
|
|
1087
|
+
const tEnd = mark();
|
|
1088
|
+
let totalNodeRows = 0;
|
|
1089
|
+
for (const [, { rows }] of csvResult.nodeFiles)
|
|
1090
|
+
totalNodeRows += rows;
|
|
1091
|
+
// `mode` records which load path ran. In overlap mode `csv-emit` is the wall
|
|
1092
|
+
// to streamAllCSVsToDisk's return (node COPY overlapped part of it) and
|
|
1093
|
+
// `copy-nodes` is the RESIDUAL node-COPY await after emit returned — it
|
|
1094
|
+
// trends to 0 as the overlap hides node COPY behind relationship emit. In
|
|
1095
|
+
// serial mode the buckets carry their legacy, disjoint meaning.
|
|
1096
|
+
logger_js_1.logger.warn(`[lbug-load prof] mode=${SERIAL ? 'serial' : 'overlap'} csv-emit=${span(tStart, tCsv)}ms ` +
|
|
1097
|
+
`copy-nodes=${span(tCsv, tCopyNodes)}ms copy-rels=${span(tCopyNodes, tCopyRels)}ms ` +
|
|
1098
|
+
`fallback=${span(tCopyRels, tFallback)}ms total=${span(tStart, tEnd)}ms ` +
|
|
1099
|
+
`(${totalNodeRows} nodes, ${insertedRels} rels)`);
|
|
1100
|
+
}
|
|
1101
|
+
return { success: true, insertedRels, skippedRels, warnings };
|
|
1102
|
+
};
|
|
1103
|
+
exports.loadGraphToLbug = loadGraphToLbug;
|
|
1104
|
+
// LadybugDB default ESCAPE is '\' (backslash), but our CSV uses RFC 4180 escaping ("" for literal quotes).
|
|
1105
|
+
// Source code content is full of backslashes which confuse the auto-detection.
|
|
1106
|
+
// We MUST explicitly set ESCAPE='"' to use RFC 4180 escaping, and disable auto_detect to prevent
|
|
1107
|
+
// LadybugDB from overriding our settings based on sample rows.
|
|
1108
|
+
//
|
|
1109
|
+
// PARALLEL=false IS LOAD-BEARING FOR CORRECTNESS — DO NOT FLIP IT (#2203).
|
|
1110
|
+
// LadybugDB's parallel CSV reader (Kuzu-derived; default PARALLEL=true) splits the
|
|
1111
|
+
// file into byte ranges parsed concurrently, and CANNOT determine line boundaries
|
|
1112
|
+
// when a quoted field contains an embedded newline — it errors with "Quoted newlines
|
|
1113
|
+
// are not supported in parallel CSV reader. Please specify PARALLEL=FALSE", or worse,
|
|
1114
|
+
// mis-parses silently (upstream kuzudb/kuzu#5778, still open). Our `content`/`text`
|
|
1115
|
+
// columns hold source code, so quoted multiline fields are guaranteed. PARALLEL=false
|
|
1116
|
+
// is therefore required, not conservative. The multiline-quoted round-trip in
|
|
1117
|
+
// test/integration/copy-parallel-invariant.test.ts fails loudly if this is ever flipped.
|
|
1118
|
+
// Exported so that test asserts the invariant statically as well.
|
|
1119
|
+
exports.COPY_CSV_OPTS = `(HEADER=true, ESCAPE='"', DELIM=',', QUOTE='"', PARALLEL=false, auto_detect=false)`;
|
|
1120
|
+
// Multi-language table names that were created with backticks in CODE_ELEMENT_BASE
|
|
1121
|
+
// and must always be referenced with backticks in queries
|
|
1122
|
+
const BACKTICK_TABLES = new Set([
|
|
1123
|
+
'Struct',
|
|
1124
|
+
'Enum',
|
|
1125
|
+
'Macro',
|
|
1126
|
+
'Typedef',
|
|
1127
|
+
'Union',
|
|
1128
|
+
'Namespace',
|
|
1129
|
+
'Trait',
|
|
1130
|
+
'Impl',
|
|
1131
|
+
'TypeAlias',
|
|
1132
|
+
'Const',
|
|
1133
|
+
'Static',
|
|
1134
|
+
'Property',
|
|
1135
|
+
'Record',
|
|
1136
|
+
'Delegate',
|
|
1137
|
+
'Annotation',
|
|
1138
|
+
'Constructor',
|
|
1139
|
+
'Template',
|
|
1140
|
+
'Module',
|
|
1141
|
+
]);
|
|
1142
|
+
const escapeTableName = (table) => {
|
|
1143
|
+
return BACKTICK_TABLES.has(table) ? `\`${table}\`` : table;
|
|
1144
|
+
};
|
|
1145
|
+
/**
|
|
1146
|
+
* Format one JS value as a Cypher literal for the adapter's string-built
|
|
1147
|
+
* statements: NULL/undefined → `NULL`, numbers pass through unquoted,
|
|
1148
|
+
* everything else becomes a single-quoted string literal escaped via
|
|
1149
|
+
* {@link escapeCypherString} (backslashes first, then quotes).
|
|
1150
|
+
*
|
|
1151
|
+
* Replaces three per-function closures that used SQL-style `''` doubling —
|
|
1152
|
+
* LadybugDB REJECTS doubling, so every value containing a quote made the
|
|
1153
|
+
* whole statement a parser error, invisible wherever the call site swallowed
|
|
1154
|
+
* per-row failures (#2409 escaping sweep, completed for tri-review
|
|
1155
|
+
* 4669518496 P2-2). Those closures also rewrote literal `\n`/`\r` into
|
|
1156
|
+
* two-character escape sequences; raw LF/CR are legal inside LadybugDB
|
|
1157
|
+
* single-quoted literals (live-probed on @ladybugdb/core 0.18.0), so the
|
|
1158
|
+
* replaces are gone and content now round-trips byte-identical.
|
|
1159
|
+
*/
|
|
1160
|
+
const formatCypherValue = (v) => {
|
|
1161
|
+
if (v === null || v === undefined)
|
|
1162
|
+
return 'NULL';
|
|
1163
|
+
if (typeof v === 'number')
|
|
1164
|
+
return String(v);
|
|
1165
|
+
return `'${(0, cypher_escape_js_1.escapeCypherString)(String(v))}'`;
|
|
1166
|
+
};
|
|
1167
|
+
const formatCypherStringArray = (value) => {
|
|
1168
|
+
const items = Array.isArray(value)
|
|
1169
|
+
? value.filter((item) => typeof item === 'string')
|
|
1170
|
+
: [];
|
|
1171
|
+
return `[${items.map(formatCypherValue).join(', ')}]`;
|
|
1172
|
+
};
|
|
1173
|
+
/**
|
|
1174
|
+
* Fallback: insert relationships one-by-one if COPY fails.
|
|
1175
|
+
*
|
|
1176
|
+
* Exported for the quoted-id round-trip tests in
|
|
1177
|
+
* `test/integration/lbug-core-adapter.test.ts` (the `DELETE_FILES_CHUNK_SIZE`
|
|
1178
|
+
* exported-for-tests precedent); production callers stay in this module.
|
|
1179
|
+
* Bails silently when the adapter singleton is closed.
|
|
1180
|
+
*
|
|
1181
|
+
* KNOWN PRE-EXISTING NARROWING (distinct from the `''` escaping bug, NOT
|
|
1182
|
+
* fixed here): the row regex below matches CSV fields with `[^"]*`, so an id
|
|
1183
|
+
* containing a double quote (CSV-escaped as `""`) never matches and the edge
|
|
1184
|
+
* is skipped. Tracked as part of the quote-in-id divergence documented in
|
|
1185
|
+
* `rel-pair-routing.ts`.
|
|
1186
|
+
*/
|
|
1187
|
+
const fallbackRelationshipInserts = async (validRelLines, validTables, getNodeLabel) => {
|
|
1188
|
+
if (!conn)
|
|
1189
|
+
return;
|
|
1190
|
+
const escapeLabel = (label) => {
|
|
1191
|
+
return BACKTICK_TABLES.has(label) ? `\`${label}\`` : label;
|
|
1192
|
+
};
|
|
1193
|
+
for (let i = 1; i < validRelLines.length; i++) {
|
|
1194
|
+
const line = validRelLines[i];
|
|
1195
|
+
try {
|
|
1196
|
+
const match = line.match(/"([^"]*)","([^"]*)","([^"]*)",([0-9.]+),"([^"]*)",([0-9-]+)/);
|
|
1197
|
+
if (!match)
|
|
1198
|
+
continue;
|
|
1199
|
+
const [, fromId, toId, relType, confidenceStr, reason, stepStr] = match;
|
|
1200
|
+
const fromLabel = getNodeLabel(fromId);
|
|
1201
|
+
const toLabel = getNodeLabel(toId);
|
|
1202
|
+
if (!validTables.has(fromLabel) || !validTables.has(toLabel))
|
|
1203
|
+
continue;
|
|
1204
|
+
const confidence = parseFloat(confidenceStr) || 1.0;
|
|
1205
|
+
const step = parseInt(stepStr) || 0;
|
|
1206
|
+
await queryAndDrain(conn, `
|
|
1207
|
+
MATCH (a:${escapeLabel(fromLabel)} {id: ${formatCypherValue(fromId)} }),
|
|
1208
|
+
(b:${escapeLabel(toLabel)} {id: ${formatCypherValue(toId)} })
|
|
1209
|
+
CREATE (a)-[:${schema_js_1.REL_TABLE_NAME} {type: ${formatCypherValue(relType)}, confidence: ${confidence}, reason: ${formatCypherValue(reason)}, step: ${step}}]->(b)
|
|
1210
|
+
`);
|
|
1211
|
+
}
|
|
1212
|
+
catch {
|
|
1213
|
+
// skip
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
exports.fallbackRelationshipInserts = fallbackRelationshipInserts;
|
|
1218
|
+
/** Tables with isExported column (TypeScript/JS-native types) */
|
|
1219
|
+
const TABLES_WITH_EXPORTED = new Set([
|
|
1220
|
+
'Function',
|
|
1221
|
+
'Class',
|
|
1222
|
+
'Interface',
|
|
1223
|
+
'Method',
|
|
1224
|
+
'CodeElement',
|
|
1225
|
+
]);
|
|
1226
|
+
const getCopyQuery = (table, filePath) => {
|
|
1227
|
+
const t = escapeTableName(table);
|
|
1228
|
+
if (table === 'File') {
|
|
1229
|
+
return `COPY ${t}(id, name, filePath, content) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1230
|
+
}
|
|
1231
|
+
if (table === 'Folder') {
|
|
1232
|
+
return `COPY ${t}(id, name, filePath) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1233
|
+
}
|
|
1234
|
+
if (table === 'Community') {
|
|
1235
|
+
return `COPY ${t}(id, label, heuristicLabel, keywords, description, enrichedBy, cohesion, symbolCount) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1236
|
+
}
|
|
1237
|
+
if (table === 'Process') {
|
|
1238
|
+
return `COPY ${t}(id, label, heuristicLabel, processType, stepCount, communities, entryPointId, terminalId) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1239
|
+
}
|
|
1240
|
+
if (table === 'Section') {
|
|
1241
|
+
return `COPY ${t}(id, name, filePath, startLine, endLine, level, content, description) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1242
|
+
}
|
|
1243
|
+
if (table === 'Route') {
|
|
1244
|
+
return `COPY ${t}(id, name, filePath, responseKeys, errorKeys, middleware, method, handlerSymbolId) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1245
|
+
}
|
|
1246
|
+
if (table === 'Tool') {
|
|
1247
|
+
return `COPY ${t}(id, name, filePath, description) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1248
|
+
}
|
|
1249
|
+
if (table === 'BasicBlock') {
|
|
1250
|
+
// Taint/PDG substrate (issue #2080) — no name column. `callees` is the
|
|
1251
|
+
// statement-precise inter-procedural reach substrate (space-joined leaf names);
|
|
1252
|
+
// `calleeIds` is its SOUND parallel (space-joined resolved callee ids, #2227).
|
|
1253
|
+
return `COPY ${t}(id, filePath, startLine, endLine, text, callees, calleeIds) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1254
|
+
}
|
|
1255
|
+
if (table === 'Class') {
|
|
1256
|
+
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description, frameworkAnnotations) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1257
|
+
}
|
|
1258
|
+
if (table === 'Method') {
|
|
1259
|
+
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description, parameterCount, returnType) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1260
|
+
}
|
|
1261
|
+
if (table === 'Property') {
|
|
1262
|
+
return `COPY ${t}(id, name, filePath, startLine, endLine, content, description, declaredType) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1263
|
+
}
|
|
1264
|
+
// TypeScript/JS code element tables have isExported; multi-language tables do not
|
|
1265
|
+
if (TABLES_WITH_EXPORTED.has(table)) {
|
|
1266
|
+
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1267
|
+
}
|
|
1268
|
+
// Multi-language tables (Struct, Impl, Trait, Macro, etc.)
|
|
1269
|
+
return `COPY ${t}(id, name, filePath, startLine, endLine, content, description) FROM "${filePath}" ${exports.COPY_CSV_OPTS}`;
|
|
1270
|
+
};
|
|
1271
|
+
exports.getCopyQuery = getCopyQuery;
|
|
1272
|
+
/**
|
|
1273
|
+
* Insert a single node to LadybugDB
|
|
1274
|
+
* @param label - Node type (File, Function, Class, etc.)
|
|
1275
|
+
* @param properties - Node properties
|
|
1276
|
+
* @param dbPath - Path to LadybugDB database (optional if already initialized)
|
|
1277
|
+
*/
|
|
1278
|
+
const insertNodeToLbug = async (label, properties, dbPath) => {
|
|
1279
|
+
// Use provided dbPath or fall back to module-level db
|
|
1280
|
+
const targetDbPath = dbPath || (db ? undefined : null);
|
|
1281
|
+
if (!targetDbPath && !db) {
|
|
1282
|
+
throw new Error('LadybugDB not initialized. Provide dbPath or call initLbug first.');
|
|
1283
|
+
}
|
|
1284
|
+
try {
|
|
1285
|
+
// Values go through the module-scope formatCypherValue — the old local
|
|
1286
|
+
// closure used `''` doubling, which LadybugDB rejects (#2409 escaping
|
|
1287
|
+
// sweep, tri-review 4669518496 P2-2).
|
|
1288
|
+
// Build INSERT query based on node type
|
|
1289
|
+
const t = escapeTableName(label);
|
|
1290
|
+
let query;
|
|
1291
|
+
if (label === 'File') {
|
|
1292
|
+
query = `CREATE (n:File {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, content: ${formatCypherValue(properties.content || '')}})`;
|
|
1293
|
+
}
|
|
1294
|
+
else if (label === 'Folder') {
|
|
1295
|
+
query = `CREATE (n:Folder {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}})`;
|
|
1296
|
+
}
|
|
1297
|
+
else if (label === 'Section') {
|
|
1298
|
+
const descPart = properties.description
|
|
1299
|
+
? `, description: ${formatCypherValue(properties.description)}`
|
|
1300
|
+
: '';
|
|
1301
|
+
query = `CREATE (n:Section {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, level: ${properties.level || 1}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
|
|
1302
|
+
}
|
|
1303
|
+
else if (label === 'BasicBlock') {
|
|
1304
|
+
// Taint/PDG substrate (issue #2080) — no name column. `calleeIds` (#2227)
|
|
1305
|
+
// is the sound resolved-id parallel to the leaf-name `callees` set.
|
|
1306
|
+
query = `CREATE (n:BasicBlock {id: ${formatCypherValue(properties.id)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, text: ${formatCypherValue(properties.text || '')}, callees: ${formatCypherValue(properties.callees || '')}, calleeIds: ${formatCypherValue(properties.calleeIds || '')}})`;
|
|
1307
|
+
}
|
|
1308
|
+
else if (label === 'Class') {
|
|
1309
|
+
const descPart = properties.description
|
|
1310
|
+
? `, description: ${formatCypherValue(properties.description)}`
|
|
1311
|
+
: '';
|
|
1312
|
+
query = `CREATE (n:Class {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${formatCypherValue(properties.content || '')}${descPart}, frameworkAnnotations: ${formatCypherStringArray(properties.frameworkAnnotations)}})`;
|
|
1313
|
+
}
|
|
1314
|
+
else if (TABLES_WITH_EXPORTED.has(label)) {
|
|
1315
|
+
const descPart = properties.description
|
|
1316
|
+
? `, description: ${formatCypherValue(properties.description)}`
|
|
1317
|
+
: '';
|
|
1318
|
+
query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
|
|
1319
|
+
}
|
|
1320
|
+
else if (label === 'Property') {
|
|
1321
|
+
const descPart = properties.description
|
|
1322
|
+
? `, description: ${formatCypherValue(properties.description)}`
|
|
1323
|
+
: '';
|
|
1324
|
+
query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${formatCypherValue(properties.content || '')}${descPart}, declaredType: ${formatCypherValue(properties.declaredType || '')}})`;
|
|
1325
|
+
}
|
|
1326
|
+
else {
|
|
1327
|
+
// Multi-language tables (Struct, Impl, Trait, Macro, etc.) — no isExported
|
|
1328
|
+
const descPart = properties.description
|
|
1329
|
+
? `, description: ${formatCypherValue(properties.description)}`
|
|
1330
|
+
: '';
|
|
1331
|
+
query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
|
|
1332
|
+
}
|
|
1333
|
+
// Use per-query connection if dbPath provided (avoids lock conflicts)
|
|
1334
|
+
if (targetDbPath) {
|
|
1335
|
+
const tempHandle = await (0, lbug_config_js_1.openLbugConnection)(lbug, targetDbPath);
|
|
1336
|
+
try {
|
|
1337
|
+
await queryAndDrain(tempHandle.conn, query);
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
finally {
|
|
1341
|
+
await (0, lbug_config_js_1.closeLbugConnection)(tempHandle);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
else if (conn) {
|
|
1345
|
+
// Use existing persistent connection (when called from analyze)
|
|
1346
|
+
await queryAndDrain(conn, query);
|
|
1347
|
+
return true;
|
|
1348
|
+
}
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
catch (e) {
|
|
1352
|
+
// Node may already exist or other error
|
|
1353
|
+
logger_js_1.logger.error({ err: e.message }, `Failed to insert ${label} node:`);
|
|
1354
|
+
return false;
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
exports.insertNodeToLbug = insertNodeToLbug;
|
|
1358
|
+
/**
|
|
1359
|
+
* Batch insert multiple nodes to LadybugDB using a single connection
|
|
1360
|
+
* @param nodes - Array of {label, properties} to insert
|
|
1361
|
+
* @param dbPath - Path to LadybugDB database
|
|
1362
|
+
* @returns Object with success count and error count
|
|
1363
|
+
*/
|
|
1364
|
+
const batchInsertNodesToLbug = async (nodes, dbPath) => {
|
|
1365
|
+
if (nodes.length === 0)
|
|
1366
|
+
return { inserted: 0, failed: 0 };
|
|
1367
|
+
// Values go through the module-scope formatCypherValue — the old local
|
|
1368
|
+
// closure used `''` doubling, which LadybugDB rejects; the per-node catch
|
|
1369
|
+
// below counted every quoted value as a silent `failed` (#2409 escaping
|
|
1370
|
+
// sweep, tri-review 4669518496 P2-2).
|
|
1371
|
+
// Open a single connection for all inserts
|
|
1372
|
+
const tempHandle = await (0, lbug_config_js_1.openLbugConnection)(lbug, dbPath);
|
|
1373
|
+
const tempConn = tempHandle.conn;
|
|
1374
|
+
let inserted = 0;
|
|
1375
|
+
let failed = 0;
|
|
1376
|
+
try {
|
|
1377
|
+
for (const { label, properties } of nodes) {
|
|
1378
|
+
try {
|
|
1379
|
+
let query;
|
|
1380
|
+
// Use MERGE instead of CREATE for upsert behavior (handles duplicates gracefully)
|
|
1381
|
+
const t = escapeTableName(label);
|
|
1382
|
+
if (label === 'File') {
|
|
1383
|
+
query = `MERGE (n:File {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.content = ${formatCypherValue(properties.content || '')}`;
|
|
1384
|
+
}
|
|
1385
|
+
else if (label === 'Folder') {
|
|
1386
|
+
query = `MERGE (n:Folder {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}`;
|
|
1387
|
+
}
|
|
1388
|
+
else if (label === 'Section') {
|
|
1389
|
+
const descPart = properties.description
|
|
1390
|
+
? `, n.description = ${formatCypherValue(properties.description)}`
|
|
1391
|
+
: '';
|
|
1392
|
+
query = `MERGE (n:Section {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.level = ${properties.level || 1}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
|
|
1393
|
+
}
|
|
1394
|
+
else if (label === 'BasicBlock') {
|
|
1395
|
+
// Taint/PDG substrate (issue #2080) — no name column. `calleeIds`
|
|
1396
|
+
// (#2227) is the sound resolved-id parallel to the `callees` set.
|
|
1397
|
+
query = `MERGE (n:BasicBlock {id: ${formatCypherValue(properties.id)}}) SET n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.text = ${formatCypherValue(properties.text || '')}, n.callees = ${formatCypherValue(properties.callees || '')}, n.calleeIds = ${formatCypherValue(properties.calleeIds || '')}`;
|
|
1398
|
+
}
|
|
1399
|
+
else if (label === 'Class') {
|
|
1400
|
+
const descPart = properties.description
|
|
1401
|
+
? `, n.description = ${formatCypherValue(properties.description)}`
|
|
1402
|
+
: '';
|
|
1403
|
+
query = `MERGE (n:Class {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${formatCypherValue(properties.content || '')}${descPart}, n.frameworkAnnotations = ${formatCypherStringArray(properties.frameworkAnnotations)}`;
|
|
1404
|
+
}
|
|
1405
|
+
else if (TABLES_WITH_EXPORTED.has(label)) {
|
|
1406
|
+
const descPart = properties.description
|
|
1407
|
+
? `, n.description = ${formatCypherValue(properties.description)}`
|
|
1408
|
+
: '';
|
|
1409
|
+
query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
|
|
1410
|
+
}
|
|
1411
|
+
else if (label === 'Property') {
|
|
1412
|
+
const descPart = properties.description
|
|
1413
|
+
? `, n.description = ${formatCypherValue(properties.description)}`
|
|
1414
|
+
: '';
|
|
1415
|
+
query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${formatCypherValue(properties.content || '')}${descPart}, n.declaredType = ${formatCypherValue(properties.declaredType || '')}`;
|
|
1416
|
+
}
|
|
1417
|
+
else {
|
|
1418
|
+
const descPart = properties.description
|
|
1419
|
+
? `, n.description = ${formatCypherValue(properties.description)}`
|
|
1420
|
+
: '';
|
|
1421
|
+
query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
|
|
1422
|
+
}
|
|
1423
|
+
await queryAndDrain(tempConn, query);
|
|
1424
|
+
inserted++;
|
|
1425
|
+
}
|
|
1426
|
+
catch (e) {
|
|
1427
|
+
// Don't console.error here - it corrupts MCP JSON-RPC on stderr
|
|
1428
|
+
failed++;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
finally {
|
|
1433
|
+
await (0, lbug_config_js_1.closeLbugConnection)(tempHandle);
|
|
1434
|
+
}
|
|
1435
|
+
return { inserted, failed };
|
|
1436
|
+
};
|
|
1437
|
+
exports.batchInsertNodesToLbug = batchInsertNodesToLbug;
|
|
1438
|
+
const executeQuery = async (cypher) => {
|
|
1439
|
+
return await (0, exports.executePrepared)(cypher, {});
|
|
1440
|
+
};
|
|
1441
|
+
exports.executeQuery = executeQuery;
|
|
1442
|
+
const streamQuery = async (cypher, onRow) => {
|
|
1443
|
+
if ((0, wal_driver_state_js_1.isWalDriverActive)()) {
|
|
1444
|
+
// streamQuery reads rows on the singleton connection WITHOUT withConnLock; if
|
|
1445
|
+
// the WAL-checkpoint driver is live, those reads could race a CHECKPOINT — the
|
|
1446
|
+
// #2264 corruption window. Today the serve/read path never runs the driver
|
|
1447
|
+
// (analyze runs in a forked worker), so this fails loud only if a future
|
|
1448
|
+
// in-process analyze overlaps a stream. Run analysis in a worker, or stop the
|
|
1449
|
+
// driver before streaming. See conn-lock.ts.
|
|
1450
|
+
throw new Error('streamQuery cannot run while the WAL-checkpoint driver is active (it would ' +
|
|
1451
|
+
'race a CHECKPOINT on the unlocked read connection — #2264).');
|
|
1452
|
+
}
|
|
1453
|
+
if (!conn) {
|
|
1454
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
1455
|
+
}
|
|
1456
|
+
const queryResult = await conn.query(cypher);
|
|
1457
|
+
const results = Array.isArray(queryResult) ? queryResult : [queryResult];
|
|
1458
|
+
const result = results[0];
|
|
1459
|
+
let rowCount = 0;
|
|
1460
|
+
let streamError;
|
|
1461
|
+
try {
|
|
1462
|
+
while (await result.hasNext()) {
|
|
1463
|
+
const row = await result.getNext();
|
|
1464
|
+
await onRow(row);
|
|
1465
|
+
rowCount++;
|
|
1466
|
+
}
|
|
1467
|
+
return rowCount;
|
|
1468
|
+
}
|
|
1469
|
+
catch (err) {
|
|
1470
|
+
streamError = err;
|
|
1471
|
+
throw err;
|
|
1472
|
+
}
|
|
1473
|
+
finally {
|
|
1474
|
+
try {
|
|
1475
|
+
await drainQueryResult(results);
|
|
1476
|
+
}
|
|
1477
|
+
catch (err) {
|
|
1478
|
+
if (streamError === undefined)
|
|
1479
|
+
throw err;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
exports.streamQuery = streamQuery;
|
|
1484
|
+
/**
|
|
1485
|
+
* Execute a single parameterized query (prepare/execute pattern).
|
|
1486
|
+
* Prevents Cypher injection by binding values as parameters.
|
|
1487
|
+
*/
|
|
1488
|
+
const executePrepared = async (cypher, params) => {
|
|
1489
|
+
const c = conn;
|
|
1490
|
+
if (!c) {
|
|
1491
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
1492
|
+
}
|
|
1493
|
+
return (0, conn_lock_js_1.withConnLock)(async () => {
|
|
1494
|
+
const stmt = await c.prepare(cypher);
|
|
1495
|
+
if (!stmt.isSuccess()) {
|
|
1496
|
+
const errMsg = await stmt.getErrorMessage();
|
|
1497
|
+
throw new Error(`Prepare failed: ${errMsg}`);
|
|
1498
|
+
}
|
|
1499
|
+
const queryResult = await c.execute(stmt, params);
|
|
1500
|
+
return await readQueryRows(queryResult);
|
|
1501
|
+
});
|
|
1502
|
+
};
|
|
1503
|
+
exports.executePrepared = executePrepared;
|
|
1504
|
+
const executeWithReusedStatement = async (cypher, paramsList) => {
|
|
1505
|
+
const c = conn;
|
|
1506
|
+
if (!c) {
|
|
1507
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
1508
|
+
}
|
|
1509
|
+
if (paramsList.length === 0)
|
|
1510
|
+
return;
|
|
1511
|
+
const SUB_BATCH_SIZE = 4;
|
|
1512
|
+
for (let i = 0; i < paramsList.length; i += SUB_BATCH_SIZE) {
|
|
1513
|
+
const subBatch = paramsList.slice(i, i + SUB_BATCH_SIZE);
|
|
1514
|
+
// One critical section per sub-batch: the prepare + its executes run with
|
|
1515
|
+
// exclusive access to the connection (so the WAL checkpoint driver cannot
|
|
1516
|
+
// interleave a CHECKPOINT mid-batch), while the lock is released between
|
|
1517
|
+
// sub-batches to let the driver checkpoint during a long writeback.
|
|
1518
|
+
await (0, conn_lock_js_1.withConnLock)(async () => {
|
|
1519
|
+
const stmt = await c.prepare(cypher);
|
|
1520
|
+
if (!stmt.isSuccess()) {
|
|
1521
|
+
const errMsg = await stmt.getErrorMessage();
|
|
1522
|
+
throw new Error(`Prepare failed: ${errMsg}`);
|
|
1523
|
+
}
|
|
1524
|
+
try {
|
|
1525
|
+
for (const params of subBatch) {
|
|
1526
|
+
await drainQueryResult(await c.execute(stmt, params));
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
catch (e) {
|
|
1530
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1531
|
+
const queryPreview = cypher.replace(/\s+/g, ' ').slice(0, 120);
|
|
1532
|
+
throw new Error(`Batch execution failed for rows ${i + 1}-${i + subBatch.length}: ${msg} (${queryPreview})`);
|
|
1533
|
+
}
|
|
1534
|
+
// Note: LadybugDB PreparedStatement doesn't require explicit close()
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
exports.executeWithReusedStatement = executeWithReusedStatement;
|
|
1539
|
+
const getLbugStats = async () => {
|
|
1540
|
+
const c = conn;
|
|
1541
|
+
if (!c)
|
|
1542
|
+
return { nodes: 0, edges: 0 };
|
|
1543
|
+
// Called during analyze finalize while the WAL-checkpoint driver is still
|
|
1544
|
+
// running; each count read takes the connection lock so it cannot execute
|
|
1545
|
+
// concurrently with a driver CHECKPOINT. Per-query locking lets the driver
|
|
1546
|
+
// checkpoint between table counts rather than waiting for the whole sweep.
|
|
1547
|
+
let totalNodes = 0;
|
|
1548
|
+
for (const tableName of schema_js_1.NODE_TABLES) {
|
|
1549
|
+
try {
|
|
1550
|
+
totalNodes += await (0, conn_lock_js_1.withConnLock)(async () => {
|
|
1551
|
+
const queryResult = await c.query(`MATCH (n:${escapeTableName(tableName)}) RETURN count(n) AS cnt`);
|
|
1552
|
+
const nodeRows = await readQueryRows(queryResult);
|
|
1553
|
+
return nodeRows.length > 0 ? Number(nodeRows[0]?.cnt ?? nodeRows[0]?.[0] ?? 0) : 0;
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
catch {
|
|
1557
|
+
// ignore
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
let totalEdges = 0;
|
|
1561
|
+
try {
|
|
1562
|
+
totalEdges = await (0, conn_lock_js_1.withConnLock)(async () => {
|
|
1563
|
+
const queryResult = await c.query(`MATCH ()-[r:${schema_js_1.REL_TABLE_NAME}]->() RETURN count(r) AS cnt`);
|
|
1564
|
+
const edgeRows = await readQueryRows(queryResult);
|
|
1565
|
+
return edgeRows.length > 0 ? Number(edgeRows[0]?.cnt ?? edgeRows[0]?.[0] ?? 0) : 0;
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
catch {
|
|
1569
|
+
// ignore
|
|
1570
|
+
}
|
|
1571
|
+
return { nodes: totalNodes, edges: totalEdges };
|
|
1572
|
+
};
|
|
1573
|
+
exports.getLbugStats = getLbugStats;
|
|
1574
|
+
/**
|
|
1575
|
+
* embeddings 子系统已删除——恒返回空缓存。
|
|
1576
|
+
*/
|
|
1577
|
+
const loadCachedEmbeddings = async () => {
|
|
1578
|
+
return { embeddingNodeIds: new Set(), embeddings: [] };
|
|
1579
|
+
};
|
|
1580
|
+
exports.loadCachedEmbeddings = loadCachedEmbeddings;
|
|
1581
|
+
/**
|
|
1582
|
+
* Fetch existing embedding hashes from CodeEmbedding table for incremental embedding.
|
|
1583
|
+
* Returns a Map<nodeId, contentHash> suitable for passing to `runEmbeddingPipeline`.
|
|
1584
|
+
* Handles legacy DBs without the `contentHash` column (all rows treated as stale with empty hash).
|
|
1585
|
+
* Returns undefined if the CodeEmbedding table does not exist.
|
|
1586
|
+
*
|
|
1587
|
+
* @param execQuery - Cypher query executor (typically pool-adapter's `executeQuery`)
|
|
1588
|
+
*/
|
|
1589
|
+
const fetchExistingEmbeddingHashes = async (execQuery) => {
|
|
1590
|
+
try {
|
|
1591
|
+
const rows = await execQuery(`MATCH (e:${schema_js_1.EMBEDDING_TABLE_NAME}) RETURN e.nodeId AS nodeId, e.chunkIndex AS chunkIndex, e.startLine AS startLine, e.endLine AS endLine, e.contentHash AS contentHash`);
|
|
1592
|
+
if (!rows || rows.length === 0)
|
|
1593
|
+
return undefined;
|
|
1594
|
+
const map = new Map();
|
|
1595
|
+
for (const r of rows) {
|
|
1596
|
+
const nodeId = r.nodeId ?? r[0];
|
|
1597
|
+
const chunkIndex = r.chunkIndex ?? r[1];
|
|
1598
|
+
const startLine = r.startLine ?? r[2];
|
|
1599
|
+
const endLine = r.endLine ?? r[3];
|
|
1600
|
+
const hash = r.contentHash ?? r[4] ?? schema_js_1.STALE_HASH_SENTINEL;
|
|
1601
|
+
if (nodeId) {
|
|
1602
|
+
const hasChunkMetadata = chunkIndex !== undefined &&
|
|
1603
|
+
chunkIndex !== null &&
|
|
1604
|
+
startLine !== undefined &&
|
|
1605
|
+
startLine !== null &&
|
|
1606
|
+
endLine !== undefined &&
|
|
1607
|
+
endLine !== null;
|
|
1608
|
+
// Empty/null contentHash or missing chunk metadata means legacy row — treat as stale.
|
|
1609
|
+
map.set(nodeId, hasChunkMetadata && hash ? hash : schema_js_1.STALE_HASH_SENTINEL);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return map;
|
|
1613
|
+
}
|
|
1614
|
+
catch (err) {
|
|
1615
|
+
const msg = err?.message ?? '';
|
|
1616
|
+
if (isMissingColumnOrTableError(msg)) {
|
|
1617
|
+
// Legacy rows missing chunk-aware columns — treat every row as stale.
|
|
1618
|
+
try {
|
|
1619
|
+
const rows = await execQuery(`MATCH (e:${schema_js_1.EMBEDDING_TABLE_NAME}) RETURN e.nodeId AS nodeId`);
|
|
1620
|
+
if (!rows || rows.length === 0)
|
|
1621
|
+
return undefined;
|
|
1622
|
+
const map = new Map();
|
|
1623
|
+
for (const r of rows) {
|
|
1624
|
+
const nodeId = r.nodeId ?? r[0];
|
|
1625
|
+
if (nodeId)
|
|
1626
|
+
map.set(nodeId, schema_js_1.STALE_HASH_SENTINEL);
|
|
1627
|
+
}
|
|
1628
|
+
logger_js_1.logger.info(`[embed] ${map.size} nodes in legacy DB (missing chunk-aware columns) — all treated as stale`);
|
|
1629
|
+
return map;
|
|
1630
|
+
}
|
|
1631
|
+
catch (fallbackErr) {
|
|
1632
|
+
const fallbackMsg = fallbackErr?.message ?? '';
|
|
1633
|
+
if (isMissingColumnOrTableError(fallbackMsg)) {
|
|
1634
|
+
logger_js_1.logger.info(`[embed] CodeEmbedding table not yet present — full embedding run (${fallbackMsg})`);
|
|
1635
|
+
return undefined;
|
|
1636
|
+
}
|
|
1637
|
+
throw fallbackErr;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
throw err;
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
exports.fetchExistingEmbeddingHashes = fetchExistingEmbeddingHashes;
|
|
1644
|
+
/**
|
|
1645
|
+
* Flush the WAL so all pending writes are visible to subsequent readers.
|
|
1646
|
+
*
|
|
1647
|
+
* Best-effort: swallows errors from older LadybugDB versions or schemaless
|
|
1648
|
+
* databases that do not support the CHECKPOINT command. A no-op when there
|
|
1649
|
+
* is nothing pending, so safe (and cheap) to call unconditionally after any
|
|
1650
|
+
* write path.
|
|
1651
|
+
*
|
|
1652
|
+
* Use this instead of safeClose when the connection must stay open
|
|
1653
|
+
* (e.g. the /api/embed handler that keeps serving queries after flushing).
|
|
1654
|
+
*
|
|
1655
|
+
* @see safeClose — CHECKPOINT + connection/database close
|
|
1656
|
+
*/
|
|
1657
|
+
const flushWAL = async () => {
|
|
1658
|
+
const c = conn;
|
|
1659
|
+
if (!c)
|
|
1660
|
+
return;
|
|
1661
|
+
try {
|
|
1662
|
+
await (0, conn_lock_js_1.withConnLock)(async () => {
|
|
1663
|
+
const checkpointResult = await c.query('CHECKPOINT');
|
|
1664
|
+
await drainQueryResult(checkpointResult);
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
catch (err) {
|
|
1668
|
+
logger_js_1.logger.debug(`GitNexus: LadybugDB CHECKPOINT skipped/failed during WAL flush: ${summarizeError(err)}`);
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
exports.flushWAL = flushWAL;
|
|
1672
|
+
/**
|
|
1673
|
+
* Issue a manual `CHECKPOINT` against the current connection and surface
|
|
1674
|
+
* any engine error to the caller. Unlike {@link flushWAL}, this variant
|
|
1675
|
+
* does NOT swallow Ladybug rename/remove IO failures — the manual
|
|
1676
|
+
* checkpoint driver (`wal-checkpoint-driver.ts`) relies on the rejection
|
|
1677
|
+
* to drive its bounded retry loop. Returns `false` when no connection is
|
|
1678
|
+
* open (the caller treats this as a no-op success — there is no WAL to
|
|
1679
|
+
* flush). Returns `true` after a successful CHECKPOINT + drain.
|
|
1680
|
+
*
|
|
1681
|
+
* The split from `flushWAL` is deliberate: every other CHECKPOINT site
|
|
1682
|
+
* (server flush, safeClose) is best-effort and prefers a silent skip;
|
|
1683
|
+
* the manual driver, by contrast, must observe failures to decide
|
|
1684
|
+
* whether to retry.
|
|
1685
|
+
*/
|
|
1686
|
+
const tryFlushWAL = async () => {
|
|
1687
|
+
const c = conn;
|
|
1688
|
+
if (!c)
|
|
1689
|
+
return false;
|
|
1690
|
+
// Runs on the periodic WAL-checkpoint driver. The lock makes this CHECKPOINT
|
|
1691
|
+
// wait for any in-flight COPY / writeback on the singleton connection instead
|
|
1692
|
+
// of executing concurrently with it (the `analyze --pdg` heap-corruption bug).
|
|
1693
|
+
await (0, conn_lock_js_1.withConnLock)(async () => {
|
|
1694
|
+
const checkpointResult = await c.query('CHECKPOINT');
|
|
1695
|
+
await drainQueryResult(checkpointResult);
|
|
1696
|
+
});
|
|
1697
|
+
return true;
|
|
1698
|
+
};
|
|
1699
|
+
exports.tryFlushWAL = tryFlushWAL;
|
|
1700
|
+
/**
|
|
1701
|
+
* Flush the WAL and close the connection and database handles.
|
|
1702
|
+
*
|
|
1703
|
+
* Consolidates the CHECKPOINT + close pattern into a single function so
|
|
1704
|
+
* callers never call conn.close() or db.close() directly (#1376).
|
|
1705
|
+
* An ESLint no-restricted-syntax rule enforces this — see eslint.config.mjs.
|
|
1706
|
+
*
|
|
1707
|
+
* @see flushWAL — CHECKPOINT-only (connection stays open)
|
|
1708
|
+
* @see closeLbug — safeClose + module state reset (full teardown)
|
|
1709
|
+
*/
|
|
1710
|
+
const safeClose = async () => {
|
|
1711
|
+
await (0, exports.flushWAL)();
|
|
1712
|
+
// Capture before close — currentDbPath stays set so the Windows post-close
|
|
1713
|
+
// probe below knows which file to wait on.
|
|
1714
|
+
const closingDbPath = currentDbPath;
|
|
1715
|
+
if (conn) {
|
|
1716
|
+
try {
|
|
1717
|
+
// eslint-disable-next-line no-restricted-syntax -- sole authorised close site
|
|
1718
|
+
await conn.close();
|
|
1719
|
+
}
|
|
1720
|
+
catch {
|
|
1721
|
+
/* best-effort */
|
|
1722
|
+
}
|
|
1723
|
+
conn = null;
|
|
1724
|
+
}
|
|
1725
|
+
if (db) {
|
|
1726
|
+
try {
|
|
1727
|
+
// eslint-disable-next-line no-restricted-syntax -- sole authorised close site
|
|
1728
|
+
await db.close();
|
|
1729
|
+
}
|
|
1730
|
+
catch {
|
|
1731
|
+
/* best-effort */
|
|
1732
|
+
}
|
|
1733
|
+
db = null;
|
|
1734
|
+
}
|
|
1735
|
+
// Windows: libuv reports `db.close()` resolved before the kernel has
|
|
1736
|
+
// released the file handle. A subsequent `new Database(samePath)` in
|
|
1737
|
+
// the same process can race the release. The probe (lbug-config.ts)
|
|
1738
|
+
// forces any residual lock to surface as EBUSY/EPERM/EACCES so the
|
|
1739
|
+
// open-time retry absorbs the lag.
|
|
1740
|
+
if (process.platform === 'win32' && closingDbPath) {
|
|
1741
|
+
const released = await (0, lbug_config_js_1.waitForWindowsHandleRelease)(closingDbPath);
|
|
1742
|
+
if (!released) {
|
|
1743
|
+
// Probe exhausted with a lock code still in flight. The next
|
|
1744
|
+
// openLbugConnection will absorb whatever residual lag remains, but
|
|
1745
|
+
// a chronic warning helps operators spot AV interference (Windows
|
|
1746
|
+
// Defender holding the file far past the 250ms budget).
|
|
1747
|
+
logger_js_1.logger.warn({ dbPath: closingDbPath }, '⚠️ LadybugDB file handle still locked after close (Windows). If this repeats, check antivirus/Defender exclusions for the GitNexus storage directory.');
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (closingDbPath) {
|
|
1751
|
+
await (0, sidecar_recovery_js_1.finalizeLbugSidecarsAfterClose)(closingDbPath, { logger: logger_js_1.logger });
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
exports.safeClose = safeClose;
|
|
1755
|
+
/**
|
|
1756
|
+
* CHECKPOINT for durability, then DELIBERATELY skip the native connection/database
|
|
1757
|
+
* teardown. The name encodes the contract — there is no boolean flag to misuse:
|
|
1758
|
+
* call this ONLY from a path that guarantees a `process.exit` immediately after
|
|
1759
|
+
* (the CLI analyze success/SIGINT paths and the forked worker).
|
|
1760
|
+
*
|
|
1761
|
+
* LadybugDB's ClientContext/Connection destructor can double-free after large
|
|
1762
|
+
* --pdg writes (gdb: `double free or corruption` in ClientContext::~ClientContext
|
|
1763
|
+
* via NodeConnection::Close), aborting the process AFTER a fully-written,
|
|
1764
|
+
* checkpointed index. flushWAL already persisted the data; process exit reclaims
|
|
1765
|
+
* the native handles. We leave the handles referenced and module state intact so a
|
|
1766
|
+
* GC finalizer cannot run the same destructor before exit, and any post-analyze
|
|
1767
|
+
* read reuses the live connection. Mirrors the pool adapter's fire-and-forget
|
|
1768
|
+
* native teardown (pool-adapter.ts) and the ONNX native-cleanup philosophy.
|
|
1769
|
+
* Workaround for a LadybugDB engine bug (to be reported upstream).
|
|
1770
|
+
*
|
|
1771
|
+
* SAFETY: only valid when a process.exit is guaranteed to follow. Long-lived
|
|
1772
|
+
* callers (MCP server, tests) leave `skipNativeCloseOnExit` unset, so
|
|
1773
|
+
* runFullAnalysis closes for real via {@link closeLbug} — never this.
|
|
1774
|
+
*/
|
|
1775
|
+
const closeLbugBeforeExit = async () => {
|
|
1776
|
+
await (0, exports.flushWAL)();
|
|
1777
|
+
// NOTE (#2264): unlike safeClose, this deliberately does NOT run
|
|
1778
|
+
// finalizeLbugSidecarsAfterClose. That step inspects/quarantines orphan WAL +
|
|
1779
|
+
// sidecar files and is designed to run AFTER the native close has released the
|
|
1780
|
+
// WAL handle; running it here — with the connection still open — would risk a
|
|
1781
|
+
// Windows file-lock on the in-use WAL for no benefit. The CHECKPOINT above
|
|
1782
|
+
// already made the index durable, and the next run's preflightLbugSidecars
|
|
1783
|
+
// reconciles any residual WAL on open. The deferred sidecar housekeeping is the
|
|
1784
|
+
// accepted trade-off of skipping the native close to dodge the destructor
|
|
1785
|
+
// double-free.
|
|
1786
|
+
};
|
|
1787
|
+
exports.closeLbugBeforeExit = closeLbugBeforeExit;
|
|
1788
|
+
const closeLbug = async () => {
|
|
1789
|
+
await (0, exports.safeClose)();
|
|
1790
|
+
currentDbPath = null;
|
|
1791
|
+
vectorExtensionLoaded = false;
|
|
1792
|
+
vectorIndexEnsured = false;
|
|
1793
|
+
};
|
|
1794
|
+
exports.closeLbug = closeLbug;
|
|
1795
|
+
/**
|
|
1796
|
+
* Thrown by {@link wipeLbugDbFiles} when a data-bearing member of the
|
|
1797
|
+
* LadybugDB file family is still present after the bounded
|
|
1798
|
+
* remove-and-verify retries (#2409, tri-review 4669518496 P2-4), and by
|
|
1799
|
+
* run-analyze's dirty-recovery block when the crashed run's sidecars can
|
|
1800
|
+
* neither be parked nor removed (this shipping review, FIX 1 — same lock
|
|
1801
|
+
* class, same remediation, and the CLI already renders this type).
|
|
1802
|
+
*
|
|
1803
|
+
* Classify by TYPE (`err instanceof LbugWipeError`) — the repo norm from
|
|
1804
|
+
* #2385 — never by message text. The MESSAGE is nonetheless fully
|
|
1805
|
+
* self-contained (headline + blocked paths + remediation) because
|
|
1806
|
+
* `cgraph serve` forwards only `err.message` over worker IPC
|
|
1807
|
+
* (analyze-worker-core.ts), so the serve surface has nothing but this
|
|
1808
|
+
* string to show the user. The holder framing deliberately covers the
|
|
1809
|
+
* own-process case (FIX 2, finder A): the blocking handle is often a
|
|
1810
|
+
* lingering one from THIS process's just-closed DB or a transient AV scan
|
|
1811
|
+
* — not necessarily another process — so an immediate re-run often
|
|
1812
|
+
* succeeds.
|
|
1813
|
+
*/
|
|
1814
|
+
class LbugWipeError extends Error {
|
|
1815
|
+
/** Paths still present (or unverifiable) after all retries. */
|
|
1816
|
+
survivors;
|
|
1817
|
+
constructor(survivors, options) {
|
|
1818
|
+
super(`${options?.headline ??
|
|
1819
|
+
`Failed to remove the LadybugDB index files — still present after ` +
|
|
1820
|
+
`${lbug_config_js_1.HANDLE_RELEASE_PROBE_ATTEMPTS} attempts:`}\n` +
|
|
1821
|
+
survivors.map((p) => ` - ${p}`).join('\n') +
|
|
1822
|
+
`\nThe blocking handle may be another process, a lingering handle from this ` +
|
|
1823
|
+
`process's just-closed database, or an antivirus scan — an immediate re-run ` +
|
|
1824
|
+
`often succeeds. If it persists, ${(0, sidecar_recovery_js_1.lbugLockRemediation)('re-run the analyze')}.`);
|
|
1825
|
+
this.name = 'LbugWipeError';
|
|
1826
|
+
this.survivors = survivors;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
exports.LbugWipeError = LbugWipeError;
|
|
1830
|
+
/**
|
|
1831
|
+
* Remove the LadybugDB file family and VERIFY each member is really gone.
|
|
1832
|
+
*
|
|
1833
|
+
* Owns the canonical 4-file family list — `<lbugPath>`, `.wal`, `.shadow`,
|
|
1834
|
+
* `.lock` — so run-analyze's two wipe sites (full rebuild + the #2409
|
|
1835
|
+
* escalation valve) can never drift apart. `.shadow` is included because a
|
|
1836
|
+
* checkpoint-in-flight crash leaves a shadow sidecar, and a stale shadow next
|
|
1837
|
+
* to a freshly created DB file is replay poison on the next open (#2409).
|
|
1838
|
+
*
|
|
1839
|
+
* Verification contract (tri-review 4669518496 P2-4 — the old inline loops
|
|
1840
|
+
* swallowed rm failures and let `initLbug` reopen a still-populated DB the
|
|
1841
|
+
* run believed it wiped): after `fs.rm({ recursive, force })`, each path is
|
|
1842
|
+
* probed and counts as GONE only when the probe rejects with **ENOENT**. A
|
|
1843
|
+
* resolving probe, or a rejection in the EPERM/EBUSY/EACCES class (Windows
|
|
1844
|
+
* delete-pending / handle-release lag — see HANDLE_RELEASE_LOCK_CODES in
|
|
1845
|
+
* lbug-config.ts), or any other code means the path is not verifiably gone:
|
|
1846
|
+
* it is retried on the shared handle-release budget
|
|
1847
|
+
* (HANDLE_RELEASE_PROBE_ATTEMPTS × linear HANDLE_RELEASE_PROBE_DELAY_MS,
|
|
1848
|
+
* lbug-config.ts — the previous private mirror constants were
|
|
1849
|
+
* documentation-coupled copies) and then handled by CLASS (this shipping
|
|
1850
|
+
* review, FIX 2):
|
|
1851
|
+
*
|
|
1852
|
+
* - DATA-BEARING members (`<lbugPath>`, `.wal`, `.shadow`) — a survivor
|
|
1853
|
+
* means the reopen would resurrect rows this run believes wiped: throw
|
|
1854
|
+
* a typed {@link LbugWipeError}.
|
|
1855
|
+
* - `.lock` — contentless: `initLbug` recreates it, and a genuinely held
|
|
1856
|
+
* lock surfaces as initLbug's own lock-busy classification (a better
|
|
1857
|
+
* error than this one). A `.lock`-only survivor (an AV-held
|
|
1858
|
+
* delete-pending handle outlasting the budget previously failed a
|
|
1859
|
+
* perfectly sound rebuild) logs a warning and CONTINUES.
|
|
1860
|
+
*
|
|
1861
|
+
* Linux unlinked-but-open (name gone, holder keeps the old inode) probes
|
|
1862
|
+
* ENOENT and is accepted by design — both production wipe sites run after a
|
|
1863
|
+
* real `closeLbug()`.
|
|
1864
|
+
*
|
|
1865
|
+
* Deliberately OUT of this contract: `cleanupOldKuzuFiles`
|
|
1866
|
+
* (repo-manager.ts) sweeps the LEGACY kuzu-era file family during storage
|
|
1867
|
+
* migration — different family, best-effort by design; and
|
|
1868
|
+
* `sweepStaleSidecars` (lbug-config.ts) is a test-fixture-gated open-retry
|
|
1869
|
+
* fallback that must never delete production files. Neither wipes the live
|
|
1870
|
+
* DB the run is about to recreate, so neither needs (or may share) the
|
|
1871
|
+
* loud-failure contract here.
|
|
1872
|
+
*/
|
|
1873
|
+
const wipeLbugDbFiles = async (lbugPath) => {
|
|
1874
|
+
const lockPath = `${lbugPath}.lock`;
|
|
1875
|
+
const family = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.shadow`, lockPath];
|
|
1876
|
+
let survivors = [];
|
|
1877
|
+
for (let attempt = 1; attempt <= lbug_config_js_1.HANDLE_RELEASE_PROBE_ATTEMPTS; attempt++) {
|
|
1878
|
+
survivors = [];
|
|
1879
|
+
for (const f of family) {
|
|
1880
|
+
try {
|
|
1881
|
+
await promises_1.default.rm(f, { recursive: true, force: true });
|
|
1882
|
+
}
|
|
1883
|
+
catch {
|
|
1884
|
+
// `force: true` swallows ENOENT, so a rejection is a real failure —
|
|
1885
|
+
// but the ENOENT-probe below stays authoritative either way (another
|
|
1886
|
+
// process may have removed the path between the rm and the probe).
|
|
1887
|
+
}
|
|
1888
|
+
const gone = await promises_1.default.access(f).then(() => false, // still present
|
|
1889
|
+
(err) => err?.code === 'ENOENT');
|
|
1890
|
+
if (!gone)
|
|
1891
|
+
survivors.push(f);
|
|
1892
|
+
}
|
|
1893
|
+
if (survivors.length === 0)
|
|
1894
|
+
return;
|
|
1895
|
+
if (attempt < lbug_config_js_1.HANDLE_RELEASE_PROBE_ATTEMPTS) {
|
|
1896
|
+
await (0, lbug_config_js_1.sleep)(lbug_config_js_1.HANDLE_RELEASE_PROBE_DELAY_MS * attempt);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
// Class split (FIX 2): the contentless `.lock` never fails the wipe.
|
|
1900
|
+
const dataSurvivors = survivors.filter((f) => f !== lockPath);
|
|
1901
|
+
if (survivors.includes(lockPath)) {
|
|
1902
|
+
logger_js_1.logger.warn(`GitNexus: ${lockPath} is still present after the wipe retries — continuing: the ` +
|
|
1903
|
+
'lock file is contentless and initLbug recreates it; a genuinely held lock will ' +
|
|
1904
|
+
"surface as the reopen's own lock-busy error.");
|
|
1905
|
+
}
|
|
1906
|
+
if (dataSurvivors.length > 0) {
|
|
1907
|
+
throw new LbugWipeError(dataSurvivors);
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
exports.wipeLbugDbFiles = wipeLbugDbFiles;
|
|
1911
|
+
const isLbugReady = () => conn !== null && db !== null;
|
|
1912
|
+
exports.isLbugReady = isLbugReady;
|
|
1913
|
+
/**
|
|
1914
|
+
* Multi-label alternation over exactly the labels that can own embedding
|
|
1915
|
+
* rows: EMBEDDABLE_LABELS plus File, which embedding-pipeline.ts embeds as
|
|
1916
|
+
* the zero-symbol fallback for text-only repositories (#2454). Reserved
|
|
1917
|
+
* keywords are backtick-escaped via {@link escapeTableName}. Probed on
|
|
1918
|
+
* @ladybugdb/core 0.18.0 (this shipping review, FIX 4): the full multi-label
|
|
1919
|
+
* alternation parses, executes, and deletes exactly the joined rows —
|
|
1920
|
+
* replacing the unlabeled `MATCH (n)` that scanned EVERY node table per
|
|
1921
|
+
* chunk (BasicBlock-dominated under `--pdg`) when only embeddable labels
|
|
1922
|
+
* can match an embedding row. Including File is free for code repositories:
|
|
1923
|
+
* they never hold File embedding rows, so the extra label joins nothing.
|
|
1924
|
+
*/
|
|
1925
|
+
const embeddableLabelMatch = () => ['File', ...EMBEDDABLE_LABELS].map((l) => escapeTableName(l)).join('|');
|
|
1926
|
+
// LADYBUGDB-CONTRACT: matches @ladybugdb/core ^0.18.0 native binder text,
|
|
1927
|
+
// probe-recorded: `Binder exception: Table CodeEmbedding does not exist.`
|
|
1928
|
+
// When bumping LadybugDB, re-validate — `git grep "LADYBUGDB-CONTRACT"`
|
|
1929
|
+
// enumerates every version-coupled spot.
|
|
1930
|
+
const isMissingEmbeddingTableError = (err) => {
|
|
1931
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1932
|
+
return msg.includes(`Table ${schema_js_1.EMBEDDING_TABLE_NAME} does not exist`);
|
|
1933
|
+
};
|
|
1934
|
+
/**
|
|
1935
|
+
* Delete all nodes (and their relationships) for a specific file from LadybugDB
|
|
1936
|
+
* @param filePath - The file path to delete nodes for
|
|
1937
|
+
* @param dbPath - Optional path to LadybugDB for per-query connection
|
|
1938
|
+
* @returns Object with counts of deleted nodes
|
|
1939
|
+
*/
|
|
1940
|
+
const deleteNodesForFile = async (filePath, dbPath) => {
|
|
1941
|
+
const usePerQuery = !!dbPath;
|
|
1942
|
+
// Set up connection (either use existing or create per-query)
|
|
1943
|
+
let tempHandle = null;
|
|
1944
|
+
let tempConn = null;
|
|
1945
|
+
let targetConn = conn;
|
|
1946
|
+
if (usePerQuery) {
|
|
1947
|
+
tempHandle = await (0, lbug_config_js_1.openLbugConnection)(lbug, dbPath);
|
|
1948
|
+
tempConn = tempHandle.conn;
|
|
1949
|
+
targetConn = tempConn;
|
|
1950
|
+
}
|
|
1951
|
+
else if (!conn) {
|
|
1952
|
+
throw new Error('LadybugDB not initialized. Provide dbPath or call initLbug first.');
|
|
1953
|
+
}
|
|
1954
|
+
try {
|
|
1955
|
+
let deletedNodes = 0;
|
|
1956
|
+
const escapedPath = (0, cypher_escape_js_1.escapeCypherString)(filePath);
|
|
1957
|
+
// Delete the file's embedding rows FIRST, while their owning nodes are
|
|
1958
|
+
// still present: node ids are label-first — generateId = `${label}:${name}`
|
|
1959
|
+
// (src/lib/utils.ts) with qualified names that embed the file path — so
|
|
1960
|
+
// the old `e.nodeId STARTS WITH '<filePath>'` shape never matched a row
|
|
1961
|
+
// (tri-review 4669518496 P2-1). Join through the nodes on exact id
|
|
1962
|
+
// equality instead, scoped to the embeddable labels (FIX 4 — see
|
|
1963
|
+
// embeddableLabelMatch); ordering is load-bearing — after the DETACH
|
|
1964
|
+
// DELETE loop below the join would match nothing.
|
|
1965
|
+
try {
|
|
1966
|
+
await queryAndDrain(targetConn, `MATCH (n:${embeddableLabelMatch()}) WHERE n.filePath = '${escapedPath}' ` +
|
|
1967
|
+
`MATCH (e:${schema_js_1.EMBEDDING_TABLE_NAME}) WHERE e.nodeId = n.id DELETE e`);
|
|
1968
|
+
}
|
|
1969
|
+
catch (err) {
|
|
1970
|
+
// Deliberately legacy-permissive (pinned contract:
|
|
1971
|
+
// lbug-conn-serialization U5 and lbug-core-adapter expect this variant
|
|
1972
|
+
// to resolve `{deletedNodes: 0}` even on a bogus dbPath): the singular
|
|
1973
|
+
// variant swallows per-statement failures wholesale — its per-table
|
|
1974
|
+
// loop below does the same — so a partial rethrow here would be
|
|
1975
|
+
// incoherent with the rest of the function. The STRICT
|
|
1976
|
+
// rethrow-except-missing-table policy lives in deleteNodesForFiles,
|
|
1977
|
+
// the #2409 incremental writeback path (FIX 4). The one case worth a
|
|
1978
|
+
// diagnostic is the missing embedding table.
|
|
1979
|
+
if (isMissingEmbeddingTableError(err)) {
|
|
1980
|
+
logger_js_1.logger.warn({ err }, `deleteNodesForFile: ${schema_js_1.EMBEDDING_TABLE_NAME} table does not exist — ` +
|
|
1981
|
+
'skipping embedding-row deletes for this DB.');
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
// Delete nodes from each table that has filePath
|
|
1985
|
+
// DETACH DELETE removes the node and all its relationships
|
|
1986
|
+
for (const tableName of schema_js_1.NODE_TABLES) {
|
|
1987
|
+
// Skip tables that don't have filePath (Community, Process)
|
|
1988
|
+
if (tableName === 'Community' || tableName === 'Process')
|
|
1989
|
+
continue;
|
|
1990
|
+
try {
|
|
1991
|
+
// First count how many we'll delete. On the singleton connection this
|
|
1992
|
+
// count runs inside withConnLock (incremental --pdg writeback executes
|
|
1993
|
+
// while the WAL driver is live); per-query/temp connections skip the
|
|
1994
|
+
// lock, matching queryAndDrain's `targetConn === conn` gate — the sibling
|
|
1995
|
+
// DETACH DELETE below already routes through it. (#2264)
|
|
1996
|
+
const tn = escapeTableName(tableName);
|
|
1997
|
+
const countCypher = `MATCH (n:${tn}) WHERE n.filePath = '${escapedPath}' RETURN count(n) AS cnt`;
|
|
1998
|
+
const runCount = async () => readQueryRows(await targetConn.query(countCypher));
|
|
1999
|
+
const rows = isSharedSingletonConn(targetConn)
|
|
2000
|
+
? await (0, conn_lock_js_1.withConnLock)(runCount)
|
|
2001
|
+
: await runCount();
|
|
2002
|
+
const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0);
|
|
2003
|
+
if (count > 0) {
|
|
2004
|
+
// Delete nodes (and implicitly their relationships via DETACH)
|
|
2005
|
+
await queryAndDrain(targetConn, `MATCH (n:${tn}) WHERE n.filePath = '${escapedPath}' DETACH DELETE n`);
|
|
2006
|
+
deletedNodes += count;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
catch (e) {
|
|
2010
|
+
// Some tables may not support this query, skip
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
return { deletedNodes };
|
|
2014
|
+
}
|
|
2015
|
+
finally {
|
|
2016
|
+
// Close per-query connection if used
|
|
2017
|
+
if (tempHandle)
|
|
2018
|
+
await (0, lbug_config_js_1.closeLbugConnection)(tempHandle);
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
exports.deleteNodesForFile = deleteNodesForFile;
|
|
2022
|
+
/**
|
|
2023
|
+
* Chunk size for {@link deleteNodesForFiles}. 200 paths keeps each
|
|
2024
|
+
* statement ~13KB (well inside parser limits) while a ~700-file write set
|
|
2025
|
+
* still collapses from ~13,000 statements to 124: 31 statements per chunk
|
|
2026
|
+
* (1 CodeEmbedding join-delete + 30 filePath-bearing node tables — the
|
|
2027
|
+
* 32-table NODE_TABLES roster minus Community/Process) × 4 chunks. The
|
|
2028
|
+
* original "~40" claim under-counted the per-chunk statement fan-out
|
|
2029
|
+
* (tri-review 4669518496 accuracy sweep).
|
|
2030
|
+
*/
|
|
2031
|
+
exports.DELETE_FILES_CHUNK_SIZE = 200;
|
|
2032
|
+
/**
|
|
2033
|
+
* Batched variant of {@link deleteNodesForFile} for the incremental
|
|
2034
|
+
* writeback (#2409). One `DETACH DELETE … WHERE n.filePath IN […]` per
|
|
2035
|
+
* node table per chunk of paths, instead of a count + delete per table
|
|
2036
|
+
* per FILE. The per-file loop issued ~13,000 single-row write
|
|
2037
|
+
* transactions on a ~700-file write set — a WAL-append storm that made
|
|
2038
|
+
* the incremental path slower than a full rebuild and is the write
|
|
2039
|
+
* pattern behind the native mid-writeback deaths reported in #2409.
|
|
2040
|
+
*
|
|
2041
|
+
* NO general error swallowing: a zero-match chunk is a no-op success by
|
|
2042
|
+
* construction (every node table except Community/Process has a filePath
|
|
2043
|
+
* column), so anything thrown here is a real engine failure the caller
|
|
2044
|
+
* must see — silently skipping was exactly how #2409 hid its root cause.
|
|
2045
|
+
* The single tolerated exception (FIX 4) is the missing-embedding-table
|
|
2046
|
+
* binder error on the embedding join-delete: a DB created without
|
|
2047
|
+
* EMBEDDING_SCHEMA cannot own embedding rows, so skipping that one
|
|
2048
|
+
* statement is sound, while failing would brick every incremental run on
|
|
2049
|
+
* such a DB until `--force`. Statement count per chunk is unchanged by the
|
|
2050
|
+
* multi-label join: 1 embedding join-delete + 30 node-table deletes = 31
|
|
2051
|
+
* (the rejected per-label fallback shape would have been 19 + 30 = 49).
|
|
2052
|
+
* Singleton-connection only: the analyze writeback owns the write lock,
|
|
2053
|
+
* and `queryAndDrain` routes through `withConnLock` for it (the WAL
|
|
2054
|
+
* checkpoint driver is live during this).
|
|
2055
|
+
*/
|
|
2056
|
+
const deleteNodesForFiles = async (filePaths, options = {}) => {
|
|
2057
|
+
if (!conn) {
|
|
2058
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2059
|
+
}
|
|
2060
|
+
const targetConn = conn;
|
|
2061
|
+
let warnedMissingEmbeddingTable = false;
|
|
2062
|
+
for (let i = 0; i < filePaths.length; i += exports.DELETE_FILES_CHUNK_SIZE) {
|
|
2063
|
+
const chunk = filePaths.slice(i, i + exports.DELETE_FILES_CHUNK_SIZE);
|
|
2064
|
+
const listLiteral = `[${chunk.map((p) => `'${(0, cypher_escape_js_1.escapeCypherString)(p)}'`).join(', ')}]`;
|
|
2065
|
+
// Embedding rows key on their OWNING NODE's id: generateId builds
|
|
2066
|
+
// label-first ids — `${label}:${name}` (src/lib/utils.ts) with qualified
|
|
2067
|
+
// names that embed the file path (e.g. `Function:src/f.ts:fn0:1`) — so
|
|
2068
|
+
// the previous bare-path `e.nodeId STARTS WITH '<filePath>'` OR-chain
|
|
2069
|
+
// could never match anything (tri-review 4669518496 P2-1: the embedding
|
|
2070
|
+
// delete was a no-op). Join through the nodes instead: one multi-label
|
|
2071
|
+
// MATCH over exactly the embeddable labels (FIX 4, probe-proven on
|
|
2072
|
+
// 0.18.0 — see embeddableLabelMatch; the old unlabeled `MATCH (n)`
|
|
2073
|
+
// scanned every node table per chunk, BasicBlock-dominated under
|
|
2074
|
+
// `--pdg`, when only embeddable labels can own rows), and
|
|
2075
|
+
// `e.nodeId = n.id` equality is exact — no `File:a.ts` / `File:a.tsx`
|
|
2076
|
+
// prefix collisions. ORDER IS LOAD-BEARING: this must run BEFORE the
|
|
2077
|
+
// DETACH DELETE loop below — once the nodes are gone the join matches
|
|
2078
|
+
// nothing (empirically verified against @ladybugdb/core 0.18.0).
|
|
2079
|
+
try {
|
|
2080
|
+
await queryAndDrain(targetConn, `MATCH (n:${embeddableLabelMatch()}) WHERE n.filePath IN ${listLiteral} ` +
|
|
2081
|
+
`MATCH (e:${schema_js_1.EMBEDDING_TABLE_NAME}) WHERE e.nodeId = n.id DELETE e`);
|
|
2082
|
+
}
|
|
2083
|
+
catch (err) {
|
|
2084
|
+
// Tolerate exactly the missing-embedding-table binder error: a
|
|
2085
|
+
// build-variant DB without EMBEDDING_SCHEMA would otherwise brick
|
|
2086
|
+
// every incremental run until `--force` (FIX 4). The no-swallow
|
|
2087
|
+
// policy stays for every real failure — anything else rethrows.
|
|
2088
|
+
if (!isMissingEmbeddingTableError(err))
|
|
2089
|
+
throw err;
|
|
2090
|
+
if (!warnedMissingEmbeddingTable) {
|
|
2091
|
+
warnedMissingEmbeddingTable = true;
|
|
2092
|
+
logger_js_1.logger.warn({ err }, `deleteNodesForFiles: ${schema_js_1.EMBEDDING_TABLE_NAME} table does not exist — ` +
|
|
2093
|
+
'skipping embedding-row deletes for this writeback.');
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
for (const tableName of schema_js_1.NODE_TABLES) {
|
|
2097
|
+
// Community/Process are graph-wide (no filePath); the orchestrator
|
|
2098
|
+
// drops them wholesale via deleteAllCommunitiesAndProcesses.
|
|
2099
|
+
if (tableName === 'Community' || tableName === 'Process')
|
|
2100
|
+
continue;
|
|
2101
|
+
const tn = escapeTableName(tableName);
|
|
2102
|
+
await queryAndDrain(targetConn, `MATCH (n:${tn}) WHERE n.filePath IN ${listLiteral} DETACH DELETE n`);
|
|
2103
|
+
}
|
|
2104
|
+
options.onChunk?.(Math.min(i + exports.DELETE_FILES_CHUNK_SIZE, filePaths.length), filePaths.length);
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
exports.deleteNodesForFiles = deleteNodesForFiles;
|
|
2108
|
+
const getEmbeddingTableName = () => schema_js_1.EMBEDDING_TABLE_NAME;
|
|
2109
|
+
exports.getEmbeddingTableName = getEmbeddingTableName;
|
|
2110
|
+
/**
|
|
2111
|
+
* Return the distinct repo-relative paths of files that import
|
|
2112
|
+
* `targetFilePath` according to the IMPORTS edges currently in the
|
|
2113
|
+
* DB. Used by the incremental writeback path to expand the
|
|
2114
|
+
* "files-to-rewrite" set so that files importing a changed file get
|
|
2115
|
+
* their edges (which may have been refined by cross-file resolution)
|
|
2116
|
+
* re-emitted, rather than left stale in the DB.
|
|
2117
|
+
*
|
|
2118
|
+
* The DB query reads the *previous* run's state — pre-pipeline, before
|
|
2119
|
+
* any nodes are deleted — so the returned importers are "files that
|
|
2120
|
+
* USED TO import the target". That's the right set to invalidate:
|
|
2121
|
+
* those are the files whose edges in the DB might no longer match
|
|
2122
|
+
* what cross-file resolution produces given the changed file's new
|
|
2123
|
+
* exports.
|
|
2124
|
+
*/
|
|
2125
|
+
const queryImporters = async (targetFilePath) => {
|
|
2126
|
+
const c = conn;
|
|
2127
|
+
if (!c) {
|
|
2128
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2129
|
+
}
|
|
2130
|
+
const escaped = (0, cypher_escape_js_1.escapeCypherString)(targetFilePath);
|
|
2131
|
+
const cypher = `
|
|
2132
|
+
MATCH (a)-[r:${schema_js_1.REL_TABLE_NAME}]->(b)
|
|
2133
|
+
WHERE r.type = 'IMPORTS' AND b.filePath = '${escaped}'
|
|
2134
|
+
RETURN DISTINCT a.filePath AS importer
|
|
2135
|
+
`;
|
|
2136
|
+
// Runs inside the connection lock: queryImporters is called in the importer-BFS
|
|
2137
|
+
// loop during incremental --pdg writeback while the WAL driver is live, so an
|
|
2138
|
+
// unlocked conn.query here could race a concurrent CHECKPOINT on the singleton.
|
|
2139
|
+
return (0, conn_lock_js_1.withConnLock)(async () => {
|
|
2140
|
+
let queryResult;
|
|
2141
|
+
try {
|
|
2142
|
+
queryResult = await c.query(cypher);
|
|
2143
|
+
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
2144
|
+
const rows = await result.getAll();
|
|
2145
|
+
const out = [];
|
|
2146
|
+
for (const row of rows) {
|
|
2147
|
+
const v = row.importer;
|
|
2148
|
+
if (typeof v === 'string' && v.length > 0)
|
|
2149
|
+
out.push(v);
|
|
2150
|
+
}
|
|
2151
|
+
return out;
|
|
2152
|
+
}
|
|
2153
|
+
catch {
|
|
2154
|
+
return [];
|
|
2155
|
+
}
|
|
2156
|
+
finally {
|
|
2157
|
+
if (queryResult)
|
|
2158
|
+
await (0, query_result_utils_js_1.closeQueryResults)(queryResult);
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
};
|
|
2162
|
+
exports.queryImporters = queryImporters;
|
|
2163
|
+
/**
|
|
2164
|
+
* Batched variant of {@link queryImporters} for the incremental importer
|
|
2165
|
+
* BFS (#2409): distinct importers of ANY of the target paths, one query per
|
|
2166
|
+
* chunk per BFS depth instead of one query per frontier FILE (a ~700-file
|
|
2167
|
+
* frontier was ~700 sequential round-trips, each taking the connection lock
|
|
2168
|
+
* against the live WAL checkpoint driver — ~5.6s of the writeback measured).
|
|
2169
|
+
*
|
|
2170
|
+
* Same contract as the singular form: reads the pre-pipeline DB state and
|
|
2171
|
+
* swallows per-chunk query failures into a smaller result (correctness
|
|
2172
|
+
* degrades on that branch — under-expansion means possibly-stale edges —
|
|
2173
|
+
* but the DB stays writable and the writeback proceeds). Unlike the singular
|
|
2174
|
+
* form the degradation is not silent (tri-review 4669518496 P2-5): every
|
|
2175
|
+
* dropped chunk is logged and reported through `options.onChunkFailure`, so
|
|
2176
|
+
* the orchestrator can count it into the #2410 crash diagnostics
|
|
2177
|
+
* (`incrementalInProgress.droppedImporterChunks`).
|
|
2178
|
+
*/
|
|
2179
|
+
const queryImportersBatch = async (targetFilePaths, options = {}) => {
|
|
2180
|
+
const c = conn;
|
|
2181
|
+
if (!c) {
|
|
2182
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2183
|
+
}
|
|
2184
|
+
const importers = new Set();
|
|
2185
|
+
for (let i = 0; i < targetFilePaths.length; i += exports.DELETE_FILES_CHUNK_SIZE) {
|
|
2186
|
+
// `i` only ever advances in whole chunk strides, so this is exact.
|
|
2187
|
+
const chunkIndex = i / exports.DELETE_FILES_CHUNK_SIZE;
|
|
2188
|
+
const chunk = targetFilePaths.slice(i, i + exports.DELETE_FILES_CHUNK_SIZE);
|
|
2189
|
+
const listLiteral = `[${chunk.map((p) => `'${(0, cypher_escape_js_1.escapeCypherString)(p)}'`).join(', ')}]`;
|
|
2190
|
+
const cypher = `
|
|
2191
|
+
MATCH (a)-[r:${schema_js_1.REL_TABLE_NAME}]->(b)
|
|
2192
|
+
WHERE r.type = 'IMPORTS' AND b.filePath IN ${listLiteral}
|
|
2193
|
+
RETURN DISTINCT a.filePath AS importer
|
|
2194
|
+
`;
|
|
2195
|
+
await (0, conn_lock_js_1.withConnLock)(async () => {
|
|
2196
|
+
let queryResult;
|
|
2197
|
+
try {
|
|
2198
|
+
queryResult = await c.query(cypher);
|
|
2199
|
+
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
2200
|
+
const rows = await result.getAll();
|
|
2201
|
+
for (const row of rows) {
|
|
2202
|
+
const v = row.importer;
|
|
2203
|
+
if (typeof v === 'string' && v.length > 0)
|
|
2204
|
+
importers.add(v);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
catch (err) {
|
|
2208
|
+
// Degrade-don't-fail, mirroring queryImporters — but LOUDLY
|
|
2209
|
+
// (tri-review 4669518496 P2-5): a dropped chunk means every importer
|
|
2210
|
+
// it would have surfaced keeps possibly-stale edges this run, and the
|
|
2211
|
+
// old bare `catch {}` left no trace of that anywhere. pino idiom:
|
|
2212
|
+
// `err` key — `error` serializes to `{}`.
|
|
2213
|
+
logger_js_1.logger.warn({ err }, `Incremental importer BFS: dropped chunk ${chunkIndex} (${chunk.length} target path(s)) — ` +
|
|
2214
|
+
'importer expansion degrades for this run; affected importers may keep stale edges until the next full rebuild.');
|
|
2215
|
+
options.onChunkFailure?.(chunkIndex, chunk.length, err);
|
|
2216
|
+
}
|
|
2217
|
+
finally {
|
|
2218
|
+
if (queryResult)
|
|
2219
|
+
await (0, query_result_utils_js_1.closeQueryResults)(queryResult);
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
// Cypher without ORDER BY is unordered — sort so downstream chunking and
|
|
2224
|
+
// logs are stable run-to-run (matches diffFileHashes' sorted outputs).
|
|
2225
|
+
return [...importers].sort();
|
|
2226
|
+
};
|
|
2227
|
+
exports.queryImportersBatch = queryImportersBatch;
|
|
2228
|
+
/**
|
|
2229
|
+
* Drop every Community and Process node (and their MEMBER_OF /
|
|
2230
|
+
* STEP_IN_PROCESS edges via DETACH DELETE). Used at the start of an
|
|
2231
|
+
* incremental run so the communities and processes phases regenerate
|
|
2232
|
+
* them from scratch on the merged graph — required for the
|
|
2233
|
+
* "Leiden runs on the FULL graph" correctness invariant.
|
|
2234
|
+
*/
|
|
2235
|
+
const deleteAllCommunitiesAndProcesses = async () => {
|
|
2236
|
+
const c = conn;
|
|
2237
|
+
if (!c) {
|
|
2238
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2239
|
+
}
|
|
2240
|
+
// count + DETACH DELETE run inside the connection lock so they cannot execute
|
|
2241
|
+
// concurrently with the WAL-checkpoint driver's CHECKPOINT on the singleton
|
|
2242
|
+
// connection. This runs during incremental --pdg writeback while the driver is
|
|
2243
|
+
// live; mirrors the wrapped deleteAllInterprocTaintPaths / deleteAllCallSummaries.
|
|
2244
|
+
return (0, conn_lock_js_1.withConnLock)(async () => {
|
|
2245
|
+
let nodesDeleted = 0;
|
|
2246
|
+
for (const label of ['Community', 'Process']) {
|
|
2247
|
+
let countResult;
|
|
2248
|
+
try {
|
|
2249
|
+
countResult = await c.query(`MATCH (n:${label}) RETURN count(n) AS cnt`);
|
|
2250
|
+
const result = Array.isArray(countResult) ? countResult[0] : countResult;
|
|
2251
|
+
const rows = await result.getAll();
|
|
2252
|
+
const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0);
|
|
2253
|
+
if (count > 0) {
|
|
2254
|
+
await (0, query_result_utils_js_1.closeQueryResults)(await c.query(`MATCH (n:${label}) DETACH DELETE n`));
|
|
2255
|
+
nodesDeleted += count;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
catch {
|
|
2259
|
+
// Table may not exist yet on a freshly-initialized DB — fine.
|
|
2260
|
+
}
|
|
2261
|
+
finally {
|
|
2262
|
+
if (countResult)
|
|
2263
|
+
await (0, query_result_utils_js_1.closeQueryResults)(countResult);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return { nodesDeleted };
|
|
2267
|
+
});
|
|
2268
|
+
};
|
|
2269
|
+
exports.deleteAllCommunitiesAndProcesses = deleteAllCommunitiesAndProcesses;
|
|
2270
|
+
/**
|
|
2271
|
+
* Shared mechanics for the delete-all-relationships-of-one-type family
|
|
2272
|
+
* ({@link deleteAllInterprocTaintPaths}, {@link deleteAllCallSummaries},
|
|
2273
|
+
* {@link deleteAllInjects}, {@link deleteSpringAutoConfigurationDeclarations}):
|
|
2274
|
+
* count the matching CodeRelation rows, then DELETE them (relationship-level —
|
|
2275
|
+
* these are edge types, not node labels, so endpoints are untouched).
|
|
2276
|
+
*
|
|
2277
|
+
* count + DELETE run as one critical section on the singleton connection so a
|
|
2278
|
+
* concurrent WAL-checkpoint cannot corrupt native state mid-delete (#pdg).
|
|
2279
|
+
*
|
|
2280
|
+
* @param relType the CodeRelation `type` value to delete (e.g. 'INJECTS')
|
|
2281
|
+
* @param logTag the `[tag]` prefix on the abort error message
|
|
2282
|
+
* @param duplicateNoun what the abort message says would be duplicated
|
|
2283
|
+
* @param exactReasons optional reason allowlist for a shared relationship type
|
|
2284
|
+
*/
|
|
2285
|
+
const deleteAllRelationshipsOfType = async (relType, logTag, duplicateNoun, exactReasons) => {
|
|
2286
|
+
const c = conn;
|
|
2287
|
+
if (!c) {
|
|
2288
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2289
|
+
}
|
|
2290
|
+
return (0, conn_lock_js_1.withConnLock)(async () => {
|
|
2291
|
+
let edgesDeleted = 0;
|
|
2292
|
+
let countResult;
|
|
2293
|
+
const reasonFilter = exactReasons === undefined || exactReasons.length === 0
|
|
2294
|
+
? ''
|
|
2295
|
+
: ` AND (${exactReasons
|
|
2296
|
+
.map((reason) => `r.reason = '${(0, cypher_escape_js_1.escapeCypherString)(reason)}'`)
|
|
2297
|
+
.join(' OR ')})`;
|
|
2298
|
+
const predicate = `r.type = '${(0, cypher_escape_js_1.escapeCypherString)(relType)}'${reasonFilter}`;
|
|
2299
|
+
try {
|
|
2300
|
+
countResult = await c.query(`MATCH ()-[r:CodeRelation]->() WHERE ${predicate} RETURN count(r) AS cnt`);
|
|
2301
|
+
const result = Array.isArray(countResult) ? countResult[0] : countResult;
|
|
2302
|
+
const rows = await result.getAll();
|
|
2303
|
+
const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0);
|
|
2304
|
+
if (count > 0) {
|
|
2305
|
+
await (0, query_result_utils_js_1.closeQueryResults)(await c.query(`MATCH ()-[r:CodeRelation]->() WHERE ${predicate} DELETE r`));
|
|
2306
|
+
edgesDeleted = count;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
catch (err) {
|
|
2310
|
+
// A missing table on a freshly-initialized DB is the benign, expected case
|
|
2311
|
+
// (the count query above is what throws) — stay silent. Any OTHER failure
|
|
2312
|
+
// (lock, disk, native error) would leave stale rows that the subsequent
|
|
2313
|
+
// re-extract then DUPLICATES (CodeRelation has no PK), so it must ABORT
|
|
2314
|
+
// the writeback (#2084 review P2-5): re-throw so the caller's crash-
|
|
2315
|
+
// recovery dirty flag forces a clean full rebuild on the next run, rather
|
|
2316
|
+
// than silently writing duplicate rows. The benign-vs-rethrow branch is
|
|
2317
|
+
// pure, extracted, and pinned by unit tests: `classifyDeleteAllError`
|
|
2318
|
+
// (lbug-config.ts, test/unit/lbug-delete-all-error.test.ts).
|
|
2319
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2320
|
+
if ((0, lbug_config_js_1.classifyDeleteAllError)(err) === 'benign-missing-table') {
|
|
2321
|
+
if (countResult)
|
|
2322
|
+
await (0, query_result_utils_js_1.closeQueryResults)(countResult);
|
|
2323
|
+
return { edgesDeleted };
|
|
2324
|
+
}
|
|
2325
|
+
if (countResult)
|
|
2326
|
+
await (0, query_result_utils_js_1.closeQueryResults)(countResult);
|
|
2327
|
+
throw new Error(`[${logTag}] failed to clear existing ${relType} edges before incremental ` +
|
|
2328
|
+
`re-write (${msg}) — aborting to avoid ${duplicateNoun}; ` +
|
|
2329
|
+
`the next run will full-rebuild`);
|
|
2330
|
+
}
|
|
2331
|
+
if (countResult)
|
|
2332
|
+
await (0, query_result_utils_js_1.closeQueryResults)(countResult);
|
|
2333
|
+
return { edgesDeleted };
|
|
2334
|
+
});
|
|
2335
|
+
};
|
|
2336
|
+
/**
|
|
2337
|
+
* Drop every interprocedural `TAINT_PATH` relationship (#2084 M4 U6). Used at
|
|
2338
|
+
* the start of an incremental `--pdg` writeback so the `taintSummaries` phase
|
|
2339
|
+
* re-materialises them from scratch on the FULL recomputed graph.
|
|
2340
|
+
*
|
|
2341
|
+
* TAINT_PATH validity is a WHOLE-PROGRAM property (a flow A→C can be
|
|
2342
|
+
* invalidated by a change to an INTERMEDIATE function whose file is neither A
|
|
2343
|
+
* nor C). The endpoint-writability extract rule (`extractChangedSubgraph`)
|
|
2344
|
+
* cannot see that — an A→C edge between two unchanged files would be skipped
|
|
2345
|
+
* and a stale finding would survive. So, exactly like Community/Process, the
|
|
2346
|
+
* sound move is delete-all-then-rebuild: cheap because TAINT_PATH is sparse
|
|
2347
|
+
* (per-run capped), and the compute side already rebuilds every summary each
|
|
2348
|
+
* run. Relationship-level (TAINT_PATH is an edge type, not a node label), so a
|
|
2349
|
+
* plain DELETE on the typed CodeRelation rows — endpoints are untouched.
|
|
2350
|
+
*/
|
|
2351
|
+
const deleteAllInterprocTaintPaths = async () => deleteAllRelationshipsOfType('TAINT_PATH', 'taint-interproc', 'duplicate cross-function findings');
|
|
2352
|
+
exports.deleteAllInterprocTaintPaths = deleteAllInterprocTaintPaths;
|
|
2353
|
+
/**
|
|
2354
|
+
* Drop every `CALL_SUMMARY` relationship (PDG FU-C, U-C3). Used at the start of
|
|
2355
|
+
* an incremental `--pdg` writeback so the `callSummaries` phase re-materialises
|
|
2356
|
+
* them from scratch on the FULL recomputed graph.
|
|
2357
|
+
*
|
|
2358
|
+
* Mirrors {@link deleteAllInterprocTaintPaths}: CALL_SUMMARY is a self-loop edge
|
|
2359
|
+
* type (not a node label), so a plain DELETE on the typed CodeRelation rows
|
|
2360
|
+
* leaves endpoints untouched. `extractChangedSubgraph` re-includes ALL of them
|
|
2361
|
+
* from the fresh graph (`isGraphWideRelType`), so delete-all-then-rebuild keeps
|
|
2362
|
+
* an unchanged function's summary from being lost.
|
|
2363
|
+
*/
|
|
2364
|
+
const deleteAllCallSummaries = async () => deleteAllRelationshipsOfType('CALL_SUMMARY', 'call-summary', 'duplicate summaries');
|
|
2365
|
+
exports.deleteAllCallSummaries = deleteAllCallSummaries;
|
|
2366
|
+
/**
|
|
2367
|
+
* Drop every `INJECTS` relationship (DI collection injection, #2200). Used at
|
|
2368
|
+
* the start of an incremental writeback — UNCONDITIONALLY, unlike the
|
|
2369
|
+
* pdg-gated twins above, because the `di` phase runs on every persisting
|
|
2370
|
+
* analyze — so the phase re-materialises them from scratch on the FULL
|
|
2371
|
+
* recomputed graph.
|
|
2372
|
+
*
|
|
2373
|
+
* Mirrors {@link deleteAllInterprocTaintPaths}: INJECTS validity is a
|
|
2374
|
+
* whole-program property (a change to the interface, or a new/removed
|
|
2375
|
+
* implementer, on a THIRD file creates/invalidates edges between two
|
|
2376
|
+
* untouched files), so endpoint-writability extraction can't refresh them.
|
|
2377
|
+
* `extractChangedSubgraph` re-includes ALL of them from the fresh graph
|
|
2378
|
+
* (`isGraphWideRelType`), so delete-all-then-rebuild is the sound move.
|
|
2379
|
+
* Relationship-level (INJECTS is an edge type, not a node label), so a plain
|
|
2380
|
+
* DELETE on the typed CodeRelation rows — endpoints are untouched.
|
|
2381
|
+
*/
|
|
2382
|
+
const deleteAllInjects = async () => deleteAllRelationshipsOfType('INJECTS', 'di', 'duplicate INJECTS edges');
|
|
2383
|
+
exports.deleteAllInjects = deleteAllInjects;
|
|
2384
|
+
/**
|
|
2385
|
+
* Drop every Spring AOP `ADVISED_BY` relationship before incremental
|
|
2386
|
+
* writeback. Pointcut/annotation resolution is whole-program: adding a type in
|
|
2387
|
+
* a third file can shadow a wildcard annotation import or change a wildcard
|
|
2388
|
+
* execution match between two otherwise unchanged endpoint files.
|
|
2389
|
+
*/
|
|
2390
|
+
const deleteAllAdvisedBy = async () => deleteAllRelationshipsOfType('ADVISED_BY', 'spring-aop', 'duplicate ADVISED_BY edges');
|
|
2391
|
+
exports.deleteAllAdvisedBy = deleteAllAdvisedBy;
|
|
2392
|
+
/** Drop all synthetic Spring AOP evidence nodes before incremental writeback. */
|
|
2393
|
+
const deleteSpringAopEvidenceNodes = async () => {
|
|
2394
|
+
const c = conn;
|
|
2395
|
+
if (!c) {
|
|
2396
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2397
|
+
}
|
|
2398
|
+
return (0, conn_lock_js_1.withConnLock)(async () => {
|
|
2399
|
+
let countResult;
|
|
2400
|
+
const idPrefix = (0, cypher_escape_js_1.escapeCypherString)(aop_js_1.SPRING_AOP_EVIDENCE_ID_PREFIX);
|
|
2401
|
+
const predicate = `n.id STARTS WITH '${idPrefix}'`;
|
|
2402
|
+
try {
|
|
2403
|
+
countResult = await c.query(`MATCH (n:CodeElement) WHERE ${predicate} RETURN count(n) AS cnt`);
|
|
2404
|
+
const result = Array.isArray(countResult) ? countResult[0] : countResult;
|
|
2405
|
+
const rows = await result.getAll();
|
|
2406
|
+
const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0);
|
|
2407
|
+
if (count > 0) {
|
|
2408
|
+
await (0, query_result_utils_js_1.closeQueryResults)(await c.query(`MATCH (n:CodeElement) WHERE ${predicate} DETACH DELETE n`));
|
|
2409
|
+
}
|
|
2410
|
+
if (countResult)
|
|
2411
|
+
await (0, query_result_utils_js_1.closeQueryResults)(countResult);
|
|
2412
|
+
return { nodesDeleted: count };
|
|
2413
|
+
}
|
|
2414
|
+
catch (err) {
|
|
2415
|
+
if (countResult)
|
|
2416
|
+
await (0, query_result_utils_js_1.closeQueryResults)(countResult);
|
|
2417
|
+
if ((0, lbug_config_js_1.classifyDeleteAllError)(err) === 'benign-missing-table') {
|
|
2418
|
+
return { nodesDeleted: 0 };
|
|
2419
|
+
}
|
|
2420
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2421
|
+
throw new Error('[spring-aop] failed to clear synthetic evidence before incremental re-write ' +
|
|
2422
|
+
`(${message}) — aborting to avoid stale advice metadata; the next run will full-rebuild`);
|
|
2423
|
+
}
|
|
2424
|
+
});
|
|
2425
|
+
};
|
|
2426
|
+
exports.deleteSpringAopEvidenceNodes = deleteSpringAopEvidenceNodes;
|
|
2427
|
+
/**
|
|
2428
|
+
* Drop Spring-owned auto-configuration `DECLARES` relationships before
|
|
2429
|
+
* incremental writeback. `DECLARES` is generic, so exact reason filtering is
|
|
2430
|
+
* required: other metadata systems must retain their own declarations.
|
|
2431
|
+
*/
|
|
2432
|
+
const deleteSpringAutoConfigurationDeclarations = async () => deleteAllRelationshipsOfType('DECLARES', 'spring-auto-configuration', 'duplicate auto-configuration declarations', auto_configuration_js_1.SPRING_AUTO_CONFIGURATION_REASONS);
|
|
2433
|
+
exports.deleteSpringAutoConfigurationDeclarations = deleteSpringAutoConfigurationDeclarations;
|
|
2434
|
+
/**
|
|
2435
|
+
* Drop synthetic source-unavailable auto-configuration Class nodes before
|
|
2436
|
+
* incremental writeback. The fresh full graph re-emits every still-needed
|
|
2437
|
+
* synthetic node; deleting first also removes placeholders that became stale
|
|
2438
|
+
* when a real source class appeared.
|
|
2439
|
+
*/
|
|
2440
|
+
const deleteSpringAutoConfigurationSyntheticClasses = async () => {
|
|
2441
|
+
const c = conn;
|
|
2442
|
+
if (!c) {
|
|
2443
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2444
|
+
}
|
|
2445
|
+
return (0, conn_lock_js_1.withConnLock)(async () => {
|
|
2446
|
+
let countResult;
|
|
2447
|
+
const idPrefix = (0, cypher_escape_js_1.escapeCypherString)(auto_configuration_js_1.SPRING_AUTO_CONFIGURATION_SYNTHETIC_ID_PREFIX);
|
|
2448
|
+
const predicate = `n.id STARTS WITH '${idPrefix}'`;
|
|
2449
|
+
try {
|
|
2450
|
+
countResult = await c.query(`MATCH (n:Class) WHERE ${predicate} RETURN count(n) AS cnt`);
|
|
2451
|
+
const result = Array.isArray(countResult) ? countResult[0] : countResult;
|
|
2452
|
+
const rows = await result.getAll();
|
|
2453
|
+
const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0);
|
|
2454
|
+
if (count > 0) {
|
|
2455
|
+
await (0, query_result_utils_js_1.closeQueryResults)(await c.query(`MATCH (n:Class) WHERE ${predicate} DETACH DELETE n`));
|
|
2456
|
+
}
|
|
2457
|
+
if (countResult)
|
|
2458
|
+
await (0, query_result_utils_js_1.closeQueryResults)(countResult);
|
|
2459
|
+
return { nodesDeleted: count };
|
|
2460
|
+
}
|
|
2461
|
+
catch (err) {
|
|
2462
|
+
if (countResult)
|
|
2463
|
+
await (0, query_result_utils_js_1.closeQueryResults)(countResult);
|
|
2464
|
+
if ((0, lbug_config_js_1.classifyDeleteAllError)(err) === 'benign-missing-table') {
|
|
2465
|
+
return { nodesDeleted: 0 };
|
|
2466
|
+
}
|
|
2467
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2468
|
+
throw new Error('[spring-auto-configuration] failed to clear synthetic Class nodes before ' +
|
|
2469
|
+
`incremental re-write (${message}) — aborting to avoid stale placeholders; ` +
|
|
2470
|
+
'the next run will full-rebuild');
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
};
|
|
2474
|
+
exports.deleteSpringAutoConfigurationSyntheticClasses = deleteSpringAutoConfigurationSyntheticClasses;
|
|
2475
|
+
// ============================================================================
|
|
2476
|
+
// VECTOR Extension Functions
|
|
2477
|
+
// ============================================================================
|
|
2478
|
+
/**
|
|
2479
|
+
* Load the VECTOR extension on the supplied connection (or the singleton
|
|
2480
|
+
* writable connection when none is given). Returns false when VECTOR is
|
|
2481
|
+
* unavailable so semantic search can fall back to exact scan.
|
|
2482
|
+
*/
|
|
2483
|
+
const loadVectorExtension = async (targetConn, opts = {}) => {
|
|
2484
|
+
const useModuleState = targetConn === undefined;
|
|
2485
|
+
if (useModuleState && vectorExtensionLoaded)
|
|
2486
|
+
return true;
|
|
2487
|
+
// No platform gate. Windows was hard-refused here for years on the strength
|
|
2488
|
+
// of an early-era report that in-process INSTALL VECTOR could SIGSEGV
|
|
2489
|
+
// (#1365) — but the extension server ships win_amd64 VECTOR artifacts for
|
|
2490
|
+
// every 0.18.x extension version (probed live: v0.18.0 and v0.18.1 both
|
|
2491
|
+
// serve a real PE32+ DLL; the pinned 0.18.2 core resolves its extension
|
|
2492
|
+
// directory to 0.18.1, strace-verified), and INSTALL now runs in a spawned
|
|
2493
|
+
// child process (installDuckDbExtensionOutOfProcess), so even a crashing
|
|
2494
|
+
// installer kills only the child and degrades to `false` here. LOAD of a
|
|
2495
|
+
// present extension file is an ordinary in-process load whose failures
|
|
2496
|
+
// surface as catchable errors.
|
|
2497
|
+
const c = targetConn ?? conn;
|
|
2498
|
+
if (!c) {
|
|
2499
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2500
|
+
}
|
|
2501
|
+
const loaded = await extension_loader_js_1.extensionManager.ensure((sql) => queryAndDrain(c, sql), 'VECTOR', 'VECTOR', opts);
|
|
2502
|
+
if (loaded && useModuleState)
|
|
2503
|
+
vectorExtensionLoaded = true;
|
|
2504
|
+
return loaded;
|
|
2505
|
+
};
|
|
2506
|
+
exports.loadVectorExtension = loadVectorExtension;
|
|
2507
|
+
/**
|
|
2508
|
+
* Create the HNSW vector index on the CodeEmbedding table.
|
|
2509
|
+
*
|
|
2510
|
+
* MUST run via `conn.query()` (here through `queryAndDrain`), NOT through the
|
|
2511
|
+
* prepared `executeQuery`/`conn.prepare()` path: `CALL CREATE_VECTOR_INDEX(...)`
|
|
2512
|
+
* compiles to multiple statements, which LadybugDB cannot prepare — it fails
|
|
2513
|
+
* with "Connection Exception: We do not support prepare multiple statements."
|
|
2514
|
+
* Routing index creation through `executeQuery` (prepared) is exactly what
|
|
2515
|
+
* broke vector-index creation during `analyze` (#2114; the singleton
|
|
2516
|
+
* `executeQuery` was switched to the prepared path in #1655).
|
|
2517
|
+
*
|
|
2518
|
+
* Returns `true` on success (or when the index already exists — idempotent so
|
|
2519
|
+
* incremental re-runs don't spuriously downgrade to exact scan), `false` when
|
|
2520
|
+
* the VECTOR extension is unavailable or the connection is read-only. Any other
|
|
2521
|
+
* failure propagates so the caller can log it.
|
|
2522
|
+
*/
|
|
2523
|
+
const createVectorIndex = async () => {
|
|
2524
|
+
if (!conn) {
|
|
2525
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2526
|
+
}
|
|
2527
|
+
// Already built on this connection — skip the round-trip.
|
|
2528
|
+
if (vectorIndexEnsured)
|
|
2529
|
+
return true;
|
|
2530
|
+
if (!(await (0, exports.loadVectorExtension)())) {
|
|
2531
|
+
return false;
|
|
2532
|
+
}
|
|
2533
|
+
try {
|
|
2534
|
+
await queryAndDrain(conn, schema_js_1.CREATE_VECTOR_INDEX_QUERY);
|
|
2535
|
+
vectorIndexEnsured = true;
|
|
2536
|
+
return true;
|
|
2537
|
+
}
|
|
2538
|
+
catch (e) {
|
|
2539
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2540
|
+
// Idempotent: a prior analyze already built the HNSW index.
|
|
2541
|
+
if (msg.includes('already exists')) {
|
|
2542
|
+
vectorIndexEnsured = true;
|
|
2543
|
+
return true;
|
|
2544
|
+
}
|
|
2545
|
+
// Read-only DB (e.g. the MCP query pool): writable analyze owns creation.
|
|
2546
|
+
if ((0, exports.isReadOnlyDbError)(e))
|
|
2547
|
+
return false;
|
|
2548
|
+
throw e;
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
exports.createVectorIndex = createVectorIndex;
|
|
2552
|
+
/**
|
|
2553
|
+
* The field reads every index-catalog consumer needs, each hedging the
|
|
2554
|
+
* named-record form against the positional one exactly once.
|
|
2555
|
+
*
|
|
2556
|
+
* They exist because the hedge used to be inlined at several call sites
|
|
2557
|
+
* (#2841 review). Everything version-coupled about the row shape now lives in
|
|
2558
|
+
* this one block.
|
|
2559
|
+
*/
|
|
2560
|
+
const indexRowTable = (row) => row?.table_name ?? row?.[0];
|
|
2561
|
+
exports.indexRowTable = indexRowTable;
|
|
2562
|
+
const indexRowType = (row) => row?.index_type ?? row?.[2];
|
|
2563
|
+
exports.indexRowType = indexRowType;
|
|
2564
|
+
/**
|
|
2565
|
+
* Read the index catalog on the writable connection, or `undefined` when it
|
|
2566
|
+
* cannot be read.
|
|
2567
|
+
*
|
|
2568
|
+
* `SHOW_INDEXES` is readable WITHOUT any extension loaded and reports
|
|
2569
|
+
* `extension_loaded` per index, so the extension-gated-DML checks below settle
|
|
2570
|
+
* the common "this DB carries no such index" case with one local read and no
|
|
2571
|
+
* error-string sniffing. It runs through the unprepared `conn.query()` path
|
|
2572
|
+
* like every other `CALL` procedure here (#2114).
|
|
2573
|
+
*
|
|
2574
|
+
* `undefined` means "could not prove anything" and every caller must treat it
|
|
2575
|
+
* as fail-closed (assume an index may be present), never as "no indexes". All
|
|
2576
|
+
* readers below honour that (#2841 review H3). To hand ONE read to several
|
|
2577
|
+
* gates, use {@link readIndexCatalogSnapshot} — passing this `undefined` on
|
|
2578
|
+
* cannot be distinguished from passing nothing at all.
|
|
2579
|
+
*/
|
|
2580
|
+
const readIndexCatalogRows = async () => {
|
|
2581
|
+
const targetConn = conn;
|
|
2582
|
+
if (!targetConn) {
|
|
2583
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2584
|
+
}
|
|
2585
|
+
try {
|
|
2586
|
+
return (await (0, conn_lock_js_1.withConnLock)(async () => readQueryRows(await targetConn.query('CALL SHOW_INDEXES() RETURN *'))));
|
|
2587
|
+
}
|
|
2588
|
+
catch (err) {
|
|
2589
|
+
logger_js_1.logger.warn({ err }, 'Could not read the LadybugDB index catalog (CALL SHOW_INDEXES()); ' +
|
|
2590
|
+
'extension-gated DML checks must assume an index may be present.');
|
|
2591
|
+
return undefined;
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
exports.readIndexCatalogRows = readIndexCatalogRows;
|
|
2595
|
+
/**
|
|
2596
|
+
* The failed half of an {@link IndexCatalogSnapshot}: the caller DID read the
|
|
2597
|
+
* catalog and could not prove anything.
|
|
2598
|
+
*
|
|
2599
|
+
* It exists because `undefined` was overloaded (#2841 review §5.A). The gates
|
|
2600
|
+
* below took `indexRows?: IndexCatalogRow[]`, so "my read failed" and "I passed
|
|
2601
|
+
* you nothing" were the SAME value, and each gate's `?? (await
|
|
2602
|
+
* readIndexCatalogRows())` silently re-read the catalog — turning the one shared
|
|
2603
|
+
* read the call site documents into three round-trips and three identical
|
|
2604
|
+
* warnings on the failure path, with the two gates free to decide from DIFFERENT
|
|
2605
|
+
* snapshots. A distinct sentinel makes "read, unreadable" a value the parameter
|
|
2606
|
+
* can carry, so a supplied snapshot is never re-read.
|
|
2607
|
+
*/
|
|
2608
|
+
exports.INDEX_CATALOG_UNREADABLE = Symbol('cgraph:index-catalog-unreadable');
|
|
2609
|
+
/**
|
|
2610
|
+
* {@link readIndexCatalogRows} in snapshot form — what callers should read once
|
|
2611
|
+
* and pass to EVERY extension-gated-DML gate in a run, so the "one shared
|
|
2612
|
+
* `SHOW_INDEXES` read" invariant holds on the failure branch too (#2841 review
|
|
2613
|
+
* §5.A). The `IndexCatalogRow[] | undefined` spelling stays available for
|
|
2614
|
+
* callers that only want the rows.
|
|
2615
|
+
*/
|
|
2616
|
+
const readIndexCatalogSnapshot = async () => (await (0, exports.readIndexCatalogRows)()) ?? exports.INDEX_CATALOG_UNREADABLE;
|
|
2617
|
+
exports.readIndexCatalogSnapshot = readIndexCatalogSnapshot;
|
|
2618
|
+
/**
|
|
2619
|
+
* Resolve a gate's optional `indexRows` argument into the rows it must judge,
|
|
2620
|
+
* reading the catalog AT MOST ONCE and ONLY when the caller supplied nothing.
|
|
2621
|
+
*
|
|
2622
|
+
* The `??` is meaningful again (#2841 review §5.A): `undefined` in can now only
|
|
2623
|
+
* mean "no snapshot supplied", because a caller whose own read failed passes
|
|
2624
|
+
* {@link INDEX_CATALOG_UNREADABLE}, which is truthy and short-circuits it.
|
|
2625
|
+
* `undefined` OUT keeps its documented meaning — "could not prove anything",
|
|
2626
|
+
* which every caller of {@link readIndexCatalogRows} treats as fail-closed.
|
|
2627
|
+
*/
|
|
2628
|
+
const resolveGateRows = async (indexRows) => {
|
|
2629
|
+
const snapshot = indexRows ?? (await (0, exports.readIndexCatalogSnapshot)());
|
|
2630
|
+
return snapshot === exports.INDEX_CATALOG_UNREADABLE ? undefined : snapshot;
|
|
2631
|
+
};
|
|
2632
|
+
exports.resolveGateRows = resolveGateRows;
|
|
2633
|
+
/**
|
|
2634
|
+
* Make DML against {@link EMBEDDING_TABLE_NAME} legal on the writable
|
|
2635
|
+
* connection when it can be, and report whether it is.
|
|
2636
|
+
*
|
|
2637
|
+
* LadybugDB refuses EVERY mutation of a table carrying an HNSW index while
|
|
2638
|
+
* the VECTOR extension is not loaded on that connection: `DELETE` fails with
|
|
2639
|
+
* "Trying to delete from an index on table CodeEmbedding but its extension is
|
|
2640
|
+
* not loaded", `CREATE` with the matching "insert into an index" variant,
|
|
2641
|
+
* `DROP TABLE` is refused while the index references it, and `SET` — even on
|
|
2642
|
+
* a NON-indexed property — segfaults the process outright. Probed against
|
|
2643
|
+
* @ladybugdb/core 0.18.2 (the lockfile-pinned version) and 0.18.0 — every
|
|
2644
|
+
* result identical on both (#2623).
|
|
2645
|
+
*
|
|
2646
|
+
* Dropping the index is NOT an available recovery: `CALL DROP_VECTOR_INDEX`
|
|
2647
|
+
* is itself a VECTOR-extension function and resolves to "Catalog exception:
|
|
2648
|
+
* function DROP_VECTOR_INDEX is not defined" in exactly the state it would
|
|
2649
|
+
* need to rescue. Loading the extension is the only in-place repair, which is
|
|
2650
|
+
* why this returns a verdict instead of attempting a fixup.
|
|
2651
|
+
*
|
|
2652
|
+
* `true` = embedding-row DML is safe: either VECTOR is now loaded, or the
|
|
2653
|
+
* table carries no index to trip over. `false` = genuinely blocked (index
|
|
2654
|
+
* present, extension unloadable); the analyze orchestrator answers that by
|
|
2655
|
+
* escalating to the wipe-and-rebuild write plan instead of failing
|
|
2656
|
+
* mid-writeback.
|
|
2657
|
+
*
|
|
2658
|
+
* Cheap by construction: one {@link readIndexCatalogRows} read settles the
|
|
2659
|
+
* common "this repo never built an embedding index" case without touching the
|
|
2660
|
+
* extension machinery at all, so a VECTOR-less machine is not charged a bounded
|
|
2661
|
+
* INSTALL attempt on every incremental analyze. (That read's own mechanics and
|
|
2662
|
+
* fail-closed contract are documented there, not re-explained here — #2841
|
|
2663
|
+
* review §5.H.)
|
|
2664
|
+
*
|
|
2665
|
+
* @param indexRows An {@link IndexCatalogSnapshot} the caller already read, so
|
|
2666
|
+
* one `SHOW_INDEXES` read can settle every gate in a run. FRESHNESS CONTRACT:
|
|
2667
|
+
* the snapshot must have been taken on THIS connection with nothing in between
|
|
2668
|
+
* that creates or drops an index — the gate's verdict is only as current as the
|
|
2669
|
+
* rows it is handed. Pass {@link INDEX_CATALOG_UNREADABLE} (what
|
|
2670
|
+
* {@link readIndexCatalogSnapshot} returns) when your own read failed; that
|
|
2671
|
+
* fails closed here WITHOUT a second read. Omit the argument entirely to have
|
|
2672
|
+
* the gate read the catalog itself.
|
|
2673
|
+
*/
|
|
2674
|
+
const ensureEmbeddingRowDmlSafe = async (indexRows) => {
|
|
2675
|
+
// Unconditional precondition (#2841 review §5.B). This check used to run on
|
|
2676
|
+
// every call; adding `indexRows` moved it inside `readIndexCatalogRows`, where
|
|
2677
|
+
// a caller-supplied snapshot skips it — so a closed DB could be answered
|
|
2678
|
+
// `true` where it previously threw. The verdict is only meaningful for the
|
|
2679
|
+
// live writable connection, so assert that before looking at the argument.
|
|
2680
|
+
if (!conn) {
|
|
2681
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2682
|
+
}
|
|
2683
|
+
// Catalog FIRST. The overwhelmingly common case on a repo that never enabled
|
|
2684
|
+
// embeddings is "no index at all", and that is provable with one local read
|
|
2685
|
+
// — no extension needed. Loading first would make every incremental analyze
|
|
2686
|
+
// on a VECTOR-less machine pay a bounded out-of-process INSTALL attempt (the
|
|
2687
|
+
// `auto` policy) plus an "extension unavailable" warning, for a repo that
|
|
2688
|
+
// can never hit this hazard.
|
|
2689
|
+
const rows = await (0, exports.resolveGateRows)(indexRows);
|
|
2690
|
+
// Any non-HASH index on the embedding table gates DML. Keyed on index TYPE,
|
|
2691
|
+
// not name, so an index built under a different name still counts; the
|
|
2692
|
+
// implicit primary-key HASH index is engine-internal and never gates.
|
|
2693
|
+
const indexGatesDml = rows === undefined ||
|
|
2694
|
+
rows.some((row) => {
|
|
2695
|
+
if ((0, exports.indexRowTable)(row) !== schema_js_1.EMBEDDING_TABLE_NAME)
|
|
2696
|
+
return false;
|
|
2697
|
+
return (0, exports.indexRowType)(row) !== 'HASH';
|
|
2698
|
+
});
|
|
2699
|
+
if (!indexGatesDml)
|
|
2700
|
+
return true;
|
|
2701
|
+
return await (0, exports.loadVectorExtension)(undefined, { policy: (0, extension_loader_js_1.resolveAnalyzeInstallPolicy)() });
|
|
2702
|
+
};
|
|
2703
|
+
exports.ensureEmbeddingRowDmlSafe = ensureEmbeddingRowDmlSafe;
|