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
|
@@ -10,16 +10,83 @@
|
|
|
10
10
|
* path is resolved relative to `import.meta.url`.
|
|
11
11
|
*/
|
|
12
12
|
import path from 'path';
|
|
13
|
+
import { existsSync, statSync } from 'node:fs';
|
|
13
14
|
import { fork } from 'child_process';
|
|
14
15
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
15
16
|
import { createRequire } from 'node:module';
|
|
16
|
-
import { getStoragePath } from '../storage/repo-manager.js';
|
|
17
|
+
import { canonicalizePath, getStoragePath, INDEX_METADATA_FILE, listRegisteredRepos, registryPathEquals, } from '../storage/repo-manager.js';
|
|
17
18
|
import { logger } from '../core/logger.js';
|
|
18
19
|
const _require = createRequire(import.meta.url);
|
|
19
20
|
const MAX_WORKER_RETRIES = 2;
|
|
21
|
+
/**
|
|
22
|
+
* The worker reports `complete` over IPC before its on-disk finalization
|
|
23
|
+
* (LadybugDB checkpoint + native handle release + metadata write) is visible
|
|
24
|
+
* at `getStoragePath(targetPath)` — observed up to ~6.5s behind the IPC
|
|
25
|
+
* message. Opening the database inside that window is what the pre-IPC
|
|
26
|
+
* ordering was meant to prevent and is actively dangerous: reads fail with
|
|
27
|
+
* binder errors or return an empty graph, the open can quarantine the
|
|
28
|
+
* in-flight WAL, and the native layer racing the rewrite has crashed the
|
|
29
|
+
* whole server (SIGSEGV-class exit, no output) on slow CI runners.
|
|
30
|
+
*/
|
|
31
|
+
const FINALIZE_SETTLE_TIMEOUT_MS = 60_000;
|
|
32
|
+
const FINALIZE_SETTLE_POLL_MS = 200;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve once the analyzed repo's index is settled at `storagePath`: the
|
|
35
|
+
* LadybugDB file and metadata both exist AND were (re)written by THIS job
|
|
36
|
+
* (mtime >= jobStartMs — bare existence is not enough, a re-analysis leaves
|
|
37
|
+
* the previous index in place while it works), and no transient WAL/shadow/
|
|
38
|
+
* checkpoint sidecars remain (the worker's native close has finished).
|
|
39
|
+
*
|
|
40
|
+
* Never rejects. Timing out logs and proceeds (pre-gate behavior) rather
|
|
41
|
+
* than failing a job whose analysis genuinely succeeded — e.g. a no-op
|
|
42
|
+
* non-force analyze legitimately rewrites nothing.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Look up the analyzed repo's registered storage path. The request's
|
|
46
|
+
* user-provided path is used only as a comparison key; the filesystem probes
|
|
47
|
+
* below run against the registry's own `storagePath` — the server-owned
|
|
48
|
+
* record readers resolve through, and not a user-controlled value
|
|
49
|
+
* (CodeQL js/path-injection).
|
|
50
|
+
*/
|
|
51
|
+
const registeredStoragePath = async (targetPath) => {
|
|
52
|
+
const target = canonicalizePath(path.resolve(targetPath));
|
|
53
|
+
const entries = await listRegisteredRepos();
|
|
54
|
+
const entry = entries.find((e) => registryPathEquals(canonicalizePath(e.path), target));
|
|
55
|
+
return entry?.storagePath ?? null;
|
|
56
|
+
};
|
|
57
|
+
const waitForSettledIndex = async (targetPath, jobStartMs) => {
|
|
58
|
+
const settled = (storagePath) => {
|
|
59
|
+
try {
|
|
60
|
+
const lbugStat = statSync(path.join(storagePath, 'lbug'));
|
|
61
|
+
const metaStat = statSync(path.join(storagePath, INDEX_METADATA_FILE));
|
|
62
|
+
return (lbugStat.mtimeMs >= jobStartMs &&
|
|
63
|
+
metaStat.mtimeMs >= jobStartMs &&
|
|
64
|
+
['lbug.wal', 'lbug.shadow', 'lbug.wal.checkpoint'].every((f) => !existsSync(path.join(storagePath, f))));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false; // not written yet
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const deadline = Date.now() + FINALIZE_SETTLE_TIMEOUT_MS;
|
|
71
|
+
for (;;) {
|
|
72
|
+
// Re-resolved each round: the worker registers the repo as part of the
|
|
73
|
+
// finalization this gate is waiting out.
|
|
74
|
+
const storagePath = await registeredStoragePath(targetPath);
|
|
75
|
+
if (storagePath && settled(storagePath))
|
|
76
|
+
return;
|
|
77
|
+
if (Date.now() > deadline) {
|
|
78
|
+
logger.warn({ targetPath }, 'analyze finalization not visible after timeout; completing job anyway');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, FINALIZE_SETTLE_POLL_MS));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
20
84
|
export function createLaunchAnalysisWorker(deps) {
|
|
21
|
-
const { jobManager, backend, acquireRepoLock, releaseRepoLock } = deps;
|
|
85
|
+
const { jobManager, backend, acquireRepoLock, releaseRepoLock, closeDbHandle } = deps;
|
|
22
86
|
return function launchAnalysisWorker(job, targetPath, opts) {
|
|
87
|
+
// For waitForSettledIndex: files (re)written by this job have mtimes at or
|
|
88
|
+
// after this instant. Taken before the fork so no worker write predates it.
|
|
89
|
+
const jobStartMs = Date.now();
|
|
23
90
|
// Acquire shared repo lock (keyed on storagePath to match embed handler)
|
|
24
91
|
const analyzeLockKey = getStoragePath(targetPath);
|
|
25
92
|
const lockErr = acquireRepoLock(analyzeLockKey);
|
|
@@ -67,10 +134,17 @@ export function createLaunchAnalysisWorker(deps) {
|
|
|
67
134
|
}
|
|
68
135
|
else if (msg.type === 'complete') {
|
|
69
136
|
releaseRepoLock(analyzeLockKey);
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
137
|
+
// Before marking complete: (1) wait for the worker's on-disk
|
|
138
|
+
// finalization to settle (see waitForSettledIndex), (2) evict the
|
|
139
|
+
// cached DB handle — same invalidation DELETE /api/repo performs, a
|
|
140
|
+
// handle opened before the rewrite reads pre-rewrite state — and
|
|
141
|
+
// only then (3) reinitialize the backend. This makes the ordering
|
|
142
|
+
// comment below true in practice: the repo is actually queryable
|
|
143
|
+
// when the client receives the SSE complete event.
|
|
144
|
+
waitForSettledIndex(targetPath, jobStartMs)
|
|
145
|
+
.then(() => closeDbHandle())
|
|
146
|
+
.catch(() => { }) // best-effort: eviction failure must not fail the job
|
|
147
|
+
.then(() => backend.init())
|
|
74
148
|
.then(() => {
|
|
75
149
|
jobManager.updateJob(job.id, { status: 'complete', repoName: msg.result.repoName });
|
|
76
150
|
})
|
|
@@ -84,6 +158,8 @@ export function createLaunchAnalysisWorker(deps) {
|
|
|
84
158
|
}
|
|
85
159
|
else if (msg.type === 'error') {
|
|
86
160
|
releaseRepoLock(analyzeLockKey);
|
|
161
|
+
// A failed (force) analyze may still have rewritten DB files first.
|
|
162
|
+
void closeDbHandle().catch(() => { });
|
|
87
163
|
jobManager.updateJob(job.id, { status: 'failed', error: msg.message });
|
|
88
164
|
}
|
|
89
165
|
});
|
package/dist/server/api.d.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* CORS is restricted to localhost, private/LAN networks, and the deployed site.
|
|
9
9
|
*/
|
|
10
10
|
import express from 'express';
|
|
11
|
+
import { type RegistryEntry } from '../storage/repo-manager.js';
|
|
11
12
|
import { type GraphNode, type GraphRelationship } from '../_shared/index.js';
|
|
13
|
+
import { JobManager } from './analyze-job.js';
|
|
12
14
|
/**
|
|
13
15
|
* Determine whether an HTTP Origin header value is allowed by CORS policy.
|
|
14
16
|
*
|
|
@@ -49,6 +51,34 @@ export declare const registerWebUI: (app: express.Express, staticDir: string | n
|
|
|
49
51
|
export declare const writeNdjsonRecord: (res: express.Response, record: GraphStreamRecord, signal?: AbortSignal) => Promise<void>;
|
|
50
52
|
export declare const getNodeQuery: (table: string, includeContent: boolean) => string;
|
|
51
53
|
export declare const streamGraphNdjson: (res: express.Response, includeContent?: boolean, signal?: AbortSignal) => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Mount an SSE progress endpoint for a JobManager.
|
|
56
|
+
* Handles: initial state, terminal events, heartbeat, event IDs, client disconnect.
|
|
57
|
+
*
|
|
58
|
+
* Terminal payloads carry `repoPath` (the analyzed path) alongside the display
|
|
59
|
+
* `repoName` so clients can reconnect by path identity — with duplicate
|
|
60
|
+
* basenames, a name-only reconnect resolves to the first same-named sibling.
|
|
61
|
+
* Exported for unit tests that lock the wire payload shape.
|
|
62
|
+
*/
|
|
63
|
+
export declare const mountSSEProgress: (app: express.Express, routePath: string, jm: JobManager) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a `?repo=` request param against the registry in two tiers:
|
|
66
|
+
*
|
|
67
|
+
* 1. Path claim — any input containing a separator ('/' or '\\', which
|
|
68
|
+
* cover path.sep on every platform) is treated as a path claim and
|
|
69
|
+
* resolved by canonical registry path ONLY. A miss fails closed
|
|
70
|
+
* (null, never a basename fallback) so a stale or wrong path can
|
|
71
|
+
* never silently retarget a same-named sibling repo (#2419).
|
|
72
|
+
* Within this tier, only absolute or Windows-shaped ('\\') claims
|
|
73
|
+
* are worth canonicalizing; relative claims like 'org/name' or
|
|
74
|
+
* './repo' are rejected immediately WITHOUT touching the filesystem
|
|
75
|
+
* — canonicalizing them would run an attacker-influenced
|
|
76
|
+
* CWD-relative realpathSync probe on un-rate-limited GET routes,
|
|
77
|
+
* and no legitimate caller sends relative paths.
|
|
78
|
+
* 2. Name fallback — bare names (no separators) keep the legacy
|
|
79
|
+
* basename/name match for older callers.
|
|
80
|
+
*/
|
|
81
|
+
export declare const resolveRegisteredRepoEntry: (repos: RegistryEntry[], repoName?: string) => RegistryEntry | null;
|
|
52
82
|
/**
|
|
53
83
|
* Handle a GET /api/file request body. Extracted from createServer's route
|
|
54
84
|
* registration so it can be unit-tested without spinning up an HTTP server
|
package/dist/server/api.js
CHANGED
|
@@ -12,12 +12,13 @@ import cors from 'cors';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import fs from 'fs/promises';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
|
-
import { loadMeta, listRegisteredRepos, getStoragePath } from '../storage/repo-manager.js';
|
|
15
|
+
import { canonicalizePath, cloneDirBelongsToEntry, loadMeta, listRegisteredRepos, getStoragePath, registryPathEquals, } from '../storage/repo-manager.js';
|
|
16
16
|
import { executeQuery, executePrepared, executeWithReusedStatement, streamQuery, flushWAL, closeLbug, withLbugDb, isReadOnlyDbError, } from '../core/lbug/lbug-adapter.js';
|
|
17
17
|
import { isValidQueryParams } from '../core/lbug/query-params.js';
|
|
18
18
|
import { NODE_TABLES } from '../_shared/index.js';
|
|
19
19
|
import { searchFTSFromLbug } from '../core/search/bm25-index.js';
|
|
20
20
|
import { hybridSearch } from '../core/search/hybrid-search.js';
|
|
21
|
+
import { ftsDegradedWarning } from '../core/search/fts-indexes.js';
|
|
21
22
|
import { LocalBackend } from '../mcp/local/local-backend.js';
|
|
22
23
|
import { mountMCPEndpoints } from './mcp-http.js';
|
|
23
24
|
import { fileURLToPath } from 'url';
|
|
@@ -380,8 +381,13 @@ export const streamGraphNdjson = async (res, includeContent = false, signal) =>
|
|
|
380
381
|
/**
|
|
381
382
|
* Mount an SSE progress endpoint for a JobManager.
|
|
382
383
|
* Handles: initial state, terminal events, heartbeat, event IDs, client disconnect.
|
|
384
|
+
*
|
|
385
|
+
* Terminal payloads carry `repoPath` (the analyzed path) alongside the display
|
|
386
|
+
* `repoName` so clients can reconnect by path identity — with duplicate
|
|
387
|
+
* basenames, a name-only reconnect resolves to the first same-named sibling.
|
|
388
|
+
* Exported for unit tests that lock the wire payload shape.
|
|
383
389
|
*/
|
|
384
|
-
const mountSSEProgress = (app, routePath, jm) => {
|
|
390
|
+
export const mountSSEProgress = (app, routePath, jm) => {
|
|
385
391
|
app.get(routePath, (req, res) => {
|
|
386
392
|
let jobId;
|
|
387
393
|
try {
|
|
@@ -411,6 +417,7 @@ const mountSSEProgress = (app, routePath, jm) => {
|
|
|
411
417
|
eventId++;
|
|
412
418
|
res.write(`id: ${eventId}\nevent: ${job.status}\ndata: ${JSON.stringify({
|
|
413
419
|
repoName: job.repoName,
|
|
420
|
+
repoPath: job.repoPath,
|
|
414
421
|
error: job.error,
|
|
415
422
|
})}\n\n`);
|
|
416
423
|
res.end();
|
|
@@ -434,6 +441,7 @@ const mountSSEProgress = (app, routePath, jm) => {
|
|
|
434
441
|
const eventJob = jm.getJob(jobId);
|
|
435
442
|
res.write(`id: ${eventId}\nevent: ${progress.phase}\ndata: ${JSON.stringify({
|
|
436
443
|
repoName: eventJob?.repoName,
|
|
444
|
+
repoPath: eventJob?.repoPath,
|
|
437
445
|
error: eventJob?.error,
|
|
438
446
|
})}\n\n`);
|
|
439
447
|
clearInterval(heartbeat);
|
|
@@ -476,6 +484,43 @@ const requestedRepo = (req) => {
|
|
|
476
484
|
}
|
|
477
485
|
return undefined;
|
|
478
486
|
};
|
|
487
|
+
const repoParamBasename = (repoName) => repoName.replace(/\\/g, '/').split('/').filter(Boolean).pop() ?? repoName;
|
|
488
|
+
/**
|
|
489
|
+
* Resolve a `?repo=` request param against the registry in two tiers:
|
|
490
|
+
*
|
|
491
|
+
* 1. Path claim — any input containing a separator ('/' or '\\', which
|
|
492
|
+
* cover path.sep on every platform) is treated as a path claim and
|
|
493
|
+
* resolved by canonical registry path ONLY. A miss fails closed
|
|
494
|
+
* (null, never a basename fallback) so a stale or wrong path can
|
|
495
|
+
* never silently retarget a same-named sibling repo (#2419).
|
|
496
|
+
* Within this tier, only absolute or Windows-shaped ('\\') claims
|
|
497
|
+
* are worth canonicalizing; relative claims like 'org/name' or
|
|
498
|
+
* './repo' are rejected immediately WITHOUT touching the filesystem
|
|
499
|
+
* — canonicalizing them would run an attacker-influenced
|
|
500
|
+
* CWD-relative realpathSync probe on un-rate-limited GET routes,
|
|
501
|
+
* and no legitimate caller sends relative paths.
|
|
502
|
+
* 2. Name fallback — bare names (no separators) keep the legacy
|
|
503
|
+
* basename/name match for older callers.
|
|
504
|
+
*/
|
|
505
|
+
export const resolveRegisteredRepoEntry = (repos, repoName) => {
|
|
506
|
+
if (!repoName)
|
|
507
|
+
return repos[0] ?? null;
|
|
508
|
+
const looksLikePath = path.isAbsolute(repoName) || repoName.includes('/') || repoName.includes('\\');
|
|
509
|
+
if (looksLikePath) {
|
|
510
|
+
// Relative path claims fail closed with zero filesystem probes.
|
|
511
|
+
if (!path.isAbsolute(repoName) && !repoName.includes('\\'))
|
|
512
|
+
return null;
|
|
513
|
+
const requestedPath = canonicalizePath(repoName);
|
|
514
|
+
const pathMatch = repos.find((r) => registryPathEquals(canonicalizePath(r.path), requestedPath));
|
|
515
|
+
if (pathMatch)
|
|
516
|
+
return pathMatch;
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const normalizedName = repoParamBasename(repoName);
|
|
520
|
+
return (repos.find((r) => r.name === normalizedName) ||
|
|
521
|
+
repos.find((r) => r.name.toLowerCase() === normalizedName.toLowerCase()) ||
|
|
522
|
+
null);
|
|
523
|
+
};
|
|
479
524
|
/**
|
|
480
525
|
* Handle a GET /api/file request body. Extracted from createServer's route
|
|
481
526
|
* registration so it can be unit-tested without spinning up an HTTP server
|
|
@@ -702,6 +747,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
702
747
|
backend,
|
|
703
748
|
acquireRepoLock,
|
|
704
749
|
releaseRepoLock,
|
|
750
|
+
closeDbHandle: closeLbug,
|
|
705
751
|
});
|
|
706
752
|
/**
|
|
707
753
|
* Maximum time the hold-queue will wait for an active analysis job to complete.
|
|
@@ -712,19 +758,8 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
712
758
|
// Pass `req` to enable early exit if the client disconnects during the hold-queue wait.
|
|
713
759
|
const resolveRepo = async (repoName, isRetry = false, req) => {
|
|
714
760
|
const repos = await listRegisteredRepos();
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
// e.g. "C:\Users\LENOVO\.gitnexus\repos\todo.txt-cli" -> "todo.txt-cli"
|
|
718
|
-
const normalizedName = repoName ? path.basename(repoName) : undefined;
|
|
719
|
-
if (normalizedName) {
|
|
720
|
-
found =
|
|
721
|
-
repos.find((r) => r.name === normalizedName) ||
|
|
722
|
-
repos.find((r) => r.name.toLowerCase() === normalizedName.toLowerCase()) ||
|
|
723
|
-
null;
|
|
724
|
-
}
|
|
725
|
-
else if (repos.length > 0) {
|
|
726
|
-
found = repos[0]; // default to first repo
|
|
727
|
-
}
|
|
761
|
+
const found = resolveRegisteredRepoEntry(repos, repoName);
|
|
762
|
+
const normalizedName = repoName ? repoParamBasename(repoName) : undefined;
|
|
728
763
|
// If not yet in the registry, check whether a background job is actively cloning or
|
|
729
764
|
// analyzing this repo. Hold the connection open (up to 5 minutes) until it completes.
|
|
730
765
|
// We only wait for in-progress jobs ('queued'|'cloning'|'analyzing') — a 'complete' job
|
|
@@ -757,7 +792,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
757
792
|
if (currentJob.status === 'complete') {
|
|
758
793
|
await backend.init();
|
|
759
794
|
const freshRepos = await listRegisteredRepos();
|
|
760
|
-
return freshRepos
|
|
795
|
+
return resolveRegisteredRepoEntry(freshRepos, repoName);
|
|
761
796
|
}
|
|
762
797
|
await new Promise((r) => setTimeout(r, 1000));
|
|
763
798
|
}
|
|
@@ -774,7 +809,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
774
809
|
logger.debug({ repoName: String(normalizedName).replace(/[\r\n]/g, ' ') }, '[debug] resolveRepo 404, triggering deep init');
|
|
775
810
|
}
|
|
776
811
|
await backend.init();
|
|
777
|
-
return await resolveRepo(
|
|
812
|
+
return await resolveRepo(repoName, true, req);
|
|
778
813
|
}
|
|
779
814
|
return found;
|
|
780
815
|
};
|
|
@@ -826,6 +861,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
826
861
|
res.json(repos.map((r) => ({
|
|
827
862
|
name: r.name,
|
|
828
863
|
path: r.path,
|
|
864
|
+
repoPath: r.path,
|
|
829
865
|
indexedAt: r.indexedAt,
|
|
830
866
|
lastCommit: r.lastCommit,
|
|
831
867
|
stats: r.stats,
|
|
@@ -836,7 +872,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
836
872
|
}
|
|
837
873
|
});
|
|
838
874
|
// Get repo info
|
|
839
|
-
|
|
875
|
+
// Rate-limited (CodeQL js/missing-rate-limiting): resolveRepo canonicalizes
|
|
876
|
+
// the attacker-supplied ?repo= param (realpathSync probe for absolute /
|
|
877
|
+
// Windows-shaped claims). Default 60 rpm/IP — web callers hit this route
|
|
878
|
+
// only on connect/switch, never in a polling loop.
|
|
879
|
+
app.get('/api/repo', createRouteLimiter(), async (req, res) => {
|
|
840
880
|
try {
|
|
841
881
|
const entry = await resolveRepo(requestedRepo(req), false, req);
|
|
842
882
|
if (!entry) {
|
|
@@ -906,7 +946,10 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
906
946
|
catch {
|
|
907
947
|
/* repo name not eligible for a clone dir (local repo) */
|
|
908
948
|
}
|
|
909
|
-
|
|
949
|
+
// Only remove the clone dir when it is *this* entry's path — a local
|
|
950
|
+
// repo registered under the same name would otherwise take a cloned
|
|
951
|
+
// sibling's checkout down with it (see cloneDirBelongsToEntry).
|
|
952
|
+
if (cloneDir && cloneDirBelongsToEntry(cloneDir, entry.path)) {
|
|
910
953
|
try {
|
|
911
954
|
const stat = await fs.stat(cloneDir);
|
|
912
955
|
if (stat.isDirectory()) {
|
|
@@ -1140,8 +1183,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1140
1183
|
}, { readOnly: true });
|
|
1141
1184
|
const response = { results: results.searchResults ?? results };
|
|
1142
1185
|
if (results.ftsAvailable === false) {
|
|
1143
|
-
response.warning =
|
|
1144
|
-
'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --repair-fts (or gitnexus analyze --force) to rebuild indexes.';
|
|
1186
|
+
response.warning = ftsDegradedWarning();
|
|
1145
1187
|
}
|
|
1146
1188
|
res.json(response);
|
|
1147
1189
|
}
|
|
@@ -52,7 +52,7 @@ import { fileURLToPath } from 'url';
|
|
|
52
52
|
// the main thread (the #1983 OOM). Because the two stores share this version,
|
|
53
53
|
// any future change to the `ParsedFile` serialization shape MUST bump
|
|
54
54
|
// SCHEMA_BUMP so both invalidate in lockstep.
|
|
55
|
-
const SCHEMA_BUMP =
|
|
55
|
+
const SCHEMA_BUMP = 12; // #2391 follow-up: extractPythonModuleConstants changed what it EMITS for the same source (binding mutual-exclusivity clears stale imports; RHS refs are snapshotted; `$imp$N` aliases). `moduleConstants` is cached verbatim, so a warm shard built pre-fix would replay stale/WRONG folds and the correctness fixes would silently no-op on upgrade — bump to force re-extraction. (11 = #2391: ExtractedDecoratorRoute gained `routePathExpr`/`routePathOperands` + ParseWorkerResult gained per-file `moduleConstants`. 10 = PR #2200: Property nodes gained `rawDeclaredType` + `annotations` for Spring DI)
|
|
56
56
|
const GITNEXUS_PKG_VERSION = (() => {
|
|
57
57
|
try {
|
|
58
58
|
// package.json sits at gitnexus/package.json — two levels up from
|
|
@@ -54,6 +54,18 @@ export declare const canonicalizePath: (p: string) => string;
|
|
|
54
54
|
* lookups/dedup/finalize checks all share so they answer identically.
|
|
55
55
|
*/
|
|
56
56
|
export declare const registryPathEquals: (a: string, b: string) => boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Does the clone dir derived from an entry's *name* actually belong to that
|
|
59
|
+
* entry? Registry names are not unique across storage locations: a cloned
|
|
60
|
+
* repo under `~/.gitnexus/repos/<name>` and a local repo registered under the
|
|
61
|
+
* same name share a `getCloneDir(entry.name)` result. The server's delete
|
|
62
|
+
* handler must therefore never remove the clone dir based on the name alone —
|
|
63
|
+
* only when the entry's own `path` resolves to that dir (mirroring its step-2b
|
|
64
|
+
* rule that cleanup is driven off `entry.path`, so a same-named sibling's
|
|
65
|
+
* clone is never removed). Both sides are canonicalised so symlinked or
|
|
66
|
+
* differently-spelled forms of the same dir still match.
|
|
67
|
+
*/
|
|
68
|
+
export declare const cloneDirBelongsToEntry: (cloneDir: string, entryPath: string) => boolean;
|
|
57
69
|
export interface RepoMeta {
|
|
58
70
|
repoPath: string;
|
|
59
71
|
lastCommit: string;
|
|
@@ -74,6 +86,33 @@ export interface RepoMeta {
|
|
|
74
86
|
processes?: number;
|
|
75
87
|
embeddings?: number;
|
|
76
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Capability stamps for what THIS analyze run actually produced (mirrors
|
|
91
|
+
* the meta literal in run-analyze.ts — typed here so the stamp site is
|
|
92
|
+
* compile-checked; tri-review 4669518496 P1/U3: `vectorSearch.status`
|
|
93
|
+
* must never claim 'vector-index' unless the run verified or recreated
|
|
94
|
+
* the HNSW index). Forensic today — no programmatic readers (`doctor`
|
|
95
|
+
* prints platform-derived capabilities, query routing never consults
|
|
96
|
+
* meta). The status unions mirror `CapabilityStatus` /
|
|
97
|
+
* `SemanticSearchMode` in core/platform/capabilities.ts; inlined to keep
|
|
98
|
+
* storage/ free of a core/ type dependency.
|
|
99
|
+
*/
|
|
100
|
+
capabilities?: {
|
|
101
|
+
graph: {
|
|
102
|
+
provider: string;
|
|
103
|
+
status: 'available' | 'degraded' | 'unavailable';
|
|
104
|
+
};
|
|
105
|
+
fts: {
|
|
106
|
+
provider: string;
|
|
107
|
+
status: 'available' | 'degraded' | 'unavailable';
|
|
108
|
+
};
|
|
109
|
+
vectorSearch: {
|
|
110
|
+
provider: string;
|
|
111
|
+
status: 'vector-index' | 'exact-scan' | 'unavailable';
|
|
112
|
+
exactScanLimit: number;
|
|
113
|
+
reason?: string;
|
|
114
|
+
};
|
|
115
|
+
};
|
|
77
116
|
/**
|
|
78
117
|
* Bumped whenever incremental-indexing invariants change in an
|
|
79
118
|
* incompatible way (delete-and-rewrite logic, subgraph extraction,
|
|
@@ -110,9 +149,29 @@ export interface RepoMeta {
|
|
|
110
149
|
incrementalInProgress?: {
|
|
111
150
|
/** When the run started (epoch ms). */
|
|
112
151
|
startedAt: number;
|
|
152
|
+
/** Last dirty-flag refresh (epoch ms). */
|
|
153
|
+
updatedAt?: number;
|
|
113
154
|
/** Number of files in the writable set, for diagnostic logs.
|
|
114
155
|
* `0` on the full-rebuild path (no incremental write set exists). */
|
|
115
156
|
toWriteCount: number;
|
|
157
|
+
/** Last completed writeback phase before the process stopped. */
|
|
158
|
+
phase?: string;
|
|
159
|
+
/** Directly changed/added files before importer expansion. */
|
|
160
|
+
directWriteCount?: number;
|
|
161
|
+
/** Extra files pulled into the writable set by importer BFS. */
|
|
162
|
+
importerExpansion?: number;
|
|
163
|
+
/** Files in the effective write set after graph-boundary expansion. */
|
|
164
|
+
effectiveWriteCount?: number;
|
|
165
|
+
/** Files whose persisted rows were scheduled for deletion. */
|
|
166
|
+
deleteCount?: number;
|
|
167
|
+
/** Added-file shadow seeds included in importer BFS. */
|
|
168
|
+
shadowSeedCount?: number;
|
|
169
|
+
/** Importer-BFS chunks dropped by failed IMPORTS queries (#2410 +
|
|
170
|
+
* tri-review 4669518496 P2-5). Stamped only when > 0: a dropped chunk
|
|
171
|
+
* means the importer expansion silently shrank, so a crash's
|
|
172
|
+
* diagnostics must show whether the write set was already
|
|
173
|
+
* under-expanded when the run died. */
|
|
174
|
+
droppedImporterChunks?: number;
|
|
116
175
|
};
|
|
117
176
|
/**
|
|
118
177
|
* Name of the git branch this index represents (#2106). Absent for the
|
|
@@ -242,8 +301,13 @@ export interface RepoMeta {
|
|
|
242
301
|
* URL-only id). The incremental writeback preserves unchanged-file rows, so a
|
|
243
302
|
* top-up against a pre-v5 index would strand old url-keyed Route nodes alongside
|
|
244
303
|
* new composite-keyed ones — force a full re-analyze instead.
|
|
304
|
+
* v6: line-number storage flipped to uniform 0-based for the last 1-based
|
|
305
|
+
* GraphNode emitters — COBOL/JCL/markdown/scope (#2377/#2379/#2380). Incremental
|
|
306
|
+
* writeback preserves unchanged-file rows, so a top-up against a pre-v6 index
|
|
307
|
+
* would MIX old 1-based rows with new 0-based ones — and the 1-based MCP display
|
|
308
|
+
* would render the stale rows one line too high — so force a full re-analyze.
|
|
245
309
|
*/
|
|
246
|
-
export declare const INCREMENTAL_SCHEMA_VERSION =
|
|
310
|
+
export declare const INCREMENTAL_SCHEMA_VERSION = 6;
|
|
247
311
|
export interface IndexedRepo {
|
|
248
312
|
repoPath: string;
|
|
249
313
|
storagePath: string;
|
|
@@ -71,6 +71,18 @@ export const canonicalizePath = (p) => {
|
|
|
71
71
|
* lookups/dedup/finalize checks all share so they answer identically.
|
|
72
72
|
*/
|
|
73
73
|
export const registryPathEquals = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
74
|
+
/**
|
|
75
|
+
* Does the clone dir derived from an entry's *name* actually belong to that
|
|
76
|
+
* entry? Registry names are not unique across storage locations: a cloned
|
|
77
|
+
* repo under `~/.gitnexus/repos/<name>` and a local repo registered under the
|
|
78
|
+
* same name share a `getCloneDir(entry.name)` result. The server's delete
|
|
79
|
+
* handler must therefore never remove the clone dir based on the name alone —
|
|
80
|
+
* only when the entry's own `path` resolves to that dir (mirroring its step-2b
|
|
81
|
+
* rule that cleanup is driven off `entry.path`, so a same-named sibling's
|
|
82
|
+
* clone is never removed). Both sides are canonicalised so symlinked or
|
|
83
|
+
* differently-spelled forms of the same dir still match.
|
|
84
|
+
*/
|
|
85
|
+
export const cloneDirBelongsToEntry = (cloneDir, entryPath) => registryPathEquals(canonicalizePath(cloneDir), canonicalizePath(entryPath));
|
|
74
86
|
/**
|
|
75
87
|
* Bumped whenever incremental-indexing invariants change incompatibly.
|
|
76
88
|
* v2: `BasicBlock.callees` column added (statement-precise inter-procedural
|
|
@@ -90,8 +102,13 @@ export const registryPathEquals = (a, b) => process.platform === 'win32' ? a.toL
|
|
|
90
102
|
* URL-only id). The incremental writeback preserves unchanged-file rows, so a
|
|
91
103
|
* top-up against a pre-v5 index would strand old url-keyed Route nodes alongside
|
|
92
104
|
* new composite-keyed ones — force a full re-analyze instead.
|
|
105
|
+
* v6: line-number storage flipped to uniform 0-based for the last 1-based
|
|
106
|
+
* GraphNode emitters — COBOL/JCL/markdown/scope (#2377/#2379/#2380). Incremental
|
|
107
|
+
* writeback preserves unchanged-file rows, so a top-up against a pre-v6 index
|
|
108
|
+
* would MIX old 1-based rows with new 0-based ones — and the 1-based MCP display
|
|
109
|
+
* would render the stale rows one line too high — so force a full re-analyze.
|
|
93
110
|
*/
|
|
94
|
-
export const INCREMENTAL_SCHEMA_VERSION =
|
|
111
|
+
export const INCREMENTAL_SCHEMA_VERSION = 6;
|
|
95
112
|
const GITNEXUS_DIR = '.gitnexus';
|
|
96
113
|
const GITNEXUS_EXCLUDE_ENTRY = `${GITNEXUS_DIR}/`;
|
|
97
114
|
export const INDEX_METADATA_FILE = 'gitnexus.json';
|
|
@@ -391,6 +391,49 @@ function buildAfterToolContext(input) {
|
|
|
391
391
|
return parts.length > 0 ? parts.join('\n\n') : null;
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Fallback augmentation for the #2396 path: when a GitNexus process holds the
|
|
396
|
+
* lbug DB write lock the CLI `augment` can't run, so point the agent at the MCP
|
|
397
|
+
* `query` tool instead. Phrased conditionally ("if the MCP tools are live") so it
|
|
398
|
+
* stays truthful on every owner path — a confirmed MCP owner, a `serve` owner, or
|
|
399
|
+
* a fail-closed probe where no server is actually confirmed. `pattern` is embedded
|
|
400
|
+
* verbatim; the caller (writeAdditionalContext) JSON-escapes it structurally.
|
|
401
|
+
*/
|
|
402
|
+
function buildMcpQueryHint(pattern) {
|
|
403
|
+
return (
|
|
404
|
+
`[GitNexus] Local augment is unavailable (the graph DB is held by another ` +
|
|
405
|
+
`GitNexus process). If the GitNexus MCP tools are live in this session, call ` +
|
|
406
|
+
`the GitNexus \`query\` MCP tool (e.g. mcp__gitnexus__query) with ` +
|
|
407
|
+
`search_query "${pattern}".`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* #2396 throttle: emit the MCP-query hint at most once per repo per window, so an
|
|
413
|
+
* owner-locked session isn't nudged on every search. Window (ms) via
|
|
414
|
+
* GITNEXUS_MCP_HINT_THROTTLE_MS (default 10min; 0/invalid disables). Best-effort —
|
|
415
|
+
* any fs error falls back to emitting.
|
|
416
|
+
* ponytail: per-repo mtime marker, shared across concurrent sessions on the same
|
|
417
|
+
* repo; add per-session dedup only if that sharing becomes a problem.
|
|
418
|
+
*/
|
|
419
|
+
function shouldEmitMcpHint(gitNexusDir) {
|
|
420
|
+
const raw = process.env.GITNEXUS_MCP_HINT_THROTTLE_MS;
|
|
421
|
+
const windowMs = raw === undefined || raw === '' ? 600000 : Number(raw);
|
|
422
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) return true;
|
|
423
|
+
const marker = path.join(gitNexusDir, '.mcp-hint-shown');
|
|
424
|
+
try {
|
|
425
|
+
if (Date.now() - fs.statSync(marker).mtimeMs < windowMs) return false;
|
|
426
|
+
} catch {
|
|
427
|
+
/* marker missing/unreadable → emit */
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
fs.writeFileSync(marker, '');
|
|
431
|
+
} catch {
|
|
432
|
+
/* best-effort; still emit */
|
|
433
|
+
}
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
|
|
394
437
|
function runAugment(gitNexusDir, cwd, pattern) {
|
|
395
438
|
// Acquire the per-repo slot BEFORE the DB-owner probe (#2163): the probe
|
|
396
439
|
// itself spawns lsof/ps, so it must be bounded by the same ≤3-per-repo cap
|
|
@@ -410,12 +453,16 @@ function runAugment(gitNexusDir, cwd, pattern) {
|
|
|
410
453
|
}
|
|
411
454
|
try {
|
|
412
455
|
if (hasGitNexusServerOwner(gitNexusDir)) {
|
|
413
|
-
//
|
|
414
|
-
//
|
|
456
|
+
// #2396: the MCP server holds the DB write lock, so a competing CLI
|
|
457
|
+
// `augment` would only contend on it (LadybugDB is single-writer). The
|
|
458
|
+
// session has the GitNexus MCP tools live — route the augmentation to the
|
|
459
|
+
// agent via additionalContext instead of dropping it. Mirror the skip
|
|
460
|
+
// reason to stderr only under GITNEXUS_DEBUG (strict-runner contract,
|
|
461
|
+
// #1913); the hint itself rides the sanctioned additionalContext channel.
|
|
415
462
|
if (isDebugEnabled()) {
|
|
416
463
|
process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
|
|
417
464
|
}
|
|
418
|
-
return '';
|
|
465
|
+
return shouldEmitMcpHint(gitNexusDir) ? buildMcpQueryHint(pattern) : '';
|
|
419
466
|
}
|
|
420
467
|
const cliPath = resolveCliPath();
|
|
421
468
|
const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
|
|
@@ -349,6 +349,49 @@ function runGitNexusCli(cliPath, args, cwd, timeout) {
|
|
|
349
349
|
});
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Fallback augmentation for the #2396 path: when a GitNexus process holds the
|
|
354
|
+
* lbug DB write lock the CLI `augment` can't run, so point the agent at the MCP
|
|
355
|
+
* `query` tool instead. Phrased conditionally ("if the MCP tools are live") so it
|
|
356
|
+
* stays truthful on every owner path — a confirmed MCP owner, a `serve` owner, or
|
|
357
|
+
* a fail-closed probe where no server is actually confirmed. `pattern` is embedded
|
|
358
|
+
* verbatim; the caller (sendHookResponse) JSON-escapes it structurally.
|
|
359
|
+
*/
|
|
360
|
+
function buildMcpQueryHint(pattern) {
|
|
361
|
+
return (
|
|
362
|
+
`[GitNexus] Local augment is unavailable (the graph DB is held by another ` +
|
|
363
|
+
`GitNexus process). If the GitNexus MCP tools are live in this session, call ` +
|
|
364
|
+
`the GitNexus \`query\` MCP tool (e.g. mcp__gitnexus__query) with ` +
|
|
365
|
+
`search_query "${pattern}".`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* #2396 throttle: emit the MCP-query hint at most once per repo per window, so an
|
|
371
|
+
* owner-locked session isn't nudged on every search. Window (ms) via
|
|
372
|
+
* GITNEXUS_MCP_HINT_THROTTLE_MS (default 10min; 0/invalid disables). Best-effort —
|
|
373
|
+
* any fs error falls back to emitting.
|
|
374
|
+
* ponytail: per-repo mtime marker, shared across concurrent sessions on the same
|
|
375
|
+
* repo; add per-session dedup only if that sharing becomes a problem.
|
|
376
|
+
*/
|
|
377
|
+
function shouldEmitMcpHint(gitNexusDir) {
|
|
378
|
+
const raw = process.env.GITNEXUS_MCP_HINT_THROTTLE_MS;
|
|
379
|
+
const windowMs = raw === undefined || raw === '' ? 600000 : Number(raw);
|
|
380
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) return true;
|
|
381
|
+
const marker = path.join(gitNexusDir, '.mcp-hint-shown');
|
|
382
|
+
try {
|
|
383
|
+
if (Date.now() - fs.statSync(marker).mtimeMs < windowMs) return false;
|
|
384
|
+
} catch {
|
|
385
|
+
/* marker missing/unreadable → emit */
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
fs.writeFileSync(marker, '');
|
|
389
|
+
} catch {
|
|
390
|
+
/* best-effort; still emit */
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
352
395
|
/**
|
|
353
396
|
* PreToolUse handler — augment searches with graph context.
|
|
354
397
|
*/
|
|
@@ -385,18 +428,25 @@ function handlePreToolUse(input) {
|
|
|
385
428
|
let result = '';
|
|
386
429
|
try {
|
|
387
430
|
if (hasGitNexusServerOwner(gitNexusDir)) {
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
431
|
+
// #2396: the MCP server holds the DB write lock, so a competing CLI
|
|
432
|
+
// `augment` would only contend on it (LadybugDB is single-writer). But the
|
|
433
|
+
// session that triggered this hook has the GitNexus MCP tools live — route
|
|
434
|
+
// the augmentation to the agent via additionalContext instead of silently
|
|
435
|
+
// doing nothing. Mirror the skip reason to stderr only under GITNEXUS_DEBUG
|
|
436
|
+
// (strict-runner contract, #1913); the hint itself rides the sanctioned
|
|
437
|
+
// additionalContext stdout channel the successful augment already uses.
|
|
391
438
|
if (isDebugEnabled()) {
|
|
392
439
|
process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
|
|
393
440
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
441
|
+
if (shouldEmitMcpHint(gitNexusDir)) {
|
|
442
|
+
result = buildMcpQueryHint(pattern);
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
const cliPath = resolveCliPath();
|
|
446
|
+
const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
|
|
447
|
+
if (!child.error && child.status === 0) {
|
|
448
|
+
result = extractAugmentContext(child.stderr || '');
|
|
449
|
+
}
|
|
400
450
|
}
|
|
401
451
|
} catch {
|
|
402
452
|
/* graceful failure */
|