gitnexus 1.6.10-rc.13 → 1.6.10-rc.15

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 (33) hide show
  1. package/dist/cli/analyze.js +60 -2
  2. package/dist/cli/clean.js +17 -3
  3. package/dist/cli/cli-message.d.ts +1 -1
  4. package/dist/cli/doctor.d.ts +13 -0
  5. package/dist/cli/doctor.js +35 -0
  6. package/dist/cli/i18n/en.d.ts +5 -4
  7. package/dist/cli/i18n/en.js +5 -4
  8. package/dist/cli/i18n/resources.d.ts +6 -4
  9. package/dist/cli/i18n/zh-CN.d.ts +1 -0
  10. package/dist/cli/i18n/zh-CN.js +5 -4
  11. package/dist/cli/index.js +1 -1
  12. package/dist/core/augmentation/engine.js +4 -3
  13. package/dist/core/embeddings/embedding-pipeline.d.ts +20 -0
  14. package/dist/core/embeddings/embedding-pipeline.js +10 -2
  15. package/dist/core/incremental/escalation-gate.d.ts +38 -0
  16. package/dist/core/incremental/escalation-gate.js +48 -0
  17. package/dist/core/incremental/shadow-candidates.d.ts +3 -3
  18. package/dist/core/incremental/shadow-candidates.js +3 -3
  19. package/dist/core/incremental/subgraph-extract.d.ts +2 -2
  20. package/dist/core/incremental/subgraph-extract.js +2 -2
  21. package/dist/core/lbug/cypher-escape.d.ts +20 -0
  22. package/dist/core/lbug/cypher-escape.js +20 -0
  23. package/dist/core/lbug/lbug-adapter.d.ts +147 -1
  24. package/dist/core/lbug/lbug-adapter.js +384 -55
  25. package/dist/core/lbug/lbug-config.d.ts +28 -0
  26. package/dist/core/lbug/lbug-config.js +114 -4
  27. package/dist/core/lbug/sidecar-recovery.d.ts +140 -0
  28. package/dist/core/lbug/sidecar-recovery.js +327 -9
  29. package/dist/core/run-analyze.js +384 -106
  30. package/dist/core/wiki/graph-queries.js +6 -5
  31. package/dist/storage/repo-manager.d.ts +33 -0
  32. package/package.json +1 -1
  33. package/scripts/cross-platform-tests.ts +13 -0
@@ -12,13 +12,14 @@ import os from 'os';
12
12
  import { spawn } from 'child_process';
13
13
  import v8 from 'v8';
14
14
  import cliProgress from 'cli-progress';
15
- import { isLbugReady } from '../core/lbug/lbug-adapter.js';
15
+ import { isLbugReady, LbugWipeError } from '../core/lbug/lbug-adapter.js';
16
16
  import { boundedCheckpointBeforeExit } from '../core/lbug/shutdown-helpers.js';
17
- import { isLbugCheckpointIoError, isWalCorruptionError, parseWalCheckpointThreshold, WAL_RECOVERY_SUGGESTION, } from '../core/lbug/lbug-config.js';
17
+ import { getOsPageSize, isLbugCheckpointIoError, isLbugPageSizeFrameError, isPageSizeAwareLadybug, isWalCorruptionError, parseWalCheckpointThreshold, WAL_RECOVERY_SUGGESTION, } from '../core/lbug/lbug-config.js';
18
18
  import { getStoragePaths, getGlobalRegistryPath, RegistryNameCollisionError, AnalysisNotFinalizedError, assertAnalysisFinalized, } from '../storage/repo-manager.js';
19
19
  import { getGitRoot, hasGitDir, getDefaultBranch } from '../storage/git.js';
20
20
  import { loadAnalyzeConfig, mergeAnalyzeOptions, resolveDefaultBranch, validateBranchName, GitNexusRcError, } from './analyze-config.js';
21
21
  import { runFullAnalysis } from '../core/run-analyze.js';
22
+ import { getRuntimeFingerprint } from '../core/platform/capabilities.js';
22
23
  import { getMaxFileSizeBannerMessage } from '../core/ingestion/utils/max-file-size.js';
23
24
  import { warnMissingOptionalGrammars, getOptionalGrammarExtensions } from './optional-grammars.js';
24
25
  import { glob } from 'glob';
@@ -1237,6 +1238,63 @@ const analyzeCommandImpl = async (inputPath, cliOptions) => {
1237
1238
  process.exitCode = 1;
1238
1239
  return;
1239
1240
  }
1241
+ // DB-family wipe failure (#2409, tri-review 4669518496 P2-4): the rebuild
1242
+ // could not verify the LadybugDB file family was removed — usually another
1243
+ // process (MCP server, serve worker, antivirus) holding the index open.
1244
+ // Keyed on the error *type* (repo norm from #2385), never message text.
1245
+ // The message itself is fully self-contained (survivor paths + stop-MCP /
1246
+ // AV-exclusion / re-run guidance) because the serve worker forwards only
1247
+ // `err.message` over IPC — this branch just renders it without the
1248
+ // raw-stack fallback below.
1249
+ if (err instanceof LbugWipeError) {
1250
+ cliError(` ${msg.replace(/\n/g, '\n ')}\n`, {
1251
+ recoveryHint: 'lbug-wipe-failed',
1252
+ });
1253
+ process.exitCode = 1;
1254
+ return;
1255
+ }
1256
+ // Buffer-manager frame-release failure on non-4K-page kernels (#1231).
1257
+ // LadybugDB <= 0.17.x assumed 4 KiB OS pages when releasing evicted
1258
+ // frames; Raspberry Pi 5 (16 KiB kernel pages) and other arm64 systems
1259
+ // crash mid-COPY with a raw native message. 0.18.0 detects the page size
1260
+ // at runtime, so the actionable fix depends on which side of that
1261
+ // boundary the installed @ladybugdb/core is.
1262
+ if (isLbugPageSizeFrameError(err)) {
1263
+ const pageSize = getOsPageSize();
1264
+ const ladybug = getRuntimeFingerprint().ladybugdb;
1265
+ const pageLine = pageSize !== undefined && pageSize !== 4096
1266
+ ? ` Detected OS page size: ${pageSize} bytes (non-4K — e.g. Raspberry Pi 5 16K kernel, Asahi Linux).\n`
1267
+ : '';
1268
+ // The upgrade variant must not assert version facts about an unknown
1269
+ // version — mirror the doctor-side wording rule (#2424 review R2).
1270
+ const upgradeIntro = ladybug === undefined
1271
+ ? ` The installed @ladybugdb/core version is unknown — it may predate the\n` +
1272
+ ` runtime OS-page-size detection added in 0.18.0.\n`
1273
+ : ` The installed @ladybugdb/core (${ladybug}) assumes 4 KiB pages in its buffer\n` +
1274
+ ` manager.\n`;
1275
+ const guidance = isPageSizeAwareLadybug(ladybug)
1276
+ ? ` The installed @ladybugdb/core (${ladybug}) already detects the OS page size at runtime,\n` +
1277
+ ` so this configuration was expected to work. Please report it:\n` +
1278
+ ` https://github.com/abhigyanpatwari/GitNexus/issues/1231\n` +
1279
+ ` and include: gitnexus --version, node --version, getconf PAGE_SIZE, uname -a,\n` +
1280
+ ` and the full error message above.\n`
1281
+ : upgradeIntro +
1282
+ ` Upgrade GitNexus to a release that bundles @ladybugdb/core >= 0.18.0\n` +
1283
+ ` (gitnexus >= 1.6.9), which detects the OS page size at runtime:\n` +
1284
+ ` npm install -g gitnexus@latest\n` +
1285
+ ` Last-resort workaround on Raspberry Pi 5: boot the 4 KiB-page kernel\n` +
1286
+ ` (config.txt: kernel=kernel8.img), at the cost of Pi 5 optimizations.\n`;
1287
+ // Embed the raw native text (indented, no stack) so "the full error
1288
+ // message above" is fulfillable — same idiom as the LbugWipeError
1289
+ // branch. The errno suffix and the 0.18.0 guard's frame/granule numbers
1290
+ // are the discriminating triage content (#2424 review P2).
1291
+ cliError(` LadybugDB's buffer manager failed to release frame memory.\n` +
1292
+ ` ${msg.replace(/\n/g, '\n ')}\n` +
1293
+ pageLine +
1294
+ guidance, { recoveryHint: 'lbug-page-size', pageSize, ladybugVersion: ladybug });
1295
+ process.exitCode = 1;
1296
+ return;
1297
+ }
1240
1298
  // Local embedding runtime unsupported on this platform (macOS Intel ships no
1241
1299
  // darwin/x64 ONNX native binding, #1515). The guard threw before importing
1242
1300
  // transformers.js, so this is a clean, actionable GitNexus message. Checked
package/dist/cli/clean.js CHANGED
@@ -8,7 +8,7 @@ import fs from 'fs/promises';
8
8
  import path from 'path';
9
9
  import { logger } from '../core/logger.js';
10
10
  import { findRepo, unregisterRepo, listRegisteredRepos, assertSafeStoragePath, getStoragePaths, removeBranchIndex, UnsafeStoragePathError, } from '../storage/repo-manager.js';
11
- import { cleanQuarantinedMissingShadowWals, inspectLbugSidecars, listQuarantinedMissingShadowWals, } from '../core/lbug/sidecar-recovery.js';
11
+ import { cleanParkedLbugSidecars, inspectLbugSidecars, listParkedLbugSidecars, } from '../core/lbug/sidecar-recovery.js';
12
12
  import { t } from './i18n/index.js';
13
13
  export const cleanCommand = async (options) => {
14
14
  // --branch <name>: remove a single non-primary branch's index (#2106 R7).
@@ -62,7 +62,13 @@ export const cleanCommand = async (options) => {
62
62
  }
63
63
  const lbugPath = path.join(repo.storagePath, 'lbug');
64
64
  const state = await inspectLbugSidecars(lbugPath);
65
- const quarantined = await listQuarantinedMissingShadowWals(lbugPath);
65
+ // Single roster authority (this shipping review, FIX 5): the aggregate
66
+ // covers both parked-sidecar families — the timestamped missing-shadow
67
+ // WAL quarantines AND the fixed-name `.dirty-recovery` parks (`.next`
68
+ // residues included) left by a dirty-flag recovery rebuild (#2409). The
69
+ // previous inline concatenations here were how the `.next` residue
70
+ // stayed invisible to this surface.
71
+ const quarantined = await listParkedLbugSidecars(lbugPath);
66
72
  console.log(t('clean.lbugSidecars.state', { state: state.kind }));
67
73
  if (quarantined.length === 0) {
68
74
  console.log(t('clean.lbugSidecars.none'));
@@ -76,8 +82,16 @@ export const cleanCommand = async (options) => {
76
82
  console.log(`\n${t('common.runForceConfirm')}`);
77
83
  return;
78
84
  }
79
- const deleted = await cleanQuarantinedMissingShadowWals(lbugPath);
85
+ const { deleted, failed } = await cleanParkedLbugSidecars(lbugPath);
80
86
  console.log(t('clean.lbugSidecars.deleted', { count: deleted.length }));
87
+ // A locked parked file no longer crashes the clean mid-command (FIX 5)
88
+ // — the rest were deleted above; report what remains and why.
89
+ if (failed.length > 0) {
90
+ console.log(t('clean.lbugSidecars.failed', { count: failed.length }));
91
+ for (const file of failed) {
92
+ console.log(` - ${file}`);
93
+ }
94
+ }
81
95
  return;
82
96
  }
83
97
  // --all flag: clean all indexed repos
@@ -12,7 +12,7 @@ import { type CliMessageKey, type CliMessageVars } from './i18n/index.js';
12
12
  * Consumers can import this type to narrow log-record `recoveryHint`
13
13
  * fields without restating the literal list.
14
14
  */
15
- export type RecoveryHint = 'wal-corruption' | 'wal-checkpoint-threshold' | 'heap-oom-respawn' | 'native-worker-abort' | 'hf-endpoint-unreachable' | 'http-embedding-endpoint-error' | 'embedding-dims-invalid' | 'local-embedding-unsupported' | 'local-embedding-stack-missing' | 'large-repo' | 'npm-resolution' | 'module-not-found' | 'gitnexusrc-invalid' | 'default-branch-invalid';
15
+ export type RecoveryHint = 'wal-corruption' | 'wal-checkpoint-threshold' | 'lbug-wipe-failed' | 'lbug-page-size' | 'heap-oom-respawn' | 'native-worker-abort' | 'hf-endpoint-unreachable' | 'http-embedding-endpoint-error' | 'embedding-dims-invalid' | 'local-embedding-unsupported' | 'local-embedding-stack-missing' | 'large-repo' | 'npm-resolution' | 'module-not-found' | 'gitnexusrc-invalid' | 'default-branch-invalid';
16
16
  /**
17
17
  * Common shape for the optional structured-field bag passed to
18
18
  * `cliError`/`cliWarn`/`cliInfo`. Typed so the `recoveryHint` slot is
@@ -23,4 +23,17 @@ export declare function localEmbeddingDoctorStatus(opts: {
23
23
  status: string;
24
24
  detail: string | null;
25
25
  };
26
+ /**
27
+ * Page-size lines for the `doctor` Runtime section (#1231). Pure so the
28
+ * warning gate can be unit-tested without running the whole command (the
29
+ * `localEmbeddingDoctorStatus` precedent above) — but takes the probed
30
+ * values as plain params rather than injectable probes, because `undefined`
31
+ * is a *meaningful* pageSize state here (probe unavailable / win32) and
32
+ * would collide with a "not provided → use default" DI convention.
33
+ *
34
+ * Returns 0 lines (page size unknown), 1 line (page size), or 2 lines
35
+ * (page size + non-4K warning when the installed @ladybugdb/core does not
36
+ * detect the OS page size at runtime).
37
+ */
38
+ export declare function pageSizeDoctorLines(pageSize: number | undefined, ladybugVersion: string | undefined): string[];
26
39
  export declare const doctorCommand: () => Promise<void>;
@@ -5,6 +5,7 @@ import { getLocalEmbeddingRuntimeBlocker, localEmbeddingPrefixUnloadableMessage,
5
5
  import { isPrefixRuntimeLoadable, resolveEmbeddingRuntime, } from '../core/embeddings/runtime-install.js';
6
6
  import { cudaRedirectDoctorStatus } from '../core/embeddings/onnxruntime-node-resolver.js';
7
7
  import { checkLbugNative, probeFtsExtensionLoad } from '../core/lbug/native-check.js';
8
+ import { getOsPageSize, isPageSizeAwareLadybug } from '../core/lbug/lbug-config.js';
8
9
  import { diagnoseExtensionLoad } from '../core/lbug/extension-load-error.js';
9
10
  import { getExtensionInstallPolicy } from '../core/lbug/extension-loader.js';
10
11
  import { t } from './i18n/index.js';
@@ -87,6 +88,33 @@ export function localEmbeddingDoctorStatus(opts) {
87
88
  }
88
89
  return { status: '✓ local embeddings supported', detail: null };
89
90
  }
91
+ /**
92
+ * Page-size lines for the `doctor` Runtime section (#1231). Pure so the
93
+ * warning gate can be unit-tested without running the whole command (the
94
+ * `localEmbeddingDoctorStatus` precedent above) — but takes the probed
95
+ * values as plain params rather than injectable probes, because `undefined`
96
+ * is a *meaningful* pageSize state here (probe unavailable / win32) and
97
+ * would collide with a "not provided → use default" DI convention.
98
+ *
99
+ * Returns 0 lines (page size unknown), 1 line (page size), or 2 lines
100
+ * (page size + non-4K warning when the installed @ladybugdb/core does not
101
+ * detect the OS page size at runtime).
102
+ */
103
+ export function pageSizeDoctorLines(pageSize, ladybugVersion) {
104
+ if (pageSize === undefined)
105
+ return [];
106
+ const lines = [` ${padDisplayEnd('page size', 10)}${pageSize}`];
107
+ if (pageSize > 4096 && !isPageSizeAwareLadybug(ladybugVersion)) {
108
+ // Don't assert "< 0.18.0" as fact when the version is unresolvable
109
+ // (#2424 review R2) — name the unknown state instead.
110
+ const versionClause = ladybugVersion === undefined
111
+ ? 'an unknown @ladybugdb/core version (may predate 0.18.0)'
112
+ : `@ladybugdb/core < 0.18.0`;
113
+ lines.push(` ${padDisplayEnd('', 10)}⚠ non-4K page size with ${versionClause} — ` +
114
+ `'gitnexus analyze' may fail during COPY (#1231). Upgrade gitnexus (npm install -g gitnexus@latest).`);
115
+ }
116
+ return lines;
117
+ }
90
118
  export const doctorCommand = async () => {
91
119
  const fingerprint = getRuntimeFingerprint();
92
120
  const capabilities = getRuntimeCapabilities();
@@ -97,6 +125,13 @@ export const doctorCommand = async () => {
97
125
  console.log(` ${label('doctor.labels.node', 10)}${fingerprint.node}`);
98
126
  console.log(` ${label('doctor.labels.gitnexus', 10)}${fingerprint.gitnexus}`);
99
127
  console.log(` ${label('doctor.labels.ladybugdb', 10)}${fingerprint.ladybugdb ?? 'unknown'}`);
128
+ // OS page size next to the LadybugDB version because the two interact:
129
+ // @ladybugdb/core < 0.18.0 assumed 4 KiB pages in its buffer manager and
130
+ // crashes mid-COPY on 16 KiB/64 KiB-page kernels (#1231). Literal label
131
+ // (like the 'native' line below) to avoid adding i18n keys.
132
+ for (const line of pageSizeDoctorLines(getOsPageSize(), fingerprint.ladybugdb)) {
133
+ console.log(line);
134
+ }
100
135
  const nativeCheck = checkLbugNative();
101
136
  if (nativeCheck.ok) {
102
137
  console.log(` ${padDisplayEnd('native', 10)}✓ lbugjs.node loaded`);
@@ -40,9 +40,10 @@ export declare const en: {
40
40
  readonly 'clean.deleteBranch': "This will delete the branch index \"{{branch}}\" at: {{path}}";
41
41
  readonly 'clean.deletedBranch': "Deleted branch index: {{branch}}";
42
42
  readonly 'clean.lbugSidecars.state': "LadybugDB sidecar state: {{state}}";
43
- readonly 'clean.lbugSidecars.none': "No quarantined LadybugDB missing-shadow WAL sidecars found.";
44
- readonly 'clean.lbugSidecars.preview': "This will delete {{count}} quarantined LadybugDB missing-shadow WAL sidecar(s):";
45
- readonly 'clean.lbugSidecars.deleted': "Deleted {{count}} quarantined LadybugDB missing-shadow WAL sidecar(s).";
43
+ readonly 'clean.lbugSidecars.none': "No parked LadybugDB recovery sidecars found (missing-shadow WAL quarantines or dirty-recovery parks).";
44
+ readonly 'clean.lbugSidecars.preview': "This will delete {{count}} parked LadybugDB recovery sidecar(s) (missing-shadow WAL quarantines and dirty-recovery parks):";
45
+ readonly 'clean.lbugSidecars.deleted': "Deleted {{count}} parked LadybugDB recovery sidecar(s).";
46
+ readonly 'clean.lbugSidecars.failed': "Could not delete {{count}} locked file(s) — stop the process holding them (GitNexus MCP/serve or an antivirus scan) and re-run:";
46
47
  readonly 'remove.nothingToRemove': "Nothing to remove: {{message}}";
47
48
  readonly 'remove.deleteTarget': "This will delete the GitNexus index for: {{name}}";
48
49
  readonly 'remove.removed': "Removed: {{name}}";
@@ -175,7 +176,7 @@ export declare const en: {
175
176
  readonly 'help.option.uninstall.force': "Apply the changes (default is a dry-run preview)";
176
177
  readonly 'help.option.clean.all': "Clean all indexed repos";
177
178
  readonly 'help.option.clean.branch': "Delete only the named branch index (not the workspace index)";
178
- readonly 'help.option.clean.lbugSidecars': "Clean quarantined LadybugDB missing-shadow WAL sidecars";
179
+ readonly 'help.option.clean.lbugSidecars': "Clean parked LadybugDB recovery sidecars (missing-shadow WAL quarantines and dirty-recovery parks)";
179
180
  readonly 'help.option.wiki.force': "Force full regeneration even if up to date";
180
181
  readonly 'help.option.wiki.provider': "LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)";
181
182
  readonly 'help.option.wiki.model': "LLM model or Azure deployment name (default: minimax/minimax-m2.5)";
@@ -40,9 +40,10 @@ export const en = {
40
40
  'clean.deleteBranch': 'This will delete the branch index "{{branch}}" at: {{path}}',
41
41
  'clean.deletedBranch': 'Deleted branch index: {{branch}}',
42
42
  'clean.lbugSidecars.state': 'LadybugDB sidecar state: {{state}}',
43
- 'clean.lbugSidecars.none': 'No quarantined LadybugDB missing-shadow WAL sidecars found.',
44
- 'clean.lbugSidecars.preview': 'This will delete {{count}} quarantined LadybugDB missing-shadow WAL sidecar(s):',
45
- 'clean.lbugSidecars.deleted': 'Deleted {{count}} quarantined LadybugDB missing-shadow WAL sidecar(s).',
43
+ 'clean.lbugSidecars.none': 'No parked LadybugDB recovery sidecars found (missing-shadow WAL quarantines or dirty-recovery parks).',
44
+ 'clean.lbugSidecars.preview': 'This will delete {{count}} parked LadybugDB recovery sidecar(s) (missing-shadow WAL quarantines and dirty-recovery parks):',
45
+ 'clean.lbugSidecars.deleted': 'Deleted {{count}} parked LadybugDB recovery sidecar(s).',
46
+ 'clean.lbugSidecars.failed': 'Could not delete {{count}} locked file(s) — stop the process holding them (GitNexus MCP/serve or an antivirus scan) and re-run:',
46
47
  'remove.nothingToRemove': 'Nothing to remove: {{message}}',
47
48
  'remove.deleteTarget': 'This will delete the GitNexus index for: {{name}}',
48
49
  'remove.removed': 'Removed: {{name}}',
@@ -175,7 +176,7 @@ export const en = {
175
176
  'help.option.uninstall.force': 'Apply the changes (default is a dry-run preview)',
176
177
  'help.option.clean.all': 'Clean all indexed repos',
177
178
  'help.option.clean.branch': 'Delete only the named branch index (not the workspace index)',
178
- 'help.option.clean.lbugSidecars': 'Clean quarantined LadybugDB missing-shadow WAL sidecars',
179
+ 'help.option.clean.lbugSidecars': 'Clean parked LadybugDB recovery sidecars (missing-shadow WAL quarantines and dirty-recovery parks)',
179
180
  'help.option.wiki.force': 'Force full regeneration even if up to date',
180
181
  'help.option.wiki.provider': 'LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)',
181
182
  'help.option.wiki.model': 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)',
@@ -41,9 +41,10 @@ export declare const cliResources: {
41
41
  readonly 'clean.deleteBranch': "This will delete the branch index \"{{branch}}\" at: {{path}}";
42
42
  readonly 'clean.deletedBranch': "Deleted branch index: {{branch}}";
43
43
  readonly 'clean.lbugSidecars.state': "LadybugDB sidecar state: {{state}}";
44
- readonly 'clean.lbugSidecars.none': "No quarantined LadybugDB missing-shadow WAL sidecars found.";
45
- readonly 'clean.lbugSidecars.preview': "This will delete {{count}} quarantined LadybugDB missing-shadow WAL sidecar(s):";
46
- readonly 'clean.lbugSidecars.deleted': "Deleted {{count}} quarantined LadybugDB missing-shadow WAL sidecar(s).";
44
+ readonly 'clean.lbugSidecars.none': "No parked LadybugDB recovery sidecars found (missing-shadow WAL quarantines or dirty-recovery parks).";
45
+ readonly 'clean.lbugSidecars.preview': "This will delete {{count}} parked LadybugDB recovery sidecar(s) (missing-shadow WAL quarantines and dirty-recovery parks):";
46
+ readonly 'clean.lbugSidecars.deleted': "Deleted {{count}} parked LadybugDB recovery sidecar(s).";
47
+ readonly 'clean.lbugSidecars.failed': "Could not delete {{count}} locked file(s) — stop the process holding them (GitNexus MCP/serve or an antivirus scan) and re-run:";
47
48
  readonly 'remove.nothingToRemove': "Nothing to remove: {{message}}";
48
49
  readonly 'remove.deleteTarget': "This will delete the GitNexus index for: {{name}}";
49
50
  readonly 'remove.removed': "Removed: {{name}}";
@@ -176,7 +177,7 @@ export declare const cliResources: {
176
177
  readonly 'help.option.uninstall.force': "Apply the changes (default is a dry-run preview)";
177
178
  readonly 'help.option.clean.all': "Clean all indexed repos";
178
179
  readonly 'help.option.clean.branch': "Delete only the named branch index (not the workspace index)";
179
- readonly 'help.option.clean.lbugSidecars': "Clean quarantined LadybugDB missing-shadow WAL sidecars";
180
+ readonly 'help.option.clean.lbugSidecars': "Clean parked LadybugDB recovery sidecars (missing-shadow WAL quarantines and dirty-recovery parks)";
180
181
  readonly 'help.option.wiki.force': "Force full regeneration even if up to date";
181
182
  readonly 'help.option.wiki.provider': "LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)";
182
183
  readonly 'help.option.wiki.model': "LLM model or Azure deployment name (default: minimax/minimax-m2.5)";
@@ -290,6 +291,7 @@ export declare const cliResources: {
290
291
  'clean.lbugSidecars.none': string;
291
292
  'clean.lbugSidecars.preview': string;
292
293
  'clean.lbugSidecars.deleted': string;
294
+ 'clean.lbugSidecars.failed': string;
293
295
  'remove.nothingToRemove': string;
294
296
  'remove.deleteTarget': string;
295
297
  'remove.removed': string;
@@ -43,6 +43,7 @@ export declare const zhCN: {
43
43
  'clean.lbugSidecars.none': string;
44
44
  'clean.lbugSidecars.preview': string;
45
45
  'clean.lbugSidecars.deleted': string;
46
+ 'clean.lbugSidecars.failed': string;
46
47
  'remove.nothingToRemove': string;
47
48
  'remove.deleteTarget': string;
48
49
  'remove.removed': string;
@@ -40,9 +40,10 @@ export const zhCN = {
40
40
  'clean.deleteBranch': '将删除分支索引 “{{branch}}”,路径:{{path}}',
41
41
  'clean.deletedBranch': '已删除分支索引:{{branch}}',
42
42
  'clean.lbugSidecars.state': 'LadybugDB sidecar 状态:{{state}}',
43
- 'clean.lbugSidecars.none': '未找到已隔离的 LadybugDB missing-shadow WAL sidecar。',
44
- 'clean.lbugSidecars.preview': '将删除 {{count}} 个已隔离的 LadybugDB missing-shadow WAL sidecar:',
45
- 'clean.lbugSidecars.deleted': '已删除 {{count}} 个已隔离的 LadybugDB missing-shadow WAL sidecar。',
43
+ 'clean.lbugSidecars.none': '未找到已暂存的 LadybugDB 恢复 sidecar(missing-shadow WAL 隔离文件或 dirty-recovery 暂存文件)。',
44
+ 'clean.lbugSidecars.preview': '将删除 {{count}} 个已暂存的 LadybugDB 恢复 sidecar(missing-shadow WAL 隔离文件与 dirty-recovery 暂存文件):',
45
+ 'clean.lbugSidecars.deleted': '已删除 {{count}} 个已暂存的 LadybugDB 恢复 sidecar。',
46
+ 'clean.lbugSidecars.failed': '有 {{count}} 个文件被锁定而无法删除 — 请停止占用它们的进程(GitNexus MCP/serve 或杀毒软件扫描)后重试:',
46
47
  'remove.nothingToRemove': '无需移除:{{message}}',
47
48
  'remove.deleteTarget': '将删除该仓库的 GitNexus 索引:{{name}}',
48
49
  'remove.removed': '已移除:{{name}}',
@@ -175,7 +176,7 @@ export const zhCN = {
175
176
  'help.option.uninstall.force': '应用更改(默认仅为预演预览)',
176
177
  'help.option.clean.all': '清理所有已索引仓库',
177
178
  'help.option.clean.branch': '仅删除指定分支的索引(不影响工作区索引)',
178
- 'help.option.clean.lbugSidecars': '清理已隔离的 LadybugDB missing-shadow WAL sidecar',
179
+ 'help.option.clean.lbugSidecars': '清理已暂存的 LadybugDB 恢复 sidecar(missing-shadow WAL 隔离文件与 dirty-recovery 暂存文件)',
179
180
  'help.option.wiki.force': '即使已是最新也强制完整重新生成',
180
181
  'help.option.wiki.provider': 'LLM 提供商:openai、openrouter、azure、custom、cursor、claude、codex 或 opencode(默认:openai)',
181
182
  'help.option.wiki.model': 'LLM 模型或 Azure deployment 名称(默认:minimax/minimax-m2.5)',
package/dist/cli/index.js CHANGED
@@ -170,7 +170,7 @@ program
170
170
  .option('-f, --force', 'Skip confirmation prompt')
171
171
  .option('--all', 'Clean all indexed repos')
172
172
  .option('--branch <name>', 'Delete only the named branch index (not the workspace index)')
173
- .option('--lbug-sidecars', 'Clean quarantined LadybugDB missing-shadow WAL sidecars')
173
+ .option('--lbug-sidecars', 'Clean parked LadybugDB recovery sidecars (missing-shadow WAL quarantines and dirty-recovery parks)')
174
174
  .action(createLazyAction(() => import('./clean.js'), 'cleanCommand'));
175
175
  program
176
176
  .command('remove <target>')
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import path from 'path';
17
17
  import { listRegisteredRepos } from '../../storage/repo-manager.js';
18
+ import { escapeCypherString } from '../lbug/cypher-escape.js';
18
19
  /**
19
20
  * Find the best matching repo for a given working directory.
20
21
  * Matches by checking if cwd is within the repo's path.
@@ -76,7 +77,7 @@ async function findRepoForCwd(cwd) {
76
77
  export async function augment(pattern, cwd) {
77
78
  if (!pattern || pattern.length < 3)
78
79
  return '';
79
- const patternFirstWord = pattern.trim().replace(/'/g, "''").split(/\s+/)[0];
80
+ const patternFirstWord = escapeCypherString(pattern.trim()).split(/\s+/)[0];
80
81
  if (!patternFirstWord || patternFirstWord.length < 2)
81
82
  return '';
82
83
  const workDir = cwd || process.cwd();
@@ -97,7 +98,7 @@ export async function augment(pattern, cwd) {
97
98
  // Step 2: Map BM25 file results to symbols
98
99
  const symbolMatches = [];
99
100
  for (const result of bm25Results.slice(0, 5)) {
100
- const escaped = result.filePath.replace(/'/g, "''");
101
+ const escaped = escapeCypherString(result.filePath);
101
102
  try {
102
103
  const symbols = await executeQuery(repoId, `
103
104
  MATCH (n) WHERE n.filePath = '${escaped}'
@@ -147,7 +148,7 @@ export async function augment(pattern, cwd) {
147
148
  .filter((sym, i, arr) => arr.findIndex((s) => s.nodeId === sym.nodeId) === i);
148
149
  if (uniqueSymbols.length === 0)
149
150
  return '';
150
- const idList = uniqueSymbols.map((s) => `'${s.nodeId.replace(/'/g, "''")}'`).join(', ');
151
+ const idList = uniqueSymbols.map((s) => `'${escapeCypherString(s.nodeId)}'`).join(', ');
151
152
  // Batch fetch callers
152
153
  const callersMap = new Map();
153
154
  try {
@@ -51,6 +51,26 @@ export declare const batchInsertEmbeddings: (executeWithReusedStatement: (cypher
51
51
  embedding: number[];
52
52
  contentHash?: string;
53
53
  }>) => Promise<void>;
54
+ /**
55
+ * Create the vector index for semantic search (indexes the CodeEmbedding table).
56
+ *
57
+ * Keeps the embedding-specific extension-install policy gate here
58
+ * (ensureVectorExtensionAvailable → resolveEmbeddingInstallPolicy, default
59
+ * `auto` for the analyze write path), then delegates the actual
60
+ * `CALL CREATE_VECTOR_INDEX(...)` to the adapter, which runs it through the
61
+ * unprepared `conn.query()` path. It must NOT go through the injected
62
+ * `executeQuery` (prepared `conn.prepare()`): LadybugDB cannot prepare that
63
+ * procedure and fails with "We do not support prepare multiple statements" —
64
+ * the silent degrade in #2114.
65
+ *
66
+ * Exported for run-analyze's wipe-and-restore seam (tri-review 4669518496
67
+ * P1): a full-rebuild/escalated write wipes the DB files — index included —
68
+ * and a preserve-only run restores embedding ROWS without ever reaching the
69
+ * pipeline call sites below, so the orchestrator recreates the index through
70
+ * this same policy-gated, warn-on-failure entry point. Consumed there via
71
+ * dynamic import only (lazy-embeddings convention, #2370).
72
+ */
73
+ export declare const buildVectorIndex: () => Promise<boolean>;
54
74
  export interface EmbeddingPipelineResult {
55
75
  nodesProcessed: number;
56
76
  chunksProcessed: number;
@@ -18,6 +18,7 @@ import { DEFAULT_VECTOR_MAX_DISTANCE, getVectorMaxDistance, resolveEmbeddingConf
18
18
  import { rankExactEmbeddingRows } from './exact-search.js';
19
19
  import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME, STALE_HASH_SENTINEL } from '../lbug/schema.js';
20
20
  import { loadVectorExtension, createVectorIndex } from '../lbug/lbug-adapter.js';
21
+ import { escapeCypherString } from '../lbug/cypher-escape.js';
21
22
  import { getExactScanLimit } from '../platform/capabilities.js';
22
23
  import { logger } from '../logger.js';
23
24
  const isDev = process.env.NODE_ENV === 'development';
@@ -163,8 +164,15 @@ export const batchInsertEmbeddings = async (executeWithReusedStatement, updates)
163
164
  * `executeQuery` (prepared `conn.prepare()`): LadybugDB cannot prepare that
164
165
  * procedure and fails with "We do not support prepare multiple statements" —
165
166
  * the silent degrade in #2114.
167
+ *
168
+ * Exported for run-analyze's wipe-and-restore seam (tri-review 4669518496
169
+ * P1): a full-rebuild/escalated write wipes the DB files — index included —
170
+ * and a preserve-only run restores embedding ROWS without ever reaching the
171
+ * pipeline call sites below, so the orchestrator recreates the index through
172
+ * this same policy-gated, warn-on-failure entry point. Consumed there via
173
+ * dynamic import only (lazy-embeddings convention, #2370).
166
174
  */
167
- const buildVectorIndex = async () => {
175
+ export const buildVectorIndex = async () => {
168
176
  // This pre-check applies the embedding-specific install policy
169
177
  // (resolveEmbeddingInstallPolicy, default `auto` for analyze) before reaching
170
178
  // the adapter. The adapter's createVectorIndex() calls loadVectorExtension()
@@ -536,7 +544,7 @@ export const semanticSearch = async (executeQuery, query, k = 10, maxDistance =
536
544
  // Batch-fetch metadata per label
537
545
  const results = [];
538
546
  for (const [label, items] of byLabel) {
539
- const idList = items.map((i) => `'${i.nodeId.replace(/'/g, "''")}'`).join(', ');
547
+ const idList = items.map((i) => `'${escapeCypherString(i.nodeId)}'`).join(', ');
540
548
  try {
541
549
  const nodeQuery = `
542
550
  MATCH (n:\`${label}\`) WHERE n.id IN [${idList}]
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Escalation gate for the incremental DB writeback (#2409).
3
+ *
4
+ * When the effective write set covers most of the repo, per-file surgery is
5
+ * strictly worse than the proven wipe-and-bulk-COPY plan — the same data
6
+ * volume lands either way, but the surgical plan pays per-table deletes plus
7
+ * COPY-into-non-empty tables, and at that size it measured SLOWER than a full
8
+ * DB load. The orchestrator (`run-analyze.ts`) consults this predicate to
9
+ * decide which write plan to run; only the DB write plan changes on
10
+ * escalation — fileHashes/meta bookkeeping is identical.
11
+ *
12
+ * Extracted to a pure module (tri-review 4669518496) so the AND-gate's
13
+ * boundary corners are pinned by unit tests without multi-minute
14
+ * orchestration permutations — an `&&`→`||` mutation here can no longer
15
+ * survive CI.
16
+ */
17
+ export declare const INCREMENTAL_MAX_WRITE_FRACTION = 0.5;
18
+ export declare const INCREMENTAL_ESCALATION_MIN_FILES = 50;
19
+ /**
20
+ * Should the incremental writeback escalate from per-file surgery to a full
21
+ * wipe-and-bulk-COPY write plan?
22
+ *
23
+ * @param deleteCount Files whose rows will be DETACH-DELETEd
24
+ * (effective write set ∪ deleted files, deduped).
25
+ * @param effectiveWriteCount Size of the effective write set (toWrite ∪
26
+ * importer-BFS expansion ∪ boundary-crossing files).
27
+ * @param totalFiles Current repo file count (denominator).
28
+ *
29
+ * POPULATION MISMATCH (tri-review 4669518496, documented not "fixed"): the
30
+ * numerator counts effective-write-set members, which include importer-BFS
31
+ * results read from the PRE-pipeline DB — those can be now-DELETED paths that
32
+ * the CURRENT file list (the denominator) no longer contains. The fraction is
33
+ * therefore not a true subset ratio and can exceed 1 on delete-heavy runs.
34
+ * That errs toward escalation, which is the safe direction (the full write
35
+ * plan is always correct); the valve's log line clamps the DISPLAYED
36
+ * percentage to 100 so operators aren't shown ">100% of the repo".
37
+ */
38
+ export declare const shouldEscalateIncrementalWrite: (deleteCount: number, effectiveWriteCount: number, totalFiles: number) => boolean;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Escalation gate for the incremental DB writeback (#2409).
3
+ *
4
+ * When the effective write set covers most of the repo, per-file surgery is
5
+ * strictly worse than the proven wipe-and-bulk-COPY plan — the same data
6
+ * volume lands either way, but the surgical plan pays per-table deletes plus
7
+ * COPY-into-non-empty tables, and at that size it measured SLOWER than a full
8
+ * DB load. The orchestrator (`run-analyze.ts`) consults this predicate to
9
+ * decide which write plan to run; only the DB write plan changes on
10
+ * escalation — fileHashes/meta bookkeeping is identical.
11
+ *
12
+ * Extracted to a pure module (tri-review 4669518496) so the AND-gate's
13
+ * boundary corners are pinned by unit tests without multi-minute
14
+ * orchestration permutations — an `&&`→`||` mutation here can no longer
15
+ * survive CI.
16
+ */
17
+ // Escalation cap (#2409): above this fraction of the repo's files, the
18
+ // surgical delete-and-COPY writeback is replaced by the full-rebuild write
19
+ // plan (wipe + bulk COPY of the already-built graph). 0.5 is a coarse
20
+ // crossover knob, not a tuned constant — lower it if surgical writebacks
21
+ // above ~30% ever measure slower than the full COPY.
22
+ export const INCREMENTAL_MAX_WRITE_FRACTION = 0.5;
23
+ // …but only at a scale where the surgical plan's overhead is real. Tiny
24
+ // repos hit huge fractions from a single edit (7 files → one touch can
25
+ // pull in 5) while both write plans finish in well under a second there —
26
+ // escalating would churn the DB files for nothing.
27
+ export const INCREMENTAL_ESCALATION_MIN_FILES = 50;
28
+ /**
29
+ * Should the incremental writeback escalate from per-file surgery to a full
30
+ * wipe-and-bulk-COPY write plan?
31
+ *
32
+ * @param deleteCount Files whose rows will be DETACH-DELETEd
33
+ * (effective write set ∪ deleted files, deduped).
34
+ * @param effectiveWriteCount Size of the effective write set (toWrite ∪
35
+ * importer-BFS expansion ∪ boundary-crossing files).
36
+ * @param totalFiles Current repo file count (denominator).
37
+ *
38
+ * POPULATION MISMATCH (tri-review 4669518496, documented not "fixed"): the
39
+ * numerator counts effective-write-set members, which include importer-BFS
40
+ * results read from the PRE-pipeline DB — those can be now-DELETED paths that
41
+ * the CURRENT file list (the denominator) no longer contains. The fraction is
42
+ * therefore not a true subset ratio and can exceed 1 on delete-heavy runs.
43
+ * That errs toward escalation, which is the safe direction (the full write
44
+ * plan is always correct); the valve's log line clamps the DISPLAYED
45
+ * percentage to 100 so operators aren't shown ">100% of the repo".
46
+ */
47
+ export const shouldEscalateIncrementalWrite = (deleteCount, effectiveWriteCount, totalFiles) => deleteCount >= INCREMENTAL_ESCALATION_MIN_FILES &&
48
+ effectiveWriteCount / Math.max(1, totalFiles) > INCREMENTAL_MAX_WRITE_FRACTION;
@@ -2,9 +2,9 @@
2
2
  * Shadow-candidate path derivation for incremental indexing.
3
3
  *
4
4
  * Background — Bugbot review on PR #1479:
5
- * queryImporters() on a NEWLY ADDED file returns 0 importers in the
6
- * pre-pipeline DB, because the new file's IMPORTS rows haven't been
7
- * written yet. But pre-existing files may have IMPORTS edges that
5
+ * the importer BFS (queryImportersBatch) on a NEWLY ADDED file returns
6
+ * 0 importers in the pre-pipeline DB, because the new file's IMPORTS
7
+ * rows haven't been written yet. But pre-existing files may have IMPORTS edges that
8
8
  * *resolved to a sibling path*, and the newcomer can now steal that
9
9
  * resolution under standard JS/TS module-resolution rules. Without
10
10
  * pulling those pre-existing files into the writable set, their
@@ -2,9 +2,9 @@
2
2
  * Shadow-candidate path derivation for incremental indexing.
3
3
  *
4
4
  * Background — Bugbot review on PR #1479:
5
- * queryImporters() on a NEWLY ADDED file returns 0 importers in the
6
- * pre-pipeline DB, because the new file's IMPORTS rows haven't been
7
- * written yet. But pre-existing files may have IMPORTS edges that
5
+ * the importer BFS (queryImportersBatch) on a NEWLY ADDED file returns
6
+ * 0 importers in the pre-pipeline DB, because the new file's IMPORTS
7
+ * rows haven't been written yet. But pre-existing files may have IMPORTS edges that
8
8
  * *resolved to a sibling path*, and the newcomer can now steal that
9
9
  * resolution under standard JS/TS module-resolution rules. Without
10
10
  * pulling those pre-existing files into the writable set, their
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * `extractChangedSubgraph` intentionally does NOT expand the set it is
24
24
  * given — expansion is the orchestrator's job, so the SAME expanded set
25
- * can be fed to both `deleteNodesForFile` and this function (asymmetry
25
+ * can be fed to both `deleteNodesForFiles` and this function (asymmetry
26
26
  * between the delete set and the write set silently corrupts the DB).
27
27
  * `computeEffectiveWriteSet` below performs the boundary-crossing 1-hop
28
28
  * walk; the orchestrator composes it with its importer-BFS expansion and
@@ -57,7 +57,7 @@ export declare const extractChangedSubgraph: (fullGraph: KnowledgeGraph, toWrite
57
57
  * deleted + rewritten in lockstep with the changed side.
58
58
  *
59
59
  * Single pass over the edge list. Does NOT mutate `toWriteSet`. The
60
- * orchestrator MUST feed the returned set to both `deleteNodesForFile`
60
+ * orchestrator MUST feed the returned set to both `deleteNodesForFiles`
61
61
  * and `extractChangedSubgraph` — feeding the unexpanded set to either
62
62
  * one leaves stale rows or PK-conflicts at COPY time.
63
63
  */
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * `extractChangedSubgraph` intentionally does NOT expand the set it is
24
24
  * given — expansion is the orchestrator's job, so the SAME expanded set
25
- * can be fed to both `deleteNodesForFile` and this function (asymmetry
25
+ * can be fed to both `deleteNodesForFiles` and this function (asymmetry
26
26
  * between the delete set and the write set silently corrupts the DB).
27
27
  * `computeEffectiveWriteSet` below performs the boundary-crossing 1-hop
28
28
  * walk; the orchestrator composes it with its importer-BFS expansion and
@@ -118,7 +118,7 @@ export const extractChangedSubgraph = (fullGraph, toWriteSet) => {
118
118
  * deleted + rewritten in lockstep with the changed side.
119
119
  *
120
120
  * Single pass over the edge list. Does NOT mutate `toWriteSet`. The
121
- * orchestrator MUST feed the returned set to both `deleteNodesForFile`
121
+ * orchestrator MUST feed the returned set to both `deleteNodesForFiles`
122
122
  * and `extractChangedSubgraph` — feeding the unexpanded set to either
123
123
  * one leaves stale rows or PK-conflicts at COPY time.
124
124
  */
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Escape a value for embedding in a single-quoted Cypher string literal.
3
+ *
4
+ * LadybugDB's parser uses backslash escapes and REJECTS SQL-style `''`
5
+ * doubling — `'we''ird'` is a parser error, not an escaped quote (#2409
6
+ * review). Every call site that used doubling produced a query that never
7
+ * parsed; the failures were invisible wherever the site swallowed errors
8
+ * (per-file deletes skipping quoted paths, importer BFS returning [],
9
+ * augment/wiki batch lookups silently missing rows).
10
+ *
11
+ * Backslashes are escaped first, then quotes — reversing the order would
12
+ * double the backslash that the quote escape just introduced. For values
13
+ * inside single-quoted literals only; table/label names go through
14
+ * `escapeTableName` in lbug-adapter instead.
15
+ *
16
+ * NOT for CSV emission: the COPY path (csv-generator.ts) quotes fields for
17
+ * LadybugDB's CSV reader (`ESCAPE='"'`), a different grammar with its own
18
+ * rules — its `''` usages are not this bug.
19
+ */
20
+ export declare const escapeCypherString: (value: string) => string;