gitnexus 1.6.6-rc.2 → 1.6.6-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/dist/_shared/index.d.ts +1 -1
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/scope-resolution/reference-site.d.ts +8 -0
- package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/registries/context.d.ts +18 -1
- package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/registries/context.js.map +1 -1
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +4 -2
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/registries/lookup-core.js +8 -23
- package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -1
- package/dist/cli/analyze.js +65 -3
- package/dist/cli/eval-server.d.ts +14 -3
- package/dist/cli/eval-server.js +79 -9
- package/dist/cli/index.js +3 -1
- package/dist/cli/serve.js +6 -1
- package/dist/cli/setup.js +6 -4
- package/dist/cli/wiki.d.ts +1 -0
- package/dist/cli/wiki.js +32 -8
- package/dist/core/ingestion/languages/cpp/captures.js +165 -9
- package/dist/core/ingestion/languages/cpp/constraint-filter.js +102 -32
- package/dist/core/ingestion/languages/cpp/type-classifier.d.ts +9 -9
- package/dist/core/ingestion/languages/cpp/type-classifier.js +12 -8
- package/dist/core/ingestion/languages/java.js +35 -0
- package/dist/core/ingestion/languages/javascript/arity.d.ts +11 -0
- package/dist/core/ingestion/languages/javascript/arity.js +11 -0
- package/dist/core/ingestion/languages/javascript/captures.d.ts +30 -0
- package/dist/core/ingestion/languages/javascript/captures.js +655 -0
- package/dist/core/ingestion/languages/javascript/import-target.d.ts +29 -0
- package/dist/core/ingestion/languages/javascript/import-target.js +53 -0
- package/dist/core/ingestion/languages/javascript/index.d.ts +48 -0
- package/dist/core/ingestion/languages/javascript/index.js +48 -0
- package/dist/core/ingestion/languages/javascript/interpret.d.ts +29 -0
- package/dist/core/ingestion/languages/javascript/interpret.js +41 -0
- package/dist/core/ingestion/languages/javascript/merge-bindings.d.ts +16 -0
- package/dist/core/ingestion/languages/javascript/merge-bindings.js +18 -0
- package/dist/core/ingestion/languages/javascript/query.d.ts +52 -0
- package/dist/core/ingestion/languages/javascript/query.js +413 -0
- package/dist/core/ingestion/languages/javascript/scope-resolver.d.ts +37 -0
- package/dist/core/ingestion/languages/javascript/scope-resolver.js +73 -0
- package/dist/core/ingestion/languages/javascript/simple-hooks.d.ts +32 -0
- package/dist/core/ingestion/languages/javascript/simple-hooks.js +37 -0
- package/dist/core/ingestion/languages/typescript/simple-hooks.d.ts +10 -0
- package/dist/core/ingestion/languages/typescript/simple-hooks.js +4 -1
- package/dist/core/ingestion/languages/typescript.js +15 -0
- package/dist/core/ingestion/model/field-registry.d.ts +17 -3
- package/dist/core/ingestion/model/field-registry.js +19 -4
- package/dist/core/ingestion/model/owned-members-lookup.d.ts +19 -0
- package/dist/core/ingestion/model/owned-members-lookup.js +43 -0
- package/dist/core/ingestion/model/type-registry.d.ts +9 -0
- package/dist/core/ingestion/model/type-registry.js +18 -0
- package/dist/core/ingestion/registry-primary-flag.js +1 -0
- package/dist/core/ingestion/resolve-references.d.ts +3 -1
- package/dist/core/ingestion/resolve-references.js +5 -1
- package/dist/core/ingestion/scope-extractor.js +4 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +1 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +37 -7
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +2 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +8 -1
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +2 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +4 -2
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +48 -9
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +2 -0
- package/dist/core/lbug/lbug-adapter.d.ts +3 -1
- package/dist/core/lbug/lbug-adapter.js +33 -19
- package/dist/core/lbug/lbug-config.d.ts +1 -1
- package/dist/core/lbug/lbug-config.js +1 -1
- package/dist/core/lbug/pool-adapter.d.ts +19 -7
- package/dist/core/lbug/pool-adapter.js +70 -44
- package/dist/core/lbug/query-params.d.ts +1 -0
- package/dist/core/lbug/query-params.js +21 -0
- package/dist/core/search/bm25-index.js +4 -6
- package/dist/core/wiki/generator.d.ts +13 -0
- package/dist/core/wiki/generator.js +45 -4
- package/dist/core/wiki/llm-client.d.ts +1 -1
- package/dist/core/wiki/llm-client.js +23 -6
- package/dist/mcp/local/local-backend.d.ts +18 -3
- package/dist/mcp/local/local-backend.js +127 -11
- package/dist/mcp/tools.js +10 -0
- package/dist/server/api.d.ts +3 -0
- package/dist/server/api.js +58 -30
- package/package.json +3 -3
- package/web/assets/agent-SmDhIVj9.js +608 -0
- package/web/assets/{architectureDiagram-UL44E2DR-DvIkEGkp.js → architectureDiagram-UL44E2DR-ChS6kfgC.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-CqJ0oo1x.js → chunk-LCXTWHL2-CikfiTEP.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-D0M6vdq9.js → chunk-RG4AUYOV-D0pH0sP0.js} +1 -1
- package/web/assets/{classDiagram-KGZ6W3CR-BB-saqC1.js → classDiagram-KGZ6W3CR-VJjLBrQ3.js} +1 -1
- package/web/assets/{classDiagram-v2-72OJOZXJ-Bp2bAlS5.js → classDiagram-v2-72OJOZXJ-CL_v399n.js} +1 -1
- package/web/assets/{diagram-3NCE3AQN-BUxnDPOY.js → diagram-3NCE3AQN-DTTuOnDj.js} +1 -1
- package/web/assets/{diagram-GF46GFSD-BJTyqqvI.js → diagram-GF46GFSD-Di3e_png.js} +1 -1
- package/web/assets/{diagram-QXG6HAR7-DilMHamP.js → diagram-QXG6HAR7-CVQY1hhk.js} +1 -1
- package/web/assets/{diagram-WEQXMOUZ-8KUrarg1.js → diagram-WEQXMOUZ-BoxS7CN3.js} +1 -1
- package/web/assets/{erDiagram-L5TCEMPS-Doj9mSiW.js → erDiagram-L5TCEMPS-CLTuHusW.js} +1 -1
- package/web/assets/{flowDiagram-H6V6AXG4-BOSR-i3p.js → flowDiagram-H6V6AXG4-CQfVb2C4.js} +1 -1
- package/web/assets/{index-j4-NuleT.js → index-BuC2I_Cj.js} +5 -5
- package/web/assets/{infoDiagram-3YFTVSEB-Ce_TvtaV.js → infoDiagram-3YFTVSEB-BOk6LlXV.js} +1 -1
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-4MXIDCSF.js → ishikawaDiagram-BNXS4ZKH-C0sASGB6.js} +1 -1
- package/web/assets/{kanban-definition-75IXJCU3-yYdfm8Kk.js → kanban-definition-75IXJCU3-DT9Lizpz.js} +1 -1
- package/web/assets/{mindmap-definition-2TDM6QVE-DFNEKM77.js → mindmap-definition-2TDM6QVE-BEYaB7hb.js} +1 -1
- package/web/assets/{pieDiagram-CU6KROY3-iZAqpvhd.js → pieDiagram-CU6KROY3-DCgHNkyY.js} +1 -1
- package/web/assets/{requirementDiagram-JXO7QTGE-CtDijhnf.js → requirementDiagram-JXO7QTGE-D6vDU9M2.js} +1 -1
- package/web/assets/{sequenceDiagram-VS2MUI6T-BdjvyCHM.js → sequenceDiagram-VS2MUI6T-CJrGCTXy.js} +1 -1
- package/web/assets/{stateDiagram-7D4R322I-e5odN9fG.js → stateDiagram-7D4R322I-BvxQnQyL.js} +1 -1
- package/web/assets/{stateDiagram-v2-36443NZ5-vPsvjR2D.js → stateDiagram-v2-36443NZ5-D8ziO9W2.js} +1 -1
- package/web/assets/{timeline-definition-O6YCAMPW-DPAksNQp.js → timeline-definition-O6YCAMPW-B45j2q3m.js} +1 -1
- package/web/assets/{vennDiagram-MWXL3ELB-Dtlfdt7J.js → vennDiagram-MWXL3ELB-CRkZRDhi.js} +1 -1
- package/web/assets/{wardleyDiagram-CUQ6CDDI-B23uRMUC.js → wardleyDiagram-CUQ6CDDI-BekUytZJ.js} +1 -1
- package/web/assets/{xychartDiagram-N2JHSOCM-DxwHeksW.js → xychartDiagram-N2JHSOCM-BeMqwSgr.js} +1 -1
- package/web/index.html +1 -1
- package/web/assets/agent-D2kLt6Dl.js +0 -597
|
@@ -8,7 +8,7 @@ import lbug from '@ladybugdb/core';
|
|
|
8
8
|
import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, STALE_HASH_SENTINEL, } from './schema.js';
|
|
9
9
|
import { streamAllCSVsToDisk } from './csv-generator.js';
|
|
10
10
|
import { extensionManager } from './extension-loader.js';
|
|
11
|
-
import { closeLbugConnection, isDbBusyError, isOpenRetryExhausted, openLbugConnection, waitForWindowsHandleRelease, } from './lbug-config.js';
|
|
11
|
+
import { closeLbugConnection, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
|
|
12
12
|
import { isVectorExtensionSupportedByPlatform } from '../platform/capabilities.js';
|
|
13
13
|
import { logger } from '../logger.js';
|
|
14
14
|
/**
|
|
@@ -112,6 +112,7 @@ export const splitRelCsvByLabelPair = async (csvPath, csvDir, validTables, getNo
|
|
|
112
112
|
let db = null;
|
|
113
113
|
let conn = null;
|
|
114
114
|
let currentDbPath = null;
|
|
115
|
+
let currentDbReadOnly = false;
|
|
115
116
|
let ftsLoaded = false;
|
|
116
117
|
let vectorExtensionLoaded = false;
|
|
117
118
|
/**
|
|
@@ -367,12 +368,13 @@ export const initLbug = async (dbPath) => {
|
|
|
367
368
|
* database is busy (e.g. `gitnexus analyze` holds the write lock).
|
|
368
369
|
* Each retry waits DB_LOCK_RETRY_DELAY_MS * attempt milliseconds.
|
|
369
370
|
*/
|
|
370
|
-
export const withLbugDb = async (dbPath, operation) => {
|
|
371
|
+
export const withLbugDb = async (dbPath, operation, options = {}) => {
|
|
371
372
|
let lastError;
|
|
373
|
+
const readOnly = options.readOnly === true;
|
|
372
374
|
for (let attempt = 1; attempt <= DB_LOCK_RETRY_ATTEMPTS; attempt++) {
|
|
373
375
|
try {
|
|
374
376
|
return await runWithSessionLock(async () => {
|
|
375
|
-
await ensureLbugInitialized(dbPath);
|
|
377
|
+
await ensureLbugInitialized(dbPath, readOnly);
|
|
376
378
|
return operation();
|
|
377
379
|
});
|
|
378
380
|
}
|
|
@@ -402,14 +404,14 @@ export const withLbugDb = async (dbPath, operation) => {
|
|
|
402
404
|
// but TypeScript needs an explicit throw to satisfy the return type.
|
|
403
405
|
throw lastError;
|
|
404
406
|
};
|
|
405
|
-
const ensureLbugInitialized = async (dbPath) => {
|
|
406
|
-
if (conn && currentDbPath === dbPath) {
|
|
407
|
+
const ensureLbugInitialized = async (dbPath, readOnly = false) => {
|
|
408
|
+
if (conn && currentDbPath === dbPath && currentDbReadOnly === readOnly) {
|
|
407
409
|
return { db, conn };
|
|
408
410
|
}
|
|
409
|
-
await doInitLbug(dbPath);
|
|
411
|
+
await doInitLbug(dbPath, readOnly);
|
|
410
412
|
return { db, conn };
|
|
411
413
|
};
|
|
412
|
-
const doInitLbug = async (dbPath) => {
|
|
414
|
+
const doInitLbug = async (dbPath, readOnly = false) => {
|
|
413
415
|
// Different database requested — close the old one first
|
|
414
416
|
if (conn || db) {
|
|
415
417
|
await safeClose();
|
|
@@ -486,9 +488,12 @@ const doInitLbug = async (dbPath) => {
|
|
|
486
488
|
// Ensure parent directory exists
|
|
487
489
|
const parentDir = path.dirname(dbPath);
|
|
488
490
|
await fs.mkdir(parentDir, { recursive: true });
|
|
489
|
-
const opened =
|
|
491
|
+
const opened = readOnly
|
|
492
|
+
? await openLbugConnection(lbug, dbPath, { readOnly: true })
|
|
493
|
+
: await openLbugConnection(lbug, dbPath);
|
|
490
494
|
db = opened.db;
|
|
491
495
|
conn = opened.conn;
|
|
496
|
+
currentDbReadOnly = readOnly;
|
|
492
497
|
}
|
|
493
498
|
finally {
|
|
494
499
|
await releaseInitLock();
|
|
@@ -508,7 +513,23 @@ const doInitLbug = async (dbPath) => {
|
|
|
508
513
|
// anyway and any genuine cross-process lock contention surfaces
|
|
509
514
|
// on the next operation via withLbugDb's retry. Logging it here
|
|
510
515
|
// would just be noise in CI.
|
|
511
|
-
|
|
516
|
+
//
|
|
517
|
+
// WAL corruption: the first DDL write after DB open triggers WAL
|
|
518
|
+
// replay — if the WAL file was left in a corrupt state by an
|
|
519
|
+
// interrupted previous run, the native engine throws here. Rather
|
|
520
|
+
// than logging a WARN and continuing in a broken state, close the
|
|
521
|
+
// DB cleanly and surface an actionable error so the caller (serve,
|
|
522
|
+
// MCP, analyze) can exit with a clear recovery message.
|
|
523
|
+
if (isWalCorruptionError(err)) {
|
|
524
|
+
await safeClose();
|
|
525
|
+
currentDbPath = null;
|
|
526
|
+
ftsLoaded = false;
|
|
527
|
+
vectorExtensionLoaded = false;
|
|
528
|
+
ensuredFTSIndexes.clear();
|
|
529
|
+
throw new Error(`LadybugDB WAL corruption detected at ${dbPath}. ${WAL_RECOVERY_SUGGESTION}\n` +
|
|
530
|
+
` Original error: ${msg.slice(0, 200)}`);
|
|
531
|
+
}
|
|
532
|
+
if (!msg.includes('already exists') && !isDbBusyError(err) && !isReadOnlyDbError(err)) {
|
|
512
533
|
logger.warn(`⚠️ Schema creation warning: ${msg.slice(0, 120)}`);
|
|
513
534
|
}
|
|
514
535
|
}
|
|
@@ -915,11 +936,7 @@ export const batchInsertNodesToLbug = async (nodes, dbPath) => {
|
|
|
915
936
|
return { inserted, failed };
|
|
916
937
|
};
|
|
917
938
|
export const executeQuery = async (cypher) => {
|
|
918
|
-
|
|
919
|
-
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
920
|
-
}
|
|
921
|
-
const queryResult = await conn.query(cypher);
|
|
922
|
-
return await readQueryRows(queryResult);
|
|
939
|
+
return await executePrepared(cypher, {});
|
|
923
940
|
};
|
|
924
941
|
export const streamQuery = async (cypher, onRow) => {
|
|
925
942
|
if (!conn) {
|
|
@@ -1501,17 +1518,14 @@ export const queryFTS = async (tableName, indexName, query, limit = 20, conjunct
|
|
|
1501
1518
|
if (!conn) {
|
|
1502
1519
|
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
1503
1520
|
}
|
|
1504
|
-
// Escape backslashes and single quotes to prevent Cypher injection
|
|
1505
|
-
const escapedQuery = query.replace(/\\/g, '\\\\').replace(/'/g, "''");
|
|
1506
1521
|
const cypher = `
|
|
1507
|
-
CALL QUERY_FTS_INDEX('${tableName}', '${indexName}',
|
|
1522
|
+
CALL QUERY_FTS_INDEX('${tableName}', '${indexName}', $query, conjunctive := ${conjunctive})
|
|
1508
1523
|
RETURN node, score
|
|
1509
1524
|
ORDER BY score DESC
|
|
1510
1525
|
LIMIT ${limit}
|
|
1511
1526
|
`;
|
|
1512
1527
|
try {
|
|
1513
|
-
const
|
|
1514
|
-
const rows = await readQueryRows(queryResult);
|
|
1528
|
+
const rows = await executePrepared(cypher, { query });
|
|
1515
1529
|
return rows.map((row) => {
|
|
1516
1530
|
const node = row.node || row[0] || {};
|
|
1517
1531
|
const score = row.score ?? row[1] ?? 0;
|
|
@@ -32,7 +32,7 @@ import type lbug from '@ladybugdb/core';
|
|
|
32
32
|
* integer; anything invalid falls back to the default.
|
|
33
33
|
*/
|
|
34
34
|
export declare const LBUG_MAX_DB_SIZE: number;
|
|
35
|
-
export declare const WAL_RECOVERY_SUGGESTION = "WAL corruption detected. Run `gitnexus analyze` to rebuild the index.";
|
|
35
|
+
export declare const WAL_RECOVERY_SUGGESTION = "WAL corruption detected. Run `gitnexus analyze --force` to rebuild the index.";
|
|
36
36
|
export declare function isWalCorruptionError(err: unknown): boolean;
|
|
37
37
|
type LbugModule = typeof lbug;
|
|
38
38
|
export interface LbugDatabaseOptions {
|
|
@@ -44,7 +44,7 @@ export const LBUG_MAX_DB_SIZE = (() => {
|
|
|
44
44
|
})();
|
|
45
45
|
/** Matches WAL corruption errors from the LadybugDB engine. */
|
|
46
46
|
const WAL_CORRUPTION_RE = /corrupt(ed)?\s+wal|invalid\s+wal\s+record|wal.*corrupt|checksum.*wal/i;
|
|
47
|
-
export const WAL_RECOVERY_SUGGESTION = 'WAL corruption detected. Run `gitnexus analyze` to rebuild the index.';
|
|
47
|
+
export const WAL_RECOVERY_SUGGESTION = 'WAL corruption detected. Run `gitnexus analyze --force` to rebuild the index.';
|
|
48
48
|
export function isWalCorruptionError(err) {
|
|
49
49
|
if (!err)
|
|
50
50
|
return false;
|
|
@@ -15,6 +15,25 @@
|
|
|
15
15
|
* from the same Database is the officially supported concurrency pattern.
|
|
16
16
|
*/
|
|
17
17
|
import lbug from '@ladybugdb/core';
|
|
18
|
+
/**
|
|
19
|
+
* Probe whether a Windows FTS extension binary is locally installed under
|
|
20
|
+
* ~/.lbdb/extension/<any-version>/win_amd64/fts/. Returns true on the first
|
|
21
|
+
* version dir whose libfts.lbug_extension exists on disk; false if the
|
|
22
|
+
* extension root is missing or contains no FTS binary.
|
|
23
|
+
*
|
|
24
|
+
* Gates the Windows skip-FTS-load guard below so we only skip the load
|
|
25
|
+
* when no extension binary is present. When at least one binary exists,
|
|
26
|
+
* loadFTSExtension is called with policy: 'load-only' — LadybugDB resolves
|
|
27
|
+
* LOAD EXTENSION fts to its version-specific path internally, and the
|
|
28
|
+
* ExtensionManager's tryLoad try/catch handles version-mismatch errors
|
|
29
|
+
* cleanly without ever attempting dlopen of a stale binary. The install
|
|
30
|
+
* path that the #1199/#1217 SIGSEGV documented is never exercised at
|
|
31
|
+
* query time.
|
|
32
|
+
*
|
|
33
|
+
* Exported so unit tests can exercise the probe directly against a
|
|
34
|
+
* temp-dir plus spied `os.homedir()` — see lbug-pool-win-fts-probe.test.ts.
|
|
35
|
+
*/
|
|
36
|
+
export declare function hasLocalWinFtsExtension(): Promise<boolean>;
|
|
18
37
|
/**
|
|
19
38
|
* Listeners notified when a pool entry is torn down (LRU eviction, idle
|
|
20
39
|
* timeout, explicit close). Used by upper layers (e.g. the BM25 search
|
|
@@ -81,10 +100,3 @@ export declare const closeLbug: (repoId?: string) => Promise<void>;
|
|
|
81
100
|
* Check if a specific repo's pool is active
|
|
82
101
|
*/
|
|
83
102
|
export declare const isLbugReady: (repoId: string) => boolean;
|
|
84
|
-
/** Regex to detect write operations in user-supplied Cypher queries.
|
|
85
|
-
* Note: CALL is NOT blocked — it's used for read-only FTS (CALL QUERY_FTS_INDEX)
|
|
86
|
-
* and vector search (CALL QUERY_VECTOR_INDEX). The database is opened in
|
|
87
|
-
* read-only mode as defense-in-depth against write procedures. */
|
|
88
|
-
export declare const CYPHER_WRITE_RE: RegExp;
|
|
89
|
-
/** Check if a Cypher query contains write operations */
|
|
90
|
-
export declare function isWriteQuery(query: string): boolean;
|
|
@@ -15,9 +15,48 @@
|
|
|
15
15
|
* from the same Database is the officially supported concurrency pattern.
|
|
16
16
|
*/
|
|
17
17
|
import fs from 'fs/promises';
|
|
18
|
+
import os from 'os';
|
|
19
|
+
import path from 'path';
|
|
18
20
|
import lbug from '@ladybugdb/core';
|
|
19
|
-
import { loadFTSExtension } from './lbug-adapter.js';
|
|
20
|
-
import { createLbugDatabase, isWalCorruptionError } from './lbug-config.js';
|
|
21
|
+
import { isReadOnlyDbError, loadFTSExtension } from './lbug-adapter.js';
|
|
22
|
+
import { createLbugDatabase, isWalCorruptionError, WAL_RECOVERY_SUGGESTION, } from './lbug-config.js';
|
|
23
|
+
/**
|
|
24
|
+
* Probe whether a Windows FTS extension binary is locally installed under
|
|
25
|
+
* ~/.lbdb/extension/<any-version>/win_amd64/fts/. Returns true on the first
|
|
26
|
+
* version dir whose libfts.lbug_extension exists on disk; false if the
|
|
27
|
+
* extension root is missing or contains no FTS binary.
|
|
28
|
+
*
|
|
29
|
+
* Gates the Windows skip-FTS-load guard below so we only skip the load
|
|
30
|
+
* when no extension binary is present. When at least one binary exists,
|
|
31
|
+
* loadFTSExtension is called with policy: 'load-only' — LadybugDB resolves
|
|
32
|
+
* LOAD EXTENSION fts to its version-specific path internally, and the
|
|
33
|
+
* ExtensionManager's tryLoad try/catch handles version-mismatch errors
|
|
34
|
+
* cleanly without ever attempting dlopen of a stale binary. The install
|
|
35
|
+
* path that the #1199/#1217 SIGSEGV documented is never exercised at
|
|
36
|
+
* query time.
|
|
37
|
+
*
|
|
38
|
+
* Exported so unit tests can exercise the probe directly against a
|
|
39
|
+
* temp-dir plus spied `os.homedir()` — see lbug-pool-win-fts-probe.test.ts.
|
|
40
|
+
*/
|
|
41
|
+
export async function hasLocalWinFtsExtension() {
|
|
42
|
+
try {
|
|
43
|
+
const extRoot = path.join(os.homedir(), '.lbdb', 'extension');
|
|
44
|
+
const versions = await fs.readdir(extRoot);
|
|
45
|
+
for (const v of versions) {
|
|
46
|
+
try {
|
|
47
|
+
await fs.stat(path.join(extRoot, v, 'win_amd64', 'fts', 'libfts.lbug_extension'));
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* missing for this version, keep looking */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* no .lbdb/extension dir */
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
21
60
|
const pool = new Map();
|
|
22
61
|
const poolCloseListeners = new Set();
|
|
23
62
|
/**
|
|
@@ -310,8 +349,7 @@ async function doInitLbug(repoId, dbPath) {
|
|
|
310
349
|
break;
|
|
311
350
|
}
|
|
312
351
|
catch (retryErr) {
|
|
313
|
-
throw new Error(`LadybugDB WAL corruption detected for ${repoId}. ` +
|
|
314
|
-
`Run \`gitnexus analyze\` to rebuild the index. ` +
|
|
352
|
+
throw new Error(`LadybugDB WAL corruption detected for ${repoId}. ${WAL_RECOVERY_SUGGESTION} ` +
|
|
315
353
|
`(${retryErr instanceof Error ? retryErr.message : String(retryErr)})`);
|
|
316
354
|
}
|
|
317
355
|
}
|
|
@@ -348,14 +386,24 @@ async function doInitLbug(repoId, dbPath) {
|
|
|
348
386
|
// install; analyze owns extension installation. If LOAD fails, search
|
|
349
387
|
// features degrade gracefully and the user-facing query path proceeds.
|
|
350
388
|
if (!shared.ftsLoaded) {
|
|
351
|
-
// Windows guard: LOAD EXTENSION fts crashes with SIGSEGV on Windows
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
389
|
+
// Windows guard: LOAD EXTENSION fts crashes with SIGSEGV on Windows during
|
|
390
|
+
// *install* — the @ladybugdb/core out-of-process installer hits an unhandled
|
|
391
|
+
// error path that signals SIGSEGV instead of throwing (see #1199, #1217).
|
|
392
|
+
// The previous unconditional skip was over-broad: it also disabled FTS on
|
|
393
|
+
// hosts where the binary was already on disk and only needed LOAD, leaving
|
|
394
|
+
// BM25 silently degraded with no error path (see #1690).
|
|
395
|
+
//
|
|
396
|
+
// Probe ~/.lbdb/extension/*/win_amd64/fts/ first. If any binary is on disk
|
|
397
|
+
// we run loadFTSExtension(..., 'load-only'); the install path is never
|
|
398
|
+
// exercised, and LadybugDB's version-specific resolution + ExtensionManager
|
|
399
|
+
// try/catch handle stale/zero-byte siblings cleanly (verified empirically
|
|
400
|
+
// on Win10 + Node 22.19 + gitnexus 1.6.5 + @ladybugdb/core 0.16.1). With
|
|
401
|
+
// no binary at all, we fall back to the upstream skip so install-time
|
|
402
|
+
// SIGSEGV continues to be avoided.
|
|
357
403
|
if (process.platform === 'win32') {
|
|
358
|
-
shared.ftsLoaded =
|
|
404
|
+
shared.ftsLoaded = (await hasLocalWinFtsExtension())
|
|
405
|
+
? await loadFTSExtension(available[0], { policy: 'load-only' })
|
|
406
|
+
: true;
|
|
359
407
|
}
|
|
360
408
|
else {
|
|
361
409
|
shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
|
|
@@ -415,10 +463,12 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
|
|
|
415
463
|
// Load FTS extension if not already loaded on this Database.
|
|
416
464
|
// policy: 'load-only' — same contract as initLbug above; the read pool
|
|
417
465
|
// must not block on a network install during query execution.
|
|
418
|
-
// Windows guard: same
|
|
466
|
+
// Windows guard: same probe-then-load policy as doInitLbug above.
|
|
419
467
|
if (!shared.ftsLoaded) {
|
|
420
468
|
if (process.platform === 'win32') {
|
|
421
|
-
shared.ftsLoaded =
|
|
469
|
+
shared.ftsLoaded = (await hasLocalWinFtsExtension())
|
|
470
|
+
? await loadFTSExtension(available[0], { policy: 'load-only' })
|
|
471
|
+
: true;
|
|
422
472
|
}
|
|
423
473
|
else {
|
|
424
474
|
shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
|
|
@@ -506,28 +556,7 @@ function withTimeout(promise, ms, label) {
|
|
|
506
556
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
507
557
|
}
|
|
508
558
|
export const executeQuery = async (repoId, cypher) => {
|
|
509
|
-
|
|
510
|
-
if (!entry) {
|
|
511
|
-
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
512
|
-
}
|
|
513
|
-
if (isWriteQuery(cypher)) {
|
|
514
|
-
throw new Error('Write operations are not allowed. The pool adapter is read-only.');
|
|
515
|
-
}
|
|
516
|
-
entry.lastUsed = Date.now();
|
|
517
|
-
const conn = await checkout(entry);
|
|
518
|
-
silenceStdout();
|
|
519
|
-
activeQueryCount++;
|
|
520
|
-
try {
|
|
521
|
-
const queryResult = await withTimeout(conn.query(cypher), QUERY_TIMEOUT_MS, 'Query');
|
|
522
|
-
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
523
|
-
const rows = await result.getAll();
|
|
524
|
-
return rows;
|
|
525
|
-
}
|
|
526
|
-
finally {
|
|
527
|
-
activeQueryCount--;
|
|
528
|
-
restoreStdout();
|
|
529
|
-
checkin(entry, conn);
|
|
530
|
-
}
|
|
559
|
+
return await executeParameterized(repoId, cypher, {});
|
|
531
560
|
};
|
|
532
561
|
/**
|
|
533
562
|
* Execute a parameterized query on a specific repo's connection pool.
|
|
@@ -553,6 +582,12 @@ export const executeParameterized = async (repoId, cypher, params) => {
|
|
|
553
582
|
const rows = await result.getAll();
|
|
554
583
|
return rows;
|
|
555
584
|
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
if (isReadOnlyDbError(err)) {
|
|
587
|
+
throw new Error('Write operations are not allowed. The pool adapter is read-only.');
|
|
588
|
+
}
|
|
589
|
+
throw err;
|
|
590
|
+
}
|
|
556
591
|
finally {
|
|
557
592
|
activeQueryCount--;
|
|
558
593
|
restoreStdout();
|
|
@@ -581,12 +616,3 @@ export const closeLbug = async (repoId) => {
|
|
|
581
616
|
* Check if a specific repo's pool is active
|
|
582
617
|
*/
|
|
583
618
|
export const isLbugReady = (repoId) => pool.has(repoId);
|
|
584
|
-
/** Regex to detect write operations in user-supplied Cypher queries.
|
|
585
|
-
* Note: CALL is NOT blocked — it's used for read-only FTS (CALL QUERY_FTS_INDEX)
|
|
586
|
-
* and vector search (CALL QUERY_VECTOR_INDEX). The database is opened in
|
|
587
|
-
* read-only mode as defense-in-depth against write procedures. */
|
|
588
|
-
export const CYPHER_WRITE_RE = /(?<!:)\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH|FOREACH|INSTALL|LOAD)\b/i;
|
|
589
|
-
/** Check if a Cypher query contains write operations */
|
|
590
|
-
export function isWriteQuery(query) {
|
|
591
|
-
return CYPHER_WRITE_RE.test(query);
|
|
592
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isValidQueryParams: (value: unknown) => value is Record<string, unknown>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return true only for plain-object payloads that can be safely used as
|
|
3
|
+
* named parameter maps in prepared Cypher execution.
|
|
4
|
+
*
|
|
5
|
+
* Validation criteria:
|
|
6
|
+
* - must be a JavaScript object (`typeof value === 'object'`)
|
|
7
|
+
* - must not be `null`
|
|
8
|
+
* - must not be an array
|
|
9
|
+
* - must have a plain-object prototype
|
|
10
|
+
* - values must be scalar bindable values (string | number | boolean | null)
|
|
11
|
+
*
|
|
12
|
+
* Rationale: prepared-statement params are key/value maps; rejecting null/array
|
|
13
|
+
* and non-plain objects keeps binding behavior predictable and avoids passing
|
|
14
|
+
* complex host objects to Ladybug parameter binding.
|
|
15
|
+
*/
|
|
16
|
+
const isBindableScalar = (value) => value === null || ['string', 'number', 'boolean'].includes(typeof value);
|
|
17
|
+
export const isValidQueryParams = (value) => value !== null &&
|
|
18
|
+
typeof value === 'object' &&
|
|
19
|
+
!Array.isArray(value) &&
|
|
20
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) &&
|
|
21
|
+
Object.values(value).every(isBindableScalar);
|
|
@@ -12,16 +12,14 @@ import { FTS_INDEXES } from './fts-schema.js';
|
|
|
12
12
|
* caller can distinguish "zero matches" from "index missing".
|
|
13
13
|
*/
|
|
14
14
|
async function queryFTSViaExecutor(executor, tableName, indexName, query, limit) {
|
|
15
|
-
// Escape single quotes and backslashes to prevent Cypher injection
|
|
16
|
-
const escapedQuery = query.replace(/\\/g, '\\\\').replace(/'/g, "''");
|
|
17
15
|
const cypher = `
|
|
18
|
-
CALL QUERY_FTS_INDEX('${tableName}', '${indexName}',
|
|
16
|
+
CALL QUERY_FTS_INDEX('${tableName}', '${indexName}', $query, conjunctive := false)
|
|
19
17
|
RETURN node, score
|
|
20
18
|
ORDER BY score DESC
|
|
21
19
|
LIMIT ${limit}
|
|
22
20
|
`;
|
|
23
21
|
try {
|
|
24
|
-
const rows = await executor(cypher);
|
|
22
|
+
const rows = await executor(cypher, { query });
|
|
25
23
|
return rows.map((row) => {
|
|
26
24
|
const node = row.node || row[0] || {};
|
|
27
25
|
const score = row.score ?? row[1] ?? 0;
|
|
@@ -55,8 +53,8 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
55
53
|
// IMPORTANT: FTS queries run sequentially to avoid connection contention.
|
|
56
54
|
// The MCP pool supports multiple connections, but FTS is best run serially.
|
|
57
55
|
const poolMod = await import('../lbug/pool-adapter.js');
|
|
58
|
-
const {
|
|
59
|
-
const executor = (cypher) =>
|
|
56
|
+
const { executeParameterized } = poolMod;
|
|
57
|
+
const executor = (cypher, params) => executeParameterized(repoId, cypher, params);
|
|
60
58
|
for (const { table, indexName } of FTS_INDEXES) {
|
|
61
59
|
const result = await queryFTSViaExecutor(executor, table, indexName, query, limit);
|
|
62
60
|
if (result !== null) {
|
|
@@ -16,11 +16,14 @@ export interface WikiOptions {
|
|
|
16
16
|
concurrency?: number;
|
|
17
17
|
/** If true, stop after building module tree for user review */
|
|
18
18
|
reviewOnly?: boolean;
|
|
19
|
+
/** Output language for generated documentation (e.g. 'english', 'chinese', 'spanish') */
|
|
20
|
+
lang?: string;
|
|
19
21
|
}
|
|
20
22
|
export interface WikiMeta {
|
|
21
23
|
fromCommit: string;
|
|
22
24
|
generatedAt: string;
|
|
23
25
|
model: string;
|
|
26
|
+
lang: string;
|
|
24
27
|
moduleFiles: Record<string, string[]>;
|
|
25
28
|
moduleTree: ModuleTreeNode[];
|
|
26
29
|
}
|
|
@@ -62,6 +65,16 @@ export declare class WikiGenerator {
|
|
|
62
65
|
* Also touches the DB connection periodically to prevent idle timeout.
|
|
63
66
|
*/
|
|
64
67
|
private streamOpts;
|
|
68
|
+
/**
|
|
69
|
+
* Return the effective lang string: strip control characters, trim, cap at 50 chars,
|
|
70
|
+
* then validate against a character allowlist. Returns '' if the value is absent or invalid.
|
|
71
|
+
* Used for both prompt construction and meta storage/comparison so they are always in sync.
|
|
72
|
+
*/
|
|
73
|
+
private effectiveLang;
|
|
74
|
+
/**
|
|
75
|
+
* Append an output-language instruction to a system prompt when --lang is set.
|
|
76
|
+
*/
|
|
77
|
+
private buildSystemPrompt;
|
|
65
78
|
/**
|
|
66
79
|
* Route LLM call to the appropriate provider (OpenAI-compatible or Cursor CLI).
|
|
67
80
|
*/
|
|
@@ -89,6 +89,27 @@ export class WikiGenerator {
|
|
|
89
89
|
},
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Return the effective lang string: strip control characters, trim, cap at 50 chars,
|
|
94
|
+
* then validate against a character allowlist. Returns '' if the value is absent or invalid.
|
|
95
|
+
* Used for both prompt construction and meta storage/comparison so they are always in sync.
|
|
96
|
+
*/
|
|
97
|
+
effectiveLang() {
|
|
98
|
+
const lang = (this.options.lang ?? '')
|
|
99
|
+
.replace(/[\x00-\x1F\x7F]/g, '')
|
|
100
|
+
.trim()
|
|
101
|
+
.slice(0, 50);
|
|
102
|
+
return /^[a-zA-Z -]+$/.test(lang) ? lang : '';
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Append an output-language instruction to a system prompt when --lang is set.
|
|
106
|
+
*/
|
|
107
|
+
buildSystemPrompt(base) {
|
|
108
|
+
const lang = this.effectiveLang();
|
|
109
|
+
if (!lang)
|
|
110
|
+
return base;
|
|
111
|
+
return `${base}\n\nIMPORTANT: Write ALL documentation content in ${lang}. This includes prose, code comments in examples, and diagram labels. Note: page titles (H1 headings) are generated separately and will remain in English.`;
|
|
112
|
+
}
|
|
92
113
|
/**
|
|
93
114
|
* Route LLM call to the appropriate provider (OpenAI-compatible or Cursor CLI).
|
|
94
115
|
*/
|
|
@@ -112,6 +133,13 @@ export class WikiGenerator {
|
|
|
112
133
|
const forceMode = this.options.force;
|
|
113
134
|
// Up-to-date check (skip if --force)
|
|
114
135
|
if (!forceMode && existingMeta && existingMeta.fromCommit === currentCommit) {
|
|
136
|
+
const currentLang = this.effectiveLang();
|
|
137
|
+
const metaLang = existingMeta.lang ?? '';
|
|
138
|
+
if (currentLang !== metaLang) {
|
|
139
|
+
const prevDisplay = metaLang || 'english (default)';
|
|
140
|
+
const nextDisplay = currentLang || 'english (default)';
|
|
141
|
+
throw new Error(`Wiki was generated in ${prevDisplay}; use --force to regenerate in ${nextDisplay}.`);
|
|
142
|
+
}
|
|
115
143
|
// Still regenerate the HTML viewer in case it's missing
|
|
116
144
|
await this.ensureHTMLViewer();
|
|
117
145
|
return { pagesGenerated: 0, mode: 'up-to-date', failedModules: [] };
|
|
@@ -139,6 +167,13 @@ export class WikiGenerator {
|
|
|
139
167
|
let result;
|
|
140
168
|
try {
|
|
141
169
|
if (!forceMode && existingMeta && existingMeta.fromCommit) {
|
|
170
|
+
const currentLang = this.effectiveLang();
|
|
171
|
+
const metaLang = existingMeta.lang ?? '';
|
|
172
|
+
if (currentLang !== metaLang) {
|
|
173
|
+
const prevDisplay = metaLang || 'english (default)';
|
|
174
|
+
const nextDisplay = currentLang || 'english (default)';
|
|
175
|
+
throw new Error(`Wiki was generated in ${prevDisplay}; use --force to regenerate in ${nextDisplay}.`);
|
|
176
|
+
}
|
|
142
177
|
result = await this.incrementalUpdate(existingMeta, currentCommit);
|
|
143
178
|
}
|
|
144
179
|
else {
|
|
@@ -257,6 +292,7 @@ export class WikiGenerator {
|
|
|
257
292
|
fromCommit: currentCommit,
|
|
258
293
|
generatedAt: new Date().toISOString(),
|
|
259
294
|
model: this.llmConfig.model,
|
|
295
|
+
lang: this.effectiveLang(),
|
|
260
296
|
moduleFiles,
|
|
261
297
|
moduleTree,
|
|
262
298
|
});
|
|
@@ -298,6 +334,9 @@ export class WikiGenerator {
|
|
|
298
334
|
FILE_LIST: fileList,
|
|
299
335
|
DIRECTORY_TREE: dirTree,
|
|
300
336
|
});
|
|
337
|
+
// Grouping is a structured-data phase (JSON output), not documentation.
|
|
338
|
+
// Do NOT apply buildSystemPrompt here — a language instruction would risk
|
|
339
|
+
// translating module-name keys, breaking slug stability and JSON parsing.
|
|
301
340
|
const response = await this.invokeLLM(prompt, GROUPING_SYSTEM_PROMPT, this.streamOpts('Grouping files', 15, 13));
|
|
302
341
|
const grouping = this.parseGroupingResponse(response.content, files);
|
|
303
342
|
// Convert to tree nodes
|
|
@@ -444,8 +483,8 @@ export class WikiGenerator {
|
|
|
444
483
|
INCOMING_CALLS: formatCallEdges(interCalls.incoming),
|
|
445
484
|
PROCESSES: formatProcesses(processes),
|
|
446
485
|
});
|
|
447
|
-
const response = await this.invokeLLM(prompt, MODULE_SYSTEM_PROMPT, this.streamOpts(node.name));
|
|
448
|
-
//
|
|
486
|
+
const response = await this.invokeLLM(prompt, this.buildSystemPrompt(MODULE_SYSTEM_PROMPT), this.streamOpts(node.name));
|
|
487
|
+
// H1 uses the English module name (stable slug source); body is LLM-translated.
|
|
449
488
|
const pageContent = sanitizeMermaidMarkdown(`# ${node.name}\n\n${response.content}`);
|
|
450
489
|
await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
|
|
451
490
|
}
|
|
@@ -480,7 +519,7 @@ export class WikiGenerator {
|
|
|
480
519
|
CROSS_MODULE_CALLS: formatCallEdges(crossCalls),
|
|
481
520
|
CROSS_PROCESSES: formatProcesses(processes),
|
|
482
521
|
});
|
|
483
|
-
const response = await this.invokeLLM(prompt, PARENT_SYSTEM_PROMPT, this.streamOpts(node.name));
|
|
522
|
+
const response = await this.invokeLLM(prompt, this.buildSystemPrompt(PARENT_SYSTEM_PROMPT), this.streamOpts(node.name));
|
|
484
523
|
const pageContent = sanitizeMermaidMarkdown(`# ${node.name}\n\n${response.content}`);
|
|
485
524
|
await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
|
|
486
525
|
}
|
|
@@ -516,7 +555,7 @@ export class WikiGenerator {
|
|
|
516
555
|
MODULE_EDGES: edgesText,
|
|
517
556
|
TOP_PROCESSES: formatProcesses(topProcesses),
|
|
518
557
|
});
|
|
519
|
-
const response = await this.invokeLLM(prompt, OVERVIEW_SYSTEM_PROMPT, this.streamOpts('Generating overview', 88));
|
|
558
|
+
const response = await this.invokeLLM(prompt, this.buildSystemPrompt(OVERVIEW_SYSTEM_PROMPT), this.streamOpts('Generating overview', 88));
|
|
520
559
|
const pageContent = sanitizeMermaidMarkdown(`# ${path.basename(this.repoPath)} — Wiki\n\n${response.content}`);
|
|
521
560
|
await fs.writeFile(path.join(this.wikiDir, 'overview.md'), pageContent, 'utf-8');
|
|
522
561
|
}
|
|
@@ -538,6 +577,7 @@ export class WikiGenerator {
|
|
|
538
577
|
...existingMeta,
|
|
539
578
|
fromCommit: currentCommit,
|
|
540
579
|
generatedAt: new Date().toISOString(),
|
|
580
|
+
lang: this.effectiveLang(),
|
|
541
581
|
});
|
|
542
582
|
return { pagesGenerated: 0, mode: 'incremental', failedModules: [] };
|
|
543
583
|
}
|
|
@@ -627,6 +667,7 @@ export class WikiGenerator {
|
|
|
627
667
|
fromCommit: currentCommit,
|
|
628
668
|
generatedAt: new Date().toISOString(),
|
|
629
669
|
model: this.llmConfig.model,
|
|
670
|
+
lang: this.effectiveLang(),
|
|
630
671
|
});
|
|
631
672
|
this.onProgress('done', 100, 'Incremental update complete');
|
|
632
673
|
return { pagesGenerated, mode: 'incremental', failedModules: [...this.failedModules] };
|
|
@@ -19,7 +19,7 @@ export interface LLMConfig {
|
|
|
19
19
|
apiVersion?: string;
|
|
20
20
|
/** When true, strips sampling params and uses max_completion_tokens instead of max_tokens */
|
|
21
21
|
isReasoningModel?: boolean;
|
|
22
|
-
/** Per-attempt fetch timeout in ms
|
|
22
|
+
/** Per-attempt fetch timeout in ms. Omit to disable request timeouts. */
|
|
23
23
|
requestTimeoutMs?: number;
|
|
24
24
|
/** Max fetch attempts before giving up (default: 3). */
|
|
25
25
|
maxAttempts?: number;
|
|
@@ -38,6 +38,19 @@ export async function resolveLLMConfig(overrides) {
|
|
|
38
38
|
export function estimateTokens(text) {
|
|
39
39
|
return Math.ceil(text.length / 4);
|
|
40
40
|
}
|
|
41
|
+
function formatTimeoutDuration(timeoutMs) {
|
|
42
|
+
if (timeoutMs >= 1000 && timeoutMs % 1000 === 0) {
|
|
43
|
+
return `${timeoutMs / 1000}s`;
|
|
44
|
+
}
|
|
45
|
+
return `${timeoutMs}ms`;
|
|
46
|
+
}
|
|
47
|
+
function isTimeoutLikeError(err) {
|
|
48
|
+
if (!(err instanceof Error))
|
|
49
|
+
return false;
|
|
50
|
+
if (err.name === 'TimeoutError' || err.name === 'AbortError')
|
|
51
|
+
return true;
|
|
52
|
+
return /time(d)?\s*out|timeout/i.test(err.message);
|
|
53
|
+
}
|
|
41
54
|
/**
|
|
42
55
|
* Validate that a base URL supplied for LLM API calls is a safe HTTP/HTTPS
|
|
43
56
|
* endpoint (CWE-918 / CodeQL js/http-to-file-access).
|
|
@@ -166,12 +179,12 @@ export async function callLLM(prompt, config, systemPrompt, options) {
|
|
|
166
179
|
...authHeaders,
|
|
167
180
|
},
|
|
168
181
|
body: JSON.stringify(body),
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
// Request timeout is opt-in for wiki generation. Large local
|
|
183
|
+
// model runs can legitimately take well over a minute, so the
|
|
184
|
+
// default runtime path must not impose a hidden 60s ceiling.
|
|
185
|
+
signal: config.requestTimeoutMs !== undefined
|
|
186
|
+
? AbortSignal.timeout(config.requestTimeoutMs)
|
|
187
|
+
: undefined,
|
|
175
188
|
}, {
|
|
176
189
|
breakerKey: `wiki-llm-${new URL(url).host}`,
|
|
177
190
|
retry: { maxAttempts: config.maxAttempts ?? 3, baseDelayMs: 2_000, capDelayMs: 30_000 },
|
|
@@ -185,6 +198,10 @@ export async function callLLM(prompt, config, systemPrompt, options) {
|
|
|
185
198
|
const errorText = await err.response.text().catch(() => 'unknown error');
|
|
186
199
|
throw new Error(`LLM API error (${err.response.status} after retries): ${errorText.slice(0, 500)}`);
|
|
187
200
|
}
|
|
201
|
+
if (config.requestTimeoutMs !== undefined && isTimeoutLikeError(err)) {
|
|
202
|
+
throw new Error(`LLM request timed out after ${formatTimeoutDuration(config.requestTimeoutMs)}. ` +
|
|
203
|
+
'Increase --timeout or omit it to disable the request timeout.');
|
|
204
|
+
}
|
|
188
205
|
throw err;
|
|
189
206
|
}
|
|
190
207
|
if (!response.ok) {
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
* Supports multiple indexed repositories via a global registry.
|
|
6
6
|
* LadybugDB connections are opened lazily per repo on first query.
|
|
7
7
|
*/
|
|
8
|
-
import { isWriteQuery } from '../../core/lbug/pool-adapter.js';
|
|
9
|
-
export { isWriteQuery };
|
|
10
8
|
import { type RegistryEntry } from '../../storage/repo-manager.js';
|
|
11
9
|
import { GroupService } from '../../core/group/service.js';
|
|
12
10
|
/**
|
|
@@ -59,6 +57,22 @@ interface RepoHandle {
|
|
|
59
57
|
remoteUrl?: string;
|
|
60
58
|
stats?: RegistryEntry['stats'];
|
|
61
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the git diff cwd for detect_changes, auto-detecting linked worktrees.
|
|
62
|
+
*
|
|
63
|
+
* When `launchCwd` is a linked worktree of the same canonical repository as
|
|
64
|
+
* `repoPath` (i.e. `getGitRoot(launchCwd)` differs from `repoPath` but both
|
|
65
|
+
* share the same `getCanonicalRepoRoot`), returns the worktree's git root so
|
|
66
|
+
* that `git diff` sees the correct working directory and index.
|
|
67
|
+
*
|
|
68
|
+
* Returns `repoPath` unchanged in all other cases (non-worktree, git
|
|
69
|
+
* unavailable, unrelated repo).
|
|
70
|
+
*
|
|
71
|
+
* Extracted as a module-level export so tests can pass any `launchCwd` instead
|
|
72
|
+
* of relying on `process.cwd()`, which is fixed to the server launch directory
|
|
73
|
+
* and cannot be changed mid-process.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveWorktreeCwd(repoPath: string, launchCwd: string): string;
|
|
62
76
|
export declare class LocalBackend {
|
|
63
77
|
private repos;
|
|
64
78
|
private contextCache;
|
|
@@ -192,7 +206,7 @@ export declare class LocalBackend {
|
|
|
192
206
|
* Semantic vector search helper
|
|
193
207
|
*/
|
|
194
208
|
private semanticSearch;
|
|
195
|
-
executeCypher(repoName: string, query: string): Promise<any>;
|
|
209
|
+
executeCypher(repoName: string, query: string, params?: Record<string, unknown>): Promise<any>;
|
|
196
210
|
private cypher;
|
|
197
211
|
/**
|
|
198
212
|
* Format raw Cypher result rows as a markdown table for LLM readability.
|
|
@@ -354,3 +368,4 @@ export declare class LocalBackend {
|
|
|
354
368
|
queryProcessDetail(name: string, repoName?: string): Promise<any>;
|
|
355
369
|
disconnect(): Promise<void>;
|
|
356
370
|
}
|
|
371
|
+
export {};
|