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
|
@@ -6,14 +6,16 @@ import { finished } from 'stream/promises';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import lbug from '@ladybugdb/core';
|
|
8
8
|
import { closeQueryResults } from './query-result-utils.js';
|
|
9
|
+
import { escapeCypherString } from './cypher-escape.js';
|
|
9
10
|
import { withConnLock } from './conn-lock.js';
|
|
10
11
|
import { isWalDriverActive } from './wal-driver-state.js';
|
|
11
12
|
import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, CREATE_VECTOR_INDEX_QUERY, STALE_HASH_SENTINEL, } from './schema.js';
|
|
12
13
|
import { streamAllCSVsToDisk } from './csv-generator.js';
|
|
13
14
|
import { getNodeLabel as deriveNodeLabel } from './rel-pair-routing.js';
|
|
15
|
+
import { EMBEDDABLE_LABELS } from '../embeddings/types.js';
|
|
14
16
|
import { extensionManager } from './extension-loader.js';
|
|
15
|
-
import { classifyDeleteAllError, closeLbugConnection, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, toNativeSafePath, resolveNativeSafeStorageDir, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
|
|
16
|
-
import { finalizeLbugSidecarsAfterClose,
|
|
17
|
+
import { classifyDeleteAllError, closeLbugConnection, HANDLE_RELEASE_PROBE_ATTEMPTS, HANDLE_RELEASE_PROBE_DELAY_MS, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, sleep, toNativeSafePath, resolveNativeSafeStorageDir, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
|
|
18
|
+
import { finalizeLbugSidecarsAfterClose, guardWalQuarantine, isMissingShadowSidecarError, isReadOnlyShadowReplayError, lbugLockRemediation, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, shadowSidecarRecoveryMessage, } from './sidecar-recovery.js';
|
|
17
19
|
import { isVectorExtensionSupportedByPlatform } from '../platform/capabilities.js';
|
|
18
20
|
import { logger } from '../logger.js';
|
|
19
21
|
/**
|
|
@@ -409,23 +411,14 @@ const queryAndDrain = async (targetConn, cypher) => {
|
|
|
409
411
|
};
|
|
410
412
|
const READ_ONLY_SHADOW_REPLAY_PROBE = 'MATCH (n) RETURN n LIMIT 1';
|
|
411
413
|
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* Throws shadowSidecarRecoveryMessage immediately when the WAL is large,
|
|
418
|
-
* preserving the uncheckpointed pages for explicit operator recovery.
|
|
419
|
-
* Returns silently when the WAL is absent, tiny, or in any other state
|
|
420
|
-
* where the existing recovery path is safe to proceed.
|
|
414
|
+
* Serve-side entry to the shared WAL-quarantine safety gate. Refuses (throws)
|
|
415
|
+
* when the `.shadow` is present on disk or the orphan WAL is too large to
|
|
416
|
+
* safely discard; returns silently otherwise. The policy itself lives in
|
|
417
|
+
* `guardWalQuarantine` (sidecar-recovery.ts) so serve and the MCP pool share
|
|
418
|
+
* one source of truth (PR #1747 review D2; issue #2382 review, Finding B).
|
|
421
419
|
*/
|
|
422
420
|
const refuseLargeWalQuarantine = async (dbPath, mode, triggeringErr) => {
|
|
423
|
-
|
|
424
|
-
if (state.kind === 'orphan-wal') {
|
|
425
|
-
logger.warn(`GitNexus: refusing to quarantine large WAL (${state.walBytes} bytes) at ${dbPath}.wal during ${mode} recovery; ` +
|
|
426
|
-
'manual recovery required — run `gitnexus analyze --force <repo-path> --index-only`.');
|
|
427
|
-
throw new Error(shadowSidecarRecoveryMessage(dbPath, triggeringErr));
|
|
428
|
-
}
|
|
421
|
+
await guardWalQuarantine(dbPath, mode, triggeringErr, logger);
|
|
429
422
|
};
|
|
430
423
|
const reopenReadOnlyAfterMissingShadow = async (dbPath, err) => {
|
|
431
424
|
await refuseLargeWalQuarantine(dbPath, 'read-only', err);
|
|
@@ -1091,8 +1084,43 @@ const BACKTICK_TABLES = new Set([
|
|
|
1091
1084
|
const escapeTableName = (table) => {
|
|
1092
1085
|
return BACKTICK_TABLES.has(table) ? `\`${table}\`` : table;
|
|
1093
1086
|
};
|
|
1094
|
-
/**
|
|
1095
|
-
|
|
1087
|
+
/**
|
|
1088
|
+
* Format one JS value as a Cypher literal for the adapter's string-built
|
|
1089
|
+
* statements: NULL/undefined → `NULL`, numbers pass through unquoted,
|
|
1090
|
+
* everything else becomes a single-quoted string literal escaped via
|
|
1091
|
+
* {@link escapeCypherString} (backslashes first, then quotes).
|
|
1092
|
+
*
|
|
1093
|
+
* Replaces three per-function closures that used SQL-style `''` doubling —
|
|
1094
|
+
* LadybugDB REJECTS doubling, so every value containing a quote made the
|
|
1095
|
+
* whole statement a parser error, invisible wherever the call site swallowed
|
|
1096
|
+
* per-row failures (#2409 escaping sweep, completed for tri-review
|
|
1097
|
+
* 4669518496 P2-2). Those closures also rewrote literal `\n`/`\r` into
|
|
1098
|
+
* two-character escape sequences; raw LF/CR are legal inside LadybugDB
|
|
1099
|
+
* single-quoted literals (live-probed on @ladybugdb/core 0.18.0), so the
|
|
1100
|
+
* replaces are gone and content now round-trips byte-identical.
|
|
1101
|
+
*/
|
|
1102
|
+
const formatCypherValue = (v) => {
|
|
1103
|
+
if (v === null || v === undefined)
|
|
1104
|
+
return 'NULL';
|
|
1105
|
+
if (typeof v === 'number')
|
|
1106
|
+
return String(v);
|
|
1107
|
+
return `'${escapeCypherString(String(v))}'`;
|
|
1108
|
+
};
|
|
1109
|
+
/**
|
|
1110
|
+
* Fallback: insert relationships one-by-one if COPY fails.
|
|
1111
|
+
*
|
|
1112
|
+
* Exported for the quoted-id round-trip tests in
|
|
1113
|
+
* `test/integration/lbug-core-adapter.test.ts` (the `DELETE_FILES_CHUNK_SIZE`
|
|
1114
|
+
* exported-for-tests precedent); production callers stay in this module.
|
|
1115
|
+
* Bails silently when the adapter singleton is closed.
|
|
1116
|
+
*
|
|
1117
|
+
* KNOWN PRE-EXISTING NARROWING (distinct from the `''` escaping bug, NOT
|
|
1118
|
+
* fixed here): the row regex below matches CSV fields with `[^"]*`, so an id
|
|
1119
|
+
* containing a double quote (CSV-escaped as `""`) never matches and the edge
|
|
1120
|
+
* is skipped. Tracked as part of the quote-in-id divergence documented in
|
|
1121
|
+
* `rel-pair-routing.ts`.
|
|
1122
|
+
*/
|
|
1123
|
+
export const fallbackRelationshipInserts = async (validRelLines, validTables, getNodeLabel) => {
|
|
1096
1124
|
if (!conn)
|
|
1097
1125
|
return;
|
|
1098
1126
|
const escapeLabel = (label) => {
|
|
@@ -1111,11 +1139,10 @@ const fallbackRelationshipInserts = async (validRelLines, validTables, getNodeLa
|
|
|
1111
1139
|
continue;
|
|
1112
1140
|
const confidence = parseFloat(confidenceStr) || 1.0;
|
|
1113
1141
|
const step = parseInt(stepStr) || 0;
|
|
1114
|
-
const esc = (s) => s.replace(/'/g, "''").replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
|
1115
1142
|
await queryAndDrain(conn, `
|
|
1116
|
-
MATCH (a:${escapeLabel(fromLabel)} {id:
|
|
1117
|
-
(b:${escapeLabel(toLabel)} {id:
|
|
1118
|
-
CREATE (a)-[:${REL_TABLE_NAME} {type:
|
|
1143
|
+
MATCH (a:${escapeLabel(fromLabel)} {id: ${formatCypherValue(fromId)} }),
|
|
1144
|
+
(b:${escapeLabel(toLabel)} {id: ${formatCypherValue(toId)} })
|
|
1145
|
+
CREATE (a)-[:${REL_TABLE_NAME} {type: ${formatCypherValue(relType)}, confidence: ${confidence}, reason: ${formatCypherValue(reason)}, step: ${step}}]->(b)
|
|
1119
1146
|
`);
|
|
1120
1147
|
}
|
|
1121
1148
|
catch {
|
|
@@ -1186,52 +1213,47 @@ export const insertNodeToLbug = async (label, properties, dbPath) => {
|
|
|
1186
1213
|
throw new Error('LadybugDB not initialized. Provide dbPath or call initLbug first.');
|
|
1187
1214
|
}
|
|
1188
1215
|
try {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
if (typeof v === 'number')
|
|
1193
|
-
return String(v);
|
|
1194
|
-
// Escape backslashes first (for Windows paths), then single quotes
|
|
1195
|
-
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''").replace(/\n/g, '\\n').replace(/\r/g, '\\r')}'`;
|
|
1196
|
-
};
|
|
1216
|
+
// Values go through the module-scope formatCypherValue — the old local
|
|
1217
|
+
// closure used `''` doubling, which LadybugDB rejects (#2409 escaping
|
|
1218
|
+
// sweep, tri-review 4669518496 P2-2).
|
|
1197
1219
|
// Build INSERT query based on node type
|
|
1198
1220
|
const t = escapeTableName(label);
|
|
1199
1221
|
let query;
|
|
1200
1222
|
if (label === 'File') {
|
|
1201
|
-
query = `CREATE (n:File {id: ${
|
|
1223
|
+
query = `CREATE (n:File {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, content: ${formatCypherValue(properties.content || '')}})`;
|
|
1202
1224
|
}
|
|
1203
1225
|
else if (label === 'Folder') {
|
|
1204
|
-
query = `CREATE (n:Folder {id: ${
|
|
1226
|
+
query = `CREATE (n:Folder {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}})`;
|
|
1205
1227
|
}
|
|
1206
1228
|
else if (label === 'Section') {
|
|
1207
1229
|
const descPart = properties.description
|
|
1208
|
-
? `, description: ${
|
|
1230
|
+
? `, description: ${formatCypherValue(properties.description)}`
|
|
1209
1231
|
: '';
|
|
1210
|
-
query = `CREATE (n:Section {id: ${
|
|
1232
|
+
query = `CREATE (n:Section {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, level: ${properties.level || 1}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
|
|
1211
1233
|
}
|
|
1212
1234
|
else if (label === 'BasicBlock') {
|
|
1213
1235
|
// Taint/PDG substrate (issue #2080) — no name column. `calleeIds` (#2227)
|
|
1214
1236
|
// is the sound resolved-id parallel to the leaf-name `callees` set.
|
|
1215
|
-
query = `CREATE (n:BasicBlock {id: ${
|
|
1237
|
+
query = `CREATE (n:BasicBlock {id: ${formatCypherValue(properties.id)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, text: ${formatCypherValue(properties.text || '')}, callees: ${formatCypherValue(properties.callees || '')}, calleeIds: ${formatCypherValue(properties.calleeIds || '')}})`;
|
|
1216
1238
|
}
|
|
1217
1239
|
else if (TABLES_WITH_EXPORTED.has(label)) {
|
|
1218
1240
|
const descPart = properties.description
|
|
1219
|
-
? `, description: ${
|
|
1241
|
+
? `, description: ${formatCypherValue(properties.description)}`
|
|
1220
1242
|
: '';
|
|
1221
|
-
query = `CREATE (n:${t} {id: ${
|
|
1243
|
+
query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
|
|
1222
1244
|
}
|
|
1223
1245
|
else if (label === 'Property') {
|
|
1224
1246
|
const descPart = properties.description
|
|
1225
|
-
? `, description: ${
|
|
1247
|
+
? `, description: ${formatCypherValue(properties.description)}`
|
|
1226
1248
|
: '';
|
|
1227
|
-
query = `CREATE (n:${t} {id: ${
|
|
1249
|
+
query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${formatCypherValue(properties.content || '')}${descPart}, declaredType: ${formatCypherValue(properties.declaredType || '')}})`;
|
|
1228
1250
|
}
|
|
1229
1251
|
else {
|
|
1230
1252
|
// Multi-language tables (Struct, Impl, Trait, Macro, etc.) — no isExported
|
|
1231
1253
|
const descPart = properties.description
|
|
1232
|
-
? `, description: ${
|
|
1254
|
+
? `, description: ${formatCypherValue(properties.description)}`
|
|
1233
1255
|
: '';
|
|
1234
|
-
query = `CREATE (n:${t} {id: ${
|
|
1256
|
+
query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
|
|
1235
1257
|
}
|
|
1236
1258
|
// Use per-query connection if dbPath provided (avoids lock conflicts)
|
|
1237
1259
|
if (targetDbPath) {
|
|
@@ -1266,14 +1288,10 @@ export const insertNodeToLbug = async (label, properties, dbPath) => {
|
|
|
1266
1288
|
export const batchInsertNodesToLbug = async (nodes, dbPath) => {
|
|
1267
1289
|
if (nodes.length === 0)
|
|
1268
1290
|
return { inserted: 0, failed: 0 };
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
return String(v);
|
|
1274
|
-
// Escape backslashes first (for Windows paths), then single quotes, then newlines
|
|
1275
|
-
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''").replace(/\n/g, '\\n').replace(/\r/g, '\\r')}'`;
|
|
1276
|
-
};
|
|
1291
|
+
// Values go through the module-scope formatCypherValue — the old local
|
|
1292
|
+
// closure used `''` doubling, which LadybugDB rejects; the per-node catch
|
|
1293
|
+
// below counted every quoted value as a silent `failed` (#2409 escaping
|
|
1294
|
+
// sweep, tri-review 4669518496 P2-2).
|
|
1277
1295
|
// Open a single connection for all inserts
|
|
1278
1296
|
const tempHandle = await openLbugConnection(lbug, dbPath);
|
|
1279
1297
|
const tempConn = tempHandle.conn;
|
|
@@ -1286,39 +1304,39 @@ export const batchInsertNodesToLbug = async (nodes, dbPath) => {
|
|
|
1286
1304
|
// Use MERGE instead of CREATE for upsert behavior (handles duplicates gracefully)
|
|
1287
1305
|
const t = escapeTableName(label);
|
|
1288
1306
|
if (label === 'File') {
|
|
1289
|
-
query = `MERGE (n:File {id: ${
|
|
1307
|
+
query = `MERGE (n:File {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.content = ${formatCypherValue(properties.content || '')}`;
|
|
1290
1308
|
}
|
|
1291
1309
|
else if (label === 'Folder') {
|
|
1292
|
-
query = `MERGE (n:Folder {id: ${
|
|
1310
|
+
query = `MERGE (n:Folder {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}`;
|
|
1293
1311
|
}
|
|
1294
1312
|
else if (label === 'Section') {
|
|
1295
1313
|
const descPart = properties.description
|
|
1296
|
-
? `, n.description = ${
|
|
1314
|
+
? `, n.description = ${formatCypherValue(properties.description)}`
|
|
1297
1315
|
: '';
|
|
1298
|
-
query = `MERGE (n:Section {id: ${
|
|
1316
|
+
query = `MERGE (n:Section {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.level = ${properties.level || 1}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
|
|
1299
1317
|
}
|
|
1300
1318
|
else if (label === 'BasicBlock') {
|
|
1301
1319
|
// Taint/PDG substrate (issue #2080) — no name column. `calleeIds`
|
|
1302
1320
|
// (#2227) is the sound resolved-id parallel to the `callees` set.
|
|
1303
|
-
query = `MERGE (n:BasicBlock {id: ${
|
|
1321
|
+
query = `MERGE (n:BasicBlock {id: ${formatCypherValue(properties.id)}}) SET n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.text = ${formatCypherValue(properties.text || '')}, n.callees = ${formatCypherValue(properties.callees || '')}, n.calleeIds = ${formatCypherValue(properties.calleeIds || '')}`;
|
|
1304
1322
|
}
|
|
1305
1323
|
else if (TABLES_WITH_EXPORTED.has(label)) {
|
|
1306
1324
|
const descPart = properties.description
|
|
1307
|
-
? `, n.description = ${
|
|
1325
|
+
? `, n.description = ${formatCypherValue(properties.description)}`
|
|
1308
1326
|
: '';
|
|
1309
|
-
query = `MERGE (n:${t} {id: ${
|
|
1327
|
+
query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
|
|
1310
1328
|
}
|
|
1311
1329
|
else if (label === 'Property') {
|
|
1312
1330
|
const descPart = properties.description
|
|
1313
|
-
? `, n.description = ${
|
|
1331
|
+
? `, n.description = ${formatCypherValue(properties.description)}`
|
|
1314
1332
|
: '';
|
|
1315
|
-
query = `MERGE (n:${t} {id: ${
|
|
1333
|
+
query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${formatCypherValue(properties.content || '')}${descPart}, n.declaredType = ${formatCypherValue(properties.declaredType || '')}`;
|
|
1316
1334
|
}
|
|
1317
1335
|
else {
|
|
1318
1336
|
const descPart = properties.description
|
|
1319
|
-
? `, n.description = ${
|
|
1337
|
+
? `, n.description = ${formatCypherValue(properties.description)}`
|
|
1320
1338
|
: '';
|
|
1321
|
-
query = `MERGE (n:${t} {id: ${
|
|
1339
|
+
query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
|
|
1322
1340
|
}
|
|
1323
1341
|
await queryAndDrain(tempConn, query);
|
|
1324
1342
|
inserted++;
|
|
@@ -1749,7 +1767,140 @@ export const closeLbug = async () => {
|
|
|
1749
1767
|
vectorIndexEnsured = false;
|
|
1750
1768
|
ensuredFTSIndexes.clear();
|
|
1751
1769
|
};
|
|
1770
|
+
/**
|
|
1771
|
+
* Thrown by {@link wipeLbugDbFiles} when a data-bearing member of the
|
|
1772
|
+
* LadybugDB file family is still present after the bounded
|
|
1773
|
+
* remove-and-verify retries (#2409, tri-review 4669518496 P2-4), and by
|
|
1774
|
+
* run-analyze's dirty-recovery block when the crashed run's sidecars can
|
|
1775
|
+
* neither be parked nor removed (this shipping review, FIX 1 — same lock
|
|
1776
|
+
* class, same remediation, and the CLI already renders this type).
|
|
1777
|
+
*
|
|
1778
|
+
* Classify by TYPE (`err instanceof LbugWipeError`) — the repo norm from
|
|
1779
|
+
* #2385 — never by message text. The MESSAGE is nonetheless fully
|
|
1780
|
+
* self-contained (headline + blocked paths + remediation) because
|
|
1781
|
+
* `gitnexus serve` forwards only `err.message` over worker IPC
|
|
1782
|
+
* (analyze-worker-core.ts), so the serve surface has nothing but this
|
|
1783
|
+
* string to show the user. The holder framing deliberately covers the
|
|
1784
|
+
* own-process case (FIX 2, finder A): the blocking handle is often a
|
|
1785
|
+
* lingering one from THIS process's just-closed DB or a transient AV scan
|
|
1786
|
+
* — not necessarily another process — so an immediate re-run often
|
|
1787
|
+
* succeeds.
|
|
1788
|
+
*/
|
|
1789
|
+
export class LbugWipeError extends Error {
|
|
1790
|
+
/** Paths still present (or unverifiable) after all retries. */
|
|
1791
|
+
survivors;
|
|
1792
|
+
constructor(survivors, options) {
|
|
1793
|
+
super(`${options?.headline ??
|
|
1794
|
+
`Failed to remove the LadybugDB index files — still present after ` +
|
|
1795
|
+
`${HANDLE_RELEASE_PROBE_ATTEMPTS} attempts:`}\n` +
|
|
1796
|
+
survivors.map((p) => ` - ${p}`).join('\n') +
|
|
1797
|
+
`\nThe blocking handle may be another process, a lingering handle from this ` +
|
|
1798
|
+
`process's just-closed database, or an antivirus scan — an immediate re-run ` +
|
|
1799
|
+
`often succeeds. If it persists, ${lbugLockRemediation('re-run the analyze')}.`);
|
|
1800
|
+
this.name = 'LbugWipeError';
|
|
1801
|
+
this.survivors = survivors;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Remove the LadybugDB file family and VERIFY each member is really gone.
|
|
1806
|
+
*
|
|
1807
|
+
* Owns the canonical 4-file family list — `<lbugPath>`, `.wal`, `.shadow`,
|
|
1808
|
+
* `.lock` — so run-analyze's two wipe sites (full rebuild + the #2409
|
|
1809
|
+
* escalation valve) can never drift apart. `.shadow` is included because a
|
|
1810
|
+
* checkpoint-in-flight crash leaves a shadow sidecar, and a stale shadow next
|
|
1811
|
+
* to a freshly created DB file is replay poison on the next open (#2409).
|
|
1812
|
+
*
|
|
1813
|
+
* Verification contract (tri-review 4669518496 P2-4 — the old inline loops
|
|
1814
|
+
* swallowed rm failures and let `initLbug` reopen a still-populated DB the
|
|
1815
|
+
* run believed it wiped): after `fs.rm({ recursive, force })`, each path is
|
|
1816
|
+
* probed and counts as GONE only when the probe rejects with **ENOENT**. A
|
|
1817
|
+
* resolving probe, or a rejection in the EPERM/EBUSY/EACCES class (Windows
|
|
1818
|
+
* delete-pending / handle-release lag — see HANDLE_RELEASE_LOCK_CODES in
|
|
1819
|
+
* lbug-config.ts), or any other code means the path is not verifiably gone:
|
|
1820
|
+
* it is retried on the shared handle-release budget
|
|
1821
|
+
* (HANDLE_RELEASE_PROBE_ATTEMPTS × linear HANDLE_RELEASE_PROBE_DELAY_MS,
|
|
1822
|
+
* lbug-config.ts — the previous private mirror constants were
|
|
1823
|
+
* documentation-coupled copies) and then handled by CLASS (this shipping
|
|
1824
|
+
* review, FIX 2):
|
|
1825
|
+
*
|
|
1826
|
+
* - DATA-BEARING members (`<lbugPath>`, `.wal`, `.shadow`) — a survivor
|
|
1827
|
+
* means the reopen would resurrect rows this run believes wiped: throw
|
|
1828
|
+
* a typed {@link LbugWipeError}.
|
|
1829
|
+
* - `.lock` — contentless: `initLbug` recreates it, and a genuinely held
|
|
1830
|
+
* lock surfaces as initLbug's own lock-busy classification (a better
|
|
1831
|
+
* error than this one). A `.lock`-only survivor (an AV-held
|
|
1832
|
+
* delete-pending handle outlasting the budget previously failed a
|
|
1833
|
+
* perfectly sound rebuild) logs a warning and CONTINUES.
|
|
1834
|
+
*
|
|
1835
|
+
* Linux unlinked-but-open (name gone, holder keeps the old inode) probes
|
|
1836
|
+
* ENOENT and is accepted by design — both production wipe sites run after a
|
|
1837
|
+
* real `closeLbug()`.
|
|
1838
|
+
*
|
|
1839
|
+
* Deliberately OUT of this contract: `cleanupOldKuzuFiles`
|
|
1840
|
+
* (repo-manager.ts) sweeps the LEGACY kuzu-era file family during storage
|
|
1841
|
+
* migration — different family, best-effort by design; and
|
|
1842
|
+
* `sweepStaleSidecars` (lbug-config.ts) is a test-fixture-gated open-retry
|
|
1843
|
+
* fallback that must never delete production files. Neither wipes the live
|
|
1844
|
+
* DB the run is about to recreate, so neither needs (or may share) the
|
|
1845
|
+
* loud-failure contract here.
|
|
1846
|
+
*/
|
|
1847
|
+
export const wipeLbugDbFiles = async (lbugPath) => {
|
|
1848
|
+
const lockPath = `${lbugPath}.lock`;
|
|
1849
|
+
const family = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.shadow`, lockPath];
|
|
1850
|
+
let survivors = [];
|
|
1851
|
+
for (let attempt = 1; attempt <= HANDLE_RELEASE_PROBE_ATTEMPTS; attempt++) {
|
|
1852
|
+
survivors = [];
|
|
1853
|
+
for (const f of family) {
|
|
1854
|
+
try {
|
|
1855
|
+
await fs.rm(f, { recursive: true, force: true });
|
|
1856
|
+
}
|
|
1857
|
+
catch {
|
|
1858
|
+
// `force: true` swallows ENOENT, so a rejection is a real failure —
|
|
1859
|
+
// but the ENOENT-probe below stays authoritative either way (another
|
|
1860
|
+
// process may have removed the path between the rm and the probe).
|
|
1861
|
+
}
|
|
1862
|
+
const gone = await fs.access(f).then(() => false, // still present
|
|
1863
|
+
(err) => err?.code === 'ENOENT');
|
|
1864
|
+
if (!gone)
|
|
1865
|
+
survivors.push(f);
|
|
1866
|
+
}
|
|
1867
|
+
if (survivors.length === 0)
|
|
1868
|
+
return;
|
|
1869
|
+
if (attempt < HANDLE_RELEASE_PROBE_ATTEMPTS) {
|
|
1870
|
+
await sleep(HANDLE_RELEASE_PROBE_DELAY_MS * attempt);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
// Class split (FIX 2): the contentless `.lock` never fails the wipe.
|
|
1874
|
+
const dataSurvivors = survivors.filter((f) => f !== lockPath);
|
|
1875
|
+
if (survivors.includes(lockPath)) {
|
|
1876
|
+
logger.warn(`GitNexus: ${lockPath} is still present after the wipe retries — continuing: the ` +
|
|
1877
|
+
'lock file is contentless and initLbug recreates it; a genuinely held lock will ' +
|
|
1878
|
+
"surface as the reopen's own lock-busy error.");
|
|
1879
|
+
}
|
|
1880
|
+
if (dataSurvivors.length > 0) {
|
|
1881
|
+
throw new LbugWipeError(dataSurvivors);
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1752
1884
|
export const isLbugReady = () => conn !== null && db !== null;
|
|
1885
|
+
/**
|
|
1886
|
+
* Multi-label alternation over exactly the labels that can own embedding
|
|
1887
|
+
* rows (embedding-pipeline.ts queries EMBEDDABLE_LABELS and nothing else),
|
|
1888
|
+
* reserved keywords backtick-escaped via {@link escapeTableName}. Probed on
|
|
1889
|
+
* @ladybugdb/core 0.18.0 (this shipping review, FIX 4): the full 19-label
|
|
1890
|
+
* alternation parses, executes, and deletes exactly the joined rows —
|
|
1891
|
+
* replacing the unlabeled `MATCH (n)` that scanned EVERY node table per
|
|
1892
|
+
* chunk (BasicBlock-dominated under `--pdg`) when only embeddable labels
|
|
1893
|
+
* can match an embedding row.
|
|
1894
|
+
*/
|
|
1895
|
+
const embeddableLabelMatch = () => EMBEDDABLE_LABELS.map((l) => escapeTableName(l)).join('|');
|
|
1896
|
+
// LADYBUGDB-CONTRACT: matches @ladybugdb/core ^0.18.0 native binder text,
|
|
1897
|
+
// probe-recorded: `Binder exception: Table CodeEmbedding does not exist.`
|
|
1898
|
+
// When bumping LadybugDB, re-validate — `git grep "LADYBUGDB-CONTRACT"`
|
|
1899
|
+
// enumerates every version-coupled spot.
|
|
1900
|
+
const isMissingEmbeddingTableError = (err) => {
|
|
1901
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1902
|
+
return msg.includes(`Table ${EMBEDDING_TABLE_NAME} does not exist`);
|
|
1903
|
+
};
|
|
1753
1904
|
/**
|
|
1754
1905
|
* Delete all nodes (and their relationships) for a specific file from LadybugDB
|
|
1755
1906
|
* @param filePath - The file path to delete nodes for
|
|
@@ -1772,7 +1923,34 @@ export const deleteNodesForFile = async (filePath, dbPath) => {
|
|
|
1772
1923
|
}
|
|
1773
1924
|
try {
|
|
1774
1925
|
let deletedNodes = 0;
|
|
1775
|
-
const escapedPath = filePath
|
|
1926
|
+
const escapedPath = escapeCypherString(filePath);
|
|
1927
|
+
// Delete the file's embedding rows FIRST, while their owning nodes are
|
|
1928
|
+
// still present: node ids are label-first — generateId = `${label}:${name}`
|
|
1929
|
+
// (src/lib/utils.ts) with qualified names that embed the file path — so
|
|
1930
|
+
// the old `e.nodeId STARTS WITH '<filePath>'` shape never matched a row
|
|
1931
|
+
// (tri-review 4669518496 P2-1). Join through the nodes on exact id
|
|
1932
|
+
// equality instead, scoped to the embeddable labels (FIX 4 — see
|
|
1933
|
+
// embeddableLabelMatch); ordering is load-bearing — after the DETACH
|
|
1934
|
+
// DELETE loop below the join would match nothing.
|
|
1935
|
+
try {
|
|
1936
|
+
await queryAndDrain(targetConn, `MATCH (n:${embeddableLabelMatch()}) WHERE n.filePath = '${escapedPath}' ` +
|
|
1937
|
+
`MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId = n.id DELETE e`);
|
|
1938
|
+
}
|
|
1939
|
+
catch (err) {
|
|
1940
|
+
// Deliberately legacy-permissive (pinned contract:
|
|
1941
|
+
// lbug-conn-serialization U5 and lbug-core-adapter expect this variant
|
|
1942
|
+
// to resolve `{deletedNodes: 0}` even on a bogus dbPath): the singular
|
|
1943
|
+
// variant swallows per-statement failures wholesale — its per-table
|
|
1944
|
+
// loop below does the same — so a partial rethrow here would be
|
|
1945
|
+
// incoherent with the rest of the function. The STRICT
|
|
1946
|
+
// rethrow-except-missing-table policy lives in deleteNodesForFiles,
|
|
1947
|
+
// the #2409 incremental writeback path (FIX 4). The one case worth a
|
|
1948
|
+
// diagnostic is the missing embedding table.
|
|
1949
|
+
if (isMissingEmbeddingTableError(err)) {
|
|
1950
|
+
logger.warn({ err }, `deleteNodesForFile: ${EMBEDDING_TABLE_NAME} table does not exist — ` +
|
|
1951
|
+
'skipping embedding-row deletes for this DB.');
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1776
1954
|
// Delete nodes from each table that has filePath
|
|
1777
1955
|
// DETACH DELETE removes the node and all its relationships
|
|
1778
1956
|
for (const tableName of NODE_TABLES) {
|
|
@@ -1802,13 +1980,6 @@ export const deleteNodesForFile = async (filePath, dbPath) => {
|
|
|
1802
1980
|
// Some tables may not support this query, skip
|
|
1803
1981
|
}
|
|
1804
1982
|
}
|
|
1805
|
-
// Also delete any embeddings for nodes in this file
|
|
1806
|
-
try {
|
|
1807
|
-
await queryAndDrain(targetConn, `MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId STARTS WITH '${escapedPath}' DELETE e`);
|
|
1808
|
-
}
|
|
1809
|
-
catch {
|
|
1810
|
-
// Embedding table may not exist or nodeId format may differ
|
|
1811
|
-
}
|
|
1812
1983
|
return { deletedNodes };
|
|
1813
1984
|
}
|
|
1814
1985
|
finally {
|
|
@@ -1817,6 +1988,91 @@ export const deleteNodesForFile = async (filePath, dbPath) => {
|
|
|
1817
1988
|
await closeLbugConnection(tempHandle);
|
|
1818
1989
|
}
|
|
1819
1990
|
};
|
|
1991
|
+
/**
|
|
1992
|
+
* Chunk size for {@link deleteNodesForFiles}. 200 paths keeps each
|
|
1993
|
+
* statement ~13KB (well inside parser limits) while a ~700-file write set
|
|
1994
|
+
* still collapses from ~13,000 statements to 124: 31 statements per chunk
|
|
1995
|
+
* (1 CodeEmbedding join-delete + 30 filePath-bearing node tables — the
|
|
1996
|
+
* 32-table NODE_TABLES roster minus Community/Process) × 4 chunks. The
|
|
1997
|
+
* original "~40" claim under-counted the per-chunk statement fan-out
|
|
1998
|
+
* (tri-review 4669518496 accuracy sweep).
|
|
1999
|
+
*/
|
|
2000
|
+
export const DELETE_FILES_CHUNK_SIZE = 200;
|
|
2001
|
+
/**
|
|
2002
|
+
* Batched variant of {@link deleteNodesForFile} for the incremental
|
|
2003
|
+
* writeback (#2409). One `DETACH DELETE … WHERE n.filePath IN […]` per
|
|
2004
|
+
* node table per chunk of paths, instead of a count + delete per table
|
|
2005
|
+
* per FILE. The per-file loop issued ~13,000 single-row write
|
|
2006
|
+
* transactions on a ~700-file write set — a WAL-append storm that made
|
|
2007
|
+
* the incremental path slower than a full rebuild and is the write
|
|
2008
|
+
* pattern behind the native mid-writeback deaths reported in #2409.
|
|
2009
|
+
*
|
|
2010
|
+
* NO general error swallowing: a zero-match chunk is a no-op success by
|
|
2011
|
+
* construction (every node table except Community/Process has a filePath
|
|
2012
|
+
* column), so anything thrown here is a real engine failure the caller
|
|
2013
|
+
* must see — silently skipping was exactly how #2409 hid its root cause.
|
|
2014
|
+
* The single tolerated exception (FIX 4) is the missing-embedding-table
|
|
2015
|
+
* binder error on the embedding join-delete: a DB created without
|
|
2016
|
+
* EMBEDDING_SCHEMA cannot own embedding rows, so skipping that one
|
|
2017
|
+
* statement is sound, while failing would brick every incremental run on
|
|
2018
|
+
* such a DB until `--force`. Statement count per chunk is unchanged by the
|
|
2019
|
+
* multi-label join: 1 embedding join-delete + 30 node-table deletes = 31
|
|
2020
|
+
* (the rejected per-label fallback shape would have been 19 + 30 = 49).
|
|
2021
|
+
* Singleton-connection only: the analyze writeback owns the write lock,
|
|
2022
|
+
* and `queryAndDrain` routes through `withConnLock` for it (the WAL
|
|
2023
|
+
* checkpoint driver is live during this).
|
|
2024
|
+
*/
|
|
2025
|
+
export const deleteNodesForFiles = async (filePaths, options = {}) => {
|
|
2026
|
+
if (!conn) {
|
|
2027
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2028
|
+
}
|
|
2029
|
+
const targetConn = conn;
|
|
2030
|
+
let warnedMissingEmbeddingTable = false;
|
|
2031
|
+
for (let i = 0; i < filePaths.length; i += DELETE_FILES_CHUNK_SIZE) {
|
|
2032
|
+
const chunk = filePaths.slice(i, i + DELETE_FILES_CHUNK_SIZE);
|
|
2033
|
+
const listLiteral = `[${chunk.map((p) => `'${escapeCypherString(p)}'`).join(', ')}]`;
|
|
2034
|
+
// Embedding rows key on their OWNING NODE's id: generateId builds
|
|
2035
|
+
// label-first ids — `${label}:${name}` (src/lib/utils.ts) with qualified
|
|
2036
|
+
// names that embed the file path (e.g. `Function:src/f.ts:fn0:1`) — so
|
|
2037
|
+
// the previous bare-path `e.nodeId STARTS WITH '<filePath>'` OR-chain
|
|
2038
|
+
// could never match anything (tri-review 4669518496 P2-1: the embedding
|
|
2039
|
+
// delete was a no-op). Join through the nodes instead: one multi-label
|
|
2040
|
+
// MATCH over exactly the embeddable labels (FIX 4, probe-proven on
|
|
2041
|
+
// 0.18.0 — see embeddableLabelMatch; the old unlabeled `MATCH (n)`
|
|
2042
|
+
// scanned every node table per chunk, BasicBlock-dominated under
|
|
2043
|
+
// `--pdg`, when only embeddable labels can own rows), and
|
|
2044
|
+
// `e.nodeId = n.id` equality is exact — no `File:a.ts` / `File:a.tsx`
|
|
2045
|
+
// prefix collisions. ORDER IS LOAD-BEARING: this must run BEFORE the
|
|
2046
|
+
// DETACH DELETE loop below — once the nodes are gone the join matches
|
|
2047
|
+
// nothing (empirically verified against @ladybugdb/core 0.18.0).
|
|
2048
|
+
try {
|
|
2049
|
+
await queryAndDrain(targetConn, `MATCH (n:${embeddableLabelMatch()}) WHERE n.filePath IN ${listLiteral} ` +
|
|
2050
|
+
`MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId = n.id DELETE e`);
|
|
2051
|
+
}
|
|
2052
|
+
catch (err) {
|
|
2053
|
+
// Tolerate exactly the missing-embedding-table binder error: a
|
|
2054
|
+
// build-variant DB without EMBEDDING_SCHEMA would otherwise brick
|
|
2055
|
+
// every incremental run until `--force` (FIX 4). The no-swallow
|
|
2056
|
+
// policy stays for every real failure — anything else rethrows.
|
|
2057
|
+
if (!isMissingEmbeddingTableError(err))
|
|
2058
|
+
throw err;
|
|
2059
|
+
if (!warnedMissingEmbeddingTable) {
|
|
2060
|
+
warnedMissingEmbeddingTable = true;
|
|
2061
|
+
logger.warn({ err }, `deleteNodesForFiles: ${EMBEDDING_TABLE_NAME} table does not exist — ` +
|
|
2062
|
+
'skipping embedding-row deletes for this writeback.');
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
for (const tableName of NODE_TABLES) {
|
|
2066
|
+
// Community/Process are graph-wide (no filePath); the orchestrator
|
|
2067
|
+
// drops them wholesale via deleteAllCommunitiesAndProcesses.
|
|
2068
|
+
if (tableName === 'Community' || tableName === 'Process')
|
|
2069
|
+
continue;
|
|
2070
|
+
const tn = escapeTableName(tableName);
|
|
2071
|
+
await queryAndDrain(targetConn, `MATCH (n:${tn}) WHERE n.filePath IN ${listLiteral} DETACH DELETE n`);
|
|
2072
|
+
}
|
|
2073
|
+
options.onChunk?.(Math.min(i + DELETE_FILES_CHUNK_SIZE, filePaths.length), filePaths.length);
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
1820
2076
|
export const getEmbeddingTableName = () => EMBEDDING_TABLE_NAME;
|
|
1821
2077
|
/**
|
|
1822
2078
|
* Return the distinct repo-relative paths of files that import
|
|
@@ -1838,7 +2094,7 @@ export const queryImporters = async (targetFilePath) => {
|
|
|
1838
2094
|
if (!c) {
|
|
1839
2095
|
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
1840
2096
|
}
|
|
1841
|
-
const escaped = targetFilePath
|
|
2097
|
+
const escaped = escapeCypherString(targetFilePath);
|
|
1842
2098
|
const cypher = `
|
|
1843
2099
|
MATCH (a)-[r:${REL_TABLE_NAME}]->(b)
|
|
1844
2100
|
WHERE r.type = 'IMPORTS' AND b.filePath = '${escaped}'
|
|
@@ -1870,6 +2126,70 @@ export const queryImporters = async (targetFilePath) => {
|
|
|
1870
2126
|
}
|
|
1871
2127
|
});
|
|
1872
2128
|
};
|
|
2129
|
+
/**
|
|
2130
|
+
* Batched variant of {@link queryImporters} for the incremental importer
|
|
2131
|
+
* BFS (#2409): distinct importers of ANY of the target paths, one query per
|
|
2132
|
+
* chunk per BFS depth instead of one query per frontier FILE (a ~700-file
|
|
2133
|
+
* frontier was ~700 sequential round-trips, each taking the connection lock
|
|
2134
|
+
* against the live WAL checkpoint driver — ~5.6s of the writeback measured).
|
|
2135
|
+
*
|
|
2136
|
+
* Same contract as the singular form: reads the pre-pipeline DB state and
|
|
2137
|
+
* swallows per-chunk query failures into a smaller result (correctness
|
|
2138
|
+
* degrades on that branch — under-expansion means possibly-stale edges —
|
|
2139
|
+
* but the DB stays writable and the writeback proceeds). Unlike the singular
|
|
2140
|
+
* form the degradation is not silent (tri-review 4669518496 P2-5): every
|
|
2141
|
+
* dropped chunk is logged and reported through `options.onChunkFailure`, so
|
|
2142
|
+
* the orchestrator can count it into the #2410 crash diagnostics
|
|
2143
|
+
* (`incrementalInProgress.droppedImporterChunks`).
|
|
2144
|
+
*/
|
|
2145
|
+
export const queryImportersBatch = async (targetFilePaths, options = {}) => {
|
|
2146
|
+
const c = conn;
|
|
2147
|
+
if (!c) {
|
|
2148
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
2149
|
+
}
|
|
2150
|
+
const importers = new Set();
|
|
2151
|
+
for (let i = 0; i < targetFilePaths.length; i += DELETE_FILES_CHUNK_SIZE) {
|
|
2152
|
+
// `i` only ever advances in whole chunk strides, so this is exact.
|
|
2153
|
+
const chunkIndex = i / DELETE_FILES_CHUNK_SIZE;
|
|
2154
|
+
const chunk = targetFilePaths.slice(i, i + DELETE_FILES_CHUNK_SIZE);
|
|
2155
|
+
const listLiteral = `[${chunk.map((p) => `'${escapeCypherString(p)}'`).join(', ')}]`;
|
|
2156
|
+
const cypher = `
|
|
2157
|
+
MATCH (a)-[r:${REL_TABLE_NAME}]->(b)
|
|
2158
|
+
WHERE r.type = 'IMPORTS' AND b.filePath IN ${listLiteral}
|
|
2159
|
+
RETURN DISTINCT a.filePath AS importer
|
|
2160
|
+
`;
|
|
2161
|
+
await withConnLock(async () => {
|
|
2162
|
+
let queryResult;
|
|
2163
|
+
try {
|
|
2164
|
+
queryResult = await c.query(cypher);
|
|
2165
|
+
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
2166
|
+
const rows = await result.getAll();
|
|
2167
|
+
for (const row of rows) {
|
|
2168
|
+
const v = row.importer;
|
|
2169
|
+
if (typeof v === 'string' && v.length > 0)
|
|
2170
|
+
importers.add(v);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
catch (err) {
|
|
2174
|
+
// Degrade-don't-fail, mirroring queryImporters — but LOUDLY
|
|
2175
|
+
// (tri-review 4669518496 P2-5): a dropped chunk means every importer
|
|
2176
|
+
// it would have surfaced keeps possibly-stale edges this run, and the
|
|
2177
|
+
// old bare `catch {}` left no trace of that anywhere. pino idiom:
|
|
2178
|
+
// `err` key — `error` serializes to `{}`.
|
|
2179
|
+
logger.warn({ err }, `Incremental importer BFS: dropped chunk ${chunkIndex} (${chunk.length} target path(s)) — ` +
|
|
2180
|
+
'importer expansion degrades for this run; affected importers may keep stale edges until the next full rebuild.');
|
|
2181
|
+
options.onChunkFailure?.(chunkIndex, chunk.length, err);
|
|
2182
|
+
}
|
|
2183
|
+
finally {
|
|
2184
|
+
if (queryResult)
|
|
2185
|
+
await closeQueryResults(queryResult);
|
|
2186
|
+
}
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
// Cypher without ORDER BY is unordered — sort so downstream chunking and
|
|
2190
|
+
// logs are stable run-to-run (matches diffFileHashes' sorted outputs).
|
|
2191
|
+
return [...importers].sort();
|
|
2192
|
+
};
|
|
1873
2193
|
/**
|
|
1874
2194
|
* Drop every Community and Process node (and their MEMBER_OF /
|
|
1875
2195
|
* STEP_IN_PROCESS edges via DETACH DELETE). Used at the start of an
|