gitnexus 1.6.10-rc.2 → 1.6.10-rc.21
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 +56 -12
- package/dist/cli/analyze.js +166 -5
- 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 +76 -3
- package/dist/cli/embeddings.d.ts +12 -0
- package/dist/cli/embeddings.js +49 -0
- package/dist/cli/help-i18n.js +4 -0
- package/dist/cli/i18n/en.d.ts +10 -5
- package/dist/cli/i18n/en.js +10 -5
- package/dist/cli/i18n/resources.d.ts +15 -5
- package/dist/cli/i18n/zh-CN.d.ts +5 -0
- package/dist/cli/i18n/zh-CN.js +10 -5
- package/dist/cli/index.js +12 -1
- package/dist/core/augmentation/engine.js +4 -3
- package/dist/core/embeddings/embedder.js +16 -2
- package/dist/core/embeddings/embedding-pipeline.d.ts +20 -0
- package/dist/core/embeddings/embedding-pipeline.js +10 -2
- package/dist/core/embeddings/http-client.d.ts +28 -1
- package/dist/core/embeddings/http-client.js +107 -15
- package/dist/core/embeddings/node-module-compat.d.ts +23 -0
- package/dist/core/embeddings/node-module-compat.js +23 -0
- package/dist/core/embeddings/onnxruntime-common-resolver.js +6 -3
- package/dist/core/embeddings/onnxruntime-node-resolver.js +31 -5
- 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/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/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 +316 -63
- package/dist/core/ingestion/emit-references.js +3 -2
- package/dist/core/ingestion/languages/cpp/captures.js +157 -74
- package/dist/core/ingestion/markdown-processor.js +3 -2
- package/dist/core/ingestion/parsing-processor.d.ts +3 -1
- package/dist/core/ingestion/parsing-processor.js +4 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +42 -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/tree-sitter-queries.d.ts +1 -1
- package/dist/core/ingestion/tree-sitter-queries.js +12 -1
- 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 +14 -0
- package/dist/core/ingestion/workers/worker-pool.js +85 -5
- package/dist/core/lbug/csv-generator.js +8 -2
- 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 +320 -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 +147 -1
- package/dist/core/lbug/lbug-adapter.js +390 -70
- package/dist/core/lbug/lbug-config.d.ts +28 -0
- package/dist/core/lbug/lbug-config.js +114 -4
- package/dist/core/lbug/native-check.d.ts +22 -0
- package/dist/core/lbug/native-check.js +66 -0
- package/dist/core/lbug/pool-adapter.js +10 -1
- package/dist/core/lbug/sidecar-recovery.d.ts +172 -0
- package/dist/core/lbug/sidecar-recovery.js +418 -16
- package/dist/core/platform/capabilities.js +27 -1
- package/dist/core/run-analyze.js +470 -108
- package/dist/core/search/fts-indexes.d.ts +7 -0
- package/dist/core/search/fts-indexes.js +37 -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/mcp/core/embedder.js +16 -2
- 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 +14 -0
- package/dist/mcp/local/local-backend.js +63 -19
- package/dist/mcp/local/pdg-impact.d.ts +5 -3
- package/dist/mcp/local/pdg-impact.js +5 -2
- package/dist/mcp/resources.js +1 -0
- package/dist/mcp/tools.js +1 -1
- package/dist/server/analyze-launch.d.ts +6 -0
- package/dist/server/analyze-launch.js +82 -6
- package/dist/server/api.d.ts +30 -0
- package/dist/server/api.js +63 -21
- package/dist/storage/parse-cache.js +1 -1
- package/dist/storage/repo-manager.d.ts +65 -1
- package/dist/storage/repo-manager.js +18 -1
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +50 -3
- package/hooks/claude/gitnexus-hook.cjs +59 -9
- package/package.json +5 -3
- package/scripts/cross-platform-tests.ts +43 -0
- package/scripts/ensure-fts.ts +32 -0
- package/scripts/install-duckdb-extension.mjs +85 -24
- package/scripts/run-cross-platform.ts +46 -6
- package/scripts/shard-arg.ts +30 -0
- package/web/assets/ProcessFlowModal-Dna7GkwO.js +22 -0
- package/web/assets/{agent-Dli_x7bU.js → agent-DNYDy-KA.js} +49 -49
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-BVWFvYz2.js +1 -0
- package/web/assets/architectureDiagram-UL44E2DR-BU5ZRITg.js +36 -0
- package/web/assets/{blockDiagram-7IZFK4PR-BkWhpzWB.js → blockDiagram-7IZFK4PR-Dut1BpTV.js} +2 -2
- package/web/assets/{c4Diagram-Y2BXMSZH-DtBvpzWD.js → c4Diagram-Y2BXMSZH-DOXVs7-C.js} +2 -2
- package/web/assets/chunk-2T2R6R2M-pkw2oXHH.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-4sNPJU1q.js → chunk-67TQ5CYL-zxhDWgyF.js} +3 -3
- package/web/assets/chunk-7FYTHRHK-CsmYIswB.js +32 -0
- package/web/assets/chunk-7J6CGLKN-AmkN8jJN.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-D9BM9HWZ.js} +1 -1
- package/web/assets/chunk-J5EP6P6S-L19oz55R.js +1 -0
- package/web/assets/{chunk-KGFNY3KK-JGMJvKCK.js → chunk-KGFNY3KK-tD2F6wxK.js} +1 -1
- 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-Ba9JK-6F.js} +2 -2
- package/web/assets/{chunk-LII3EMHJ-D-jm-dLa.js → chunk-LII3EMHJ-EGx-eBIu.js} +1 -1
- package/web/assets/chunk-LRIF4GLE-C3_PqUBq.js +1 -0
- package/web/assets/{chunk-QA3QBVWF-BgRW3SXd.js → chunk-QA3QBVWF-D4cSJBf6.js} +1 -1
- package/web/assets/chunk-RERM46MO-B5dAdWpP.js +1 -0
- package/web/assets/{chunk-RG4AUYOV-CqWzWsV3.js → chunk-RG4AUYOV-OqBBMwuQ.js} +4 -4
- package/web/assets/chunk-RKZBBQEN-MyIQbGgD.js +1 -0
- package/web/assets/chunk-RLI5ZMPA-CkXZ0y0U.js +1 -0
- package/web/assets/{chunk-T2UQINTJ-DSBqkzH7.js → chunk-T2UQINTJ-BJusEiGe.js} +1 -1
- 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-DyF0vyGd.js → chunk-UY5QBCOK-BCpUlARQ.js} +1 -1
- package/web/assets/chunk-VU6ZFW4Y-BeXVk45_.js +2 -0
- package/web/assets/{chunk-W44A43WB-JqMLIpR0.js → chunk-W44A43WB-BN8EQc_u.js} +1 -1
- package/web/assets/{chunk-ZXARS5L4-CxN8oiwI.js → chunk-ZXARS5L4-BI_qR2Nm.js} +1 -1
- package/web/assets/classDiagram-KGZ6W3CR-D9zSIg0x.js +1 -0
- package/web/assets/classDiagram-v2-72OJOZXJ-D9zSIg0x.js +1 -0
- package/web/assets/cose-bilkent-UX7MHV2Q-BmnZ7Mvj.js +1 -0
- package/web/assets/dagre-ND4H6XIP-CpcV5kbl.js +4 -0
- package/web/assets/diagram-3NCE3AQN-6SMxBRpj.js +43 -0
- package/web/assets/diagram-GF46GFSD-BZEdNcRZ.js +24 -0
- package/web/assets/{diagram-HNR7UZ2L-DFCfd5LI.js → diagram-HNR7UZ2L-DxKW2a2Y.js} +2 -2
- package/web/assets/diagram-QXG6HAR7-HfTHcXX_.js +24 -0
- package/web/assets/diagram-WEQXMOUZ-CyijUS8r.js +10 -0
- package/web/assets/{erDiagram-L5TCEMPS-8X1y6R4Y.js → erDiagram-L5TCEMPS-D0fvpWEX.js} +3 -3
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-CgFBM7e-.js +1 -0
- package/web/assets/flowDiagram-H6V6AXG4-Bvr9bbhk.js +162 -0
- package/web/assets/{ganttDiagram-JCBTUEKG-D3iL2Aet.js → ganttDiagram-JCBTUEKG-TzODrLzv.js} +2 -2
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DCJv7SII.js +1 -0
- package/web/assets/gitGraphDiagram-S2ZK5IYY-ONQSwJIY.js +106 -0
- package/web/assets/index-B4eB4dNZ.js +575 -0
- package/web/assets/index-CX_fADmQ.css +2 -0
- package/web/assets/info-J43DQDTF-KCYPFFUO-BfNy-9Nd.js +1 -0
- package/web/assets/infoDiagram-3YFTVSEB-DIPVUIQ6.js +2 -0
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-DfYpxnE5.js → ishikawaDiagram-BNXS4ZKH-BLjivjXL.js} +3 -3
- package/web/assets/{journeyDiagram-M6C3CM3L-CvArszLo.js → journeyDiagram-M6C3CM3L-Cetgs3NH.js} +3 -3
- package/web/assets/{kanban-definition-75IXJCU3-CpfVLLf9.js → kanban-definition-75IXJCU3-DMlCaIpf.js} +5 -5
- 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-B4GbXQBc.js} +3 -3
- 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-s27EFWgP.js → pieDiagram-CU6KROY3-D3YC_xyg.js} +2 -2
- package/web/assets/{quadrantDiagram-VICAPDV7-yXSZ2lLz.js → quadrantDiagram-VICAPDV7-B1TQnlm0.js} +2 -2
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-DeD6yN_0.js +1 -0
- package/web/assets/{requirementDiagram-JXO7QTGE-CWhqInuf.js → requirementDiagram-JXO7QTGE-f8wczLQf.js} +3 -3
- package/web/assets/rolldown-runtime-QTnfLwEv.js +1 -0
- package/web/assets/{sankeyDiagram-URQDO5SZ-1qEjlFIB.js → sankeyDiagram-URQDO5SZ-BTxzG5j0.js} +2 -2
- package/web/assets/{sequenceDiagram-VS2MUI6T-RyTlZ5cR.js → sequenceDiagram-VS2MUI6T-DuamM-IR.js} +5 -5
- package/web/assets/src-BNCXICdA.js +1 -0
- package/web/assets/stateDiagram-7D4R322I-Ch2pEUcw.js +1 -0
- package/web/assets/stateDiagram-v2-36443NZ5-7ANuoZLL.js +1 -0
- package/web/assets/{timeline-definition-O6YCAMPW-DrrqLYAf.js → timeline-definition-O6YCAMPW-07BR8R8G.js} +3 -3
- 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-B0RkWAim.js → vennDiagram-MWXL3ELB-BQX2ossk.js} +4 -4
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-DqrT9tB8.js +1 -0
- package/web/assets/{wardleyDiagram-CUQ6CDDI-boUxiQ5F.js → wardleyDiagram-CUQ6CDDI-CGIJBBiK.js} +3 -3
- package/web/assets/{xychartDiagram-N2JHSOCM-BOLm0eNm.js → xychartDiagram-N2JHSOCM-B3HNMuWp.js} +4 -4
- package/web/index.html +17 -10
- 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-7W6UQGC5-KyEG0HQg.js +0 -1
- package/web/assets/chunk-KGYTTC2M-CWC_c3H9.js +0 -161
- 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-QXG6HAR7-Di093rFg.js +0 -24
- package/web/assets/diagram-WEQXMOUZ-DZWi4SUN.js +0 -10
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-BQm9QzEa.js +0 -1
- package/web/assets/flowDiagram-H6V6AXG4-CMpY8Ufn.js +0 -162
- 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/packet-YPE3B663-LP52Z2RK-CKCrztD2.js +0 -1
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-BB7pHqoD.js +0 -1
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-DhoTOcuK.js +0 -1
- package/web/assets/stateDiagram-7D4R322I-DTjVfX4A.js +0 -1
- package/web/assets/stateDiagram-v2-36443NZ5-xY9_k9_Q.js +0 -1
- package/web/assets/treeView-BLDUP644-QA4HXRO3-ODehyGKL.js +0 -1
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-BfpgC-7o.js +0 -1
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-CNeL6VHE.js +0 -1
package/dist/core/run-analyze.js
CHANGED
|
@@ -13,11 +13,14 @@ import fs from 'fs/promises';
|
|
|
13
13
|
import { execFileSync } from 'child_process';
|
|
14
14
|
import { runPipelineFromRepo } from './ingestion/pipeline.js';
|
|
15
15
|
import { resetDegradedParseCounter } from './tree-sitter/safe-parse.js';
|
|
16
|
-
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings,
|
|
16
|
+
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings, deleteNodesForFiles, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, deleteAllCallSummaries, deleteAllInjects, queryImportersBatch, loadFTSExtension, wipeLbugDbFiles, LbugWipeError, DELETE_FILES_CHUNK_SIZE, } from './lbug/lbug-adapter.js';
|
|
17
|
+
import { escapeCypherString } from './lbug/cypher-escape.js';
|
|
17
18
|
import { createSearchFTSIndexes, initialiseSearchFTSStemmer, verifySearchFTSIndexes, } from './search/fts-indexes.js';
|
|
18
19
|
import { cjkSegmentationModeMismatch, getSearchFTSCjkSegmentation, initialiseSearchFTSCjkSegmentation, } from './search/cjk-segmentation.js';
|
|
19
|
-
import { resolveAnalyzeInstallPolicy } from './lbug/extension-loader.js';
|
|
20
|
-
import {
|
|
20
|
+
import { getExtensionCapabilities, resolveAnalyzeInstallPolicy } from './lbug/extension-loader.js';
|
|
21
|
+
import { diagnoseExtensionLoad } from './lbug/extension-load-error.js';
|
|
22
|
+
import { startWalCheckpointDriver, checkpointOnce, } from './lbug/wal-checkpoint-driver.js';
|
|
23
|
+
import { quarantineSidecarsForDirtyRecovery } from './lbug/sidecar-recovery.js';
|
|
21
24
|
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
25
|
import { DEFAULT_PDG_MAX_FUNCTION_LINES } from './ingestion/cfg/collect.js';
|
|
23
26
|
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,6 +32,7 @@ import { parseTruthyEnv, parsePositiveIntEnv } from './ingestion/utils/env.js';
|
|
|
29
32
|
import { computeFileHashes, diffFileHashes } from '../storage/file-hash.js';
|
|
30
33
|
import { extractChangedSubgraph, computeEffectiveWriteSet, } from './incremental/subgraph-extract.js';
|
|
31
34
|
import { shadowCandidatesFor } from './incremental/shadow-candidates.js';
|
|
35
|
+
import { shouldEscalateIncrementalWrite } from './incremental/escalation-gate.js';
|
|
32
36
|
import { loadParseCache, saveParseCache, pruneCache, PARSE_CACHE_VERSION, } from '../storage/parse-cache.js';
|
|
33
37
|
import { getDurableParsedFileDir, pruneAndSaveDurableParsedFileStore, } from '../storage/parsedfile-store.js';
|
|
34
38
|
import { getCurrentCommit, getCurrentBranch, getRemoteUrl, hasGitDir, getInferredRepoName, resolveRepoIdentityRoot, } from '../storage/git.js';
|
|
@@ -41,7 +45,11 @@ import { STALE_HASH_SENTINEL } from './lbug/schema.js';
|
|
|
41
45
|
* a full analyze. Kept as a named constant so the env-var/command guidance
|
|
42
46
|
* stays in one place (mirrors the VECTOR message in embedding-pipeline.ts).
|
|
43
47
|
*/
|
|
44
|
-
|
|
48
|
+
// Class-neutral lead, reused for the missing-dependency degrade path (#2383 F2):
|
|
49
|
+
// its remedy already explains that reinstalling will NOT help, so appending the
|
|
50
|
+
// generic "install with network access" tail below would contradict it.
|
|
51
|
+
const FTS_UNAVAILABLE_LEAD = 'FTS extension unavailable; skipping search-index creation.';
|
|
52
|
+
const FTS_UNAVAILABLE_MESSAGE = `${FTS_UNAVAILABLE_LEAD} ` +
|
|
45
53
|
'Full-text/BM25 search will be disabled until the LadybugDB FTS extension is ' +
|
|
46
54
|
'installed once with network access (GITNEXUS_LBUG_EXTENSION_INSTALL=auto) or ' +
|
|
47
55
|
'pre-installed for offline use. Run `gitnexus doctor` for details.';
|
|
@@ -328,6 +336,18 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
328
336
|
throw new Error('Cannot repair FTS indexes because this repository has not been analyzed yet. ' +
|
|
329
337
|
'Run `gitnexus analyze` first to create the initial index, then retry `--repair-fts`.');
|
|
330
338
|
}
|
|
339
|
+
if (existingMeta.incrementalInProgress) {
|
|
340
|
+
// #2409 / tri-review 4669518496 (R6): a dirty flag means the previous
|
|
341
|
+
// run died mid-writeback — the graph may be half-written and its WAL
|
|
342
|
+
// possibly poisoned. This branch returns early, so the dirty-recovery
|
|
343
|
+
// sidecar quarantine below would never run: repairing FTS now would
|
|
344
|
+
// open the DB and replay that WAL pre-quarantine, and even a
|
|
345
|
+
// survivable open would certify FTS over a half-written graph.
|
|
346
|
+
throw new Error('Cannot repair FTS indexes: the index is mid-incremental-recovery ' +
|
|
347
|
+
'(a previous analyze run did not complete cleanly). ' +
|
|
348
|
+
'Run `gitnexus analyze` first — it recovers the index automatically — ' +
|
|
349
|
+
'then retry `--repair-fts`.');
|
|
350
|
+
}
|
|
331
351
|
let lbugStat;
|
|
332
352
|
try {
|
|
333
353
|
lbugStat = await fs.lstat(lbugPath);
|
|
@@ -365,9 +385,23 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
365
385
|
policy: resolveAnalyzeInstallPolicy(),
|
|
366
386
|
});
|
|
367
387
|
if (!repairFtsAvailable) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
388
|
+
// Surface the load-side reason (#2374): "not pre-installed" was wrong
|
|
389
|
+
// and doctor never installed anything, so the old message trapped
|
|
390
|
+
// users in a query → repair-fts → doctor loop with no way out.
|
|
391
|
+
const rawFtsReason = getExtensionCapabilities().find((c) => c.name === 'fts')?.reason;
|
|
392
|
+
const ftsReason = rawFtsReason?.replace(/\.$/, '');
|
|
393
|
+
// A missing runtime dependency (Windows error 126, #2374) is not healed
|
|
394
|
+
// by re-installing — the file is already present. Route that class to the
|
|
395
|
+
// classified remedy (install VC++ redist / OpenSSL) instead of the old
|
|
396
|
+
// "retry the network install" text that trapped the user in a loop.
|
|
397
|
+
const { kind, remedy } = diagnoseExtensionLoad(rawFtsReason);
|
|
398
|
+
const remedyTail = kind === 'missing_dependency'
|
|
399
|
+
? ` ${remedy}`
|
|
400
|
+
: '. Retry with network access and GITNEXUS_LBUG_EXTENSION_INSTALL=auto to install it, ' +
|
|
401
|
+
'or pre-install the extension file; run `gitnexus doctor` for live FTS status.';
|
|
402
|
+
throw new Error('Cannot repair FTS indexes: the LadybugDB FTS extension failed to load' +
|
|
403
|
+
(ftsReason ? ` — ${ftsReason}` : '') +
|
|
404
|
+
remedyTail);
|
|
371
405
|
}
|
|
372
406
|
progress('fts', 85, 'Repairing search indexes...');
|
|
373
407
|
await createSearchFTSIndexes({
|
|
@@ -405,15 +439,71 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
405
439
|
// clear it, the on-disk index may be in a half-state. Cheapest path
|
|
406
440
|
// back to a known-good index is to wipe + rebuild from scratch.
|
|
407
441
|
if (existingMeta?.incrementalInProgress) {
|
|
442
|
+
const dirty = existingMeta.incrementalInProgress;
|
|
443
|
+
const dirtyDetails = typeof dirty === 'object'
|
|
444
|
+
? [
|
|
445
|
+
dirty.phase ? `phase=${dirty.phase}` : undefined,
|
|
446
|
+
`toWrite=${dirty.toWriteCount}`,
|
|
447
|
+
dirty.importerExpansion !== undefined
|
|
448
|
+
? `importerExpansion=${dirty.importerExpansion}`
|
|
449
|
+
: undefined,
|
|
450
|
+
dirty.effectiveWriteCount !== undefined
|
|
451
|
+
? `effectiveWrite=${dirty.effectiveWriteCount}`
|
|
452
|
+
: undefined,
|
|
453
|
+
dirty.deleteCount !== undefined ? `deleteCount=${dirty.deleteCount}` : undefined,
|
|
454
|
+
// Only stamped when > 0 (tri-review 4669518496 P2-5): its
|
|
455
|
+
// presence means the crashed run's importer expansion was
|
|
456
|
+
// already degraded — the write set may have been under-expanded
|
|
457
|
+
// before the crash.
|
|
458
|
+
dirty.droppedImporterChunks !== undefined
|
|
459
|
+
? `droppedImporterChunks=${dirty.droppedImporterChunks}`
|
|
460
|
+
: undefined,
|
|
461
|
+
]
|
|
462
|
+
.filter(Boolean)
|
|
463
|
+
.join(', ')
|
|
464
|
+
: 'legacy dirty flag';
|
|
408
465
|
log(
|
|
409
466
|
// "analyze run", not "incremental run" — since #2099 F1 the flag is a
|
|
410
467
|
// generic dirty marker written by BOTH writeback branches.
|
|
411
468
|
'Previous analyze run did not complete cleanly (incrementalInProgress flag set); ' +
|
|
469
|
+
`last dirty state: ${dirtyDetails}; ` +
|
|
412
470
|
'forcing full rebuild to restore a known-good index.');
|
|
413
471
|
options = { ...options, force: true };
|
|
414
472
|
// Reload meta after clearing the flag in-memory; we still want fileHashes
|
|
415
473
|
// for the post-rebuild meta carry-over, but force=true ensures the
|
|
416
474
|
// rebuild path executes.
|
|
475
|
+
//
|
|
476
|
+
// #2409 defect 2: the crashed writeback's WAL can be poisoned — replaying
|
|
477
|
+
// it kills the process natively, and the first DB open of this recovery
|
|
478
|
+
// run (the embedding-cache preservation open below) happens BEFORE the
|
|
479
|
+
// rebuild wipe that would discard it. Park the WAL/shadow sidecars aside
|
|
480
|
+
// now, while nothing is open, so every open in this run is replay-free.
|
|
481
|
+
// The rebuild wipes the DB regardless, so no committed data is at stake.
|
|
482
|
+
const { removed, failed } = await quarantineSidecarsForDirtyRecovery(lbugPath, log);
|
|
483
|
+
if (removed.length > 0) {
|
|
484
|
+
log(`Dirty-state recovery discarded ${removed.map((p) => path.basename(p)).join(', ')} ` +
|
|
485
|
+
'from the interrupted run (the file could not be moved aside, so its bytes were ' +
|
|
486
|
+
'removed — post-mortem forensics lost). Recovery proceeds with full embedding ' +
|
|
487
|
+
'preservation.');
|
|
488
|
+
}
|
|
489
|
+
if (failed.length > 0) {
|
|
490
|
+
// FIX 1 (this shipping review, replacing the tri-review 4669518496
|
|
491
|
+
// P2-3 drop-shape design): under a persistent lock the old drop-shape
|
|
492
|
+
// run derived its embedding mode as "drop", ran the WHOLE pipeline,
|
|
493
|
+
// and then died at the rebuild wipe on the very same handle — wasting
|
|
494
|
+
// minutes and zeroing embeddings on the way. A possibly-poisoned
|
|
495
|
+
// sidecar still sits next to the DB (any pre-wipe open would replay it
|
|
496
|
+
// and die), so failing here, in seconds, with the same actionable
|
|
497
|
+
// typed error the wipe would eventually throw is strictly better —
|
|
498
|
+
// and the CLI's LbugWipeError handler already renders it
|
|
499
|
+
// (recoveryHint 'lbug-wipe-failed'). The message is self-contained
|
|
500
|
+
// (headline + paths + lock guidance) because serve forwards only
|
|
501
|
+
// err.message over worker IPC.
|
|
502
|
+
throw new LbugWipeError(failed, {
|
|
503
|
+
headline: "Cannot start dirty-state recovery — the interrupted run's LadybugDB sidecars " +
|
|
504
|
+
'could neither be moved aside nor removed:',
|
|
505
|
+
});
|
|
506
|
+
}
|
|
417
507
|
}
|
|
418
508
|
// ── pdg-mode flip forces full writeback (#2099 F1) ─────────────────
|
|
419
509
|
// The incremental writeback persists only changed-file nodes, so a pdg
|
|
@@ -610,6 +700,12 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
610
700
|
// silently dropping embeddings on a mispredicted run. The re-insert
|
|
611
701
|
// step gates itself on the actual `isIncremental` value to avoid
|
|
612
702
|
// PK-conflicts when the incremental writeback path keeps the rows.
|
|
703
|
+
//
|
|
704
|
+
// This is the FIRST DB open of the run — the one #2409 defect 2 is about.
|
|
705
|
+
// On a dirty-recovery run it happens only after the sidecar quarantine
|
|
706
|
+
// moved (or removed) the crashed run's WAL/shadow; when neither was
|
|
707
|
+
// possible the dirty block above already threw a LbugWipeError, so this
|
|
708
|
+
// open is replay-free by construction (FIX 1 of this shipping review).
|
|
613
709
|
if (shouldLoadCache && existingMeta) {
|
|
614
710
|
try {
|
|
615
711
|
progress('embeddings', 0, 'Caching embeddings...');
|
|
@@ -709,11 +805,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
709
805
|
`(skipping wipe + ${allFilePaths.length - hashDiff.toWrite.length} unchanged file rows preserved)`);
|
|
710
806
|
// Set the dirty flag BEFORE any destructive DB mutation. Cleared on
|
|
711
807
|
// success at the meta-save step. Scoped to this branch's meta.json.
|
|
808
|
+
const now = Date.now();
|
|
712
809
|
await saveMeta(metaDir, {
|
|
713
810
|
...existingMeta,
|
|
714
811
|
incrementalInProgress: {
|
|
715
|
-
startedAt:
|
|
812
|
+
startedAt: now,
|
|
813
|
+
updatedAt: now,
|
|
814
|
+
phase: 'pre-write',
|
|
716
815
|
toWriteCount: hashDiff.toWrite.length,
|
|
816
|
+
directWriteCount: hashDiff.toWrite.length,
|
|
717
817
|
},
|
|
718
818
|
});
|
|
719
819
|
}
|
|
@@ -727,21 +827,26 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
727
827
|
// pdg flip, certify zombie/missing BasicBlock rows indefinitely).
|
|
728
828
|
// toWriteCount: 0 is the full-path sentinel (no incremental write set).
|
|
729
829
|
if (existingMeta) {
|
|
830
|
+
const now = Date.now();
|
|
730
831
|
await saveMeta(metaDir, {
|
|
731
832
|
...existingMeta,
|
|
732
|
-
incrementalInProgress: {
|
|
833
|
+
incrementalInProgress: {
|
|
834
|
+
startedAt: now,
|
|
835
|
+
updatedAt: now,
|
|
836
|
+
phase: 'full-rebuild',
|
|
837
|
+
toWriteCount: 0,
|
|
838
|
+
},
|
|
733
839
|
});
|
|
734
840
|
}
|
|
735
841
|
await closeLbug();
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
842
|
+
// Shared loud wipe (#2409 + tri-review 4669518496 P2-4). The 4-file
|
|
843
|
+
// family list — `.shadow` included, because a checkpoint-in-flight crash
|
|
844
|
+
// leaves a shadow sidecar that is replay poison next to a freshly created
|
|
845
|
+
// DB file — lives in wipeLbugDbFiles so this site and the escalation
|
|
846
|
+
// valve below can never drift. Failures now throw a typed LbugWipeError
|
|
847
|
+
// (ENOENT-verified removal) instead of silently letting initLbug reopen
|
|
848
|
+
// a still-populated DB this run believes it wiped.
|
|
849
|
+
await wipeLbugDbFiles(lbugPath);
|
|
745
850
|
}
|
|
746
851
|
await initLbug(lbugPath);
|
|
747
852
|
// Manual WAL checkpoint driver (#1741): periodically drain the WAL
|
|
@@ -752,12 +857,31 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
752
857
|
// Opt-out via `GITNEXUS_WAL_MANUAL_CHECKPOINT=0` (the driver itself
|
|
753
858
|
// returns a no-op handle when disabled). Analyze-only: MCP and serve
|
|
754
859
|
// paths continue to rely on the close-time CHECKPOINT in `safeClose`.
|
|
755
|
-
|
|
860
|
+
// `let`: the incremental branch's escalation valve (#2409) stops this driver
|
|
861
|
+
// around its close→wipe→reopen strategy switch and starts a fresh one.
|
|
862
|
+
let walCheckpointDriver = startWalCheckpointDriver();
|
|
756
863
|
try {
|
|
757
864
|
// All work after initLbug is wrapped in try/finally to ensure closeLbug()
|
|
758
865
|
// is called even if an error occurs — the module-level singleton DB handle
|
|
759
866
|
// must be released to avoid blocking subsequent invocations.
|
|
760
867
|
let lbugMsgCount = 0;
|
|
868
|
+
// #2409 escalation valve outcome, hoisted above the incremental branch so
|
|
869
|
+
// the vector-index recreation seam in Phase 4 below can tell "surgical
|
|
870
|
+
// incremental" (DB files survived — the HNSW index with them) apart from
|
|
871
|
+
// "escalated full write" (DB wiped, index destroyed) — tri-review
|
|
872
|
+
// 4669518496 P1.
|
|
873
|
+
let escalatedFullWrite = false;
|
|
874
|
+
// Phase 3.5's restore scope (FIX 3 of this shipping review): on the
|
|
875
|
+
// SURGICAL write plan this is the exact file set whose rows
|
|
876
|
+
// deleteNodesForFiles just removed — only THOSE files' cached embedding
|
|
877
|
+
// rows need re-inserting (everything else still sits in the DB, and
|
|
878
|
+
// re-inserting it would PK-conflict). `null` means the DB was wiped
|
|
879
|
+
// (full rebuild or escalated write): the embedding table is fresh and
|
|
880
|
+
// every cached row must come back. Deriving this in memory replaces the
|
|
881
|
+
// old whole-table `RETURN e.id` pre-read, which rescanned data this
|
|
882
|
+
// process already holds and — worse — ran a read against the DB between
|
|
883
|
+
// writeback and finalize for no recovery benefit.
|
|
884
|
+
let deletedFilePathsForRestore = null;
|
|
761
885
|
if (isIncremental && hashDiff) {
|
|
762
886
|
// ── Incremental DB writeback ───────────────────────────────────
|
|
763
887
|
// 0. Expand the writable set with transitive importers of
|
|
@@ -782,21 +906,50 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
782
906
|
// self-acknowledged as best-effort; `--force` remains the
|
|
783
907
|
// escape hatch documented in GUARDRAILS.md.
|
|
784
908
|
//
|
|
785
|
-
// `
|
|
909
|
+
// `queryImportersBatch` reads `IMPORTS` from the pre-pipeline DB
|
|
786
910
|
// state, so the result is "files that USED TO import the
|
|
787
911
|
// target" — exactly the set whose previously-stored edges may
|
|
788
912
|
// no longer match what cross-file resolution produces this run.
|
|
789
913
|
const MAX_IMPORTER_BFS_DEPTH = 4;
|
|
914
|
+
// Escalation thresholds (#2409) live with shouldEscalateIncrementalWrite
|
|
915
|
+
// in incremental/escalation-gate.ts (pure predicate, boundary-tested).
|
|
790
916
|
const writableFiles = new Set(hashDiff.toWrite);
|
|
791
917
|
const directlyChangedCount = writableFiles.size;
|
|
792
|
-
|
|
918
|
+
const dirtyStartedAt = existingMeta.incrementalInProgress?.startedAt ?? Date.now();
|
|
919
|
+
// Dropped-chunk observability (tri-review 4669518496 P2-5): counts
|
|
920
|
+
// importer-BFS chunks whose IMPORTS query failed across ALL depths
|
|
921
|
+
// (degrade-don't-fail — the expansion shrinks instead of the run
|
|
922
|
+
// dying). Stamped into the #2410 crash diagnostics by
|
|
923
|
+
// saveIncrementalDirtyState ITSELF (FIX 6 of this shipping review),
|
|
924
|
+
// not by per-call-site spreads: the closure rebuilds its object from
|
|
925
|
+
// scratch on every call, so a count riding along at only some sites
|
|
926
|
+
// meant any newly added save site would silently erase it — exactly
|
|
927
|
+
// the phases where #2409-class crashes happen. >0-only semantics
|
|
928
|
+
// unchanged: unconditional zero-stamping would churn every
|
|
929
|
+
// strict-equality consumer of the diagnostics shape.
|
|
930
|
+
let droppedImporterChunks = 0;
|
|
931
|
+
const saveIncrementalDirtyState = async (phase, extra = {}) => {
|
|
932
|
+
await saveMeta(metaDir, {
|
|
933
|
+
...existingMeta,
|
|
934
|
+
incrementalInProgress: {
|
|
935
|
+
startedAt: dirtyStartedAt,
|
|
936
|
+
updatedAt: Date.now(),
|
|
937
|
+
phase,
|
|
938
|
+
toWriteCount: writableFiles.size,
|
|
939
|
+
directWriteCount: directlyChangedCount,
|
|
940
|
+
...(droppedImporterChunks > 0 ? { droppedImporterChunks } : {}),
|
|
941
|
+
...extra,
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
};
|
|
945
|
+
// Shadow-seed: for ADDED files, the importer query returns 0 (the new
|
|
793
946
|
// file has no IMPORTS rows in the pre-pipeline DB yet). But pre-
|
|
794
947
|
// existing unchanged files may have IMPORTS edges whose module-
|
|
795
948
|
// resolution claim the newcomer can steal under standard JS/TS
|
|
796
949
|
// resolution (Bugbot review on PR #1479). For each added file we
|
|
797
950
|
// derive the shadow candidates and, if the candidate was a known
|
|
798
951
|
// file in the prior meta, seed it into the BFS frontier so its
|
|
799
|
-
// importers — surfaced via
|
|
952
|
+
// importers — surfaced via the importer BFS — get their CALLS edges
|
|
800
953
|
// re-resolved against the new file. See shadow-candidates.ts for
|
|
801
954
|
// the full pattern catalogue.
|
|
802
955
|
const priorFileSet = new Set(existingMeta?.fileHashes ? Object.keys(existingMeta.fileHashes) : []);
|
|
@@ -809,28 +962,33 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
809
962
|
}
|
|
810
963
|
}
|
|
811
964
|
{
|
|
965
|
+
// Batched per depth level (#2409): one IN-list query per ~200-path
|
|
966
|
+
// chunk instead of one query per frontier file — a ~700-file frontier
|
|
967
|
+
// used to cost ~700 sequential lock-taking round-trips (~5.6s). The
|
|
968
|
+
// closure is identical: importers already in writableFiles are not
|
|
969
|
+
// re-frontiered, exactly like the per-file loop's membership check.
|
|
812
970
|
let frontier = [...hashDiff.toWrite, ...hashDiff.deleted, ...shadowSeed];
|
|
813
971
|
for (let depth = 0; depth < MAX_IMPORTER_BFS_DEPTH && frontier.length > 0; depth++) {
|
|
972
|
+
const importers = await queryImportersBatch(frontier, {
|
|
973
|
+
onChunkFailure: () => {
|
|
974
|
+
droppedImporterChunks += 1;
|
|
975
|
+
},
|
|
976
|
+
});
|
|
814
977
|
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. */
|
|
978
|
+
for (const i of importers) {
|
|
979
|
+
if (!writableFiles.has(i)) {
|
|
980
|
+
writableFiles.add(i);
|
|
981
|
+
nextFrontier.push(i);
|
|
828
982
|
}
|
|
829
983
|
}
|
|
830
984
|
frontier = nextFrontier;
|
|
831
985
|
}
|
|
832
986
|
}
|
|
833
987
|
const importerExpansion = writableFiles.size - directlyChangedCount;
|
|
988
|
+
await saveIncrementalDirtyState('importer-bfs', {
|
|
989
|
+
importerExpansion,
|
|
990
|
+
shadowSeedCount: shadowSeed.length,
|
|
991
|
+
});
|
|
834
992
|
if (importerExpansion > 0) {
|
|
835
993
|
log(`Incremental: +${importerExpansion} importer(s) added to writable set ` +
|
|
836
994
|
`(BFS depth ≤ ${MAX_IMPORTER_BFS_DEPTH}` +
|
|
@@ -848,69 +1006,143 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
848
1006
|
// cross-file CALLS edges that the pre-run DB couldn't
|
|
849
1007
|
// predict, e.g. a barrel re-export shifting `foo` from
|
|
850
1008
|
// B to D).
|
|
851
|
-
// The composed set is the input to BOTH
|
|
1009
|
+
// The composed set is the input to BOTH deleteNodesForFiles
|
|
852
1010
|
// and extractChangedSubgraph — asymmetry between the two would
|
|
853
1011
|
// leave stale rows or PK-conflict at COPY time.
|
|
854
1012
|
const effectiveWriteSet = computeEffectiveWriteSet(pipelineResult.graph, writableFiles);
|
|
855
1013
|
// Deduped: deleted entries may already appear via importer-BFS
|
|
856
|
-
// expansion (
|
|
857
|
-
// would otherwise
|
|
858
|
-
// (Bugbot LOW finding on PR #1479).
|
|
1014
|
+
// expansion (the importer BFS can return a now-deleted path), which
|
|
1015
|
+
// would otherwise hand deleteNodesForFiles the same path twice in one
|
|
1016
|
+
// batch (Bugbot LOW finding on PR #1479).
|
|
859
1017
|
const filesToDelete = [...new Set([...effectiveWriteSet, ...hashDiff.deleted])];
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1018
|
+
await saveIncrementalDirtyState('effective-write-set', {
|
|
1019
|
+
importerExpansion,
|
|
1020
|
+
shadowSeedCount: shadowSeed.length,
|
|
1021
|
+
effectiveWriteCount: effectiveWriteSet.size,
|
|
1022
|
+
deleteCount: filesToDelete.length,
|
|
1023
|
+
});
|
|
1024
|
+
// Escalation valve (#2409): when the effective write set covers most of
|
|
1025
|
+
// the repo, per-file surgery is strictly worse than the proven
|
|
1026
|
+
// wipe-and-bulk-COPY plan — the same data volume lands either way, but
|
|
1027
|
+
// the surgical plan pays per-table deletes plus COPY-into-non-empty
|
|
1028
|
+
// tables, and at this size it measured SLOWER than a full DB load. The
|
|
1029
|
+
// pipeline already produced the FULL graph (it always does), so only the
|
|
1030
|
+
// DB write plan changes here; fileHashes/meta bookkeeping is identical.
|
|
1031
|
+
// Thresholds + the AND-gate live in incremental/escalation-gate.ts.
|
|
1032
|
+
const writeFraction = effectiveWriteSet.size / Math.max(1, allFilePaths.length);
|
|
1033
|
+
if (shouldEscalateIncrementalWrite(filesToDelete.length, effectiveWriteSet.size, allFilePaths.length)) {
|
|
1034
|
+
escalatedFullWrite = true;
|
|
1035
|
+
log(`Incremental: effective write set covers ${effectiveWriteSet.size}/${allFilePaths.length} ` +
|
|
1036
|
+
// Display clamp only (predicate unchanged): BFS-found deleted
|
|
1037
|
+
// importers can push the numerator past the CURRENT file list, so
|
|
1038
|
+
// the raw fraction can exceed 1 — see the population-mismatch note
|
|
1039
|
+
// on shouldEscalateIncrementalWrite (tri-review 4669518496).
|
|
1040
|
+
`files (${Math.min(100, Math.round(writeFraction * 100))}%) — switching to a full DB write ` +
|
|
1041
|
+
`(wipe + bulk COPY) for this run; file-level incremental bookkeeping is unaffected.`);
|
|
1042
|
+
// toWriteCount: 0 is the established full-path dirty-flag sentinel;
|
|
1043
|
+
// the real counters ride along for crash diagnostics.
|
|
1044
|
+
await saveIncrementalDirtyState('escalated-full-write', {
|
|
1045
|
+
toWriteCount: 0,
|
|
1046
|
+
importerExpansion,
|
|
1047
|
+
shadowSeedCount: shadowSeed.length,
|
|
1048
|
+
effectiveWriteCount: effectiveWriteSet.size,
|
|
1049
|
+
deleteCount: filesToDelete.length,
|
|
1050
|
+
});
|
|
1051
|
+
// Strategy switch: stop the checkpoint driver around the close so its
|
|
1052
|
+
// in-flight CHECKPOINT can't race the reopen, drop the DB files
|
|
1053
|
+
// (sidecars included), and bulk-load the full graph into a fresh DB —
|
|
1054
|
+
// byte-for-byte the full-rebuild write plan. The wipe is the shared
|
|
1055
|
+
// ENOENT-verified helper (#2409 + tri-review 4669518496 P2-4): a
|
|
1056
|
+
// surviving family member throws a typed LbugWipeError here instead
|
|
1057
|
+
// of letting the reopen below resurrect the rows this run just chose
|
|
1058
|
+
// to replace wholesale.
|
|
1059
|
+
await walCheckpointDriver.stop();
|
|
1060
|
+
await closeLbug();
|
|
1061
|
+
await wipeLbugDbFiles(lbugPath);
|
|
1062
|
+
await initLbug(lbugPath);
|
|
1063
|
+
walCheckpointDriver = startWalCheckpointDriver();
|
|
1064
|
+
await loadGraphToLbug(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
|
|
1065
|
+
lbugMsgCount++;
|
|
1066
|
+
const pct = Math.min(84, 65 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 19));
|
|
1067
|
+
progress('lbug', pct, msg);
|
|
1068
|
+
});
|
|
871
1069
|
}
|
|
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
|
-
await
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
|
|
1070
|
+
else {
|
|
1071
|
+
// 1a. Remove the write set's existing rows — batched (#2409): one
|
|
1072
|
+
// DETACH DELETE per table per 200-file chunk. The former per-file
|
|
1073
|
+
// loop issued a count + delete per table per FILE — ~13k
|
|
1074
|
+
// single-row write transactions on a ~700-file write set — which
|
|
1075
|
+
// made this phase slower than a full rebuild and is the WAL-append
|
|
1076
|
+
// storm behind the native mid-writeback deaths in #2409. Errors
|
|
1077
|
+
// are NOT swallowed anymore: a zero-match file is a no-op by
|
|
1078
|
+
// construction, so anything thrown is a real engine failure that
|
|
1079
|
+
// must surface instead of silently skipping (that silent skip was
|
|
1080
|
+
// how #2409 hid its root cause).
|
|
1081
|
+
progress('lbug', 62, `Removing rows for changed files (0/${filesToDelete.length})...`);
|
|
1082
|
+
await deleteNodesForFiles(filesToDelete, {
|
|
1083
|
+
onChunk: (done, total) => progress('lbug', 62, `Removing rows for changed files (${done}/${total})...`),
|
|
1084
|
+
});
|
|
1085
|
+
// Surgical path: Phase 3.5 restores exactly these files' embedding
|
|
1086
|
+
// rows (FIX 3). Sound because deleteNodesForFiles propagates errors
|
|
1087
|
+
// — reaching this line means every listed file's rows are gone
|
|
1088
|
+
// deterministically — and this process holds the exclusive DB lock,
|
|
1089
|
+
// so no concurrent writer can disturb the derivation.
|
|
1090
|
+
deletedFilePathsForRestore = new Set(filesToDelete);
|
|
1091
|
+
// 2. Drop graph-wide nodes (Community, Process). They'll be re-inserted
|
|
1092
|
+
// from the fresh pipeline output below. Required for the
|
|
1093
|
+
// "Leiden runs on the FULL graph" correctness invariant.
|
|
1094
|
+
await deleteAllCommunitiesAndProcesses();
|
|
1095
|
+
// 2a. Drop INJECTS edges (DI collection injection, #2200) — their
|
|
1096
|
+
// validity is a whole-program property (a third-file change to the
|
|
1097
|
+
// interface or an implementer creates/invalidates edges between two
|
|
1098
|
+
// untouched files), so endpoint-writability extraction can't refresh
|
|
1099
|
+
// them; extractChangedSubgraph re-includes all of them from the
|
|
1100
|
+
// fresh graph (isGraphWideRelType). UNCONDITIONAL, next to the
|
|
1101
|
+
// Communities delete — NOT inside the `options.pdg` block below: the
|
|
1102
|
+
// di phase runs on every persisting analyze (same !skipGraphPhases
|
|
1103
|
+
// regime as communities/processes) while the graph-wide re-include
|
|
1104
|
+
// is unconditional, so a pdg-gated delete would append without
|
|
1105
|
+
// deleting on every non-pdg incremental run (N runs = N copies of
|
|
1106
|
+
// every INJECTS row; CodeRelation has no PK and no read-side dedup).
|
|
1107
|
+
await deleteAllInjects();
|
|
1108
|
+
// 2b. Drop interprocedural TAINT_PATH edges (#2084 M4 U6) when pdg is on
|
|
1109
|
+
// — their validity is a whole-program property (an A→C flow can be
|
|
1110
|
+
// invalidated by a change to an intermediate function on a third
|
|
1111
|
+
// file), so endpoint-writability extraction can't refresh them.
|
|
1112
|
+
// extractChangedSubgraph re-includes all of them from the fresh
|
|
1113
|
+
// graph (isGraphWideRelType), mirroring Community/Process.
|
|
1114
|
+
if (options.pdg === true) {
|
|
1115
|
+
await deleteAllInterprocTaintPaths();
|
|
1116
|
+
// 2c. Drop CALL_SUMMARY edges (PDG FU-C) on an incremental `--pdg`
|
|
1117
|
+
// writeback. They are re-included from the FULL fresh graph
|
|
1118
|
+
// (isGraphWideRelType) and the callSummaries phase recomputes every
|
|
1119
|
+
// summary each run, so delete-all-then-rebuild keeps an unchanged
|
|
1120
|
+
// function's summary from being lost — same contract as TAINT_PATH.
|
|
1121
|
+
await deleteAllCallSummaries();
|
|
1122
|
+
}
|
|
1123
|
+
// 3. Extract the changed subgraph from the FULL ctx.graph and write
|
|
1124
|
+
// only that. Unchanged-file rows in the DB stay untouched. Pass
|
|
1125
|
+
// the SAME effectiveWriteSet so the subgraph and the deletes
|
|
1126
|
+
// cover identical files (asymmetry would silently corrupt).
|
|
1127
|
+
const subgraph = extractChangedSubgraph(pipelineResult.graph, effectiveWriteSet);
|
|
1128
|
+
await saveIncrementalDirtyState('load-graph', {
|
|
1129
|
+
importerExpansion,
|
|
1130
|
+
shadowSeedCount: shadowSeed.length,
|
|
1131
|
+
effectiveWriteCount: effectiveWriteSet.size,
|
|
1132
|
+
deleteCount: filesToDelete.length,
|
|
1133
|
+
});
|
|
1134
|
+
await loadGraphToLbug(subgraph, pipelineResult.repoPath, storagePath, (msg) => {
|
|
1135
|
+
lbugMsgCount++;
|
|
1136
|
+
const pct = Math.min(84, 65 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 19));
|
|
1137
|
+
progress('lbug', pct, msg);
|
|
1138
|
+
});
|
|
903
1139
|
}
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
await
|
|
910
|
-
lbugMsgCount++;
|
|
911
|
-
const pct = Math.min(84, 65 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 19));
|
|
912
|
-
progress('lbug', pct, msg);
|
|
913
|
-
});
|
|
1140
|
+
// Boundary drain (#2409): checkpoint at the end of the incremental
|
|
1141
|
+
// writeback so the WAL it accumulated never lingers into the FTS and
|
|
1142
|
+
// embedding phases — a later crash leaves only post-checkpoint WAL for
|
|
1143
|
+
// the next open to replay. Near-instant when the periodic driver has
|
|
1144
|
+
// kept up; rides the driver's bounded retry via runCheckpointWithRetry.
|
|
1145
|
+
await checkpointOnce();
|
|
914
1146
|
}
|
|
915
1147
|
else {
|
|
916
1148
|
// ── Full rebuild ───────────────────────────────────────────────
|
|
@@ -956,23 +1188,50 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
956
1188
|
progress('fts', 90, 'Search indexes ready');
|
|
957
1189
|
}
|
|
958
1190
|
else {
|
|
959
|
-
|
|
1191
|
+
// For a missing runtime dependency (#2374) the file is present, so the
|
|
1192
|
+
// generic "install it with network access" tail in FTS_UNAVAILABLE_MESSAGE
|
|
1193
|
+
// contradicts the remedy's own "reinstalling will NOT help" (#2383 F2). Lead
|
|
1194
|
+
// with the class-neutral sentence and append only the classified remedy.
|
|
1195
|
+
const ftsReason = getExtensionCapabilities().find((c) => c.name === 'fts')?.reason;
|
|
1196
|
+
const { kind, remedy } = diagnoseExtensionLoad(ftsReason);
|
|
1197
|
+
log(kind === 'missing_dependency'
|
|
1198
|
+
? `${FTS_UNAVAILABLE_LEAD} ${remedy}`
|
|
1199
|
+
: FTS_UNAVAILABLE_MESSAGE);
|
|
960
1200
|
progress('fts', 90, 'Search indexes skipped (FTS unavailable)');
|
|
961
1201
|
}
|
|
962
1202
|
// ── Phase 3.5: Re-insert cached embeddings ────────────────────────
|
|
963
1203
|
// 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
|
-
//
|
|
1204
|
+
// - Full rebuild / escalated write: DB was wiped, every cached row
|
|
1205
|
+
// needs to come back.
|
|
1206
|
+
// - Incremental (surgical): changed/deleted files' rows were just
|
|
1207
|
+
// deleted by deleteNodesForFiles (a REAL delete since tri-review
|
|
1208
|
+
// 4669518496 P2-1 — it joins embedding rows through their owning
|
|
1209
|
+
// nodes), so changed-file vectors need to come back; unchanged-file
|
|
1210
|
+
// rows still exist. Bugbot review on PR #1479 flagged that gating
|
|
1211
|
+
// this on `!isIncremental` silently lost changed-file embeddings.
|
|
1212
|
+
//
|
|
1213
|
+
// Restore discipline (tri-review 4669518496 / KTD10, restore scope
|
|
1214
|
+
// derived in memory since FIX 3 of this shipping review) — filtered and
|
|
1215
|
+
// conflict-free, replacing the old insert-everything-and-swallow shape:
|
|
1216
|
+
// 1. Live-graph filter: rows whose nodeId no longer exists in the
|
|
1217
|
+
// freshly-built FULL graph are dropped. The cache was read BEFORE
|
|
1218
|
+
// the pipeline ran, so it still carries deleted files' rows —
|
|
1219
|
+
// re-inserting them resurrected orphans (wholesale onto the wiped
|
|
1220
|
+
// paths' empty table) now that the delete above is real.
|
|
1221
|
+
// 2. Restore-scope filter, derived WITHOUT touching the DB (the old
|
|
1222
|
+
// shape pre-read every surviving embedding id back out of the
|
|
1223
|
+
// table it had just written): on a wiped path
|
|
1224
|
+
// (`deletedFilePathsForRestore === null`) the table is fresh, so
|
|
1225
|
+
// every live row comes back; on the surgical path only rows whose
|
|
1226
|
+
// owning node's filePath is in the just-join-deleted set are
|
|
1227
|
+
// inserted — everything else still sits in the DB and would
|
|
1228
|
+
// PK-conflict. The derivation is sound because deleteNodesForFiles
|
|
1229
|
+
// propagates errors (a completed writeback means a deterministic
|
|
1230
|
+
// delete outcome) and this process holds the exclusive DB lock (no
|
|
1231
|
+
// concurrent writer).
|
|
1232
|
+
// The per-batch try/catch stays as a last-resort guard only — it no
|
|
1233
|
+
// longer fires on the happy path.
|
|
1234
|
+
let restoredEmbeddingCount = 0;
|
|
976
1235
|
if (cachedEmbeddings.length > 0) {
|
|
977
1236
|
const cachedDims = cachedEmbeddings[0].embedding.length;
|
|
978
1237
|
const { EMBEDDING_DIMS } = await import('./lbug/schema.js');
|
|
@@ -983,16 +1242,65 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
983
1242
|
cachedEmbeddingNodeIds = new Set();
|
|
984
1243
|
}
|
|
985
1244
|
else {
|
|
986
|
-
progress('embeddings', 88, `Restoring ${cachedEmbeddings.length} cached embeddings...`);
|
|
987
1245
|
const { batchInsertEmbeddings: batchInsert } = await import('./embeddings/embedding-pipeline.js');
|
|
1246
|
+
// (1) Live-graph filter — the FULL pipeline graph (always produced),
|
|
1247
|
+
// NOT the incremental subgraph, or unchanged files' rows would be
|
|
1248
|
+
// dropped from the restore set.
|
|
1249
|
+
const liveEmbeddings = cachedEmbeddings.filter((e) => pipelineResult.graph.getNode(e.nodeId) !== undefined);
|
|
1250
|
+
// (2) Restore-scope filter (see the discipline note above).
|
|
1251
|
+
const rowsToRestore = deletedFilePathsForRestore === null
|
|
1252
|
+
? liveEmbeddings
|
|
1253
|
+
: liveEmbeddings.filter((e) => {
|
|
1254
|
+
const filePath = pipelineResult.graph.getNode(e.nodeId)?.properties?.filePath;
|
|
1255
|
+
return typeof filePath === 'string' && deletedFilePathsForRestore.has(filePath);
|
|
1256
|
+
});
|
|
1257
|
+
progress('embeddings', 88, `Restoring ${rowsToRestore.length} cached embeddings...`);
|
|
988
1258
|
const EMBED_BATCH = 200;
|
|
989
|
-
for (let i = 0; i <
|
|
990
|
-
const batch =
|
|
1259
|
+
for (let i = 0; i < rowsToRestore.length; i += EMBED_BATCH) {
|
|
1260
|
+
const batch = rowsToRestore.slice(i, i + EMBED_BATCH);
|
|
991
1261
|
try {
|
|
992
1262
|
await batchInsert(executeWithReusedStatement, batch);
|
|
1263
|
+
restoredEmbeddingCount += batch.length;
|
|
993
1264
|
}
|
|
994
1265
|
catch {
|
|
995
|
-
/*
|
|
1266
|
+
/* last-resort guard — conflict-free by construction above */
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
// Legacy-orphan sweep (FIX 3, finder B): the live-graph filter's
|
|
1270
|
+
// REJECTS — cached rows whose owning node no longer exists — are the
|
|
1271
|
+
// rows stranded by the era when the embedding delete was a no-op
|
|
1272
|
+
// (tri-review 4669518496 P2-1; schema version stays 6), plus this
|
|
1273
|
+
// run's just-deleted files' rows (already join-deleted above — the
|
|
1274
|
+
// exact-id DELETE matches nothing for those, so including them is a
|
|
1275
|
+
// harmless no-op rather than worth a fragile nodeId parse to
|
|
1276
|
+
// exclude). On the SURGICAL path the true legacy orphans still sit
|
|
1277
|
+
// in the DB and the node join can never reach them again (no owning
|
|
1278
|
+
// node), so delete them by exact row id. On wiped paths the rejects
|
|
1279
|
+
// were simply not restored — nothing to sweep. Legacy-tolerant: a
|
|
1280
|
+
// sweep failure must never fail a completed writeback, so the whole
|
|
1281
|
+
// sweep warns-and-continues.
|
|
1282
|
+
if (deletedFilePathsForRestore !== null) {
|
|
1283
|
+
const orphanRowIds = cachedEmbeddings
|
|
1284
|
+
.filter((e) => pipelineResult.graph.getNode(e.nodeId) === undefined)
|
|
1285
|
+
.map((e) => `${e.nodeId}:${e.chunkIndex}`);
|
|
1286
|
+
if (orphanRowIds.length > 0) {
|
|
1287
|
+
try {
|
|
1288
|
+
for (let i = 0; i < orphanRowIds.length; i += DELETE_FILES_CHUNK_SIZE) {
|
|
1289
|
+
const chunk = orphanRowIds.slice(i, i + DELETE_FILES_CHUNK_SIZE);
|
|
1290
|
+
const listLiteral = `[${chunk
|
|
1291
|
+
.map((id) => `'${escapeCypherString(id)}'`)
|
|
1292
|
+
.join(', ')}]`;
|
|
1293
|
+
await executeQuery(`MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.id IN ${listLiteral} DELETE e`);
|
|
1294
|
+
}
|
|
1295
|
+
log(`Swept ${orphanRowIds.length} cached embedding row(s) with no live owning ` +
|
|
1296
|
+
'node — legacy orphans stranded while the embedding delete was a no-op; ' +
|
|
1297
|
+
'ids already removed with their files match nothing.');
|
|
1298
|
+
}
|
|
1299
|
+
catch (err) {
|
|
1300
|
+
log(`Warning: could not sweep ${orphanRowIds.length} orphaned embedding ` +
|
|
1301
|
+
`row(s) (${err.message}); they are unreachable by search ` +
|
|
1302
|
+
'joins and will be retried next run.');
|
|
1303
|
+
}
|
|
996
1304
|
}
|
|
997
1305
|
}
|
|
998
1306
|
}
|
|
@@ -1019,6 +1327,40 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
1019
1327
|
`\`--embeddings <n>\` to set a custom cap.`);
|
|
1020
1328
|
}
|
|
1021
1329
|
}
|
|
1330
|
+
// ── Vector-index recreation after a wipe-and-restore (tri-review
|
|
1331
|
+
// 4669518496 P1 / KTD1) ────────────────────────────────────────────
|
|
1332
|
+
// The full-rebuild and escalated-incremental write plans wipe the DB
|
|
1333
|
+
// files — the HNSW index with them. Phase 3.5 brought the embedding ROWS
|
|
1334
|
+
// back, but on a preserve-only run nothing recreates the index: semantic
|
|
1335
|
+
// search silently loses its vector lane (>10k-embedding repos return
|
|
1336
|
+
// empty under the exact-scan cap) while meta certified 'vector-index'.
|
|
1337
|
+
// Recreate it here, where every gate input is settled:
|
|
1338
|
+
// - restoredEmbeddingCount > 0 — rows actually came back;
|
|
1339
|
+
// - dbWasWiped — surgical incremental runs keep their index (HNSW
|
|
1340
|
+
// self-maintains on insert/delete); only wiped DBs lost it;
|
|
1341
|
+
// - embeddingSkipped — evaluated AFTER the deriveEmbeddingCap decision
|
|
1342
|
+
// above, NOT `!shouldGenerateEmbeddings`: when Phase 4 really runs,
|
|
1343
|
+
// the pipeline builds the index itself after all inserts (firing this
|
|
1344
|
+
// seam first would swap its bulk build for per-row live HNSW
|
|
1345
|
+
// maintenance on the hottest flow), while a capped >50k-node repo has
|
|
1346
|
+
// shouldGenerateEmbeddings=true yet never runs the pipeline — exactly
|
|
1347
|
+
// the case a naive gate would leave index-less again.
|
|
1348
|
+
// buildVectorIndex carries its own extension-policy gate and
|
|
1349
|
+
// warn-on-failure; the boolean feeds semanticMode so the finalize stamp
|
|
1350
|
+
// reflects the DB's ACTUAL state even when recreation fails (win32 /
|
|
1351
|
+
// extension unavailable → 'exact-scan').
|
|
1352
|
+
const dbWasWiped = !isIncremental || escalatedFullWrite;
|
|
1353
|
+
if (restoredEmbeddingCount > 0 && dbWasWiped && embeddingSkipped) {
|
|
1354
|
+
// Re-import at the seam rather than thread a mutable capture from
|
|
1355
|
+
// Phase 3.5 (FIX 3 of this shipping review — the captured function was
|
|
1356
|
+
// a fragile moving part): dynamic imports are memoized, and
|
|
1357
|
+
// `restoredEmbeddingCount > 0` proves Phase 3.5 already loaded the
|
|
1358
|
+
// module, so the lazy-embeddings convention (#2370) holds — no
|
|
1359
|
+
// embeddings module loads unless a restore actually happened.
|
|
1360
|
+
const { buildVectorIndex } = await import('./embeddings/embedding-pipeline.js');
|
|
1361
|
+
const vectorIndexReady = await buildVectorIndex();
|
|
1362
|
+
semanticMode = vectorIndexReady ? 'vector-index' : 'exact-scan';
|
|
1363
|
+
}
|
|
1022
1364
|
if (!embeddingSkipped) {
|
|
1023
1365
|
const { isHttpMode } = await import('./embeddings/http-client.js');
|
|
1024
1366
|
const httpMode = isHttpMode();
|
|
@@ -1068,13 +1410,33 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
1068
1410
|
}
|
|
1069
1411
|
const { getRuntimeCapabilities } = await import('./platform/capabilities.js');
|
|
1070
1412
|
const runtimeCapabilities = getRuntimeCapabilities();
|
|
1413
|
+
// `semanticMode` is authoritative when set (Phase 4 reported what it
|
|
1414
|
+
// built, or the wipe-and-restore seam above verified/recreated the index
|
|
1415
|
+
// — tri-review 4669518496 P1). When unset, prefer the PREVIOUS run's
|
|
1416
|
+
// persisted stamp over the platform capability (FIX 3, finder A): the
|
|
1417
|
+
// unset case is exactly a run that neither wiped nor generated — e.g. a
|
|
1418
|
+
// surgical incremental whose index survived in place — and such a run
|
|
1419
|
+
// cannot change whether the HNSW index exists, so carrying the persisted
|
|
1420
|
+
// observation forward is strictly more truthful than re-deriving from
|
|
1421
|
+
// what the platform COULD do. Only the two positive observations carry
|
|
1422
|
+
// ('vector-index'/'exact-scan'); 'unavailable'/absent falls through to
|
|
1423
|
+
// the platform default rather than pinning a stale negative.
|
|
1424
|
+
const persistedStatus = existingMeta?.capabilities?.vectorSearch.status;
|
|
1425
|
+
const persistedSemanticMode = persistedStatus === 'vector-index' || persistedStatus === 'exact-scan'
|
|
1426
|
+
? persistedStatus
|
|
1427
|
+
: undefined;
|
|
1071
1428
|
const effectiveSemanticMode = semanticMode ??
|
|
1429
|
+
persistedSemanticMode ??
|
|
1072
1430
|
(runtimeCapabilities.semanticMode === 'vector-index' ? 'vector-index' : 'exact-scan');
|
|
1073
1431
|
// Convert the post-run file-hash map to the on-disk Record<string,string>
|
|
1074
1432
|
// shape consumed by RepoMeta.fileHashes.
|
|
1075
1433
|
const newFileHashesRecord = {};
|
|
1076
1434
|
for (const [k, v] of newFileHashes)
|
|
1077
1435
|
newFileHashesRecord[k] = v;
|
|
1436
|
+
// Annotated so the capabilities stamp below is compile-checked against
|
|
1437
|
+
// RepoMeta's status unions (tri-review 4669518496 P1/U3) — an unannotated
|
|
1438
|
+
// literal widens the vectorSearch.status ternary to `string` and the
|
|
1439
|
+
// honesty contract silently decays to "whatever interpolates".
|
|
1078
1440
|
const meta = {
|
|
1079
1441
|
repoPath,
|
|
1080
1442
|
lastCommit: currentCommit,
|