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.
Files changed (112) hide show
  1. package/dist/_shared/index.d.ts +1 -1
  2. package/dist/_shared/index.d.ts.map +1 -1
  3. package/dist/_shared/index.js.map +1 -1
  4. package/dist/_shared/scope-resolution/reference-site.d.ts +8 -0
  5. package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -1
  6. package/dist/_shared/scope-resolution/registries/context.d.ts +18 -1
  7. package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -1
  8. package/dist/_shared/scope-resolution/registries/context.js.map +1 -1
  9. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +4 -2
  10. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -1
  11. package/dist/_shared/scope-resolution/registries/lookup-core.js +8 -23
  12. package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -1
  13. package/dist/cli/analyze.js +65 -3
  14. package/dist/cli/eval-server.d.ts +14 -3
  15. package/dist/cli/eval-server.js +79 -9
  16. package/dist/cli/index.js +3 -1
  17. package/dist/cli/serve.js +6 -1
  18. package/dist/cli/setup.js +6 -4
  19. package/dist/cli/wiki.d.ts +1 -0
  20. package/dist/cli/wiki.js +32 -8
  21. package/dist/core/ingestion/languages/cpp/captures.js +165 -9
  22. package/dist/core/ingestion/languages/cpp/constraint-filter.js +102 -32
  23. package/dist/core/ingestion/languages/cpp/type-classifier.d.ts +9 -9
  24. package/dist/core/ingestion/languages/cpp/type-classifier.js +12 -8
  25. package/dist/core/ingestion/languages/java.js +35 -0
  26. package/dist/core/ingestion/languages/javascript/arity.d.ts +11 -0
  27. package/dist/core/ingestion/languages/javascript/arity.js +11 -0
  28. package/dist/core/ingestion/languages/javascript/captures.d.ts +30 -0
  29. package/dist/core/ingestion/languages/javascript/captures.js +655 -0
  30. package/dist/core/ingestion/languages/javascript/import-target.d.ts +29 -0
  31. package/dist/core/ingestion/languages/javascript/import-target.js +53 -0
  32. package/dist/core/ingestion/languages/javascript/index.d.ts +48 -0
  33. package/dist/core/ingestion/languages/javascript/index.js +48 -0
  34. package/dist/core/ingestion/languages/javascript/interpret.d.ts +29 -0
  35. package/dist/core/ingestion/languages/javascript/interpret.js +41 -0
  36. package/dist/core/ingestion/languages/javascript/merge-bindings.d.ts +16 -0
  37. package/dist/core/ingestion/languages/javascript/merge-bindings.js +18 -0
  38. package/dist/core/ingestion/languages/javascript/query.d.ts +52 -0
  39. package/dist/core/ingestion/languages/javascript/query.js +413 -0
  40. package/dist/core/ingestion/languages/javascript/scope-resolver.d.ts +37 -0
  41. package/dist/core/ingestion/languages/javascript/scope-resolver.js +73 -0
  42. package/dist/core/ingestion/languages/javascript/simple-hooks.d.ts +32 -0
  43. package/dist/core/ingestion/languages/javascript/simple-hooks.js +37 -0
  44. package/dist/core/ingestion/languages/typescript/simple-hooks.d.ts +10 -0
  45. package/dist/core/ingestion/languages/typescript/simple-hooks.js +4 -1
  46. package/dist/core/ingestion/languages/typescript.js +15 -0
  47. package/dist/core/ingestion/model/field-registry.d.ts +17 -3
  48. package/dist/core/ingestion/model/field-registry.js +19 -4
  49. package/dist/core/ingestion/model/owned-members-lookup.d.ts +19 -0
  50. package/dist/core/ingestion/model/owned-members-lookup.js +43 -0
  51. package/dist/core/ingestion/model/type-registry.d.ts +9 -0
  52. package/dist/core/ingestion/model/type-registry.js +18 -0
  53. package/dist/core/ingestion/registry-primary-flag.js +1 -0
  54. package/dist/core/ingestion/resolve-references.d.ts +3 -1
  55. package/dist/core/ingestion/resolve-references.js +5 -1
  56. package/dist/core/ingestion/scope-extractor.js +4 -0
  57. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +1 -0
  58. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +37 -7
  59. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +2 -0
  60. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +8 -1
  61. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +2 -0
  62. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +4 -2
  63. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +48 -9
  64. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
  65. package/dist/core/ingestion/scope-resolution/pipeline/run.js +2 -0
  66. package/dist/core/lbug/lbug-adapter.d.ts +3 -1
  67. package/dist/core/lbug/lbug-adapter.js +33 -19
  68. package/dist/core/lbug/lbug-config.d.ts +1 -1
  69. package/dist/core/lbug/lbug-config.js +1 -1
  70. package/dist/core/lbug/pool-adapter.d.ts +19 -7
  71. package/dist/core/lbug/pool-adapter.js +70 -44
  72. package/dist/core/lbug/query-params.d.ts +1 -0
  73. package/dist/core/lbug/query-params.js +21 -0
  74. package/dist/core/search/bm25-index.js +4 -6
  75. package/dist/core/wiki/generator.d.ts +13 -0
  76. package/dist/core/wiki/generator.js +45 -4
  77. package/dist/core/wiki/llm-client.d.ts +1 -1
  78. package/dist/core/wiki/llm-client.js +23 -6
  79. package/dist/mcp/local/local-backend.d.ts +18 -3
  80. package/dist/mcp/local/local-backend.js +127 -11
  81. package/dist/mcp/tools.js +10 -0
  82. package/dist/server/api.d.ts +3 -0
  83. package/dist/server/api.js +58 -30
  84. package/package.json +3 -3
  85. package/web/assets/agent-SmDhIVj9.js +608 -0
  86. package/web/assets/{architectureDiagram-UL44E2DR-DvIkEGkp.js → architectureDiagram-UL44E2DR-ChS6kfgC.js} +1 -1
  87. package/web/assets/{chunk-LCXTWHL2-CqJ0oo1x.js → chunk-LCXTWHL2-CikfiTEP.js} +1 -1
  88. package/web/assets/{chunk-RG4AUYOV-D0M6vdq9.js → chunk-RG4AUYOV-D0pH0sP0.js} +1 -1
  89. package/web/assets/{classDiagram-KGZ6W3CR-BB-saqC1.js → classDiagram-KGZ6W3CR-VJjLBrQ3.js} +1 -1
  90. package/web/assets/{classDiagram-v2-72OJOZXJ-Bp2bAlS5.js → classDiagram-v2-72OJOZXJ-CL_v399n.js} +1 -1
  91. package/web/assets/{diagram-3NCE3AQN-BUxnDPOY.js → diagram-3NCE3AQN-DTTuOnDj.js} +1 -1
  92. package/web/assets/{diagram-GF46GFSD-BJTyqqvI.js → diagram-GF46GFSD-Di3e_png.js} +1 -1
  93. package/web/assets/{diagram-QXG6HAR7-DilMHamP.js → diagram-QXG6HAR7-CVQY1hhk.js} +1 -1
  94. package/web/assets/{diagram-WEQXMOUZ-8KUrarg1.js → diagram-WEQXMOUZ-BoxS7CN3.js} +1 -1
  95. package/web/assets/{erDiagram-L5TCEMPS-Doj9mSiW.js → erDiagram-L5TCEMPS-CLTuHusW.js} +1 -1
  96. package/web/assets/{flowDiagram-H6V6AXG4-BOSR-i3p.js → flowDiagram-H6V6AXG4-CQfVb2C4.js} +1 -1
  97. package/web/assets/{index-j4-NuleT.js → index-BuC2I_Cj.js} +5 -5
  98. package/web/assets/{infoDiagram-3YFTVSEB-Ce_TvtaV.js → infoDiagram-3YFTVSEB-BOk6LlXV.js} +1 -1
  99. package/web/assets/{ishikawaDiagram-BNXS4ZKH-4MXIDCSF.js → ishikawaDiagram-BNXS4ZKH-C0sASGB6.js} +1 -1
  100. package/web/assets/{kanban-definition-75IXJCU3-yYdfm8Kk.js → kanban-definition-75IXJCU3-DT9Lizpz.js} +1 -1
  101. package/web/assets/{mindmap-definition-2TDM6QVE-DFNEKM77.js → mindmap-definition-2TDM6QVE-BEYaB7hb.js} +1 -1
  102. package/web/assets/{pieDiagram-CU6KROY3-iZAqpvhd.js → pieDiagram-CU6KROY3-DCgHNkyY.js} +1 -1
  103. package/web/assets/{requirementDiagram-JXO7QTGE-CtDijhnf.js → requirementDiagram-JXO7QTGE-D6vDU9M2.js} +1 -1
  104. package/web/assets/{sequenceDiagram-VS2MUI6T-BdjvyCHM.js → sequenceDiagram-VS2MUI6T-CJrGCTXy.js} +1 -1
  105. package/web/assets/{stateDiagram-7D4R322I-e5odN9fG.js → stateDiagram-7D4R322I-BvxQnQyL.js} +1 -1
  106. package/web/assets/{stateDiagram-v2-36443NZ5-vPsvjR2D.js → stateDiagram-v2-36443NZ5-D8ziO9W2.js} +1 -1
  107. package/web/assets/{timeline-definition-O6YCAMPW-DPAksNQp.js → timeline-definition-O6YCAMPW-B45j2q3m.js} +1 -1
  108. package/web/assets/{vennDiagram-MWXL3ELB-Dtlfdt7J.js → vennDiagram-MWXL3ELB-CRkZRDhi.js} +1 -1
  109. package/web/assets/{wardleyDiagram-CUQ6CDDI-B23uRMUC.js → wardleyDiagram-CUQ6CDDI-BekUytZJ.js} +1 -1
  110. package/web/assets/{xychartDiagram-N2JHSOCM-DxwHeksW.js → xychartDiagram-N2JHSOCM-BeMqwSgr.js} +1 -1
  111. package/web/index.html +1 -1
  112. 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 = await openLbugConnection(lbug, dbPath);
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
- if (!msg.includes('already exists') && !isDbBusyError(err)) {
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
- if (!conn) {
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}', '${escapedQuery}', conjunctive := ${conjunctive})
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 queryResult = await conn.query(cypher);
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 when
352
- // the FTS extension binary is not installed locally (@ladybugdb/core native
353
- // bug — the extension loader hits an unhandled error path that signals SIGSEGV
354
- // rather than throwing a JS exception, so try/catch cannot protect here).
355
- // Skip the load on Windows; bm25-index.js catches the resulting Kuzu catalog
356
- // errors and returns empty BM25 results gracefully. Graph queries are unaffected.
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 = true;
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 SIGSEGV risk as doInitLbug above — skip on Windows.
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 = true;
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
- const entry = pool.get(repoId);
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}', '${escapedQuery}', conjunctive := false)
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 { executeQuery } = poolMod;
59
- const executor = (cypher) => executeQuery(repoId, 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
- // Write page with front matter
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 (default: 60_000). */
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
- // Per-attempt timeout. Without this each retry can hang
170
- // indefinitely on a frozen TCP connection — the per-call
171
- // signal is the only timeout `resilientFetch` honors;
172
- // `capDelayMs` only bounds the *backoff* between attempts.
173
- // Default 60s; raise via --timeout for slow models or large pages.
174
- signal: AbortSignal.timeout(config.requestTimeoutMs ?? 60_000),
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 {};