gitnexus 1.6.9 → 1.6.10-aptos.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -53
- package/dist/_shared/graph/types.d.ts +67 -3
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +1 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/integrations/resilient-fetch.d.ts +3 -1
- package/dist/_shared/integrations/resilient-fetch.d.ts.map +1 -1
- package/dist/_shared/integrations/resilient-fetch.js +4 -3
- package/dist/_shared/integrations/resilient-fetch.js.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/language-detection.js +2 -0
- package/dist/_shared/language-detection.js.map +1 -1
- package/dist/_shared/languages.d.ts +2 -0
- package/dist/_shared/languages.d.ts.map +1 -1
- package/dist/_shared/languages.js +2 -0
- package/dist/_shared/languages.js.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +2 -2
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +8 -0
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/pipeline.d.ts +1 -1
- package/dist/_shared/pipeline.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/callable-flow-site.d.ts +110 -0
- package/dist/_shared/scope-resolution/callable-flow-site.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/callable-flow-site.js +11 -0
- package/dist/_shared/scope-resolution/callable-flow-site.js.map +1 -0
- package/dist/_shared/scope-resolution/finalize-algorithm.d.ts +1 -1
- package/dist/_shared/scope-resolution/finalize-algorithm.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/finalize-algorithm.js +1 -1
- package/dist/_shared/scope-resolution/finalize-algorithm.js.map +1 -1
- package/dist/_shared/scope-resolution/language-classification.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/language-classification.js +1 -0
- package/dist/_shared/scope-resolution/language-classification.js.map +1 -1
- package/dist/_shared/scope-resolution/parsed-file.d.ts +7 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/reference-site.d.ts +9 -1
- package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/types.d.ts +20 -3
- package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +25 -44
- package/dist/cli/analyze.d.ts +5 -2
- package/dist/cli/analyze.js +197 -11
- package/dist/cli/clean.js +17 -3
- package/dist/cli/cli-message.d.ts +1 -1
- package/dist/cli/doctor.d.ts +18 -0
- package/dist/cli/doctor.js +96 -5
- package/dist/cli/editor-targets.d.ts +17 -2
- package/dist/cli/editor-targets.js +44 -0
- package/dist/cli/embeddings.d.ts +12 -0
- package/dist/cli/embeddings.js +49 -0
- package/dist/cli/eval-server.d.ts +23 -1
- package/dist/cli/eval-server.js +126 -8
- package/dist/cli/help-i18n.js +6 -0
- package/dist/cli/i18n/en.d.ts +18 -8
- package/dist/cli/i18n/en.js +18 -8
- package/dist/cli/i18n/resources.d.ts +28 -8
- package/dist/cli/i18n/zh-CN.d.ts +10 -0
- package/dist/cli/i18n/zh-CN.js +18 -8
- package/dist/cli/index.js +21 -7
- package/dist/cli/lazy-action.d.ts +7 -0
- package/dist/cli/lazy-action.js +27 -0
- package/dist/cli/mcp.js +6 -3
- package/dist/cli/move-availability.d.ts +5 -0
- package/dist/cli/move-availability.js +11 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +230 -32
- package/dist/cli/skill-gen.js +38 -18
- package/dist/cli/standard-skills.d.ts +76 -0
- package/dist/cli/standard-skills.js +45 -0
- package/dist/cli/status.d.ts +4 -1
- package/dist/cli/status.js +57 -4
- package/dist/cli/tool.js +11 -0
- package/dist/cli/uninstall.js +77 -18
- package/dist/cli/wiki.d.ts +1 -0
- package/dist/cli/wiki.js +7 -1
- package/dist/config/ignore-service.d.ts +2 -0
- package/dist/config/ignore-service.js +27 -0
- package/dist/core/analysis-features.d.ts +21 -0
- package/dist/core/analysis-features.js +59 -0
- package/dist/core/analyzer-identity.d.ts +74 -0
- package/dist/core/analyzer-identity.js +1820 -0
- package/dist/core/augmentation/engine.js +13 -8
- package/dist/core/embeddings/embedder.d.ts +3 -2
- package/dist/core/embeddings/embedder.js +24 -7
- package/dist/core/embeddings/embedding-identity.d.ts +11 -0
- package/dist/core/embeddings/embedding-identity.js +24 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +36 -1
- package/dist/core/embeddings/embedding-pipeline.js +100 -9
- package/dist/core/embeddings/http-client.d.ts +33 -3
- package/dist/core/embeddings/http-client.js +237 -25
- package/dist/core/embeddings/node-module-compat.d.ts +25 -0
- package/dist/core/embeddings/node-module-compat.js +25 -0
- package/dist/core/embeddings/onnxruntime-common-resolver.js +8 -5
- package/dist/core/embeddings/onnxruntime-node-resolver.js +33 -7
- package/dist/core/embeddings/runtime-install.d.ts +119 -0
- package/dist/core/embeddings/runtime-install.js +372 -0
- package/dist/core/embeddings/runtime-support.d.ts +48 -17
- package/dist/core/embeddings/runtime-support.js +96 -0
- package/dist/core/git-staleness.js +1 -1
- package/dist/core/group/bridge-db.js +2 -0
- package/dist/core/group/extractors/http-patterns/python.js +246 -108
- package/dist/core/group/extractors/manifest-extractor.js +4 -0
- package/dist/core/incremental/escalation-gate.d.ts +38 -0
- package/dist/core/incremental/escalation-gate.js +48 -0
- package/dist/core/incremental/shadow-candidates.d.ts +3 -3
- package/dist/core/incremental/shadow-candidates.js +3 -3
- package/dist/core/incremental/subgraph-extract.d.ts +2 -2
- package/dist/core/incremental/subgraph-extract.js +2 -2
- package/dist/core/index-freshness.d.ts +5 -0
- package/dist/core/index-freshness.js +13 -0
- package/dist/core/ingestion/class-extractors/configs/jvm.js +29 -0
- package/dist/core/ingestion/cobol/cobol-preprocessor.js +1 -1
- package/dist/core/ingestion/cobol/jcl-processor.js +9 -8
- package/dist/core/ingestion/cobol-processor.js +27 -26
- package/dist/core/ingestion/community-processor.d.ts +51 -1
- package/dist/core/ingestion/community-processor.js +323 -64
- package/dist/core/ingestion/emit-references.js +7 -2
- package/dist/core/ingestion/filesystem-walker.js +4 -0
- package/dist/core/ingestion/frameworks/spring/analysis-features.d.ts +3 -0
- package/dist/core/ingestion/frameworks/spring/analysis-features.js +7 -0
- package/dist/core/ingestion/frameworks/spring/bean-candidates.d.ts +30 -0
- package/dist/core/ingestion/frameworks/spring/bean-candidates.js +182 -0
- package/dist/core/ingestion/frameworks/spring/bean-catalog.d.ts +12 -0
- package/dist/core/ingestion/frameworks/spring/bean-catalog.js +26 -0
- package/dist/core/ingestion/frameworks/spring/config-bindings.d.ts +24 -0
- package/dist/core/ingestion/frameworks/spring/config-bindings.js +115 -0
- package/dist/core/ingestion/languages/c/captures.js +56 -0
- package/dist/core/ingestion/languages/c/query.js +17 -0
- package/dist/core/ingestion/languages/c/scope-resolver.js +6 -0
- package/dist/core/ingestion/languages/cobol/captures.js +93 -0
- package/dist/core/ingestion/languages/cobol/scope-resolver.js +7 -0
- package/dist/core/ingestion/languages/cpp/captures.js +365 -75
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +14 -0
- package/dist/core/ingestion/languages/csharp/captures.js +25 -0
- package/dist/core/ingestion/languages/dart/captures.js +90 -2
- package/dist/core/ingestion/languages/dart/extension-type-preprocess.d.ts +7 -0
- package/dist/core/ingestion/languages/dart/extension-type-preprocess.js +30 -0
- package/dist/core/ingestion/languages/dart.js +2 -0
- package/dist/core/ingestion/languages/go/captures.js +34 -0
- package/dist/core/ingestion/languages/index.d.ts +1 -0
- package/dist/core/ingestion/languages/index.js +2 -0
- package/dist/core/ingestion/languages/java/analysis-features.d.ts +3 -0
- package/dist/core/ingestion/languages/java/analysis-features.js +13 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +22 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +58 -0
- package/dist/core/ingestion/languages/java/captures.d.ts +4 -3
- package/dist/core/ingestion/languages/java/captures.js +193 -17
- package/dist/core/ingestion/languages/java/package-facts.d.ts +5 -0
- package/dist/core/ingestion/languages/java/package-facts.js +9 -0
- package/dist/core/ingestion/languages/java/package-siblings.d.ts +3 -16
- package/dist/core/ingestion/languages/java/package-siblings.js +8 -147
- package/dist/core/ingestion/languages/java/query.js +23 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +23 -0
- package/dist/core/ingestion/languages/java/spring-bean-metadata.d.ts +2 -0
- package/dist/core/ingestion/languages/java/spring-bean-metadata.js +8 -0
- package/dist/core/ingestion/languages/java/spring-config-bindings.d.ts +23 -0
- package/dist/core/ingestion/languages/java/spring-config-bindings.js +215 -0
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/languages/javascript/captures.js +16 -0
- package/dist/core/ingestion/languages/javascript/query.js +21 -0
- package/dist/core/ingestion/languages/jvm/package-facts.d.ts +35 -0
- package/dist/core/ingestion/languages/jvm/package-facts.js +40 -0
- package/dist/core/ingestion/languages/jvm/package-siblings.d.ts +14 -0
- package/dist/core/ingestion/languages/jvm/package-siblings.js +129 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +17 -3
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +42 -8
- package/dist/core/ingestion/languages/kotlin/captures.js +59 -0
- package/dist/core/ingestion/languages/kotlin/package-facts.d.ts +5 -0
- package/dist/core/ingestion/languages/kotlin/package-facts.js +9 -0
- package/dist/core/ingestion/languages/kotlin/package-siblings.d.ts +4 -0
- package/dist/core/ingestion/languages/kotlin/package-siblings.js +8 -0
- package/dist/core/ingestion/languages/kotlin/query.js +19 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +9 -1
- package/dist/core/ingestion/languages/kotlin/spring-bean-metadata.d.ts +2 -0
- package/dist/core/ingestion/languages/kotlin/spring-bean-metadata.js +8 -0
- package/dist/core/ingestion/languages/kotlin.js +5 -6
- package/dist/core/ingestion/languages/move.d.ts +1 -0
- package/dist/core/ingestion/languages/move.js +27 -0
- package/dist/core/ingestion/languages/php/captures.js +40 -0
- package/dist/core/ingestion/languages/php/import-decomposer.js +12 -0
- package/dist/core/ingestion/languages/php/import-target.d.ts +2 -1
- package/dist/core/ingestion/languages/php/import-target.js +110 -2
- package/dist/core/ingestion/languages/php/interpret.js +10 -0
- package/dist/core/ingestion/languages/php/scope-resolver.js +1 -1
- package/dist/core/ingestion/languages/python/captures.d.ts +3 -4
- package/dist/core/ingestion/languages/python/captures.js +24 -5
- package/dist/core/ingestion/languages/python/interpret.js +4 -0
- package/dist/core/ingestion/languages/python/receiver-binding.d.ts +15 -2
- package/dist/core/ingestion/languages/python/receiver-binding.js +96 -2
- package/dist/core/ingestion/languages/python/simple-hooks.d.ts +5 -5
- package/dist/core/ingestion/languages/python/simple-hooks.js +15 -5
- package/dist/core/ingestion/languages/ruby/captures.js +42 -0
- package/dist/core/ingestion/languages/rust/captures.js +13 -0
- package/dist/core/ingestion/languages/rust/interpret.js +18 -0
- package/dist/core/ingestion/languages/rust/query.js +9 -0
- package/dist/core/ingestion/languages/rust/range-binding.js +6 -2
- package/dist/core/ingestion/languages/swift/captures.js +12 -0
- package/dist/core/ingestion/languages/typescript/captures.js +21 -0
- package/dist/core/ingestion/languages/typescript/nuxt-auto-imports.js +2 -1
- package/dist/core/ingestion/languages/typescript/query.js +37 -0
- package/dist/core/ingestion/markdown-processor.js +3 -2
- package/dist/core/ingestion/model/registration-table.js +1 -0
- package/dist/core/ingestion/parsing-processor.d.ts +3 -1
- package/dist/core/ingestion/parsing-processor.js +36 -1
- package/dist/core/ingestion/pipeline-phases/index.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/index.js +2 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +56 -2
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +0 -2
- package/dist/core/ingestion/pipeline-phases/parse.js +12 -3
- package/dist/core/ingestion/pipeline-phases/processes.d.ts +8 -0
- package/dist/core/ingestion/pipeline-phases/processes.js +11 -1
- package/dist/core/ingestion/pipeline-phases/spring-config.d.ts +37 -0
- package/dist/core/ingestion/pipeline-phases/spring-config.js +433 -0
- package/dist/core/ingestion/pipeline-phases/standalone-ingest.d.ts +10 -0
- package/dist/core/ingestion/pipeline-phases/standalone-ingest.js +6 -0
- package/dist/core/ingestion/pipeline.d.ts +4 -2
- package/dist/core/ingestion/pipeline.js +5 -2
- package/dist/core/ingestion/resolve-references.js +12 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +83 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.js +131 -0
- package/dist/core/ingestion/route-extractors/python-const-resolver.d.ts +75 -0
- package/dist/core/ingestion/route-extractors/python-const-resolver.js +299 -0
- package/dist/core/ingestion/scope-extractor.js +306 -43
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +71 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.d.ts +7 -7
- package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js +8 -7
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +3 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +6 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +1 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +4 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +7 -2
- package/dist/core/ingestion/scope-resolution/passes/callable-value-flow.d.ts +47 -0
- package/dist/core/ingestion/scope-resolution/passes/callable-value-flow.js +1138 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +16 -4
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +97 -1
- package/dist/core/ingestion/scope-resolution/passes/property-dispatch.d.ts +60 -0
- package/dist/core/ingestion/scope-resolution/passes/property-dispatch.js +111 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +4 -3
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +5 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +25 -38
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +6 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +147 -35
- package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +73 -44
- package/dist/core/ingestion/taint/typescript-model.js +34 -2
- package/dist/core/ingestion/tree-sitter-queries.d.ts +2 -2
- package/dist/core/ingestion/tree-sitter-queries.js +23 -2
- package/dist/core/ingestion/type-extractors/jvm.js +17 -2
- package/dist/core/ingestion/utils/ast-helpers.d.ts +1 -0
- package/dist/core/ingestion/utils/ast-helpers.js +153 -2
- package/dist/core/ingestion/utils/callable-flow-captures.d.ts +94 -0
- package/dist/core/ingestion/utils/callable-flow-captures.js +878 -0
- package/dist/core/ingestion/utils/line-base.d.ts +20 -0
- package/dist/core/ingestion/utils/line-base.js +20 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +21 -0
- package/dist/core/ingestion/utils/symbol-labels.js +45 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +34 -0
- package/dist/core/ingestion/workers/parse-worker.js +40 -5
- package/dist/core/ingestion/workers/result-merge.js +4 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +28 -0
- package/dist/core/ingestion/workers/worker-pool.js +128 -28
- package/dist/core/lbug/csv-generator.d.ts +15 -0
- package/dist/core/lbug/csv-generator.js +124 -27
- package/dist/core/lbug/cypher-escape.d.ts +20 -0
- package/dist/core/lbug/cypher-escape.js +20 -0
- package/dist/core/lbug/extension-load-error.d.ts +67 -0
- package/dist/core/lbug/extension-load-error.js +326 -0
- package/dist/core/lbug/extension-loader.d.ts +17 -2
- package/dist/core/lbug/extension-loader.js +38 -13
- package/dist/core/lbug/lbug-adapter.d.ts +203 -2
- package/dist/core/lbug/lbug-adapter.js +527 -86
- package/dist/core/lbug/lbug-config.d.ts +56 -2
- package/dist/core/lbug/lbug-config.js +267 -7
- package/dist/core/lbug/move-columns.d.ts +7 -0
- package/dist/core/lbug/move-columns.js +86 -0
- package/dist/core/lbug/native-check.d.ts +40 -0
- package/dist/core/lbug/native-check.js +90 -0
- package/dist/core/lbug/node-table-layout.d.ts +29 -0
- package/dist/core/lbug/node-table-layout.js +119 -0
- package/dist/core/lbug/pool-adapter.d.ts +23 -1
- package/dist/core/lbug/pool-adapter.js +115 -8
- package/dist/core/lbug/schema.d.ts +4 -3
- package/dist/core/lbug/schema.js +16 -16
- package/dist/core/lbug/sidecar-recovery.d.ts +172 -0
- package/dist/core/lbug/sidecar-recovery.js +418 -16
- package/dist/core/lbug/wal-checkpoint-driver.js +3 -0
- package/dist/core/move/artifact-download.d.ts +19 -0
- package/dist/core/move/artifact-download.js +179 -0
- package/dist/core/move/compiler-facts.d.ts +120 -0
- package/dist/core/move/compiler-facts.js +9 -0
- package/dist/core/move/consistency.d.ts +28 -0
- package/dist/core/move/consistency.js +98 -0
- package/dist/core/move/constants.d.ts +98 -0
- package/dist/core/move/constants.js +136 -0
- package/dist/core/move/discovery.d.ts +10 -0
- package/dist/core/move/discovery.js +22 -0
- package/dist/core/move/entry-points.d.ts +14 -0
- package/dist/core/move/entry-points.js +40 -0
- package/dist/core/move/facts-mapper.d.ts +75 -0
- package/dist/core/move/facts-mapper.js +534 -0
- package/dist/core/move/install-lock.d.ts +14 -0
- package/dist/core/move/install-lock.js +102 -0
- package/dist/core/move/install.d.ts +39 -0
- package/dist/core/move/install.js +391 -0
- package/dist/core/move/mcp-client.d.ts +115 -0
- package/dist/core/move/mcp-client.js +436 -0
- package/dist/core/move/move-ingest.d.ts +49 -0
- package/dist/core/move/move-ingest.js +320 -0
- package/dist/core/move/provision.d.ts +19 -0
- package/dist/core/move/provision.js +91 -0
- package/dist/core/move/release.d.ts +6 -0
- package/dist/core/move/release.js +10 -0
- package/dist/core/move/symbol-id.d.ts +35 -0
- package/dist/core/move/symbol-id.js +65 -0
- package/dist/core/move/type-parser.d.ts +11 -0
- package/dist/core/move/type-parser.js +38 -0
- package/dist/core/platform/capabilities.d.ts +0 -1
- package/dist/core/platform/capabilities.js +39 -8
- package/dist/core/run-analyze.d.ts +3 -3
- package/dist/core/run-analyze.js +1001 -184
- package/dist/core/search/fts-indexes.d.ts +31 -0
- package/dist/core/search/fts-indexes.js +73 -0
- package/dist/core/search/fts-schema.js +2 -0
- package/dist/core/tree-sitter/safe-parse.d.ts +8 -3
- package/dist/core/tree-sitter/safe-parse.js +21 -7
- package/dist/core/wiki/graph-queries.js +6 -5
- package/dist/core/wiki/llm-client.d.ts +8 -2
- package/dist/core/wiki/llm-client.js +41 -5
- package/dist/mcp/core/embedder.js +16 -2
- package/dist/mcp/http-transport.d.ts +5 -0
- package/dist/mcp/http-transport.js +19 -5
- package/dist/mcp/local/bean-metadata.d.ts +2 -0
- package/dist/mcp/local/bean-metadata.js +25 -0
- package/dist/mcp/local/line-display.d.ts +22 -0
- package/dist/mcp/local/line-display.js +3 -0
- package/dist/mcp/local/local-backend.d.ts +19 -1
- package/dist/mcp/local/local-backend.js +402 -155
- package/dist/mcp/local/pdg-impact.d.ts +5 -3
- package/dist/mcp/local/pdg-impact.js +5 -2
- package/dist/mcp/output-budget.d.ts +5 -0
- package/dist/mcp/output-budget.js +52 -0
- package/dist/mcp/read-only-policy.d.ts +12 -0
- package/dist/mcp/read-only-policy.js +102 -0
- package/dist/mcp/repository-policy.d.ts +34 -0
- package/dist/mcp/repository-policy.js +328 -0
- package/dist/mcp/resources.js +32 -5
- package/dist/mcp/server.d.ts +5 -2
- package/dist/mcp/server.js +29 -9
- package/dist/mcp/tools.d.ts +0 -8
- package/dist/mcp/tools.js +102 -5
- package/dist/server/analyze-job.d.ts +3 -0
- package/dist/server/analyze-job.js +17 -0
- package/dist/server/analyze-launch.d.ts +6 -0
- package/dist/server/analyze-launch.js +82 -6
- package/dist/server/analyze-worker-core.d.ts +2 -1
- package/dist/server/analyze-worker-core.js +5 -2
- package/dist/server/analyze-worker.js +29 -6
- package/dist/server/api.d.ts +30 -0
- package/dist/server/api.js +120 -34
- package/dist/server/mcp-http.d.ts +1 -1
- package/dist/server/mcp-http.js +4 -2
- package/dist/server/upload-ingest.js +2 -1
- package/dist/server/validation.js +2 -1
- package/dist/storage/git.d.ts +31 -0
- package/dist/storage/git.js +111 -1
- package/dist/storage/parse-cache.js +7 -1
- package/dist/storage/parsedfile-store.d.ts +8 -0
- package/dist/storage/parsedfile-store.js +165 -1
- package/dist/storage/repo-manager.d.ts +194 -2
- package/dist/storage/repo-manager.js +56 -1
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +50 -3
- package/hooks/claude/gitnexus-hook.cjs +59 -9
- package/hooks/claude/pre-tool-use.sh +0 -0
- package/hooks/claude/session-start.sh +0 -0
- package/package.json +21 -15
- package/scripts/cross-platform-tests.ts +56 -0
- package/scripts/ensure-fts.ts +43 -0
- package/scripts/install-duckdb-extension.mjs +85 -24
- package/scripts/run-cross-platform.ts +61 -6
- package/scripts/shard-arg.ts +30 -0
- package/scripts/sync-plugin-manifests.mjs +192 -0
- package/skills/gitnexus-lfg/README.md +55 -0
- package/skills/gitnexus-lfg/SKILL.md +86 -0
- package/skills/gitnexus-plan/README.md +142 -0
- package/skills/gitnexus-plan/SKILL.md +348 -0
- package/skills/gitnexus-plan/references/context-ledger.md +137 -0
- package/skills/gitnexus-plan/references/context-pack.md +126 -0
- package/skills/gitnexus-plan/references/evidence-provenance.md +272 -0
- package/skills/gitnexus-plan/references/pdg-slice.md +109 -0
- package/skills/gitnexus-plan/references/plan-template.md +201 -0
- package/skills/gitnexus-plan/scripts/evidence-provenance.mjs +2084 -0
- package/skills/gitnexus-review/SKILL.md +273 -0
- package/skills/gitnexus-review/ci-personas/ci-adversarial-lens.md +42 -0
- package/skills/gitnexus-review/ci-personas/ci-blast-radius-lens.md +39 -0
- package/skills/gitnexus-review/ci-personas/ci-correctness-lens.md +37 -0
- package/skills/gitnexus-review/ci-personas/ci-coverage-lens.md +40 -0
- package/skills/gitnexus-review/ci-personas/ci-critic-lens.md +42 -0
- package/skills/gitnexus-review/ci-personas/ci-security-lens.md +39 -0
- package/skills/gitnexus-work/README.md +71 -0
- package/skills/gitnexus-work/SKILL.md +269 -0
- package/skills/gitnexus-work/references/evidence-provenance.md +272 -0
- package/skills/gitnexus-work/scripts/evidence-provenance.mjs +2084 -0
- package/web/assets/ProcessFlowModal-B_bfBySy.js +22 -0
- package/web/assets/__vite-browser-external-bMKyYQkO.js +1 -0
- package/web/assets/agent-Bqxn-abU.js +626 -0
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-BVWFvYz2.js +1 -0
- package/web/assets/architectureDiagram-UL44E2DR-CmG3uPWf.js +36 -0
- package/web/assets/{blockDiagram-7IZFK4PR-BkWhpzWB.js → blockDiagram-7IZFK4PR-DBefcjof.js} +7 -7
- package/web/assets/{c4Diagram-Y2BXMSZH-DtBvpzWD.js → c4Diagram-Y2BXMSZH-EjWnjEJl.js} +6 -6
- package/web/assets/chunk-2T2R6R2M-Bb6Sy3_j.js +4 -0
- package/web/assets/chunk-2UTLFMKG-CXlhF3Pt.js +1 -0
- package/web/assets/chunk-3SSMPTDK-rPsRM1x0.js +321 -0
- package/web/assets/chunk-4R4BOZG6-Be_F0JoX.js +159 -0
- package/web/assets/{chunk-6764PJDD-DpIS7hxD.js → chunk-6764PJDD-C7xxk063.js} +1 -1
- package/web/assets/chunk-67TQ5CYL-CA7OhIEf.js +124 -0
- package/web/assets/chunk-7FYTHRHK-CsmYIswB.js +32 -0
- package/web/assets/chunk-7J6CGLKN-BlKwgXKs.js +10 -0
- package/web/assets/chunk-7W6UQGC5-pyKGssV3.js +1 -0
- package/web/assets/chunk-C62D2QBJ-3gdL8EKc.js +1 -0
- package/web/assets/chunk-CEXFNPSA-CjwtMDkc.js +1 -0
- package/web/assets/{chunk-INKRHTLW-Nu4ri9P-.js → chunk-INKRHTLW-Ckl3_KkO.js} +13 -13
- package/web/assets/chunk-J5EP6P6S-L19oz55R.js +1 -0
- package/web/assets/{chunk-KGFNY3KK-JGMJvKCK.js → chunk-KGFNY3KK-BZeP-55u.js} +17 -17
- package/web/assets/chunk-KGYTTC2M-zm6n242I.js +1 -0
- package/web/assets/{chunk-KRXBNO2N-BW5o0KUN.js → chunk-KRXBNO2N-C-UNuKKf.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-CfLLCywY.js → chunk-LCXTWHL2-C162wE5z.js} +5 -5
- package/web/assets/chunk-LII3EMHJ-BL6lKJ_P.js +1 -0
- package/web/assets/chunk-LRIF4GLE-n59lhprL.js +1 -0
- package/web/assets/chunk-QA3QBVWF-DVXPOIxe.js +2 -0
- package/web/assets/chunk-RERM46MO-B5dAdWpP.js +1 -0
- package/web/assets/chunk-RG4AUYOV-ZNgyLFq6.js +206 -0
- package/web/assets/chunk-RKZBBQEN-MyIQbGgD.js +1 -0
- package/web/assets/chunk-RLI5ZMPA-CkXZ0y0U.js +1 -0
- package/web/assets/chunk-T2UQINTJ-BEyXhZWe.js +1 -0
- package/web/assets/chunk-UP6H54XL-BG-n7qDp.js +1 -0
- package/web/assets/chunk-UXSXWOXI-CW1_r5cS.js +1 -0
- package/web/assets/chunk-UY5QBCOK-ByqTVLvh.js +1 -0
- package/web/assets/chunk-VU6ZFW4Y-BLeasdkU.js +2 -0
- package/web/assets/{chunk-W44A43WB-JqMLIpR0.js → chunk-W44A43WB-BN8EQc_u.js} +1 -1
- package/web/assets/chunk-ZXARS5L4-C3v-4Fsw.js +1 -0
- package/web/assets/classDiagram-KGZ6W3CR-olpO3fMe.js +1 -0
- package/web/assets/classDiagram-v2-72OJOZXJ-olpO3fMe.js +1 -0
- package/web/assets/cose-bilkent-UX7MHV2Q-BmnZ7Mvj.js +1 -0
- package/web/assets/dagre-ND4H6XIP-CY4iIF7v.js +4 -0
- package/web/assets/diagram-3NCE3AQN-CO6QpzEE.js +43 -0
- package/web/assets/diagram-GF46GFSD-B2atbzmU.js +24 -0
- package/web/assets/diagram-HNR7UZ2L-DVRltWbL.js +3 -0
- package/web/assets/diagram-QXG6HAR7-DR6AHD2h.js +24 -0
- package/web/assets/diagram-WEQXMOUZ-DkpoMwTL.js +10 -0
- package/web/assets/erDiagram-L5TCEMPS-n8g_90bc.js +85 -0
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-CgFBM7e-.js +1 -0
- package/web/assets/flowDiagram-H6V6AXG4-SGMDiCkB.js +162 -0
- package/web/assets/ganttDiagram-JCBTUEKG-Cb3UBqRS.js +292 -0
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DCJv7SII.js +1 -0
- package/web/assets/gitGraphDiagram-S2ZK5IYY-Bs9CR7tq.js +106 -0
- package/web/assets/index-CX_fADmQ.css +2 -0
- package/web/assets/index-D1Z3dGGz.js +575 -0
- package/web/assets/info-J43DQDTF-KCYPFFUO-BfNy-9Nd.js +1 -0
- package/web/assets/infoDiagram-3YFTVSEB-T368on4D.js +2 -0
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-DfYpxnE5.js → ishikawaDiagram-BNXS4ZKH-XpjJRU9h.js} +6 -6
- package/web/assets/journeyDiagram-M6C3CM3L-C6wjooas.js +139 -0
- package/web/assets/kanban-definition-75IXJCU3-5_UVY79g.js +89 -0
- package/web/assets/{katex-K3KEBU37-CTc5BslQ.js → katex-K3KEBU37-qRQBy-EZ.js} +2 -2
- package/web/assets/{mindmap-definition-2TDM6QVE-DypvbOMQ.js → mindmap-definition-2TDM6QVE-8xDLzk2e.js} +29 -29
- package/web/assets/node-Ct_zna8U.js +8 -0
- package/web/assets/packet-YPE3B663-LP52Z2RK-D4qkeG9k.js +1 -0
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-B_yMKyQa.js +1 -0
- package/web/assets/pieDiagram-CU6KROY3-Bv4LpAbr.js +30 -0
- package/web/assets/preload-helper-Czpn1I53.js +1 -0
- package/web/assets/{quadrantDiagram-VICAPDV7-yXSZ2lLz.js → quadrantDiagram-VICAPDV7-DR54jOeR.js} +6 -6
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-DeD6yN_0.js +1 -0
- package/web/assets/requirementDiagram-JXO7QTGE-PEkg7sKC.js +84 -0
- package/web/assets/rolldown-runtime-QTnfLwEv.js +1 -0
- package/web/assets/sankeyDiagram-URQDO5SZ-DDWEtjcs.js +40 -0
- package/web/assets/sdk-CzHHH8tD.js +37 -0
- package/web/assets/sequenceDiagram-VS2MUI6T-Bv-uwANx.js +162 -0
- package/web/assets/src-JV30wyV8.js +1 -0
- package/web/assets/stateDiagram-7D4R322I-DVNjmkEq.js +1 -0
- package/web/assets/stateDiagram-v2-36443NZ5-CyN4Fh7_.js +1 -0
- package/web/assets/timeline-definition-O6YCAMPW-Qmsuo7qS.js +120 -0
- package/web/assets/treeView-BLDUP644-QA4HXRO3-CDuSYnd4.js +1 -0
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-imazucyJ.js +1 -0
- package/web/assets/vennDiagram-MWXL3ELB-BD1iK1SX.js +34 -0
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-DqrT9tB8.js +1 -0
- package/web/assets/wardleyDiagram-CUQ6CDDI-D6TzLVob.js +78 -0
- package/web/assets/{xychartDiagram-N2JHSOCM-BOLm0eNm.js → xychartDiagram-N2JHSOCM-CoZ_Tits.js} +6 -6
- package/web/index.html +18 -10
- package/skills/gitnexus-pr-review.md +0 -163
- package/web/assets/agent-Dli_x7bU.js +0 -660
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-DxJw65fT.js +0 -1
- package/web/assets/architectureDiagram-UL44E2DR-dEkqUZN9.js +0 -36
- package/web/assets/chunk-3SSMPTDK-B4_etUhr.js +0 -321
- package/web/assets/chunk-67TQ5CYL-4sNPJU1q.js +0 -124
- package/web/assets/chunk-7W6UQGC5-KyEG0HQg.js +0 -1
- package/web/assets/chunk-KGYTTC2M-CWC_c3H9.js +0 -161
- package/web/assets/chunk-LII3EMHJ-D-jm-dLa.js +0 -1
- package/web/assets/chunk-QA3QBVWF-BgRW3SXd.js +0 -2
- package/web/assets/chunk-RG4AUYOV-CqWzWsV3.js +0 -206
- package/web/assets/chunk-T2UQINTJ-DSBqkzH7.js +0 -1
- package/web/assets/chunk-UY5QBCOK-DyF0vyGd.js +0 -1
- package/web/assets/chunk-ZXARS5L4-CxN8oiwI.js +0 -1
- package/web/assets/classDiagram-KGZ6W3CR-CMne4tG9.js +0 -1
- package/web/assets/classDiagram-v2-72OJOZXJ-CMne4tG9.js +0 -1
- package/web/assets/cose-bilkent-UX7MHV2Q-D71wNYRJ.js +0 -1
- package/web/assets/dagre-ND4H6XIP-Co5rRx9X.js +0 -4
- package/web/assets/diagram-3NCE3AQN-DBp4O00j.js +0 -43
- package/web/assets/diagram-GF46GFSD-6N_8yS0i.js +0 -24
- package/web/assets/diagram-HNR7UZ2L-DFCfd5LI.js +0 -3
- package/web/assets/diagram-QXG6HAR7-Di093rFg.js +0 -24
- package/web/assets/diagram-WEQXMOUZ-DZWi4SUN.js +0 -10
- package/web/assets/erDiagram-L5TCEMPS-8X1y6R4Y.js +0 -85
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-BQm9QzEa.js +0 -1
- package/web/assets/flowDiagram-H6V6AXG4-CMpY8Ufn.js +0 -162
- package/web/assets/ganttDiagram-JCBTUEKG-D3iL2Aet.js +0 -292
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-Cr_Bm2Nb.js +0 -1
- package/web/assets/gitGraphDiagram-S2ZK5IYY-CPIDgxGm.js +0 -106
- package/web/assets/index-KctnI3Vq.js +0 -635
- package/web/assets/index-_lgn7hs5.css +0 -2
- package/web/assets/info-J43DQDTF-KCYPFFUO-Cs4p2oyk.js +0 -1
- package/web/assets/infoDiagram-3YFTVSEB-BaLMZkUg.js +0 -2
- package/web/assets/journeyDiagram-M6C3CM3L-CvArszLo.js +0 -139
- package/web/assets/kanban-definition-75IXJCU3-CpfVLLf9.js +0 -89
- package/web/assets/packet-YPE3B663-LP52Z2RK-CKCrztD2.js +0 -1
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-BB7pHqoD.js +0 -1
- package/web/assets/pieDiagram-CU6KROY3-s27EFWgP.js +0 -30
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-DhoTOcuK.js +0 -1
- package/web/assets/requirementDiagram-JXO7QTGE-CWhqInuf.js +0 -84
- package/web/assets/sankeyDiagram-URQDO5SZ-1qEjlFIB.js +0 -40
- package/web/assets/sequenceDiagram-VS2MUI6T-RyTlZ5cR.js +0 -162
- package/web/assets/stateDiagram-7D4R322I-DTjVfX4A.js +0 -1
- package/web/assets/stateDiagram-v2-36443NZ5-xY9_k9_Q.js +0 -1
- package/web/assets/timeline-definition-O6YCAMPW-DrrqLYAf.js +0 -120
- package/web/assets/treeView-BLDUP644-QA4HXRO3-ODehyGKL.js +0 -1
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-BfpgC-7o.js +0 -1
- package/web/assets/vennDiagram-MWXL3ELB-B0RkWAim.js +0 -34
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-CNeL6VHE.js +0 -1
- package/web/assets/wardleyDiagram-CUQ6CDDI-boUxiQ5F.js +0 -78
package/dist/core/run-analyze.js
CHANGED
|
@@ -10,14 +10,23 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import fs from 'fs/promises';
|
|
13
|
-
import {
|
|
13
|
+
import { retryRename } from '../storage/fs-atomic.js';
|
|
14
14
|
import { runPipelineFromRepo } from './ingestion/pipeline.js';
|
|
15
|
+
import { isMoveCompilerInputPath, moveCompilerRequiresFullRebuild, persistedMoveGraphRequiresCompiler, shouldProvisionMoveFlowBeforeFastPath, } from './move/constants.js';
|
|
16
|
+
import { createMoveIngestPhase } from './move/move-ingest.js';
|
|
17
|
+
import { repoHasMove } from './move/discovery.js';
|
|
18
|
+
import { getMoveFlowReleaseSelection } from './move/install.js';
|
|
19
|
+
import { ensureMoveFlowRuntime } from './move/provision.js';
|
|
15
20
|
import { resetDegradedParseCounter } from './tree-sitter/safe-parse.js';
|
|
16
|
-
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings,
|
|
17
|
-
import {
|
|
21
|
+
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings, deleteNodesForFiles, ensureEmbeddingRowDmlSafe, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, deleteAllCallSummaries, deleteAllInjects, queryImportersBatch, loadFTSExtension, wipeLbugDbFiles, LbugWipeError, DELETE_FILES_CHUNK_SIZE, } from './lbug/lbug-adapter.js';
|
|
22
|
+
import { estimateBufferPool, setBufferPoolSizeHint } from './lbug/lbug-config.js';
|
|
23
|
+
import { escapeCypherString } from './lbug/cypher-escape.js';
|
|
24
|
+
import { buildSearchIndexesOrDegrade, createSearchFTSIndexes, dropSearchFTSIndexes, initialiseSearchFTSStemmer, verifySearchFTSIndexes, } from './search/fts-indexes.js';
|
|
18
25
|
import { cjkSegmentationModeMismatch, getSearchFTSCjkSegmentation, initialiseSearchFTSCjkSegmentation, } from './search/cjk-segmentation.js';
|
|
19
|
-
import { resolveAnalyzeInstallPolicy } from './lbug/extension-loader.js';
|
|
20
|
-
import {
|
|
26
|
+
import { getExtensionCapabilities, resolveAnalyzeInstallPolicy } from './lbug/extension-loader.js';
|
|
27
|
+
import { diagnoseExtensionLoad } from './lbug/extension-load-error.js';
|
|
28
|
+
import { startWalCheckpointDriver, checkpointOnce, } from './lbug/wal-checkpoint-driver.js';
|
|
29
|
+
import { quarantineSidecarsForDirtyRecovery, inspectLbugSidecars, } from './lbug/sidecar-recovery.js';
|
|
21
30
|
import { getStoragePaths, resolveBranchPlacement, saveMeta, loadMeta, ensureGitNexusIgnored, registerRepo, adoptFlatBranchLabel, isReadOnlyFilesystemError, isRepoRegistered, cleanupOldKuzuFiles, reconcileMetadataFiles, isMissingFilesystemError, INDEX_METADATA_FILE, INCREMENTAL_SCHEMA_VERSION, } from '../storage/repo-manager.js';
|
|
22
31
|
import { DEFAULT_PDG_MAX_FUNCTION_LINES } from './ingestion/cfg/collect.js';
|
|
23
32
|
import { DEFAULT_MAX_CFG_EDGES_PER_FUNCTION, DEFAULT_PDG_MAX_REACHING_DEF_EDGES_PER_FUNCTION, DEFAULT_PDG_MAX_CDG_EDGES_PER_FUNCTION, } from './ingestion/cfg/emit.js';
|
|
@@ -29,19 +38,61 @@ import { parseTruthyEnv, parsePositiveIntEnv } from './ingestion/utils/env.js';
|
|
|
29
38
|
import { computeFileHashes, diffFileHashes } from '../storage/file-hash.js';
|
|
30
39
|
import { extractChangedSubgraph, computeEffectiveWriteSet, } from './incremental/subgraph-extract.js';
|
|
31
40
|
import { shadowCandidatesFor } from './incremental/shadow-candidates.js';
|
|
41
|
+
import { shouldEscalateIncrementalWrite } from './incremental/escalation-gate.js';
|
|
32
42
|
import { loadParseCache, saveParseCache, pruneCache, PARSE_CACHE_VERSION, } from '../storage/parse-cache.js';
|
|
33
43
|
import { getDurableParsedFileDir, pruneAndSaveDurableParsedFileStore, } from '../storage/parsedfile-store.js';
|
|
34
|
-
import { getCurrentCommit, getCurrentBranch, getRemoteUrl, hasGitDir, getInferredRepoName, resolveRepoIdentityRoot, } from '../storage/git.js';
|
|
44
|
+
import { getCurrentCommit, getCurrentBranch, getRemoteUrl, hasGitDir, getInferredRepoName, isWorkingTreeDirty, resolveRepoIdentityRoot, } from '../storage/git.js';
|
|
35
45
|
import { generateAIContextFiles } from '../cli/ai-context.js';
|
|
36
46
|
import { sanitizeDetectedBranch } from '../cli/analyze-config.js';
|
|
37
47
|
import { EMBEDDING_TABLE_NAME } from './lbug/schema.js';
|
|
38
48
|
import { STALE_HASH_SENTINEL } from './lbug/schema.js';
|
|
49
|
+
import { isSpringBeanCandidateSourceFile } from './ingestion/frameworks/spring/bean-catalog.js';
|
|
50
|
+
import { SPRING_BEAN_INVENTORY_FEATURE } from './ingestion/frameworks/spring/analysis-features.js';
|
|
51
|
+
import { SPRING_CONFIG_BINDINGS_FEATURE } from './ingestion/languages/java/analysis-features.js';
|
|
52
|
+
import { CLASS_FRAMEWORK_ANNOTATIONS_FEATURE, findAnalysisFeatureMismatches, resolveAnalysisFeatureVersions, } from './analysis-features.js';
|
|
53
|
+
import { analyzerRunnerIdentitiesEqual, finalizeAnalyzerRunnerIdentity, resolveAnalyzerRunnerIdentity, } from './analyzer-identity.js';
|
|
54
|
+
const ANALYSIS_FEATURES = [
|
|
55
|
+
CLASS_FRAMEWORK_ANNOTATIONS_FEATURE,
|
|
56
|
+
SPRING_BEAN_INVENTORY_FEATURE,
|
|
57
|
+
SPRING_CONFIG_BINDINGS_FEATURE,
|
|
58
|
+
];
|
|
59
|
+
function stringList(value) {
|
|
60
|
+
return Array.isArray(value)
|
|
61
|
+
? value.filter((item) => typeof item === 'string')
|
|
62
|
+
: [];
|
|
63
|
+
}
|
|
64
|
+
function collectFrameworkAnnotationDriftFiles(graph, persistedRows) {
|
|
65
|
+
const persistedById = new Map();
|
|
66
|
+
for (const row of persistedRows) {
|
|
67
|
+
if (typeof row.id === 'string') {
|
|
68
|
+
persistedById.set(row.id, stringList(row.frameworkAnnotations));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const driftFiles = new Set();
|
|
72
|
+
graph.forEachNode((node) => {
|
|
73
|
+
if (node.label !== 'Class')
|
|
74
|
+
return;
|
|
75
|
+
const current = stringList(node.properties.frameworkAnnotations);
|
|
76
|
+
const persisted = persistedById.get(node.id) ?? [];
|
|
77
|
+
if (current.length !== persisted.length ||
|
|
78
|
+
current.some((annotation, index) => annotation !== persisted[index])) {
|
|
79
|
+
const filePath = node.properties.filePath;
|
|
80
|
+
if (typeof filePath === 'string')
|
|
81
|
+
driftFiles.add(filePath);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return driftFiles;
|
|
85
|
+
}
|
|
39
86
|
/**
|
|
40
87
|
* Logged when the optional FTS extension cannot be loaded or installed during
|
|
41
88
|
* a full analyze. Kept as a named constant so the env-var/command guidance
|
|
42
89
|
* stays in one place (mirrors the VECTOR message in embedding-pipeline.ts).
|
|
43
90
|
*/
|
|
44
|
-
|
|
91
|
+
// Class-neutral lead, reused for the missing-dependency degrade path (#2383 F2):
|
|
92
|
+
// its remedy already explains that reinstalling will NOT help, so appending the
|
|
93
|
+
// generic "install with network access" tail below would contradict it.
|
|
94
|
+
const FTS_UNAVAILABLE_LEAD = 'FTS extension unavailable; skipping search-index creation.';
|
|
95
|
+
const FTS_UNAVAILABLE_MESSAGE = `${FTS_UNAVAILABLE_LEAD} ` +
|
|
45
96
|
'Full-text/BM25 search will be disabled until the LadybugDB FTS extension is ' +
|
|
46
97
|
'installed once with network access (GITNEXUS_LBUG_EXTENSION_INSTALL=auto) or ' +
|
|
47
98
|
'pre-installed for offline use. Run `gitnexus doctor` for details.';
|
|
@@ -251,7 +302,7 @@ export const pdgModeMismatch = (recorded, options) => {
|
|
|
251
302
|
}
|
|
252
303
|
return false;
|
|
253
304
|
};
|
|
254
|
-
export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
305
|
+
export async function runFullAnalysis(repoPath, options, callbacks, runnerIdentityAtBootstrap) {
|
|
255
306
|
const log = (msg) => callbacks.onLog?.(msg);
|
|
256
307
|
const progress = (phase, percent, message) => callbacks.onProgress(phase, percent, message);
|
|
257
308
|
// Resolve + validate operator-provided FTS config once, before the expensive
|
|
@@ -270,6 +321,10 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
270
321
|
// (parse-cache, parsedfile-store) and the kuzu-migration cleanup live there
|
|
271
322
|
// and are shared across branches (#2106 KTD7).
|
|
272
323
|
const { storagePath } = getStoragePaths(repoPath);
|
|
324
|
+
// Start each analyze with a clean buffer-pool hint: any pre-pipeline DB open
|
|
325
|
+
// (e.g. the embeddings-cache open) falls back to the default until the hint is
|
|
326
|
+
// set from the built graph below, so a prior run's size can't leak in.
|
|
327
|
+
setBufferPoolSizeHint(undefined);
|
|
273
328
|
// Clean up stale KuzuDB files from before the LadybugDB migration.
|
|
274
329
|
const kuzuResult = await cleanupOldKuzuFiles(storagePath);
|
|
275
330
|
if (kuzuResult.found && kuzuResult.needsReindex) {
|
|
@@ -328,6 +383,18 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
328
383
|
throw new Error('Cannot repair FTS indexes because this repository has not been analyzed yet. ' +
|
|
329
384
|
'Run `gitnexus analyze` first to create the initial index, then retry `--repair-fts`.');
|
|
330
385
|
}
|
|
386
|
+
if (existingMeta.incrementalInProgress) {
|
|
387
|
+
// #2409 / tri-review 4669518496 (R6): a dirty flag means the previous
|
|
388
|
+
// run died mid-writeback — the graph may be half-written and its WAL
|
|
389
|
+
// possibly poisoned. This branch returns early, so the dirty-recovery
|
|
390
|
+
// sidecar quarantine below would never run: repairing FTS now would
|
|
391
|
+
// open the DB and replay that WAL pre-quarantine, and even a
|
|
392
|
+
// survivable open would certify FTS over a half-written graph.
|
|
393
|
+
throw new Error('Cannot repair FTS indexes: the index is mid-incremental-recovery ' +
|
|
394
|
+
'(a previous analyze run did not complete cleanly). ' +
|
|
395
|
+
'Run `gitnexus analyze` first — it recovers the index automatically — ' +
|
|
396
|
+
'then retry `--repair-fts`.');
|
|
397
|
+
}
|
|
331
398
|
let lbugStat;
|
|
332
399
|
try {
|
|
333
400
|
lbugStat = await fs.lstat(lbugPath);
|
|
@@ -365,9 +432,23 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
365
432
|
policy: resolveAnalyzeInstallPolicy(),
|
|
366
433
|
});
|
|
367
434
|
if (!repairFtsAvailable) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
435
|
+
// Surface the load-side reason (#2374): "not pre-installed" was wrong
|
|
436
|
+
// and doctor never installed anything, so the old message trapped
|
|
437
|
+
// users in a query → repair-fts → doctor loop with no way out.
|
|
438
|
+
const rawFtsReason = getExtensionCapabilities().find((c) => c.name === 'fts')?.reason;
|
|
439
|
+
const ftsReason = rawFtsReason?.replace(/\.$/, '');
|
|
440
|
+
// A missing runtime dependency (Windows error 126, #2374) is not healed
|
|
441
|
+
// by re-installing — the file is already present. Route that class to the
|
|
442
|
+
// classified remedy (install VC++ redist / OpenSSL) instead of the old
|
|
443
|
+
// "retry the network install" text that trapped the user in a loop.
|
|
444
|
+
const { kind, remedy } = diagnoseExtensionLoad(rawFtsReason);
|
|
445
|
+
const remedyTail = kind === 'missing_dependency'
|
|
446
|
+
? ` ${remedy}`
|
|
447
|
+
: '. Retry with network access and GITNEXUS_LBUG_EXTENSION_INSTALL=auto to install it, ' +
|
|
448
|
+
'or pre-install the extension file; run `gitnexus doctor` for live FTS status.';
|
|
449
|
+
throw new Error('Cannot repair FTS indexes: the LadybugDB FTS extension failed to load' +
|
|
450
|
+
(ftsReason ? ` — ${ftsReason}` : '') +
|
|
451
|
+
remedyTail);
|
|
371
452
|
}
|
|
372
453
|
progress('fts', 85, 'Repairing search indexes...');
|
|
373
454
|
await createSearchFTSIndexes({
|
|
@@ -400,20 +481,114 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
400
481
|
await closeLbug().catch(() => { });
|
|
401
482
|
}
|
|
402
483
|
}
|
|
484
|
+
// Resolve once per real analysis run so every successful metadata write
|
|
485
|
+
// carries one coherent receipt. The FTS-only repair path above intentionally
|
|
486
|
+
// returns without restamping: it does not regenerate the graph represented by
|
|
487
|
+
// RepoMeta and therefore must not claim a new analyzer identity.
|
|
488
|
+
const runnerIdentity = runnerIdentityAtBootstrap ?? resolveAnalyzerRunnerIdentity(import.meta.url);
|
|
489
|
+
if (!analyzerRunnerIdentitiesEqual(runnerIdentity, runnerIdentity)) {
|
|
490
|
+
throw new Error('Analyzer bootstrap supplied a malformed runner identity receipt');
|
|
491
|
+
}
|
|
492
|
+
let resumeEmbeddingCheckpoint = false;
|
|
493
|
+
let pendingEmbeddingNodeIds = new Set();
|
|
494
|
+
let embeddingIdentityForRun;
|
|
495
|
+
if (existingMeta?.embeddingCheckpoint) {
|
|
496
|
+
if (options.dropEmbeddings) {
|
|
497
|
+
log('Discarding the interrupted embedding checkpoint (--drop-embeddings).');
|
|
498
|
+
options = { ...options, force: true };
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
const { resolveEmbeddingIdentity } = await import('./embeddings/embedding-identity.js');
|
|
502
|
+
embeddingIdentityForRun = resolveEmbeddingIdentity();
|
|
503
|
+
const checkpoint = existingMeta.embeddingCheckpoint;
|
|
504
|
+
if (checkpoint.provider !== embeddingIdentityForRun.provider) {
|
|
505
|
+
throw new Error('Cannot resume embedding checkpoint: the embedding provider configuration differs. ' +
|
|
506
|
+
'Restore the matching endpoint configuration or pass --drop-embeddings to rebuild without it.');
|
|
507
|
+
}
|
|
508
|
+
if (checkpoint.model !== embeddingIdentityForRun.model ||
|
|
509
|
+
checkpoint.dimensions !== embeddingIdentityForRun.dimensions) {
|
|
510
|
+
throw new Error(`Cannot resume embedding checkpoint: it uses ${checkpoint.model} at ` +
|
|
511
|
+
`${checkpoint.dimensions} dimensions, but this run resolves ` +
|
|
512
|
+
`${embeddingIdentityForRun.model} at ${embeddingIdentityForRun.dimensions}. ` +
|
|
513
|
+
'Restore the matching embedding configuration or pass --drop-embeddings to rebuild without it.');
|
|
514
|
+
}
|
|
515
|
+
resumeEmbeddingCheckpoint = true;
|
|
516
|
+
pendingEmbeddingNodeIds = new Set(checkpoint.pendingNodeIds ?? []);
|
|
517
|
+
log(`Previous analyze ended at an embedding checkpoint ` +
|
|
518
|
+
`(${checkpoint.nodesProcessed}/${checkpoint.totalNodes} nodes); resuming from persisted hashes` +
|
|
519
|
+
`${pendingEmbeddingNodeIds.size > 0 ? ` and regenerating ${pendingEmbeddingNodeIds.size} pending node(s)` : ''}.`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
403
522
|
// ── Crash recovery: dirty flag forces full rebuild ────────────────
|
|
404
523
|
// If the previous incremental run set incrementalInProgress and didn't
|
|
405
524
|
// clear it, the on-disk index may be in a half-state. Cheapest path
|
|
406
525
|
// back to a known-good index is to wipe + rebuild from scratch.
|
|
407
526
|
if (existingMeta?.incrementalInProgress) {
|
|
527
|
+
const dirty = existingMeta.incrementalInProgress;
|
|
528
|
+
const dirtyDetails = typeof dirty === 'object'
|
|
529
|
+
? [
|
|
530
|
+
dirty.phase ? `phase=${dirty.phase}` : undefined,
|
|
531
|
+
`toWrite=${dirty.toWriteCount}`,
|
|
532
|
+
dirty.importerExpansion !== undefined
|
|
533
|
+
? `importerExpansion=${dirty.importerExpansion}`
|
|
534
|
+
: undefined,
|
|
535
|
+
dirty.effectiveWriteCount !== undefined
|
|
536
|
+
? `effectiveWrite=${dirty.effectiveWriteCount}`
|
|
537
|
+
: undefined,
|
|
538
|
+
dirty.deleteCount !== undefined ? `deleteCount=${dirty.deleteCount}` : undefined,
|
|
539
|
+
// Only stamped when > 0 (tri-review 4669518496 P2-5): its
|
|
540
|
+
// presence means the crashed run's importer expansion was
|
|
541
|
+
// already degraded — the write set may have been under-expanded
|
|
542
|
+
// before the crash.
|
|
543
|
+
dirty.droppedImporterChunks !== undefined
|
|
544
|
+
? `droppedImporterChunks=${dirty.droppedImporterChunks}`
|
|
545
|
+
: undefined,
|
|
546
|
+
]
|
|
547
|
+
.filter(Boolean)
|
|
548
|
+
.join(', ')
|
|
549
|
+
: 'legacy dirty flag';
|
|
408
550
|
log(
|
|
409
551
|
// "analyze run", not "incremental run" — since #2099 F1 the flag is a
|
|
410
552
|
// generic dirty marker written by BOTH writeback branches.
|
|
411
553
|
'Previous analyze run did not complete cleanly (incrementalInProgress flag set); ' +
|
|
554
|
+
`last dirty state: ${dirtyDetails}; ` +
|
|
412
555
|
'forcing full rebuild to restore a known-good index.');
|
|
413
556
|
options = { ...options, force: true };
|
|
414
557
|
// Reload meta after clearing the flag in-memory; we still want fileHashes
|
|
415
558
|
// for the post-rebuild meta carry-over, but force=true ensures the
|
|
416
559
|
// rebuild path executes.
|
|
560
|
+
//
|
|
561
|
+
// #2409 defect 2: the crashed writeback's WAL can be poisoned — replaying
|
|
562
|
+
// it kills the process natively, and the first DB open of this recovery
|
|
563
|
+
// run (the embedding-cache preservation open below) happens BEFORE the
|
|
564
|
+
// rebuild wipe that would discard it. Park the WAL/shadow sidecars aside
|
|
565
|
+
// now, while nothing is open, so every open in this run is replay-free.
|
|
566
|
+
// The rebuild wipes the DB regardless, so no committed data is at stake.
|
|
567
|
+
const { removed, failed } = await quarantineSidecarsForDirtyRecovery(lbugPath, log);
|
|
568
|
+
if (removed.length > 0) {
|
|
569
|
+
log(`Dirty-state recovery discarded ${removed.map((p) => path.basename(p)).join(', ')} ` +
|
|
570
|
+
'from the interrupted run (the file could not be moved aside, so its bytes were ' +
|
|
571
|
+
'removed — post-mortem forensics lost). Recovery proceeds with full embedding ' +
|
|
572
|
+
'preservation.');
|
|
573
|
+
}
|
|
574
|
+
if (failed.length > 0) {
|
|
575
|
+
// FIX 1 (this shipping review, replacing the tri-review 4669518496
|
|
576
|
+
// P2-3 drop-shape design): under a persistent lock the old drop-shape
|
|
577
|
+
// run derived its embedding mode as "drop", ran the WHOLE pipeline,
|
|
578
|
+
// and then died at the rebuild wipe on the very same handle — wasting
|
|
579
|
+
// minutes and zeroing embeddings on the way. A possibly-poisoned
|
|
580
|
+
// sidecar still sits next to the DB (any pre-wipe open would replay it
|
|
581
|
+
// and die), so failing here, in seconds, with the same actionable
|
|
582
|
+
// typed error the wipe would eventually throw is strictly better —
|
|
583
|
+
// and the CLI's LbugWipeError handler already renders it
|
|
584
|
+
// (recoveryHint 'lbug-wipe-failed'). The message is self-contained
|
|
585
|
+
// (headline + paths + lock guidance) because serve forwards only
|
|
586
|
+
// err.message over worker IPC.
|
|
587
|
+
throw new LbugWipeError(failed, {
|
|
588
|
+
headline: "Cannot start dirty-state recovery — the interrupted run's LadybugDB sidecars " +
|
|
589
|
+
'could neither be moved aside nor removed:',
|
|
590
|
+
});
|
|
591
|
+
}
|
|
417
592
|
}
|
|
418
593
|
// ── pdg-mode flip forces full writeback (#2099 F1) ─────────────────
|
|
419
594
|
// The incremental writeback persists only changed-file nodes, so a pdg
|
|
@@ -457,6 +632,36 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
457
632
|
`forcing a full rebuild so persisted rows match the current schema.`);
|
|
458
633
|
options = { ...options, force: true };
|
|
459
634
|
}
|
|
635
|
+
// ── independently-versioned analysis capabilities ────────────────
|
|
636
|
+
// `schemaVersion` is reserved for graph-wide incremental invariants. Some
|
|
637
|
+
// persisted semantics apply only to repositories containing relevant source
|
|
638
|
+
// files, so they carry exact feature versions instead. This guard must also
|
|
639
|
+
// run before alreadyUpToDate: current main and this PR both use schema v8,
|
|
640
|
+
// while pre-PR v8 indexes lack the Class frameworkAnnotations column and
|
|
641
|
+
// Java/Kotlin Bean evidence.
|
|
642
|
+
const persistedFilePaths = Object.keys(existingMeta?.fileHashes ?? {});
|
|
643
|
+
const expectedPersistedAnalysisFeatures = resolveAnalysisFeatureVersions(ANALYSIS_FEATURES, persistedFilePaths);
|
|
644
|
+
const persistedAnalysisFeatureMismatches = existingMeta
|
|
645
|
+
? findAnalysisFeatureMismatches(existingMeta.analysisFeatures, expectedPersistedAnalysisFeatures)
|
|
646
|
+
: [];
|
|
647
|
+
let analysisFeatureMismatchLogged = false;
|
|
648
|
+
if (existingMeta && persistedAnalysisFeatureMismatches.length > 0) {
|
|
649
|
+
log(`analysis capabilities changed (${persistedAnalysisFeatureMismatches.join(', ')}); ` +
|
|
650
|
+
`forcing a full rebuild so persisted feature evidence is complete.`);
|
|
651
|
+
options = { ...options, force: true };
|
|
652
|
+
analysisFeatureMismatchLogged = true;
|
|
653
|
+
}
|
|
654
|
+
// Analyzer provenance is part of freshness, not merely diagnostics. A
|
|
655
|
+
// same-commit fast path must not preserve metadata produced by an older,
|
|
656
|
+
// malformed, or dependency/native-different runner. Force a real rebuild so
|
|
657
|
+
// the graph and its schema-v4 receipt are finalized atomically together.
|
|
658
|
+
if (existingMeta && !analyzerRunnerIdentitiesEqual(existingMeta.runnerIdentity, runnerIdentity)) {
|
|
659
|
+
const stampedRunnerSchema = existingMeta.runnerIdentity?.schemaVersion;
|
|
660
|
+
log(`analyzer runner identity changed (stamped schema ${String(stampedRunnerSchema ?? 'missing')}, ` +
|
|
661
|
+
`this build uses schema ${runnerIdentity.schemaVersion}); forcing a full rebuild so the ` +
|
|
662
|
+
'index provenance matches the analyzer and dependency/native runtime that produced it.');
|
|
663
|
+
options = { ...options, force: true };
|
|
664
|
+
}
|
|
460
665
|
if (existingMeta &&
|
|
461
666
|
cjkSegmentationModeMismatch(existingMeta.cjkSegmentation, getSearchFTSCjkSegmentation())) {
|
|
462
667
|
log(`CJK segmentation mode changed (index built with '${existingMeta.cjkSegmentation ?? 'none'}', ` +
|
|
@@ -464,8 +669,39 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
464
669
|
`text and query-time segmentation stay in sync.`);
|
|
465
670
|
options = { ...options, force: true };
|
|
466
671
|
}
|
|
672
|
+
// Resolve local runtimes before the same-commit fast path. Managed
|
|
673
|
+
// provisioning is needed for a degraded/legacy graph and whenever the
|
|
674
|
+
// configured dynamic release differs from the one that built the index.
|
|
675
|
+
const hasMovePackages = await repoHasMove(repoPath);
|
|
676
|
+
const desiredMoveFlowRelease = hasMovePackages ? await getMoveFlowReleaseSelection() : undefined;
|
|
677
|
+
const persistedMoveCompilerRequired = persistedMoveGraphRequiresCompiler(existingMeta?.moveIngestAvailable, existingMeta?.fileHashes);
|
|
678
|
+
const provisionBeforeFastPath = shouldProvisionMoveFlowBeforeFastPath(hasMovePackages, persistedMoveCompilerRequired, existingMeta?.moveCompilerIdentity, existingMeta?.moveFlowReleaseSelection, desiredMoveFlowRelease);
|
|
679
|
+
let moveFlow = hasMovePackages
|
|
680
|
+
? await ensureMoveFlowRuntime({
|
|
681
|
+
onLog: log,
|
|
682
|
+
install: provisionBeforeFastPath,
|
|
683
|
+
})
|
|
684
|
+
: null;
|
|
685
|
+
let moveCompilerRebuildApplied = false;
|
|
686
|
+
const applyMoveCompilerRebuildPolicy = () => {
|
|
687
|
+
if (!existingMeta ||
|
|
688
|
+
!moveCompilerRequiresFullRebuild(hasMovePackages, moveFlow?.identity, existingMeta.moveIngestAvailable, existingMeta.moveCompilerIdentity)) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (!moveCompilerRebuildApplied) {
|
|
692
|
+
log(existingMeta.moveIngestAvailable === true
|
|
693
|
+
? 'Move compiler changed; forcing a full rebuild so persisted facts match it.'
|
|
694
|
+
: 'Move compiler became available; forcing a full rebuild to backfill Move symbols.');
|
|
695
|
+
}
|
|
696
|
+
moveCompilerRebuildApplied = true;
|
|
697
|
+
options = { ...options, force: true };
|
|
698
|
+
};
|
|
699
|
+
applyMoveCompilerRebuildPolicy();
|
|
467
700
|
// ── Early-return: already up to date ──────────────────────────────
|
|
468
|
-
if (existingMeta &&
|
|
701
|
+
if (existingMeta &&
|
|
702
|
+
!existingMeta.embeddingCheckpoint &&
|
|
703
|
+
!options.force &&
|
|
704
|
+
existingMeta.lastCommit === currentCommit) {
|
|
469
705
|
// Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
|
|
470
706
|
if (currentCommit !== '') {
|
|
471
707
|
// For git repos, even if HEAD matches lastCommit, the working tree
|
|
@@ -480,33 +716,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
480
716
|
// Counting them as dirty would perpetually defeat the up-to-date
|
|
481
717
|
// fast path because the previous analyze just wrote them
|
|
482
718
|
// (regression vs PR #1233 behavior).
|
|
483
|
-
const dirty = (
|
|
484
|
-
try {
|
|
485
|
-
const out = execFileSync('git', [
|
|
486
|
-
'status',
|
|
487
|
-
'--porcelain',
|
|
488
|
-
'--',
|
|
489
|
-
'.',
|
|
490
|
-
':(exclude).gitnexus',
|
|
491
|
-
':(exclude).gitnexus/**',
|
|
492
|
-
':(exclude).claude',
|
|
493
|
-
':(exclude).claude/**',
|
|
494
|
-
':(exclude).cursor',
|
|
495
|
-
':(exclude).cursor/**',
|
|
496
|
-
':(exclude)AGENTS.md',
|
|
497
|
-
':(exclude)CLAUDE.md',
|
|
498
|
-
], {
|
|
499
|
-
cwd: repoPath,
|
|
500
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
501
|
-
windowsHide: true,
|
|
502
|
-
encoding: 'utf8',
|
|
503
|
-
});
|
|
504
|
-
return out.trim().length > 0;
|
|
505
|
-
}
|
|
506
|
-
catch {
|
|
507
|
-
return true; // conservative on git failure
|
|
508
|
-
}
|
|
509
|
-
})();
|
|
719
|
+
const dirty = isWorkingTreeDirty(repoPath);
|
|
510
720
|
// Registration wrinkle around the fast path (#2264). A prior
|
|
511
721
|
// `analyze --name X` that hit a name collision writes meta.json (meta-save
|
|
512
722
|
// runs before registerRepo) then fails before registering, leaving the
|
|
@@ -554,6 +764,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
554
764
|
}
|
|
555
765
|
}
|
|
556
766
|
await ensureGitNexusIgnored(repoPath);
|
|
767
|
+
await moveFlow?.client.shutdown();
|
|
557
768
|
return {
|
|
558
769
|
// `resolveRepoIdentityRoot` collapses worktree roots to the
|
|
559
770
|
// canonical repo basename (#1259) but leaves arbitrary subdirs
|
|
@@ -569,6 +780,19 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
569
780
|
}
|
|
570
781
|
}
|
|
571
782
|
}
|
|
783
|
+
// From this point onward analysis may write or derive graph data. Never run
|
|
784
|
+
// that plan without the compiler that produced an existing Move graph: a
|
|
785
|
+
// partial rebuild would silently erase Move rows or degrade global clusters.
|
|
786
|
+
if (hasMovePackages && !moveFlow) {
|
|
787
|
+
moveFlow = await ensureMoveFlowRuntime({ onLog: log });
|
|
788
|
+
if (persistedMoveCompilerRequired && !moveFlow) {
|
|
789
|
+
throw new Error('move-flow is unavailable; the existing Move graph was left unchanged. ' +
|
|
790
|
+
'Restore move-flow or set MOVE_FLOW, then retry analysis.');
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const moveFlowClient = moveFlow?.client ?? null;
|
|
794
|
+
const moveIngestAvailable = hasMovePackages ? moveFlow !== null : undefined;
|
|
795
|
+
applyMoveCompilerRebuildPolicy();
|
|
572
796
|
// ── Cache embeddings from existing index before rebuild ────────────
|
|
573
797
|
// Four modes:
|
|
574
798
|
// --embeddings -> load cache, restore, then generate any new ones
|
|
@@ -587,7 +811,9 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
587
811
|
let cachedEmbeddingNodeIds = new Set();
|
|
588
812
|
let cachedEmbeddings = [];
|
|
589
813
|
const existingEmbeddingCount = existingMeta?.stats?.embeddings ?? 0;
|
|
590
|
-
const { forceRegenerateEmbeddings, preserveExistingEmbeddings, shouldGenerateEmbeddings, shouldLoadCache, } = _deriveEmbeddingMode(options, existingEmbeddingCount);
|
|
814
|
+
const { forceRegenerateEmbeddings, preserveExistingEmbeddings, shouldGenerateEmbeddings: derivedShouldGenerateEmbeddings, shouldLoadCache: derivedShouldLoadCache, } = _deriveEmbeddingMode(options, existingEmbeddingCount);
|
|
815
|
+
const shouldGenerateEmbeddings = derivedShouldGenerateEmbeddings || resumeEmbeddingCheckpoint;
|
|
816
|
+
const shouldLoadCache = derivedShouldLoadCache || resumeEmbeddingCheckpoint;
|
|
591
817
|
if (options.dropEmbeddings && existingEmbeddingCount > 0) {
|
|
592
818
|
log(`Dropping ${existingEmbeddingCount} existing embeddings (--drop-embeddings). ` +
|
|
593
819
|
`Re-run with --embeddings to regenerate.`);
|
|
@@ -610,6 +836,12 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
610
836
|
// silently dropping embeddings on a mispredicted run. The re-insert
|
|
611
837
|
// step gates itself on the actual `isIncremental` value to avoid
|
|
612
838
|
// PK-conflicts when the incremental writeback path keeps the rows.
|
|
839
|
+
//
|
|
840
|
+
// This is the FIRST DB open of the run — the one #2409 defect 2 is about.
|
|
841
|
+
// On a dirty-recovery run it happens only after the sidecar quarantine
|
|
842
|
+
// moved (or removed) the crashed run's WAL/shadow; when neither was
|
|
843
|
+
// possible the dirty block above already threw a LbugWipeError, so this
|
|
844
|
+
// open is replay-free by construction (FIX 1 of this shipping review).
|
|
613
845
|
if (shouldLoadCache && existingMeta) {
|
|
614
846
|
try {
|
|
615
847
|
progress('embeddings', 0, 'Caching embeddings...');
|
|
@@ -643,35 +875,44 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
643
875
|
// in-place (cache hits leave entries unchanged; misses add new ones).
|
|
644
876
|
const parseCache = await loadParseCache(storagePath);
|
|
645
877
|
// ── Phase 1: Full Pipeline (0–60%) ────────────────────────────────
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
878
|
+
// `finally` guarantees the spawned move-flow child is released even if the
|
|
879
|
+
// pipeline throws — important for long-running hosts (MCP daemon, eval-server).
|
|
880
|
+
let pipelineResult;
|
|
881
|
+
try {
|
|
882
|
+
pipelineResult = await runPipelineFromRepo(repoPath, (p) => {
|
|
883
|
+
const phaseLabel = PHASE_LABELS[p.phase] || p.phase;
|
|
884
|
+
const scaled = Math.round(p.percent * 0.6);
|
|
885
|
+
const message = p.detail
|
|
886
|
+
? `${p.message || phaseLabel} (${p.detail})`
|
|
887
|
+
: p.message || phaseLabel;
|
|
888
|
+
progress(p.phase, scaled, message);
|
|
889
|
+
}, {
|
|
890
|
+
parseCache,
|
|
891
|
+
workerPoolSize: options.workerPoolSize,
|
|
892
|
+
standaloneIngestPhase: createMoveIngestPhase(moveFlowClient),
|
|
893
|
+
// CFG/PDG opt-in (#2081 M1). PipelineOptions.pdg fans out to the worker
|
|
894
|
+
// build gate (workerData.pdg) and the scope-resolution emit gate.
|
|
895
|
+
pdg: options.pdg === true,
|
|
896
|
+
pdgMaxFunctionLines: options.pdgMaxFunctionLines,
|
|
897
|
+
pdgMaxEdgesPerFunction: options.pdgMaxEdgesPerFunction,
|
|
898
|
+
pdgMaxReachingDefEdgesPerFunction: options.pdgMaxReachingDefEdgesPerFunction,
|
|
899
|
+
pdgMaxCdgEdgesPerFunction: options.pdgMaxCdgEdgesPerFunction,
|
|
900
|
+
pdgMaxTaintFindingsPerFunction: options.pdgMaxTaintFindingsPerFunction,
|
|
901
|
+
pdgMaxTaintHops: options.pdgMaxTaintHops,
|
|
902
|
+
pdgMaxInterprocFindings: options.pdgMaxInterprocFindings,
|
|
903
|
+
pdgMaxInterprocHops: options.pdgMaxInterprocHops,
|
|
904
|
+
pdgMaxInterprocEdges: options.pdgMaxInterprocEdges,
|
|
905
|
+
// Streaming/chunked PDG emit (#2202) — gated to full-rebuild runs
|
|
906
|
+
// (force === true) so the incremental writeback never reads back an
|
|
907
|
+
// offloaded BasicBlock layer. Memory-only; byte-identical output.
|
|
908
|
+
streamPdgEmit: resolveStreamPdgEmit(options),
|
|
909
|
+
pdgEmitChunkSize: resolvePdgEmitChunkSize(options),
|
|
910
|
+
fetchWrappers: options.fetchWrappers,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
finally {
|
|
914
|
+
await moveFlowClient?.shutdown();
|
|
915
|
+
}
|
|
675
916
|
// ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────
|
|
676
917
|
progress('lbug', 60, 'Loading into LadybugDB...');
|
|
677
918
|
// Compute current per-file content hashes from the pipeline's File nodes.
|
|
@@ -686,22 +927,88 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
686
927
|
}
|
|
687
928
|
});
|
|
688
929
|
const newFileHashes = await computeFileHashes(repoPath, allFilePaths);
|
|
930
|
+
const currentAnalysisFeatures = resolveAnalysisFeatureVersions(ANALYSIS_FEATURES, allFilePaths);
|
|
931
|
+
const currentAnalysisFeatureMismatches = existingMeta
|
|
932
|
+
? findAnalysisFeatureMismatches(existingMeta.analysisFeatures, currentAnalysisFeatures)
|
|
933
|
+
: [];
|
|
934
|
+
if (existingMeta &&
|
|
935
|
+
currentAnalysisFeatureMismatches.length > 0 &&
|
|
936
|
+
!analysisFeatureMismatchLogged) {
|
|
937
|
+
// Covers a repository gaining or losing its first applicable source file:
|
|
938
|
+
// the persisted file list cannot predict that transition before the
|
|
939
|
+
// pipeline, but an incremental top-up would leave unchanged rows incomplete.
|
|
940
|
+
log(`analysis capabilities changed (${currentAnalysisFeatureMismatches.join(', ')}); ` +
|
|
941
|
+
`forcing a full rebuild so persisted feature evidence is complete.`);
|
|
942
|
+
options = { ...options, force: true };
|
|
943
|
+
}
|
|
689
944
|
// Decide incremental vs full at THIS point (post-pipeline, pre-DB).
|
|
690
945
|
// All eligibility conditions are checked here against the actual
|
|
691
946
|
// pipeline output — no separate pre-pipeline prediction to desync from
|
|
692
947
|
// (Bugbot review on PR #1479: a prediction that flipped post-pipeline
|
|
693
948
|
// could skip the embedding cache load and then take the full-rebuild
|
|
694
949
|
// path, silently losing embeddings).
|
|
695
|
-
const
|
|
950
|
+
const incrementalCandidate = !options.force &&
|
|
696
951
|
!!existingMeta &&
|
|
697
952
|
existingMeta.schemaVersion === INCREMENTAL_SCHEMA_VERSION &&
|
|
953
|
+
currentAnalysisFeatureMismatches.length === 0 &&
|
|
698
954
|
!!existingMeta.fileHashes &&
|
|
699
955
|
Object.keys(existingMeta.fileHashes).length > 0 &&
|
|
700
956
|
repoHasGit &&
|
|
701
957
|
allFilePaths.length > 0;
|
|
702
|
-
const
|
|
958
|
+
const candidateHashDiff = incrementalCandidate
|
|
703
959
|
? diffFileHashes(newFileHashes, existingMeta.fileHashes)
|
|
704
960
|
: undefined;
|
|
961
|
+
const moveCompilerInputsChanged = candidateHashDiff !== undefined &&
|
|
962
|
+
[...candidateHashDiff.changed, ...candidateHashDiff.added, ...candidateHashDiff.deleted].some(isMoveCompilerInputPath);
|
|
963
|
+
const isIncremental = incrementalCandidate && !moveCompilerInputsChanged;
|
|
964
|
+
const hashDiff = isIncremental ? candidateHashDiff : undefined;
|
|
965
|
+
if (incrementalCandidate && moveCompilerInputsChanged) {
|
|
966
|
+
log('Move compiler inputs changed; using a full rebuild for package-wide consistency.');
|
|
967
|
+
}
|
|
968
|
+
// #2 atomic index publish: on a full rebuild, build the fresh DB at a temp
|
|
969
|
+
// path and swap it over the live index in one rename at the very end, so a
|
|
970
|
+
// concurrent MCP reader opening mid-build only ever sees the previous
|
|
971
|
+
// complete index (never a wiped/half-built file) and a crash leaves the old
|
|
972
|
+
// index intact. The whole build flows through the singleton connection, so
|
|
973
|
+
// only initLbug/wipeLbugDbFiles below take the temp target.
|
|
974
|
+
//
|
|
975
|
+
// POSIX only: the common CLI/serve-worker analyze paths skip the native close
|
|
976
|
+
// (closeLbugBeforeExit, #2264) and leave the build handle open at swap time.
|
|
977
|
+
// POSIX renames an open file cleanly; a same-process open handle blocks the
|
|
978
|
+
// rename on Windows. Windows keeps the current in-place behavior
|
|
979
|
+
// (buildPath === lbugPath, no swap) until that is resolved (see §12/follow-up).
|
|
980
|
+
const isFullRebuild = !(isIncremental && hashDiff);
|
|
981
|
+
// Where the swap is allowed:
|
|
982
|
+
// - POSIX renames an open file, so the usual skip-native-close (#2264) is
|
|
983
|
+
// fine and the swap always applies.
|
|
984
|
+
// - Windows can swap only when a real close is safe to release the build
|
|
985
|
+
// handle before the rename — i.e. NOT a --pdg run (the #2264 destructor
|
|
986
|
+
// crash). Unverified on Windows CI; falls back to in-place otherwise.
|
|
987
|
+
const posixSwap = process.platform !== 'win32';
|
|
988
|
+
// #2614 Windows: the forced real-close before the rename re-bets that #2264 is
|
|
989
|
+
// --pdg-only, which is unproven (the CLI/worker skip the native close
|
|
990
|
+
// UNCONDITIONALLY) and unverifiable without a Windows runner. Keep it opt-in
|
|
991
|
+
// (GITNEXUS_ATOMIC_WINDOWS_SWAP=1) so the default Windows analyze stays on the
|
|
992
|
+
// proven in-place path; enable it only to test the Windows swap.
|
|
993
|
+
const windowsSwapOk = process.platform === 'win32' &&
|
|
994
|
+
options.pdg !== true &&
|
|
995
|
+
process.env.GITNEXUS_ATOMIC_WINDOWS_SWAP === '1';
|
|
996
|
+
// Incremental atomicity copies the whole index into the temp before mutating
|
|
997
|
+
// it, which negates incremental's speed premise — so it is opt-in
|
|
998
|
+
// (GITNEXUS_ATOMIC_INCREMENTAL=1) pending a benchmark. Full rebuilds always
|
|
999
|
+
// swap where the platform allows.
|
|
1000
|
+
const wantAtomicIncremental = isIncremental && !!hashDiff && process.env.GITNEXUS_ATOMIC_INCREMENTAL === '1';
|
|
1001
|
+
// #2614 F3: the copy-then-swap stages ONLY the main lbug file, so a live index
|
|
1002
|
+
// carrying an orphan .wal/.shadow (a silently-failed prior checkpoint) would
|
|
1003
|
+
// be copied incompletely and lose that delta. Only take the atomic path when
|
|
1004
|
+
// the live index is a consolidated single file; otherwise fall back to the
|
|
1005
|
+
// in-place writeback, which the next open replays correctly.
|
|
1006
|
+
const atomicIncremental = wantAtomicIncremental && (await inspectLbugSidecars(lbugPath)).kind === 'clean';
|
|
1007
|
+
if (wantAtomicIncremental && !atomicIncremental) {
|
|
1008
|
+
log('atomic-incremental: live index carries orphan sidecars — using in-place writeback');
|
|
1009
|
+
}
|
|
1010
|
+
const useAtomicSwap = (isFullRebuild || atomicIncremental) && (posixSwap || windowsSwapOk);
|
|
1011
|
+
const buildPath = useAtomicSwap ? `${lbugPath}.new` : lbugPath;
|
|
705
1012
|
if (isIncremental && hashDiff) {
|
|
706
1013
|
log(`Incremental: changed=${hashDiff.changed.length}, ` +
|
|
707
1014
|
`added=${hashDiff.added.length}, ` +
|
|
@@ -709,13 +1016,25 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
709
1016
|
`(skipping wipe + ${allFilePaths.length - hashDiff.toWrite.length} unchanged file rows preserved)`);
|
|
710
1017
|
// Set the dirty flag BEFORE any destructive DB mutation. Cleared on
|
|
711
1018
|
// success at the meta-save step. Scoped to this branch's meta.json.
|
|
1019
|
+
const now = Date.now();
|
|
712
1020
|
await saveMeta(metaDir, {
|
|
713
1021
|
...existingMeta,
|
|
714
1022
|
incrementalInProgress: {
|
|
715
|
-
startedAt:
|
|
1023
|
+
startedAt: now,
|
|
1024
|
+
updatedAt: now,
|
|
1025
|
+
phase: 'pre-write',
|
|
716
1026
|
toWriteCount: hashDiff.toWrite.length,
|
|
1027
|
+
directWriteCount: hashDiff.toWrite.length,
|
|
717
1028
|
},
|
|
718
1029
|
});
|
|
1030
|
+
if (atomicIncremental) {
|
|
1031
|
+
// Stage the live index into the temp so the in-place delete/writeback
|
|
1032
|
+
// below mutates the COPY, and the end-of-run swap publishes it atomically.
|
|
1033
|
+
// Clear any stale temp first (a crashed run), then copy the (consolidated,
|
|
1034
|
+
// single-file) live index. Whole-file copy — hence opt-in.
|
|
1035
|
+
await wipeLbugDbFiles(buildPath);
|
|
1036
|
+
await fs.copyFile(lbugPath, buildPath);
|
|
1037
|
+
}
|
|
719
1038
|
}
|
|
720
1039
|
else {
|
|
721
1040
|
// Full rebuild path: wipe DB files first.
|
|
@@ -727,23 +1046,42 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
727
1046
|
// pdg flip, certify zombie/missing BasicBlock rows indefinitely).
|
|
728
1047
|
// toWriteCount: 0 is the full-path sentinel (no incremental write set).
|
|
729
1048
|
if (existingMeta) {
|
|
1049
|
+
const now = Date.now();
|
|
730
1050
|
await saveMeta(metaDir, {
|
|
731
1051
|
...existingMeta,
|
|
732
|
-
incrementalInProgress: {
|
|
1052
|
+
incrementalInProgress: {
|
|
1053
|
+
startedAt: now,
|
|
1054
|
+
updatedAt: now,
|
|
1055
|
+
phase: 'full-rebuild',
|
|
1056
|
+
toWriteCount: 0,
|
|
1057
|
+
},
|
|
733
1058
|
});
|
|
734
1059
|
}
|
|
735
1060
|
await closeLbug();
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1061
|
+
// Shared loud wipe (#2409 + tri-review 4669518496 P2-4). The 4-file
|
|
1062
|
+
// family list — `.shadow` included, because a checkpoint-in-flight crash
|
|
1063
|
+
// leaves a shadow sidecar that is replay poison next to a freshly created
|
|
1064
|
+
// DB file — lives in wipeLbugDbFiles so this site and the escalation
|
|
1065
|
+
// valve below can never drift. Failures now throw a typed LbugWipeError
|
|
1066
|
+
// (ENOENT-verified removal) instead of silently letting initLbug reopen
|
|
1067
|
+
// a still-populated DB this run believes it wiped.
|
|
1068
|
+
//
|
|
1069
|
+
// With the atomic swap (POSIX), this wipes the TEMP build target
|
|
1070
|
+
// (`buildPath` = `<lbugPath>.new`, clearing any stragglers from a crashed
|
|
1071
|
+
// run) and leaves the live index untouched until the end-of-run swap. On
|
|
1072
|
+
// Windows buildPath === lbugPath, so this is the original in-place wipe.
|
|
1073
|
+
await wipeLbugDbFiles(buildPath);
|
|
745
1074
|
}
|
|
746
|
-
|
|
1075
|
+
// Size the buffer pool to the graph just built by the pipeline (a page cache
|
|
1076
|
+
// over the on-disk index, which scales with node/edge count) instead of the
|
|
1077
|
+
// fixed 2 GiB default, whose eager commit dominates large-repo analyze. The
|
|
1078
|
+
// size is clamped to [COPY-safety floor, default], so it only ever shrinks
|
|
1079
|
+
// the pool; env override / no-hint paths are unchanged. See
|
|
1080
|
+
// resolveBufferManagerSize / estimateBufferPool.
|
|
1081
|
+
setBufferPoolSizeHint(estimateBufferPool(pipelineResult.graph.nodeCount + pipelineResult.graph.relationshipCount));
|
|
1082
|
+
// Full rebuild (POSIX) builds into the temp `buildPath`; incremental and
|
|
1083
|
+
// Windows use `buildPath === lbugPath` in place.
|
|
1084
|
+
await initLbug(buildPath);
|
|
747
1085
|
// Manual WAL checkpoint driver (#1741): periodically drain the WAL
|
|
748
1086
|
// from JS so the un-retriable native auto-checkpoint almost never
|
|
749
1087
|
// has work left to do. Failures of the manual CHECKPOINT are absorbed
|
|
@@ -752,12 +1090,31 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
752
1090
|
// Opt-out via `GITNEXUS_WAL_MANUAL_CHECKPOINT=0` (the driver itself
|
|
753
1091
|
// returns a no-op handle when disabled). Analyze-only: MCP and serve
|
|
754
1092
|
// paths continue to rely on the close-time CHECKPOINT in `safeClose`.
|
|
755
|
-
|
|
1093
|
+
// `let`: the incremental branch's escalation valve (#2409) stops this driver
|
|
1094
|
+
// around its close→wipe→reopen strategy switch and starts a fresh one.
|
|
1095
|
+
let walCheckpointDriver = startWalCheckpointDriver();
|
|
756
1096
|
try {
|
|
757
1097
|
// All work after initLbug is wrapped in try/finally to ensure closeLbug()
|
|
758
1098
|
// is called even if an error occurs — the module-level singleton DB handle
|
|
759
1099
|
// must be released to avoid blocking subsequent invocations.
|
|
760
1100
|
let lbugMsgCount = 0;
|
|
1101
|
+
// #2409 escalation valve outcome, hoisted above the incremental branch so
|
|
1102
|
+
// the vector-index recreation seam in Phase 4 below can tell "surgical
|
|
1103
|
+
// incremental" (DB files survived — the HNSW index with them) apart from
|
|
1104
|
+
// "escalated full write" (DB wiped, index destroyed) — tri-review
|
|
1105
|
+
// 4669518496 P1.
|
|
1106
|
+
let escalatedFullWrite = false;
|
|
1107
|
+
// Phase 3.5's restore scope (FIX 3 of this shipping review): on the
|
|
1108
|
+
// SURGICAL write plan this is the exact file set whose rows
|
|
1109
|
+
// deleteNodesForFiles just removed — only THOSE files' cached embedding
|
|
1110
|
+
// rows need re-inserting (everything else still sits in the DB, and
|
|
1111
|
+
// re-inserting it would PK-conflict). `null` means the DB was wiped
|
|
1112
|
+
// (full rebuild or escalated write): the embedding table is fresh and
|
|
1113
|
+
// every cached row must come back. Deriving this in memory replaces the
|
|
1114
|
+
// old whole-table `RETURN e.id` pre-read, which rescanned data this
|
|
1115
|
+
// process already holds and — worse — ran a read against the DB between
|
|
1116
|
+
// writeback and finalize for no recovery benefit.
|
|
1117
|
+
let deletedFilePathsForRestore = null;
|
|
761
1118
|
if (isIncremental && hashDiff) {
|
|
762
1119
|
// ── Incremental DB writeback ───────────────────────────────────
|
|
763
1120
|
// 0. Expand the writable set with transitive importers of
|
|
@@ -782,21 +1139,50 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
782
1139
|
// self-acknowledged as best-effort; `--force` remains the
|
|
783
1140
|
// escape hatch documented in GUARDRAILS.md.
|
|
784
1141
|
//
|
|
785
|
-
// `
|
|
1142
|
+
// `queryImportersBatch` reads `IMPORTS` from the pre-pipeline DB
|
|
786
1143
|
// state, so the result is "files that USED TO import the
|
|
787
1144
|
// target" — exactly the set whose previously-stored edges may
|
|
788
1145
|
// no longer match what cross-file resolution produces this run.
|
|
789
1146
|
const MAX_IMPORTER_BFS_DEPTH = 4;
|
|
1147
|
+
// Escalation thresholds (#2409) live with shouldEscalateIncrementalWrite
|
|
1148
|
+
// in incremental/escalation-gate.ts (pure predicate, boundary-tested).
|
|
790
1149
|
const writableFiles = new Set(hashDiff.toWrite);
|
|
791
1150
|
const directlyChangedCount = writableFiles.size;
|
|
792
|
-
|
|
1151
|
+
const dirtyStartedAt = existingMeta.incrementalInProgress?.startedAt ?? Date.now();
|
|
1152
|
+
// Dropped-chunk observability (tri-review 4669518496 P2-5): counts
|
|
1153
|
+
// importer-BFS chunks whose IMPORTS query failed across ALL depths
|
|
1154
|
+
// (degrade-don't-fail — the expansion shrinks instead of the run
|
|
1155
|
+
// dying). Stamped into the #2410 crash diagnostics by
|
|
1156
|
+
// saveIncrementalDirtyState ITSELF (FIX 6 of this shipping review),
|
|
1157
|
+
// not by per-call-site spreads: the closure rebuilds its object from
|
|
1158
|
+
// scratch on every call, so a count riding along at only some sites
|
|
1159
|
+
// meant any newly added save site would silently erase it — exactly
|
|
1160
|
+
// the phases where #2409-class crashes happen. >0-only semantics
|
|
1161
|
+
// unchanged: unconditional zero-stamping would churn every
|
|
1162
|
+
// strict-equality consumer of the diagnostics shape.
|
|
1163
|
+
let droppedImporterChunks = 0;
|
|
1164
|
+
const saveIncrementalDirtyState = async (phase, extra = {}) => {
|
|
1165
|
+
await saveMeta(metaDir, {
|
|
1166
|
+
...existingMeta,
|
|
1167
|
+
incrementalInProgress: {
|
|
1168
|
+
startedAt: dirtyStartedAt,
|
|
1169
|
+
updatedAt: Date.now(),
|
|
1170
|
+
phase,
|
|
1171
|
+
toWriteCount: writableFiles.size,
|
|
1172
|
+
directWriteCount: directlyChangedCount,
|
|
1173
|
+
...(droppedImporterChunks > 0 ? { droppedImporterChunks } : {}),
|
|
1174
|
+
...extra,
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
};
|
|
1178
|
+
// Shadow-seed: for ADDED files, the importer query returns 0 (the new
|
|
793
1179
|
// file has no IMPORTS rows in the pre-pipeline DB yet). But pre-
|
|
794
1180
|
// existing unchanged files may have IMPORTS edges whose module-
|
|
795
1181
|
// resolution claim the newcomer can steal under standard JS/TS
|
|
796
1182
|
// resolution (Bugbot review on PR #1479). For each added file we
|
|
797
1183
|
// derive the shadow candidates and, if the candidate was a known
|
|
798
1184
|
// file in the prior meta, seed it into the BFS frontier so its
|
|
799
|
-
// importers — surfaced via
|
|
1185
|
+
// importers — surfaced via the importer BFS — get their CALLS edges
|
|
800
1186
|
// re-resolved against the new file. See shadow-candidates.ts for
|
|
801
1187
|
// the full pattern catalogue.
|
|
802
1188
|
const priorFileSet = new Set(existingMeta?.fileHashes ? Object.keys(existingMeta.fileHashes) : []);
|
|
@@ -809,28 +1195,33 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
809
1195
|
}
|
|
810
1196
|
}
|
|
811
1197
|
{
|
|
1198
|
+
// Batched per depth level (#2409): one IN-list query per ~200-path
|
|
1199
|
+
// chunk instead of one query per frontier file — a ~700-file frontier
|
|
1200
|
+
// used to cost ~700 sequential lock-taking round-trips (~5.6s). The
|
|
1201
|
+
// closure is identical: importers already in writableFiles are not
|
|
1202
|
+
// re-frontiered, exactly like the per-file loop's membership check.
|
|
812
1203
|
let frontier = [...hashDiff.toWrite, ...hashDiff.deleted, ...shadowSeed];
|
|
813
1204
|
for (let depth = 0; depth < MAX_IMPORTER_BFS_DEPTH && frontier.length > 0; depth++) {
|
|
1205
|
+
const importers = await queryImportersBatch(frontier, {
|
|
1206
|
+
onChunkFailure: () => {
|
|
1207
|
+
droppedImporterChunks += 1;
|
|
1208
|
+
},
|
|
1209
|
+
});
|
|
814
1210
|
const nextFrontier = [];
|
|
815
|
-
for (const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
if (!writableFiles.has(i)) {
|
|
820
|
-
writableFiles.add(i);
|
|
821
|
-
nextFrontier.push(i);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
catch {
|
|
826
|
-
/* per-file importer query failure → skip; correctness degrades on
|
|
827
|
-
that branch, but DB stays writable. */
|
|
1211
|
+
for (const i of importers) {
|
|
1212
|
+
if (!writableFiles.has(i)) {
|
|
1213
|
+
writableFiles.add(i);
|
|
1214
|
+
nextFrontier.push(i);
|
|
828
1215
|
}
|
|
829
1216
|
}
|
|
830
1217
|
frontier = nextFrontier;
|
|
831
1218
|
}
|
|
832
1219
|
}
|
|
833
1220
|
const importerExpansion = writableFiles.size - directlyChangedCount;
|
|
1221
|
+
await saveIncrementalDirtyState('importer-bfs', {
|
|
1222
|
+
importerExpansion,
|
|
1223
|
+
shadowSeedCount: shadowSeed.length,
|
|
1224
|
+
});
|
|
834
1225
|
if (importerExpansion > 0) {
|
|
835
1226
|
log(`Incremental: +${importerExpansion} importer(s) added to writable set ` +
|
|
836
1227
|
`(BFS depth ≤ ${MAX_IMPORTER_BFS_DEPTH}` +
|
|
@@ -848,69 +1239,221 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
848
1239
|
// cross-file CALLS edges that the pre-run DB couldn't
|
|
849
1240
|
// predict, e.g. a barrel re-export shifting `foo` from
|
|
850
1241
|
// B to D).
|
|
851
|
-
// The composed set is the input to BOTH
|
|
1242
|
+
// The composed set is the input to BOTH deleteNodesForFiles
|
|
852
1243
|
// and extractChangedSubgraph — asymmetry between the two would
|
|
853
1244
|
// leave stale rows or PK-conflict at COPY time.
|
|
854
1245
|
const effectiveWriteSet = computeEffectiveWriteSet(pipelineResult.graph, writableFiles);
|
|
1246
|
+
// `frameworkAnnotations` is derived from cross-file JVM visibility, so
|
|
1247
|
+
// an unchanged Class row can change when a same-package declaration is
|
|
1248
|
+
// added or removed without producing an IMPORTS edge. Compare the fresh
|
|
1249
|
+
// graph against the pre-write DB and rewrite only files whose persisted
|
|
1250
|
+
// value drifted. Add them after edge-boundary expansion: relationships
|
|
1251
|
+
// touching these files are already included by extractChangedSubgraph,
|
|
1252
|
+
// while pulling every unchanged neighbor would add no correctness.
|
|
1253
|
+
// Only supported Spring Bean source changes can alter this property;
|
|
1254
|
+
// avoid materializing every persisted Class row for unrelated language
|
|
1255
|
+
// updates. Check deleted paths too so removing/renaming a Java shadowing
|
|
1256
|
+
// declaration still refreshes unchanged Spring candidates.
|
|
1257
|
+
const beanSourceChanged = hashDiff.toWrite.some(isSpringBeanCandidateSourceFile) ||
|
|
1258
|
+
hashDiff.deleted.some(isSpringBeanCandidateSourceFile);
|
|
1259
|
+
if (beanSourceChanged) {
|
|
1260
|
+
const persistedFrameworkAnnotations = (await executeQuery('MATCH (c:Class) ' + 'RETURN c.id AS id, c.frameworkAnnotations AS frameworkAnnotations'));
|
|
1261
|
+
const frameworkAnnotationDriftFiles = collectFrameworkAnnotationDriftFiles(pipelineResult.graph, persistedFrameworkAnnotations);
|
|
1262
|
+
for (const filePath of frameworkAnnotationDriftFiles)
|
|
1263
|
+
effectiveWriteSet.add(filePath);
|
|
1264
|
+
if (frameworkAnnotationDriftFiles.size > 0) {
|
|
1265
|
+
log(`Incremental: +${frameworkAnnotationDriftFiles.size} file(s) added for ` +
|
|
1266
|
+
'framework annotation property drift');
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
855
1269
|
// Deduped: deleted entries may already appear via importer-BFS
|
|
856
|
-
// expansion (
|
|
857
|
-
// would otherwise
|
|
858
|
-
// (Bugbot LOW finding on PR #1479).
|
|
1270
|
+
// expansion (the importer BFS can return a now-deleted path), which
|
|
1271
|
+
// would otherwise hand deleteNodesForFiles the same path twice in one
|
|
1272
|
+
// batch (Bugbot LOW finding on PR #1479).
|
|
859
1273
|
const filesToDelete = [...new Set([...effectiveWriteSet, ...hashDiff.deleted])];
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1274
|
+
await saveIncrementalDirtyState('effective-write-set', {
|
|
1275
|
+
importerExpansion,
|
|
1276
|
+
shadowSeedCount: shadowSeed.length,
|
|
1277
|
+
effectiveWriteCount: effectiveWriteSet.size,
|
|
1278
|
+
deleteCount: filesToDelete.length,
|
|
1279
|
+
});
|
|
1280
|
+
// Escalation valve (#2409): when the effective write set covers most of
|
|
1281
|
+
// the repo, per-file surgery is strictly worse than the proven
|
|
1282
|
+
// wipe-and-bulk-COPY plan — the same data volume lands either way, but
|
|
1283
|
+
// the surgical plan pays per-table deletes plus COPY-into-non-empty
|
|
1284
|
+
// tables, and at this size it measured SLOWER than a full DB load. The
|
|
1285
|
+
// pipeline already produced the FULL graph (it always does), so only the
|
|
1286
|
+
// DB write plan changes here; fileHashes/meta bookkeeping is identical.
|
|
1287
|
+
// Thresholds + the AND-gate live in incremental/escalation-gate.ts.
|
|
1288
|
+
const writeFraction = effectiveWriteSet.size / Math.max(1, allFilePaths.length);
|
|
1289
|
+
// VECTOR gate (#2623) — load the extension BEFORE a single embedding row
|
|
1290
|
+
// is touched. `deleteNodesForFiles` below opens with the CodeEmbedding
|
|
1291
|
+
// join-delete, and LadybugDB refuses all DML on a table carrying its HNSW
|
|
1292
|
+
// index unless VECTOR is loaded on this connection; nothing else on this
|
|
1293
|
+
// path loads it until Phase 4, so every incremental run over a DB that
|
|
1294
|
+
// already built `code_embedding_idx` died here. Same seam the FTS drop
|
|
1295
|
+
// occupies at the head of this branch (#2589): index lifecycle first,
|
|
1296
|
+
// then rows. UNCONDITIONAL — not gated on `shouldGenerateEmbeddings` —
|
|
1297
|
+
// because a DB carrying the index from an earlier `--embeddings` run hits
|
|
1298
|
+
// the identical wall on a plain incremental run.
|
|
1299
|
+
//
|
|
1300
|
+
// When VECTOR genuinely cannot load, the table is immutable (the index
|
|
1301
|
+
// cannot be dropped without the extension either), so surgery is
|
|
1302
|
+
// impossible: fall through to the escalation valve's wipe-and-COPY plan,
|
|
1303
|
+
// which rebuilds the DB files outright and needs no embedding-row DML.
|
|
1304
|
+
const embeddingRowDmlSafe = await ensureEmbeddingRowDmlSafe();
|
|
1305
|
+
if (!embeddingRowDmlSafe && cachedEmbeddings.length === 0) {
|
|
1306
|
+
// The escalation below WIPES the DB files, and Phase 3.5 restores
|
|
1307
|
+
// embedding rows from `cachedEmbeddings` — which is only populated when
|
|
1308
|
+
// `deriveEmbeddingMode` saw `meta.stats.embeddings > 0`. A DB whose meta
|
|
1309
|
+
// under-reports its embeddings (meta restored from an older run, or a
|
|
1310
|
+
// count that never got stamped) would therefore have every vector
|
|
1311
|
+
// silently destroyed by a rebuild it did not ask for. Read them now,
|
|
1312
|
+
// while the DB is still intact — a plain MATCH, which needs no VECTOR
|
|
1313
|
+
// extension. Rows whose owning node is gone are dropped by Phase 3.5's
|
|
1314
|
+
// live-graph filter, exactly as on any other wiped path.
|
|
1315
|
+
const rescued = await loadCachedEmbeddings();
|
|
1316
|
+
if (rescued.embeddings.length > 0) {
|
|
1317
|
+
cachedEmbeddings = rescued.embeddings;
|
|
1318
|
+
cachedEmbeddingNodeIds = rescued.embeddingNodeIds;
|
|
1319
|
+
log(`Preserving ${rescued.embeddings.length} embedding row(s) across the forced rebuild ` +
|
|
1320
|
+
`(the index metadata did not account for them).`);
|
|
870
1321
|
}
|
|
871
1322
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
|
|
1323
|
+
if (!embeddingRowDmlSafe ||
|
|
1324
|
+
shouldEscalateIncrementalWrite(filesToDelete.length, effectiveWriteSet.size, allFilePaths.length)) {
|
|
1325
|
+
escalatedFullWrite = true;
|
|
1326
|
+
log(!embeddingRowDmlSafe
|
|
1327
|
+
? `Incremental: the ${EMBEDDING_TABLE_NAME} vector index exists but the VECTOR ` +
|
|
1328
|
+
`extension could not be loaded, so embedding rows cannot be rewritten in place — ` +
|
|
1329
|
+
`switching to a full DB write (wipe + bulk COPY) for this run. Semantic search ` +
|
|
1330
|
+
`falls back to exact scan until VECTOR is available; run \`gitnexus doctor\` for ` +
|
|
1331
|
+
`live extension status, or set GITNEXUS_LBUG_EXTENSION_INSTALL=auto to allow one ` +
|
|
1332
|
+
`bounded install attempt.`
|
|
1333
|
+
: `Incremental: effective write set covers ${effectiveWriteSet.size}/${allFilePaths.length} ` +
|
|
1334
|
+
// Display clamp only (predicate unchanged): BFS-found deleted
|
|
1335
|
+
// importers can push the numerator past the CURRENT file list, so
|
|
1336
|
+
// the raw fraction can exceed 1 — see the population-mismatch note
|
|
1337
|
+
// on shouldEscalateIncrementalWrite (tri-review 4669518496).
|
|
1338
|
+
`files (${Math.min(100, Math.round(writeFraction * 100))}%) — switching to a full DB write ` +
|
|
1339
|
+
`(wipe + bulk COPY) for this run; file-level incremental bookkeeping is unaffected.`);
|
|
1340
|
+
// toWriteCount: 0 is the established full-path dirty-flag sentinel;
|
|
1341
|
+
// the real counters ride along for crash diagnostics.
|
|
1342
|
+
await saveIncrementalDirtyState('escalated-full-write', {
|
|
1343
|
+
toWriteCount: 0,
|
|
1344
|
+
importerExpansion,
|
|
1345
|
+
shadowSeedCount: shadowSeed.length,
|
|
1346
|
+
effectiveWriteCount: effectiveWriteSet.size,
|
|
1347
|
+
deleteCount: filesToDelete.length,
|
|
1348
|
+
});
|
|
1349
|
+
// Strategy switch: stop the checkpoint driver around the close so its
|
|
1350
|
+
// in-flight CHECKPOINT can't race the reopen, drop the DB files
|
|
1351
|
+
// (sidecars included), and bulk-load the full graph into a fresh DB —
|
|
1352
|
+
// byte-for-byte the full-rebuild write plan. The wipe is the shared
|
|
1353
|
+
// ENOENT-verified helper (#2409 + tri-review 4669518496 P2-4): a
|
|
1354
|
+
// surviving family member throws a typed LbugWipeError here instead
|
|
1355
|
+
// of letting the reopen below resurrect the rows this run just chose
|
|
1356
|
+
// to replace wholesale.
|
|
1357
|
+
await walCheckpointDriver.stop();
|
|
1358
|
+
await closeLbug();
|
|
1359
|
+
await wipeLbugDbFiles(buildPath);
|
|
1360
|
+
await initLbug(buildPath);
|
|
1361
|
+
walCheckpointDriver = startWalCheckpointDriver();
|
|
1362
|
+
await loadGraphToLbug(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
|
|
1363
|
+
lbugMsgCount++;
|
|
1364
|
+
const pct = Math.min(84, 65 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 19));
|
|
1365
|
+
progress('lbug', pct, msg);
|
|
1366
|
+
});
|
|
903
1367
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1368
|
+
else {
|
|
1369
|
+
// 1a. Drop every FTS index before touching a single row (#2589).
|
|
1370
|
+
// `deleteNodesForFiles` below DETACH DELETEs rows out of tables
|
|
1371
|
+
// that otherwise still carry the FTS index built at the end of
|
|
1372
|
+
// the PREVIOUS analyze run — Phase 3 doesn't drop+rebuild it
|
|
1373
|
+
// until well after this delete completes. LadybugDB's FTS
|
|
1374
|
+
// extension is not proven to survive DML against an indexed
|
|
1375
|
+
// table (its own docs never demonstrate it), and that ordering
|
|
1376
|
+
// is exactly what produced "FTS index 'file_fts' is
|
|
1377
|
+
// inconsistent: term is missing during delete". Dropping first
|
|
1378
|
+
// removes the hazard outright; Phase 3's createSearchFTSIndexes
|
|
1379
|
+
// rebuilds every index from the final row set regardless, so
|
|
1380
|
+
// this is a no-op on its own drop step there.
|
|
1381
|
+
await dropSearchFTSIndexes();
|
|
1382
|
+
// 1b. Remove the write set's existing rows — batched (#2409): one
|
|
1383
|
+
// DETACH DELETE per table per 200-file chunk. The former per-file
|
|
1384
|
+
// loop issued a count + delete per table per FILE — ~13k
|
|
1385
|
+
// single-row write transactions on a ~700-file write set — which
|
|
1386
|
+
// made this phase slower than a full rebuild and is the WAL-append
|
|
1387
|
+
// storm behind the native mid-writeback deaths in #2409. Errors
|
|
1388
|
+
// are NOT swallowed anymore: a zero-match file is a no-op by
|
|
1389
|
+
// construction, so anything thrown is a real engine failure that
|
|
1390
|
+
// must surface instead of silently skipping (that silent skip was
|
|
1391
|
+
// how #2409 hid its root cause).
|
|
1392
|
+
progress('lbug', 62, `Removing rows for changed files (0/${filesToDelete.length})...`);
|
|
1393
|
+
await deleteNodesForFiles(filesToDelete, {
|
|
1394
|
+
onChunk: (done, total) => progress('lbug', 62, `Removing rows for changed files (${done}/${total})...`),
|
|
1395
|
+
});
|
|
1396
|
+
// Surgical path: Phase 3.5 restores exactly these files' embedding
|
|
1397
|
+
// rows (FIX 3). Sound because deleteNodesForFiles propagates errors
|
|
1398
|
+
// — reaching this line means every listed file's rows are gone
|
|
1399
|
+
// deterministically — and this process holds the exclusive DB lock,
|
|
1400
|
+
// so no concurrent writer can disturb the derivation.
|
|
1401
|
+
deletedFilePathsForRestore = new Set(filesToDelete);
|
|
1402
|
+
// 2. Drop graph-wide nodes (Community, Process). They'll be re-inserted
|
|
1403
|
+
// from the fresh pipeline output below. Required for the
|
|
1404
|
+
// "Leiden runs on the FULL graph" correctness invariant.
|
|
1405
|
+
await deleteAllCommunitiesAndProcesses();
|
|
1406
|
+
// 2a. Drop INJECTS edges (DI collection injection, #2200) — their
|
|
1407
|
+
// validity is a whole-program property (a third-file change to the
|
|
1408
|
+
// interface or an implementer creates/invalidates edges between two
|
|
1409
|
+
// untouched files), so endpoint-writability extraction can't refresh
|
|
1410
|
+
// them; extractChangedSubgraph re-includes all of them from the
|
|
1411
|
+
// fresh graph (isGraphWideRelType). UNCONDITIONAL, next to the
|
|
1412
|
+
// Communities delete — NOT inside the `options.pdg` block below: the
|
|
1413
|
+
// di phase runs on every persisting analyze (same !skipGraphPhases
|
|
1414
|
+
// regime as communities/processes) while the graph-wide re-include
|
|
1415
|
+
// is unconditional, so a pdg-gated delete would append without
|
|
1416
|
+
// deleting on every non-pdg incremental run (N runs = N copies of
|
|
1417
|
+
// every INJECTS row; CodeRelation has no PK and no read-side dedup).
|
|
1418
|
+
await deleteAllInjects();
|
|
1419
|
+
// 2b. Drop interprocedural TAINT_PATH edges (#2084 M4 U6) when pdg is on
|
|
1420
|
+
// — their validity is a whole-program property (an A→C flow can be
|
|
1421
|
+
// invalidated by a change to an intermediate function on a third
|
|
1422
|
+
// file), so endpoint-writability extraction can't refresh them.
|
|
1423
|
+
// extractChangedSubgraph re-includes all of them from the fresh
|
|
1424
|
+
// graph (isGraphWideRelType), mirroring Community/Process.
|
|
1425
|
+
if (options.pdg === true) {
|
|
1426
|
+
await deleteAllInterprocTaintPaths();
|
|
1427
|
+
// 2c. Drop CALL_SUMMARY edges (PDG FU-C) on an incremental `--pdg`
|
|
1428
|
+
// writeback. They are re-included from the FULL fresh graph
|
|
1429
|
+
// (isGraphWideRelType) and the callSummaries phase recomputes every
|
|
1430
|
+
// summary each run, so delete-all-then-rebuild keeps an unchanged
|
|
1431
|
+
// function's summary from being lost — same contract as TAINT_PATH.
|
|
1432
|
+
await deleteAllCallSummaries();
|
|
1433
|
+
}
|
|
1434
|
+
// 3. Extract the changed subgraph from the FULL ctx.graph and write
|
|
1435
|
+
// only that. Unchanged-file rows in the DB stay untouched. Pass
|
|
1436
|
+
// the SAME effectiveWriteSet so the subgraph and the deletes
|
|
1437
|
+
// cover identical files (asymmetry would silently corrupt).
|
|
1438
|
+
const subgraph = extractChangedSubgraph(pipelineResult.graph, effectiveWriteSet);
|
|
1439
|
+
await saveIncrementalDirtyState('load-graph', {
|
|
1440
|
+
importerExpansion,
|
|
1441
|
+
shadowSeedCount: shadowSeed.length,
|
|
1442
|
+
effectiveWriteCount: effectiveWriteSet.size,
|
|
1443
|
+
deleteCount: filesToDelete.length,
|
|
1444
|
+
});
|
|
1445
|
+
await loadGraphToLbug(subgraph, pipelineResult.repoPath, storagePath, (msg) => {
|
|
1446
|
+
lbugMsgCount++;
|
|
1447
|
+
const pct = Math.min(84, 65 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 19));
|
|
1448
|
+
progress('lbug', pct, msg);
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
// Boundary drain (#2409): checkpoint at the end of the incremental
|
|
1452
|
+
// writeback so the WAL it accumulated never lingers into the FTS and
|
|
1453
|
+
// embedding phases — a later crash leaves only post-checkpoint WAL for
|
|
1454
|
+
// the next open to replay. Near-instant when the periodic driver has
|
|
1455
|
+
// kept up; rides the driver's bounded retry via runCheckpointWithRetry.
|
|
1456
|
+
await checkpointOnce();
|
|
914
1457
|
}
|
|
915
1458
|
else {
|
|
916
1459
|
// ── Full rebuild ───────────────────────────────────────────────
|
|
@@ -939,8 +1482,17 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
939
1482
|
const ftsAvailable = await loadFTSExtension(undefined, {
|
|
940
1483
|
policy: resolveAnalyzeInstallPolicy(),
|
|
941
1484
|
});
|
|
1485
|
+
// Tracks whether search indexes actually ended up usable this run — starts
|
|
1486
|
+
// as ftsAvailable (extension loaded) but flips to false below when the
|
|
1487
|
+
// build/verify step itself fails, so capabilities.fts.status / ftsSkipped
|
|
1488
|
+
// stay honest even though that failure no longer aborts the whole analyze.
|
|
1489
|
+
let ftsReady = ftsAvailable;
|
|
942
1490
|
if (ftsAvailable) {
|
|
943
|
-
|
|
1491
|
+
// Degrade rather than throw: createSearchFTSIndexes re-tokenizes every
|
|
1492
|
+
// stored row on every run, so a native tokenizer error on a single
|
|
1493
|
+
// pre-existing row (#2544/#2546) must not discard this run's otherwise-
|
|
1494
|
+
// successful graph/embeddings work — only keyword search degrades.
|
|
1495
|
+
const ftsResult = await buildSearchIndexesOrDegrade(executeQuery, {
|
|
944
1496
|
onIndexStart: options.verbose
|
|
945
1497
|
? (table, indexName) => log(`FTS: creating ${table}.${indexName}`)
|
|
946
1498
|
: undefined,
|
|
@@ -948,31 +1500,61 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
948
1500
|
? (table, indexName) => log(`FTS: ready ${table}.${indexName}`)
|
|
949
1501
|
: undefined,
|
|
950
1502
|
});
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1503
|
+
if (ftsResult.ok) {
|
|
1504
|
+
progress('fts', 90, 'Search indexes ready');
|
|
1505
|
+
}
|
|
1506
|
+
else {
|
|
1507
|
+
ftsReady = false;
|
|
1508
|
+
log(`FTS index build failed (${ftsResult.error}) — keyword search degraded this run. ` +
|
|
1509
|
+
'Graph and embeddings analysis completed successfully. Run `gitnexus analyze --repair-fts` to retry.');
|
|
1510
|
+
progress('fts', 90, 'Search indexes skipped (build failed)');
|
|
955
1511
|
}
|
|
956
|
-
progress('fts', 90, 'Search indexes ready');
|
|
957
1512
|
}
|
|
958
1513
|
else {
|
|
959
|
-
|
|
1514
|
+
// For a missing runtime dependency (#2374) the file is present, so the
|
|
1515
|
+
// generic "install it with network access" tail in FTS_UNAVAILABLE_MESSAGE
|
|
1516
|
+
// contradicts the remedy's own "reinstalling will NOT help" (#2383 F2). Lead
|
|
1517
|
+
// with the class-neutral sentence and append only the classified remedy.
|
|
1518
|
+
const ftsReason = getExtensionCapabilities().find((c) => c.name === 'fts')?.reason;
|
|
1519
|
+
const { kind, remedy } = diagnoseExtensionLoad(ftsReason);
|
|
1520
|
+
log(kind === 'missing_dependency'
|
|
1521
|
+
? `${FTS_UNAVAILABLE_LEAD} ${remedy}`
|
|
1522
|
+
: FTS_UNAVAILABLE_MESSAGE);
|
|
960
1523
|
progress('fts', 90, 'Search indexes skipped (FTS unavailable)');
|
|
961
1524
|
}
|
|
962
1525
|
// ── Phase 3.5: Re-insert cached embeddings ────────────────────────
|
|
963
1526
|
// Runs on BOTH the full-rebuild path and the incremental path:
|
|
964
|
-
// - Full rebuild: DB was wiped, every cached row
|
|
965
|
-
//
|
|
966
|
-
//
|
|
967
|
-
//
|
|
968
|
-
//
|
|
969
|
-
//
|
|
970
|
-
//
|
|
971
|
-
//
|
|
972
|
-
//
|
|
973
|
-
//
|
|
974
|
-
//
|
|
975
|
-
//
|
|
1527
|
+
// - Full rebuild / escalated write: DB was wiped, every cached row
|
|
1528
|
+
// needs to come back.
|
|
1529
|
+
// - Incremental (surgical): changed/deleted files' rows were just
|
|
1530
|
+
// deleted by deleteNodesForFiles (a REAL delete since tri-review
|
|
1531
|
+
// 4669518496 P2-1 — it joins embedding rows through their owning
|
|
1532
|
+
// nodes), so changed-file vectors need to come back; unchanged-file
|
|
1533
|
+
// rows still exist. Bugbot review on PR #1479 flagged that gating
|
|
1534
|
+
// this on `!isIncremental` silently lost changed-file embeddings.
|
|
1535
|
+
//
|
|
1536
|
+
// Restore discipline (tri-review 4669518496 / KTD10, restore scope
|
|
1537
|
+
// derived in memory since FIX 3 of this shipping review) — filtered and
|
|
1538
|
+
// conflict-free, replacing the old insert-everything-and-swallow shape:
|
|
1539
|
+
// 1. Live-graph filter: rows whose nodeId no longer exists in the
|
|
1540
|
+
// freshly-built FULL graph are dropped. The cache was read BEFORE
|
|
1541
|
+
// the pipeline ran, so it still carries deleted files' rows —
|
|
1542
|
+
// re-inserting them resurrected orphans (wholesale onto the wiped
|
|
1543
|
+
// paths' empty table) now that the delete above is real.
|
|
1544
|
+
// 2. Restore-scope filter, derived WITHOUT touching the DB (the old
|
|
1545
|
+
// shape pre-read every surviving embedding id back out of the
|
|
1546
|
+
// table it had just written): on a wiped path
|
|
1547
|
+
// (`deletedFilePathsForRestore === null`) the table is fresh, so
|
|
1548
|
+
// every live row comes back; on the surgical path only rows whose
|
|
1549
|
+
// owning node's filePath is in the just-join-deleted set are
|
|
1550
|
+
// inserted — everything else still sits in the DB and would
|
|
1551
|
+
// PK-conflict. The derivation is sound because deleteNodesForFiles
|
|
1552
|
+
// propagates errors (a completed writeback means a deterministic
|
|
1553
|
+
// delete outcome) and this process holds the exclusive DB lock (no
|
|
1554
|
+
// concurrent writer).
|
|
1555
|
+
// The per-batch try/catch stays as a last-resort guard only — it no
|
|
1556
|
+
// longer fires on the happy path.
|
|
1557
|
+
let restoredEmbeddingCount = 0;
|
|
976
1558
|
if (cachedEmbeddings.length > 0) {
|
|
977
1559
|
const cachedDims = cachedEmbeddings[0].embedding.length;
|
|
978
1560
|
const { EMBEDDING_DIMS } = await import('./lbug/schema.js');
|
|
@@ -983,16 +1565,65 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
983
1565
|
cachedEmbeddingNodeIds = new Set();
|
|
984
1566
|
}
|
|
985
1567
|
else {
|
|
986
|
-
progress('embeddings', 88, `Restoring ${cachedEmbeddings.length} cached embeddings...`);
|
|
987
1568
|
const { batchInsertEmbeddings: batchInsert } = await import('./embeddings/embedding-pipeline.js');
|
|
1569
|
+
// (1) Live-graph filter — the FULL pipeline graph (always produced),
|
|
1570
|
+
// NOT the incremental subgraph, or unchanged files' rows would be
|
|
1571
|
+
// dropped from the restore set.
|
|
1572
|
+
const liveEmbeddings = cachedEmbeddings.filter((e) => pipelineResult.graph.getNode(e.nodeId) !== undefined);
|
|
1573
|
+
// (2) Restore-scope filter (see the discipline note above).
|
|
1574
|
+
const rowsToRestore = deletedFilePathsForRestore === null
|
|
1575
|
+
? liveEmbeddings
|
|
1576
|
+
: liveEmbeddings.filter((e) => {
|
|
1577
|
+
const filePath = pipelineResult.graph.getNode(e.nodeId)?.properties?.filePath;
|
|
1578
|
+
return typeof filePath === 'string' && deletedFilePathsForRestore.has(filePath);
|
|
1579
|
+
});
|
|
1580
|
+
progress('embeddings', 88, `Restoring ${rowsToRestore.length} cached embeddings...`);
|
|
988
1581
|
const EMBED_BATCH = 200;
|
|
989
|
-
for (let i = 0; i <
|
|
990
|
-
const batch =
|
|
1582
|
+
for (let i = 0; i < rowsToRestore.length; i += EMBED_BATCH) {
|
|
1583
|
+
const batch = rowsToRestore.slice(i, i + EMBED_BATCH);
|
|
991
1584
|
try {
|
|
992
1585
|
await batchInsert(executeWithReusedStatement, batch);
|
|
1586
|
+
restoredEmbeddingCount += batch.length;
|
|
993
1587
|
}
|
|
994
1588
|
catch {
|
|
995
|
-
/*
|
|
1589
|
+
/* last-resort guard — conflict-free by construction above */
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
// Legacy-orphan sweep (FIX 3, finder B): the live-graph filter's
|
|
1593
|
+
// REJECTS — cached rows whose owning node no longer exists — are the
|
|
1594
|
+
// rows stranded by the era when the embedding delete was a no-op
|
|
1595
|
+
// (tri-review 4669518496 P2-1; schema version stays 6), plus this
|
|
1596
|
+
// run's just-deleted files' rows (already join-deleted above — the
|
|
1597
|
+
// exact-id DELETE matches nothing for those, so including them is a
|
|
1598
|
+
// harmless no-op rather than worth a fragile nodeId parse to
|
|
1599
|
+
// exclude). On the SURGICAL path the true legacy orphans still sit
|
|
1600
|
+
// in the DB and the node join can never reach them again (no owning
|
|
1601
|
+
// node), so delete them by exact row id. On wiped paths the rejects
|
|
1602
|
+
// were simply not restored — nothing to sweep. Legacy-tolerant: a
|
|
1603
|
+
// sweep failure must never fail a completed writeback, so the whole
|
|
1604
|
+
// sweep warns-and-continues.
|
|
1605
|
+
if (deletedFilePathsForRestore !== null) {
|
|
1606
|
+
const orphanRowIds = cachedEmbeddings
|
|
1607
|
+
.filter((e) => pipelineResult.graph.getNode(e.nodeId) === undefined)
|
|
1608
|
+
.map((e) => `${e.nodeId}:${e.chunkIndex}`);
|
|
1609
|
+
if (orphanRowIds.length > 0) {
|
|
1610
|
+
try {
|
|
1611
|
+
for (let i = 0; i < orphanRowIds.length; i += DELETE_FILES_CHUNK_SIZE) {
|
|
1612
|
+
const chunk = orphanRowIds.slice(i, i + DELETE_FILES_CHUNK_SIZE);
|
|
1613
|
+
const listLiteral = `[${chunk
|
|
1614
|
+
.map((id) => `'${escapeCypherString(id)}'`)
|
|
1615
|
+
.join(', ')}]`;
|
|
1616
|
+
await executeQuery(`MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.id IN ${listLiteral} DELETE e`);
|
|
1617
|
+
}
|
|
1618
|
+
log(`Swept ${orphanRowIds.length} cached embedding row(s) with no live owning ` +
|
|
1619
|
+
'node — legacy orphans stranded while the embedding delete was a no-op; ' +
|
|
1620
|
+
'ids already removed with their files match nothing.');
|
|
1621
|
+
}
|
|
1622
|
+
catch (err) {
|
|
1623
|
+
log(`Warning: could not sweep ${orphanRowIds.length} orphaned embedding ` +
|
|
1624
|
+
`row(s) (${err.message}); they are unreachable by search ` +
|
|
1625
|
+
'joins and will be retried next run.');
|
|
1626
|
+
}
|
|
996
1627
|
}
|
|
997
1628
|
}
|
|
998
1629
|
}
|
|
@@ -1002,7 +1633,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
1002
1633
|
let embeddingSkipped = true;
|
|
1003
1634
|
let semanticMode;
|
|
1004
1635
|
if (shouldGenerateEmbeddings) {
|
|
1005
|
-
const { skipForCap, capDisabled, nodeLimit } = deriveEmbeddingCap(stats.nodes, options.embeddingsNodeLimit);
|
|
1636
|
+
const { skipForCap, capDisabled, nodeLimit } = deriveEmbeddingCap(stats.nodes, resumeEmbeddingCheckpoint ? 0 : options.embeddingsNodeLimit);
|
|
1006
1637
|
if (!skipForCap) {
|
|
1007
1638
|
embeddingSkipped = false;
|
|
1008
1639
|
if (capDisabled && stats.nodes > DEFAULT_EMBEDDING_NODE_LIMIT) {
|
|
@@ -1019,11 +1650,50 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
1019
1650
|
`\`--embeddings <n>\` to set a custom cap.`);
|
|
1020
1651
|
}
|
|
1021
1652
|
}
|
|
1653
|
+
// ── Vector-index recreation after a wipe-and-restore (tri-review
|
|
1654
|
+
// 4669518496 P1 / KTD1) ────────────────────────────────────────────
|
|
1655
|
+
// The full-rebuild and escalated-incremental write plans wipe the DB
|
|
1656
|
+
// files — the HNSW index with them. Phase 3.5 brought the embedding ROWS
|
|
1657
|
+
// back, but on a preserve-only run nothing recreates the index: semantic
|
|
1658
|
+
// search silently loses its vector lane (>10k-embedding repos return
|
|
1659
|
+
// empty under the exact-scan cap) while meta certified 'vector-index'.
|
|
1660
|
+
// Recreate it here, where every gate input is settled:
|
|
1661
|
+
// - restoredEmbeddingCount > 0 — rows actually came back;
|
|
1662
|
+
// - dbWasWiped — surgical incremental runs keep their index (HNSW
|
|
1663
|
+
// self-maintains on insert/delete); only wiped DBs lost it;
|
|
1664
|
+
// - embeddingSkipped — evaluated AFTER the deriveEmbeddingCap decision
|
|
1665
|
+
// above, NOT `!shouldGenerateEmbeddings`: when Phase 4 really runs,
|
|
1666
|
+
// the pipeline builds the index itself after all inserts (firing this
|
|
1667
|
+
// seam first would swap its bulk build for per-row live HNSW
|
|
1668
|
+
// maintenance on the hottest flow), while a capped >50k-node repo has
|
|
1669
|
+
// shouldGenerateEmbeddings=true yet never runs the pipeline — exactly
|
|
1670
|
+
// the case a naive gate would leave index-less again.
|
|
1671
|
+
// buildVectorIndex carries its own extension-policy gate and
|
|
1672
|
+
// warn-on-failure; the boolean feeds semanticMode so the finalize stamp
|
|
1673
|
+
// reflects the DB's ACTUAL state even when recreation fails (extension
|
|
1674
|
+
// unavailable → 'exact-scan').
|
|
1675
|
+
const dbWasWiped = !isIncremental || escalatedFullWrite;
|
|
1676
|
+
if (restoredEmbeddingCount > 0 && dbWasWiped && embeddingSkipped) {
|
|
1677
|
+
// Re-import at the seam rather than thread a mutable capture from
|
|
1678
|
+
// Phase 3.5 (FIX 3 of this shipping review — the captured function was
|
|
1679
|
+
// a fragile moving part): dynamic imports are memoized, and
|
|
1680
|
+
// `restoredEmbeddingCount > 0` proves Phase 3.5 already loaded the
|
|
1681
|
+
// module, so the lazy-embeddings convention (#2370) holds — no
|
|
1682
|
+
// embeddings module loads unless a restore actually happened.
|
|
1683
|
+
const { buildVectorIndex } = await import('./embeddings/embedding-pipeline.js');
|
|
1684
|
+
const vectorIndexReady = await buildVectorIndex();
|
|
1685
|
+
semanticMode = vectorIndexReady ? 'vector-index' : 'exact-scan';
|
|
1686
|
+
}
|
|
1022
1687
|
if (!embeddingSkipped) {
|
|
1023
1688
|
const { isHttpMode } = await import('./embeddings/http-client.js');
|
|
1024
1689
|
const httpMode = isHttpMode();
|
|
1025
1690
|
progress('embeddings', 90, httpMode ? 'Connecting to embedding endpoint...' : 'Loading embedding model...');
|
|
1026
1691
|
const { runEmbeddingPipeline } = await import('./embeddings/embedding-pipeline.js');
|
|
1692
|
+
if (!embeddingIdentityForRun) {
|
|
1693
|
+
const { resolveEmbeddingIdentity } = await import('./embeddings/embedding-identity.js');
|
|
1694
|
+
embeddingIdentityForRun = resolveEmbeddingIdentity();
|
|
1695
|
+
}
|
|
1696
|
+
const embeddingIdentity = embeddingIdentityForRun;
|
|
1027
1697
|
// Build a Map<nodeId, contentHash> from cached embeddings for incremental mode
|
|
1028
1698
|
let existingEmbeddings;
|
|
1029
1699
|
if (cachedEmbeddingNodeIds.size > 0) {
|
|
@@ -1032,6 +1702,43 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
1032
1702
|
existingEmbeddings.set(e.nodeId, e.contentHash ?? STALE_HASH_SENTINEL);
|
|
1033
1703
|
}
|
|
1034
1704
|
}
|
|
1705
|
+
const saveEmbeddingCheckpoint = async (checkpoint, pendingNodeIds, embeddings) => {
|
|
1706
|
+
const fileHashes = {};
|
|
1707
|
+
for (const [key, value] of newFileHashes)
|
|
1708
|
+
fileHashes[key] = value;
|
|
1709
|
+
await saveMeta(metaDir, {
|
|
1710
|
+
...(existingMeta ?? {}),
|
|
1711
|
+
repoPath,
|
|
1712
|
+
lastCommit: currentCommit,
|
|
1713
|
+
indexedAt: new Date().toISOString(),
|
|
1714
|
+
runnerIdentity,
|
|
1715
|
+
branch: branchLabel ?? existingMeta?.branch,
|
|
1716
|
+
remoteUrl: hasGitDir(repoPath) ? getRemoteUrl(repoPath) : undefined,
|
|
1717
|
+
stats: {
|
|
1718
|
+
files: pipelineResult.totalFileCount,
|
|
1719
|
+
nodes: stats.nodes,
|
|
1720
|
+
edges: stats.edges,
|
|
1721
|
+
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
1722
|
+
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
1723
|
+
embeddings,
|
|
1724
|
+
},
|
|
1725
|
+
schemaVersion: hasGitDir(repoPath) ? INCREMENTAL_SCHEMA_VERSION : undefined,
|
|
1726
|
+
analysisFeatures: currentAnalysisFeatures,
|
|
1727
|
+
cjkSegmentation: getSearchFTSCjkSegmentation(),
|
|
1728
|
+
fileHashes: hasGitDir(repoPath) ? fileHashes : undefined,
|
|
1729
|
+
cacheKeys: [...parseCache.usedKeys],
|
|
1730
|
+
incrementalInProgress: undefined,
|
|
1731
|
+
embeddingCheckpoint: {
|
|
1732
|
+
at: new Date().toISOString(),
|
|
1733
|
+
...checkpoint,
|
|
1734
|
+
model: embeddingIdentity.model,
|
|
1735
|
+
dimensions: embeddingIdentity.dimensions,
|
|
1736
|
+
provider: embeddingIdentity.provider,
|
|
1737
|
+
pendingNodeIds,
|
|
1738
|
+
},
|
|
1739
|
+
pdg: resolvePdgConfig(options),
|
|
1740
|
+
});
|
|
1741
|
+
};
|
|
1035
1742
|
const embeddingResult = await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (p) => {
|
|
1036
1743
|
const scaled = 90 + Math.round((p.percent / 100) * 8);
|
|
1037
1744
|
const label = p.phase === 'loading-model'
|
|
@@ -1040,7 +1747,19 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
1040
1747
|
: 'Loading embedding model...'
|
|
1041
1748
|
: `Embedding ${p.nodesProcessed || 0}/${p.totalNodes || '?'}`;
|
|
1042
1749
|
progress('embeddings', scaled, label);
|
|
1043
|
-
}, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined, existingEmbeddings
|
|
1750
|
+
}, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined, existingEmbeddings, {
|
|
1751
|
+
forceReembedNodeIds: pendingEmbeddingNodeIds,
|
|
1752
|
+
onCheckpointWindowStart: async ({ nodeIds, ...checkpoint }) => {
|
|
1753
|
+
await saveEmbeddingCheckpoint(checkpoint, nodeIds, existingMeta?.stats?.embeddings);
|
|
1754
|
+
},
|
|
1755
|
+
onCheckpoint: async (checkpoint) => {
|
|
1756
|
+
await checkpointOnce();
|
|
1757
|
+
const countResult = await executeQuery(`MATCH (e:${EMBEDDING_TABLE_NAME}) RETURN count(e) AS cnt`);
|
|
1758
|
+
const countRow = countResult?.[0];
|
|
1759
|
+
const embeddings = Number(countRow?.cnt ?? countRow?.[0] ?? 0);
|
|
1760
|
+
await saveEmbeddingCheckpoint(checkpoint, [], embeddings);
|
|
1761
|
+
},
|
|
1762
|
+
});
|
|
1044
1763
|
if (embeddingResult.semanticMode === 'exact-scan') {
|
|
1045
1764
|
semanticMode = 'exact-scan';
|
|
1046
1765
|
log('Semantic embeddings were generated without a VECTOR index; ' +
|
|
@@ -1068,17 +1787,44 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
1068
1787
|
}
|
|
1069
1788
|
const { getRuntimeCapabilities } = await import('./platform/capabilities.js');
|
|
1070
1789
|
const runtimeCapabilities = getRuntimeCapabilities();
|
|
1790
|
+
// `semanticMode` is authoritative when set (Phase 4 reported what it
|
|
1791
|
+
// built, or the wipe-and-restore seam above verified/recreated the index
|
|
1792
|
+
// — tri-review 4669518496 P1). When unset, prefer the PREVIOUS run's
|
|
1793
|
+
// persisted stamp over the platform capability (FIX 3, finder A): the
|
|
1794
|
+
// unset case is exactly a run that neither wiped nor generated — e.g. a
|
|
1795
|
+
// surgical incremental whose index survived in place — and such a run
|
|
1796
|
+
// cannot change whether the HNSW index exists, so carrying the persisted
|
|
1797
|
+
// observation forward is strictly more truthful than re-deriving from
|
|
1798
|
+
// what the platform COULD do. Only the two positive observations carry
|
|
1799
|
+
// ('vector-index'/'exact-scan'); 'unavailable'/absent falls through to
|
|
1800
|
+
// the platform default rather than pinning a stale negative.
|
|
1801
|
+
const persistedStatus = existingMeta?.capabilities?.vectorSearch.status;
|
|
1802
|
+
const persistedSemanticMode = persistedStatus === 'vector-index' || persistedStatus === 'exact-scan'
|
|
1803
|
+
? persistedStatus
|
|
1804
|
+
: undefined;
|
|
1071
1805
|
const effectiveSemanticMode = semanticMode ??
|
|
1806
|
+
persistedSemanticMode ??
|
|
1072
1807
|
(runtimeCapabilities.semanticMode === 'vector-index' ? 'vector-index' : 'exact-scan');
|
|
1073
1808
|
// Convert the post-run file-hash map to the on-disk Record<string,string>
|
|
1074
1809
|
// shape consumed by RepoMeta.fileHashes.
|
|
1075
1810
|
const newFileHashesRecord = {};
|
|
1076
1811
|
for (const [k, v] of newFileHashes)
|
|
1077
1812
|
newFileHashesRecord[k] = v;
|
|
1813
|
+
// Incremental runs never rewrite Move facts (changed Move compiler inputs
|
|
1814
|
+
// force a full rebuild), so when the compiler is transiently unavailable
|
|
1815
|
+
// the previous record still describes the persisted facts. Stamping
|
|
1816
|
+
// false/undefined here would force a needless full rebuild the moment the
|
|
1817
|
+
// compiler returns.
|
|
1818
|
+
const preserveMoveMeta = hasMovePackages && !moveFlow && isIncremental;
|
|
1819
|
+
// Annotated so the capabilities stamp below is compile-checked against
|
|
1820
|
+
// RepoMeta's status unions (tri-review 4669518496 P1/U3) — an unannotated
|
|
1821
|
+
// literal widens the vectorSearch.status ternary to `string` and the
|
|
1822
|
+
// honesty contract silently decays to "whatever interpolates".
|
|
1078
1823
|
const meta = {
|
|
1079
1824
|
repoPath,
|
|
1080
1825
|
lastCommit: currentCommit,
|
|
1081
1826
|
indexedAt: new Date().toISOString(),
|
|
1827
|
+
runnerIdentity,
|
|
1082
1828
|
// Branch identity this index represents (#2106). Recorded for the flat
|
|
1083
1829
|
// slot too (so resolveBranchPlacement knows which branch owns it). When
|
|
1084
1830
|
// the label is null (detached HEAD / non-git re-analyze) we PRESERVE an
|
|
@@ -1110,7 +1856,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
1110
1856
|
// meta.json / `gitnexus doctor` honest about degraded search.
|
|
1111
1857
|
fts: {
|
|
1112
1858
|
provider: 'ladybugdb-fts',
|
|
1113
|
-
status:
|
|
1859
|
+
status: ftsReady ? runtimeCapabilities.fts : 'unavailable',
|
|
1114
1860
|
},
|
|
1115
1861
|
vectorSearch: {
|
|
1116
1862
|
provider: effectiveSemanticMode === 'vector-index' ? 'ladybugdb-vector' : 'exact-scan',
|
|
@@ -1124,17 +1870,30 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
1124
1870
|
// incrementalInProgress to undefined explicitly clears any prior
|
|
1125
1871
|
// dirty flag (full and incremental success paths converge here).
|
|
1126
1872
|
schemaVersion: hasGitDir(repoPath) ? INCREMENTAL_SCHEMA_VERSION : undefined,
|
|
1873
|
+
analysisFeatures: currentAnalysisFeatures,
|
|
1127
1874
|
// Always stamped with the live resolved mode (#2331/#2339) — unlike
|
|
1128
1875
|
// `pdg` below, 'none' is a meaningful value to compare, not an
|
|
1129
1876
|
// absence, so this is never conditionally omitted.
|
|
1130
1877
|
cjkSegmentation: getSearchFTSCjkSegmentation(),
|
|
1131
1878
|
fileHashes: hasGitDir(repoPath) ? newFileHashesRecord : undefined,
|
|
1879
|
+
moveIngestAvailable: preserveMoveMeta
|
|
1880
|
+
? existingMeta?.moveIngestAvailable
|
|
1881
|
+
: moveIngestAvailable,
|
|
1882
|
+
moveCompilerIdentity: preserveMoveMeta
|
|
1883
|
+
? existingMeta?.moveCompilerIdentity
|
|
1884
|
+
: moveFlow?.identity,
|
|
1885
|
+
moveFlowReleaseSelection: preserveMoveMeta
|
|
1886
|
+
? existingMeta?.moveFlowReleaseSelection
|
|
1887
|
+
: moveFlow?.identity.source === 'release'
|
|
1888
|
+
? desiredMoveFlowRelease
|
|
1889
|
+
: undefined,
|
|
1132
1890
|
// This branch's full live chunk-key set (#2106 R6). `usedKeys` is every
|
|
1133
1891
|
// chunk hash touched in this scan — cache HITS included (see parse-impl
|
|
1134
1892
|
// usedKeys.add) — so it's complete even on an incremental run. Persisted
|
|
1135
1893
|
// so a sibling branch's prune can union it and not evict our shards.
|
|
1136
1894
|
cacheKeys: [...parseCache.usedKeys],
|
|
1137
1895
|
incrementalInProgress: undefined,
|
|
1896
|
+
embeddingCheckpoint: undefined,
|
|
1138
1897
|
// The effective pdg config this run's DB rows were built under
|
|
1139
1898
|
// (#2099 F1). `undefined` on pdg-off runs — this meta is a fresh
|
|
1140
1899
|
// literal (no spread of existingMeta), so omission is what CLEARS the
|
|
@@ -1142,7 +1901,18 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
1142
1901
|
// off==off and incremental eligibility is restored.
|
|
1143
1902
|
pdg: resolvePdgConfig(options),
|
|
1144
1903
|
};
|
|
1145
|
-
|
|
1904
|
+
// Re-resolve at the commit boundary. Long analyses can overlap an npm
|
|
1905
|
+
// upgrade, rebuilt dist tree, or native dependency replacement; stamping
|
|
1906
|
+
// the start-of-run receipt after such a mutation would falsely certify a
|
|
1907
|
+
// graph produced by two analyzer identities. Stable-read validation lives
|
|
1908
|
+
// inside the resolver, and a mismatch leaves the dirty flag intact so the
|
|
1909
|
+
// next run takes the established full-recovery path.
|
|
1910
|
+
meta.runnerIdentity = finalizeAnalyzerRunnerIdentity(import.meta.url, runnerIdentity);
|
|
1911
|
+
// #2614 F1: the freshness stamp (saveMeta) is written AFTER the atomic swap
|
|
1912
|
+
// below — never here — so a concurrent MCP reader can't observe
|
|
1913
|
+
// meta.indexedAt = T_new while lbugPath still resolves to the pre-swap
|
|
1914
|
+
// inode (which latched the reader on the stale index permanently). The meta
|
|
1915
|
+
// object is fully computed at this point; only its write is deferred.
|
|
1146
1916
|
// Persist the incremental parse cache for the next run. Wraps in
|
|
1147
1917
|
// try/catch so a cache-write failure never breaks an otherwise
|
|
1148
1918
|
// successful indexing run. Prune stale chunk-hash entries first so
|
|
@@ -1266,14 +2036,61 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
1266
2036
|
// LadybugDB destructor double-free after --pdg writes — closeLbugBeforeExit
|
|
1267
2037
|
// CHECKPOINTs for durability then leaves the handles for process exit to
|
|
1268
2038
|
// reclaim (#2264). Long-lived callers close for real.
|
|
1269
|
-
|
|
2039
|
+
//
|
|
2040
|
+
// On Windows a swap must release the build handle before the rename (a
|
|
2041
|
+
// same-process open file can't be renamed), so it forces a real close —
|
|
2042
|
+
// safe because windowsSwapOk excludes --pdg (the #2264 case). POSIX renames
|
|
2043
|
+
// an open file, so it keeps the skip-native-close there.
|
|
2044
|
+
const forceRealCloseForSwap = useAtomicSwap && process.platform === 'win32';
|
|
2045
|
+
await (options.skipNativeCloseOnExit && !forceRealCloseForSwap
|
|
2046
|
+
? closeLbugBeforeExit()
|
|
2047
|
+
: closeLbug());
|
|
2048
|
+
// #2 atomic publish: the fresh index was built at buildPath (a full rebuild,
|
|
2049
|
+
// or an opt-in atomic incremental that copied the live index in first). Swap
|
|
2050
|
+
// it over the live lbugPath in one rename so an MCP reader that opened
|
|
2051
|
+
// mid-build only ever saw the previous complete index — never a wiped/
|
|
2052
|
+
// half-built file. The close above checkpoint-consolidated buildPath to a
|
|
2053
|
+
// single file (no .wal), so the rename publishes a complete index; a reader
|
|
2054
|
+
// holding the old inode keeps a consistent stale snapshot until the pool
|
|
2055
|
+
// re-opens onto the new one (the pool staleness invalidation). Runs only on
|
|
2056
|
+
// success — a thrown error skips this, leaving the live index intact and the
|
|
2057
|
+
// temp build to be cleared by the next run's wipe.
|
|
2058
|
+
// Only publish if the build actually produced a DB at buildPath. A
|
|
2059
|
+
// degenerate run (empty repo, or a mocked pipeline that never opened the
|
|
2060
|
+
// store) leaves nothing to swap — skip rather than throw ENOENT.
|
|
2061
|
+
const builtDbExists = useAtomicSwap
|
|
2062
|
+
? await fs.stat(buildPath).then(() => true, () => false)
|
|
2063
|
+
: false;
|
|
2064
|
+
if (useAtomicSwap && builtDbExists) {
|
|
2065
|
+
await retryRename(buildPath, lbugPath);
|
|
2066
|
+
// Clear any sidecars orphaned beside the replaced file. A cleanly-closed
|
|
2067
|
+
// prior index has none; a crashed one could, and it would be replay
|
|
2068
|
+
// poison next to the freshly published index. Best-effort.
|
|
2069
|
+
for (const suffix of ['.wal', '.shadow', '.wal.checkpoint']) {
|
|
2070
|
+
await fs.rm(`${lbugPath}${suffix}`, { force: true }).catch(() => { });
|
|
2071
|
+
}
|
|
2072
|
+
// #2614 F4: if the final checkpoint silently failed, the build may still
|
|
2073
|
+
// carry a residual .wal/.shadow under the temp name. MOVE it beside the
|
|
2074
|
+
// published index (not orphan/delete it) so the next open replays the
|
|
2075
|
+
// delta, rather than leaving it under a name LadybugDB never reconciles.
|
|
2076
|
+
for (const suffix of ['.wal', '.shadow']) {
|
|
2077
|
+
await fs.rename(`${buildPath}${suffix}`, `${lbugPath}${suffix}`).catch(() => { });
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
// #2614 F1: stamp the freshness metadata now that the index is published.
|
|
2081
|
+
// When meta.indexedAt becomes visible, lbugPath already resolves to the new
|
|
2082
|
+
// inode, so a reader reiniting on the stamp opens the fresh graph rather
|
|
2083
|
+
// than latching on the old one. Leaving the dirty flag set across the swap
|
|
2084
|
+
// is a crash-safety improvement: a failed swap leaves the previous index
|
|
2085
|
+
// live and the next run recovers via the full-rebuild path.
|
|
2086
|
+
await saveMeta(metaDir, meta);
|
|
1270
2087
|
progress('done', 100, 'Done');
|
|
1271
2088
|
return {
|
|
1272
2089
|
repoName: projectName,
|
|
1273
2090
|
repoPath,
|
|
1274
2091
|
stats: meta.stats,
|
|
1275
2092
|
pipelineResult,
|
|
1276
|
-
ftsSkipped: !
|
|
2093
|
+
ftsSkipped: !ftsReady,
|
|
1277
2094
|
isPrimaryBranch: !placement.branch,
|
|
1278
2095
|
};
|
|
1279
2096
|
}
|