gitnexus 1.6.9 → 1.6.10-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -41
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.js +106 -3
- package/dist/cli/cli-message.d.ts +1 -1
- package/dist/cli/doctor.d.ts +5 -0
- package/dist/cli/doctor.js +41 -3
- package/dist/cli/editor-targets.d.ts +17 -2
- package/dist/cli/editor-targets.js +44 -0
- package/dist/cli/embeddings.d.ts +12 -0
- package/dist/cli/embeddings.js +49 -0
- package/dist/cli/help-i18n.js +4 -0
- package/dist/cli/i18n/en.d.ts +5 -1
- package/dist/cli/i18n/en.js +5 -1
- package/dist/cli/i18n/resources.d.ts +9 -1
- package/dist/cli/i18n/zh-CN.d.ts +4 -0
- package/dist/cli/i18n/zh-CN.js +5 -1
- package/dist/cli/index.js +12 -1
- package/dist/cli/setup.js +204 -28
- package/dist/cli/uninstall.js +70 -16
- package/dist/core/embeddings/embedder.js +16 -2
- package/dist/core/embeddings/http-client.d.ts +28 -1
- package/dist/core/embeddings/http-client.js +107 -15
- package/dist/core/embeddings/node-module-compat.d.ts +23 -0
- package/dist/core/embeddings/node-module-compat.js +23 -0
- package/dist/core/embeddings/onnxruntime-common-resolver.js +6 -3
- package/dist/core/embeddings/onnxruntime-node-resolver.js +31 -5
- package/dist/core/embeddings/runtime-install.d.ts +119 -0
- package/dist/core/embeddings/runtime-install.js +372 -0
- package/dist/core/embeddings/runtime-support.d.ts +48 -17
- package/dist/core/embeddings/runtime-support.js +96 -0
- package/dist/core/group/extractors/http-patterns/python.js +246 -108
- package/dist/core/group/extractors/manifest-extractor.js +4 -0
- package/dist/core/ingestion/cobol/jcl-processor.js +9 -8
- package/dist/core/ingestion/cobol-processor.js +27 -26
- package/dist/core/ingestion/community-processor.d.ts +51 -1
- package/dist/core/ingestion/community-processor.js +316 -63
- package/dist/core/ingestion/emit-references.js +3 -2
- package/dist/core/ingestion/markdown-processor.js +3 -2
- package/dist/core/ingestion/parsing-processor.d.ts +3 -1
- package/dist/core/ingestion/parsing-processor.js +4 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +42 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +83 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.js +131 -0
- package/dist/core/ingestion/route-extractors/python-const-resolver.d.ts +75 -0
- package/dist/core/ingestion/route-extractors/python-const-resolver.js +299 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
- package/dist/core/ingestion/tree-sitter-queries.js +12 -1
- package/dist/core/ingestion/utils/line-base.d.ts +20 -0
- package/dist/core/ingestion/utils/line-base.js +20 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +21 -0
- package/dist/core/ingestion/utils/symbol-labels.js +45 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +34 -0
- package/dist/core/ingestion/workers/parse-worker.js +39 -4
- package/dist/core/ingestion/workers/result-merge.js +4 -0
- package/dist/core/lbug/csv-generator.js +8 -2
- package/dist/core/lbug/extension-load-error.d.ts +67 -0
- package/dist/core/lbug/extension-load-error.js +320 -0
- package/dist/core/lbug/extension-loader.d.ts +17 -2
- package/dist/core/lbug/extension-loader.js +38 -13
- package/dist/core/lbug/lbug-adapter.js +7 -16
- package/dist/core/lbug/native-check.d.ts +22 -0
- package/dist/core/lbug/native-check.js +66 -0
- package/dist/core/lbug/pool-adapter.js +10 -1
- package/dist/core/lbug/sidecar-recovery.d.ts +32 -0
- package/dist/core/lbug/sidecar-recovery.js +91 -7
- package/dist/core/platform/capabilities.js +27 -1
- package/dist/core/run-analyze.js +33 -6
- package/dist/core/search/fts-indexes.d.ts +7 -0
- package/dist/core/search/fts-indexes.js +37 -0
- package/dist/mcp/core/embedder.js +16 -2
- package/dist/mcp/local/line-display.d.ts +22 -0
- package/dist/mcp/local/line-display.js +3 -0
- package/dist/mcp/local/local-backend.d.ts +14 -0
- package/dist/mcp/local/local-backend.js +63 -19
- package/dist/mcp/local/pdg-impact.d.ts +5 -3
- package/dist/mcp/local/pdg-impact.js +5 -2
- package/dist/mcp/resources.js +1 -0
- package/dist/mcp/tools.js +1 -1
- package/dist/server/api.js +2 -2
- package/dist/storage/parse-cache.js +1 -1
- package/dist/storage/repo-manager.d.ts +6 -1
- package/dist/storage/repo-manager.js +6 -1
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +50 -3
- package/hooks/claude/gitnexus-hook.cjs +59 -9
- package/package.json +5 -3
- package/scripts/cross-platform-tests.ts +26 -0
- package/scripts/ensure-fts.ts +32 -0
- package/scripts/install-duckdb-extension.mjs +85 -24
- package/scripts/run-cross-platform.ts +46 -6
- package/scripts/shard-arg.ts +30 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ExtensionLoadDiagnosis } from './extension-load-error.js';
|
|
1
2
|
/**
|
|
2
3
|
* Lifecycle policy for an optional DuckDB extension.
|
|
3
4
|
*
|
|
@@ -20,6 +21,12 @@ export interface ExtensionCapability {
|
|
|
20
21
|
loaded: boolean;
|
|
21
22
|
/** Human-readable reason when `loaded` is false. */
|
|
22
23
|
reason?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Classified diagnosis of `reason`, computed ONCE at mark-unavailable time so
|
|
26
|
+
* per-request surfaces (ftsDegradedWarning on /api/search + MCP query) read the
|
|
27
|
+
* cached remedy instead of re-inspecting the extension file on every call (#2383 F3).
|
|
28
|
+
*/
|
|
29
|
+
diagnosis?: ExtensionLoadDiagnosis;
|
|
23
30
|
}
|
|
24
31
|
/** Per-call overrides applied on top of `ExtensionManager` defaults. */
|
|
25
32
|
export interface ExtensionEnsureOptions {
|
|
@@ -29,7 +36,7 @@ export interface ExtensionEnsureOptions {
|
|
|
29
36
|
export interface ExtensionManagerOptions {
|
|
30
37
|
policy?: ExtensionInstallPolicy;
|
|
31
38
|
installTimeoutMs?: number;
|
|
32
|
-
installExtension?: (extensionName: string, timeoutMs: number) => Promise<ExtensionInstallResult>;
|
|
39
|
+
installExtension?: (extensionName: string, timeoutMs: number, loadError?: string) => Promise<ExtensionInstallResult>;
|
|
33
40
|
warn?: (message: string) => void;
|
|
34
41
|
}
|
|
35
42
|
export declare const getExtensionInstallPolicy: () => ExtensionInstallPolicy;
|
|
@@ -58,7 +65,7 @@ export declare const getExtensionInstallChildProcessArgs: (extensionName: string
|
|
|
58
65
|
* If the child exceeds `timeoutMs` the parent kills it with SIGKILL and
|
|
59
66
|
* resolves with `timedOut: true`.
|
|
60
67
|
*/
|
|
61
|
-
export declare const installDuckDbExtensionOutOfProcess: (extensionName: string, timeoutMs?: number) => Promise<ExtensionInstallResult>;
|
|
68
|
+
export declare const installDuckDbExtensionOutOfProcess: (extensionName: string, timeoutMs?: number, loadError?: string) => Promise<ExtensionInstallResult>;
|
|
62
69
|
/**
|
|
63
70
|
* Centralized lifecycle manager for optional LadybugDB extensions.
|
|
64
71
|
*
|
|
@@ -90,6 +97,14 @@ export declare class ExtensionManager {
|
|
|
90
97
|
* paths are expected to degrade gracefully.
|
|
91
98
|
*/
|
|
92
99
|
ensure(query: (sql: string) => Promise<unknown>, name: string, label: string, opts?: ExtensionEnsureOptions): Promise<boolean>;
|
|
100
|
+
/**
|
|
101
|
+
* Attempt `LOAD EXTENSION <name>`; returns `null` on success and the
|
|
102
|
+
* collapsed error message on failure. The message is the load-side ground
|
|
103
|
+
* truth — LadybugDB distinguishes a missing extension file from a present
|
|
104
|
+
* but unloadable one (wrong platform, truncated download, version mismatch),
|
|
105
|
+
* and discarding it left users staring at "not pre-installed" when the file
|
|
106
|
+
* existed all along (#2374).
|
|
107
|
+
*/
|
|
93
108
|
private tryLoad;
|
|
94
109
|
private markLoaded;
|
|
95
110
|
private markUnavailable;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { LBUG_MAX_DB_SIZE } from './lbug-config.js';
|
|
4
|
+
import { diagnoseExtensionLoad } from './extension-load-error.js';
|
|
4
5
|
import { logger } from '../logger.js';
|
|
5
6
|
const DEFAULT_EXTENSION_INSTALL_TIMEOUT_MS = 15_000;
|
|
6
7
|
const EXTENSION_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
7
8
|
const alreadyAvailable = (message) => message.includes('already loaded') ||
|
|
8
9
|
message.includes('already installed') ||
|
|
9
10
|
message.includes('already exists');
|
|
11
|
+
/** LadybugDB errors are multi-line; collapse for single-line warn/reason strings. */
|
|
12
|
+
const oneLine = (value) => value.replace(/\s+/g, ' ').trim();
|
|
10
13
|
const resolvePolicyFromEnv = () => {
|
|
11
14
|
const raw = process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
|
|
12
15
|
if (raw === 'load-only' || raw === 'never' || raw === 'auto')
|
|
@@ -51,7 +54,7 @@ export const getExtensionInstallChildProcessArgs = (extensionName, maxDbSize = L
|
|
|
51
54
|
* If the child exceeds `timeoutMs` the parent kills it with SIGKILL and
|
|
52
55
|
* resolves with `timedOut: true`.
|
|
53
56
|
*/
|
|
54
|
-
export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutMs = getExtensionInstallTimeoutMs()) => {
|
|
57
|
+
export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutMs = getExtensionInstallTimeoutMs(), loadError) => {
|
|
55
58
|
if (!EXTENSION_NAME_PATTERN.test(extensionName)) {
|
|
56
59
|
throw new Error(`Invalid DuckDB extension name: ${extensionName}`);
|
|
57
60
|
}
|
|
@@ -60,6 +63,9 @@ export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutM
|
|
|
60
63
|
env: {
|
|
61
64
|
...process.env,
|
|
62
65
|
GITNEXUS_LBUG_EXTENSION_NAME: extensionName,
|
|
66
|
+
// The child picks INSTALL vs FORCE INSTALL from this LOAD error so it
|
|
67
|
+
// only re-downloads when the on-disk extension file is actually broken.
|
|
68
|
+
...(loadError ? { GITNEXUS_LBUG_EXTENSION_LOAD_ERROR: loadError } : {}),
|
|
63
69
|
},
|
|
64
70
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
65
71
|
windowsHide: true,
|
|
@@ -78,7 +84,7 @@ export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutM
|
|
|
78
84
|
resolve({
|
|
79
85
|
success: false,
|
|
80
86
|
timedOut: true,
|
|
81
|
-
message: `
|
|
87
|
+
message: `extension install for ${extensionName} timed out after ${timeoutMs}ms`,
|
|
82
88
|
});
|
|
83
89
|
}, timeoutMs);
|
|
84
90
|
child.on('error', (err) => {
|
|
@@ -97,8 +103,8 @@ export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutM
|
|
|
97
103
|
success: code === 0,
|
|
98
104
|
timedOut: false,
|
|
99
105
|
message: code === 0
|
|
100
|
-
? `
|
|
101
|
-
: `
|
|
106
|
+
? `extension install for ${extensionName} completed`
|
|
107
|
+
: `extension install for ${extensionName} failed with ${signal ?? `exit code ${code}`}${stderr ? `: ${stderr.trim()}` : ''}`,
|
|
102
108
|
});
|
|
103
109
|
});
|
|
104
110
|
});
|
|
@@ -152,46 +158,65 @@ export class ExtensionManager {
|
|
|
152
158
|
this.markUnavailable(name, label, 'extension install policy is "never"', warn);
|
|
153
159
|
return false;
|
|
154
160
|
}
|
|
155
|
-
|
|
161
|
+
const loadError = await this.tryLoad(query, name);
|
|
162
|
+
if (loadError === null) {
|
|
156
163
|
this.markLoaded(name);
|
|
157
164
|
return true;
|
|
158
165
|
}
|
|
159
166
|
if (policy === 'load-only') {
|
|
160
|
-
this.markUnavailable(name, label,
|
|
167
|
+
this.markUnavailable(name, label, `load-only policy (no install attempted); LOAD ${name} failed: ${loadError}`, warn);
|
|
161
168
|
return false;
|
|
162
169
|
}
|
|
163
170
|
let install = this.installAttempted.get(name);
|
|
164
171
|
if (!install) {
|
|
165
172
|
const installFn = this.options.installExtension ?? installDuckDbExtensionOutOfProcess;
|
|
166
|
-
|
|
173
|
+
// Hand the child the LOAD error so it re-downloads (FORCE) only when the
|
|
174
|
+
// present extension file is provably broken, not on every LOAD failure.
|
|
175
|
+
install = await installFn(name, timeoutMs, loadError);
|
|
167
176
|
this.installAttempted.set(name, install);
|
|
168
177
|
}
|
|
169
178
|
if (!install.success) {
|
|
170
|
-
this.markUnavailable(name, label, install.message
|
|
179
|
+
this.markUnavailable(name, label, `${install.message}; LOAD ${name} had failed: ${loadError}`, warn);
|
|
171
180
|
return false;
|
|
172
181
|
}
|
|
173
|
-
|
|
182
|
+
const retryError = await this.tryLoad(query, name);
|
|
183
|
+
if (retryError === null) {
|
|
174
184
|
this.markLoaded(name);
|
|
175
185
|
return true;
|
|
176
186
|
}
|
|
177
|
-
this.markUnavailable(name, label, `LOAD ${name} failed after successful INSTALL`, warn);
|
|
187
|
+
this.markUnavailable(name, label, `LOAD ${name} failed after successful INSTALL: ${retryError}`, warn);
|
|
178
188
|
return false;
|
|
179
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Attempt `LOAD EXTENSION <name>`; returns `null` on success and the
|
|
192
|
+
* collapsed error message on failure. The message is the load-side ground
|
|
193
|
+
* truth — LadybugDB distinguishes a missing extension file from a present
|
|
194
|
+
* but unloadable one (wrong platform, truncated download, version mismatch),
|
|
195
|
+
* and discarding it left users staring at "not pre-installed" when the file
|
|
196
|
+
* existed all along (#2374).
|
|
197
|
+
*/
|
|
180
198
|
async tryLoad(query, name) {
|
|
181
199
|
try {
|
|
182
200
|
await query(`LOAD EXTENSION ${name}`);
|
|
183
|
-
return
|
|
201
|
+
return null;
|
|
184
202
|
}
|
|
185
203
|
catch (err) {
|
|
186
204
|
const msg = err instanceof Error ? err.message : String(err);
|
|
187
|
-
return alreadyAvailable(msg);
|
|
205
|
+
return alreadyAvailable(msg) ? null : oneLine(msg);
|
|
188
206
|
}
|
|
189
207
|
}
|
|
190
208
|
markLoaded(name) {
|
|
191
209
|
this.capabilities.set(name, { name, loaded: true });
|
|
192
210
|
}
|
|
193
211
|
markUnavailable(name, label, reason, warn) {
|
|
194
|
-
|
|
212
|
+
// Classify once here (the single load-failure sink, run per Database not per
|
|
213
|
+
// request) so the hot per-request warning path does no file I/O (#2383 F3).
|
|
214
|
+
this.capabilities.set(name, {
|
|
215
|
+
name,
|
|
216
|
+
loaded: false,
|
|
217
|
+
reason,
|
|
218
|
+
diagnosis: diagnoseExtensionLoad(reason),
|
|
219
|
+
});
|
|
195
220
|
const key = `${name}:${reason}`;
|
|
196
221
|
if (this.warnedKeys.has(key))
|
|
197
222
|
return;
|
|
@@ -13,7 +13,7 @@ import { streamAllCSVsToDisk } from './csv-generator.js';
|
|
|
13
13
|
import { getNodeLabel as deriveNodeLabel } from './rel-pair-routing.js';
|
|
14
14
|
import { extensionManager } from './extension-loader.js';
|
|
15
15
|
import { classifyDeleteAllError, closeLbugConnection, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, toNativeSafePath, resolveNativeSafeStorageDir, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
|
|
16
|
-
import { finalizeLbugSidecarsAfterClose,
|
|
16
|
+
import { finalizeLbugSidecarsAfterClose, guardWalQuarantine, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, shadowSidecarRecoveryMessage, } from './sidecar-recovery.js';
|
|
17
17
|
import { isVectorExtensionSupportedByPlatform } from '../platform/capabilities.js';
|
|
18
18
|
import { logger } from '../logger.js';
|
|
19
19
|
/**
|
|
@@ -409,23 +409,14 @@ const queryAndDrain = async (targetConn, cypher) => {
|
|
|
409
409
|
};
|
|
410
410
|
const READ_ONLY_SHADOW_REPLAY_PROBE = 'MATCH (n) RETURN n LIMIT 1';
|
|
411
411
|
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* Throws shadowSidecarRecoveryMessage immediately when the WAL is large,
|
|
418
|
-
* preserving the uncheckpointed pages for explicit operator recovery.
|
|
419
|
-
* Returns silently when the WAL is absent, tiny, or in any other state
|
|
420
|
-
* where the existing recovery path is safe to proceed.
|
|
412
|
+
* Serve-side entry to the shared WAL-quarantine safety gate. Refuses (throws)
|
|
413
|
+
* when the `.shadow` is present on disk or the orphan WAL is too large to
|
|
414
|
+
* safely discard; returns silently otherwise. The policy itself lives in
|
|
415
|
+
* `guardWalQuarantine` (sidecar-recovery.ts) so serve and the MCP pool share
|
|
416
|
+
* one source of truth (PR #1747 review D2; issue #2382 review, Finding B).
|
|
421
417
|
*/
|
|
422
418
|
const refuseLargeWalQuarantine = async (dbPath, mode, triggeringErr) => {
|
|
423
|
-
|
|
424
|
-
if (state.kind === 'orphan-wal') {
|
|
425
|
-
logger.warn(`GitNexus: refusing to quarantine large WAL (${state.walBytes} bytes) at ${dbPath}.wal during ${mode} recovery; ` +
|
|
426
|
-
'manual recovery required — run `gitnexus analyze --force <repo-path> --index-only`.');
|
|
427
|
-
throw new Error(shadowSidecarRecoveryMessage(dbPath, triggeringErr));
|
|
428
|
-
}
|
|
419
|
+
await guardWalQuarantine(dbPath, mode, triggeringErr, logger);
|
|
429
420
|
};
|
|
430
421
|
const reopenReadOnlyAfterMissingShadow = async (dbPath, err) => {
|
|
431
422
|
await refuseLargeWalQuarantine(dbPath, 'read-only', err);
|
|
@@ -4,3 +4,25 @@ export interface NativeCheckResult {
|
|
|
4
4
|
message?: string;
|
|
5
5
|
}
|
|
6
6
|
export declare function checkLbugNative(overridePkgDir?: string): NativeCheckResult;
|
|
7
|
+
export interface FtsProbeResult {
|
|
8
|
+
loaded: boolean;
|
|
9
|
+
/** Collapsed LadybugDB error when `loaded` is false. */
|
|
10
|
+
reason?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Live-probe `LOAD EXTENSION fts` on a throwaway in-memory database.
|
|
14
|
+
*
|
|
15
|
+
* `doctor` used to print the static platform capability, which contradicted
|
|
16
|
+
* analyze whenever the extension file was missing or unloadable (#2374).
|
|
17
|
+
* LOAD never touches the network, so the probe is safe offline, and it
|
|
18
|
+
* surfaces LadybugDB's real error — which distinguishes a missing extension
|
|
19
|
+
* file from a present-but-broken one (wrong platform, truncated download).
|
|
20
|
+
* Dynamic import so doctor still runs when the native module itself is broken.
|
|
21
|
+
*
|
|
22
|
+
* Bounded by `timeoutMs`: an unresponsive extension file (e.g. on a hung
|
|
23
|
+
* network home dir) must never freeze `doctor` — the tool the degradation
|
|
24
|
+
* warnings send users to. `Promise.race` lets doctor report and move on; it
|
|
25
|
+
* cannot cancel an in-flight native call, so a future thread-blocking case
|
|
26
|
+
* would need an out-of-process probe.
|
|
27
|
+
*/
|
|
28
|
+
export declare function probeFtsExtensionLoad(timeoutMs?: number): Promise<FtsProbeResult>;
|
|
@@ -81,3 +81,69 @@ export function checkLbugNative(overridePkgDir) {
|
|
|
81
81
|
}
|
|
82
82
|
return { ok: true, binaryPath };
|
|
83
83
|
}
|
|
84
|
+
const DEFAULT_FTS_PROBE_TIMEOUT_MS = 10_000;
|
|
85
|
+
/** Close each result, swallowing close-time errors so a successful LOAD is not
|
|
86
|
+
* misreported as a failure (native-check keeps no static lbug dependency, so it
|
|
87
|
+
* cannot reuse the adapter's closeQueryResults — that would eagerly load the
|
|
88
|
+
* module and defeat the dynamic import below). */
|
|
89
|
+
const closeProbeResults = (result) => {
|
|
90
|
+
for (const r of Array.isArray(result) ? result : [result]) {
|
|
91
|
+
try {
|
|
92
|
+
r?.close?.();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// ignore — a close failure must not flip a successful LOAD to failed
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Live-probe `LOAD EXTENSION fts` on a throwaway in-memory database.
|
|
101
|
+
*
|
|
102
|
+
* `doctor` used to print the static platform capability, which contradicted
|
|
103
|
+
* analyze whenever the extension file was missing or unloadable (#2374).
|
|
104
|
+
* LOAD never touches the network, so the probe is safe offline, and it
|
|
105
|
+
* surfaces LadybugDB's real error — which distinguishes a missing extension
|
|
106
|
+
* file from a present-but-broken one (wrong platform, truncated download).
|
|
107
|
+
* Dynamic import so doctor still runs when the native module itself is broken.
|
|
108
|
+
*
|
|
109
|
+
* Bounded by `timeoutMs`: an unresponsive extension file (e.g. on a hung
|
|
110
|
+
* network home dir) must never freeze `doctor` — the tool the degradation
|
|
111
|
+
* warnings send users to. `Promise.race` lets doctor report and move on; it
|
|
112
|
+
* cannot cancel an in-flight native call, so a future thread-blocking case
|
|
113
|
+
* would need an out-of-process probe.
|
|
114
|
+
*/
|
|
115
|
+
export async function probeFtsExtensionLoad(timeoutMs = DEFAULT_FTS_PROBE_TIMEOUT_MS) {
|
|
116
|
+
let timer;
|
|
117
|
+
const timeout = new Promise((resolve) => {
|
|
118
|
+
timer = setTimeout(() => resolve({
|
|
119
|
+
loaded: false,
|
|
120
|
+
reason: 'probe timed out — extension file or filesystem unresponsive',
|
|
121
|
+
}), timeoutMs);
|
|
122
|
+
});
|
|
123
|
+
const probe = (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const { default: lbug } = await import('@ladybugdb/core');
|
|
126
|
+
const db = new lbug.Database(':memory:');
|
|
127
|
+
// Nested finallys so `db` is closed even if the Connection ctor throws.
|
|
128
|
+
try {
|
|
129
|
+
const conn = new lbug.Connection(db);
|
|
130
|
+
try {
|
|
131
|
+
const result = await conn.query('LOAD EXTENSION fts');
|
|
132
|
+
closeProbeResults(result);
|
|
133
|
+
return { loaded: true };
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await conn.close().catch(() => { });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await db.close().catch(() => { });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
145
|
+
return { loaded: false, reason: message.replace(/\s+/g, ' ').trim() };
|
|
146
|
+
}
|
|
147
|
+
})();
|
|
148
|
+
return await Promise.race([probe, timeout]).finally(() => clearTimeout(timer));
|
|
149
|
+
}
|
|
@@ -19,7 +19,7 @@ import lbug from '@ladybugdb/core';
|
|
|
19
19
|
import { isReadOnlyDbError, loadFTSExtension } from './lbug-adapter.js';
|
|
20
20
|
import { closeQueryResults } from './query-result-utils.js';
|
|
21
21
|
import { createLbugDatabase, isWalCorruptionError, toNativeSafePath, WAL_RECOVERY_SUGGESTION, } from './lbug-config.js';
|
|
22
|
-
import { isMissingFsError, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, statIfExists, } from './sidecar-recovery.js';
|
|
22
|
+
import { guardWalQuarantine, isMissingFsError, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, statIfExists, } from './sidecar-recovery.js';
|
|
23
23
|
const pool = new Map();
|
|
24
24
|
const poolCloseListeners = new Set();
|
|
25
25
|
/**
|
|
@@ -349,6 +349,12 @@ const poolSidecarLogger = {
|
|
|
349
349
|
* See plan: docs/plans/2026-05-21-001-fix-pr-1747-quarantine-enoent-and-large-wal-plan.md (U2)
|
|
350
350
|
*/
|
|
351
351
|
async function tryQuarantineForMissingShadow(dbPath, opts) {
|
|
352
|
+
// Refuse (throw) before renaming a live WAL when the shadow is present on
|
|
353
|
+
// disk or the orphan WAL is too large — parity with the serve path's
|
|
354
|
+
// refuseLargeWalQuarantine (issue #2382 review, Finding B). Kept OUTSIDE the
|
|
355
|
+
// try so the actionable recovery message propagates to the MCP caller rather
|
|
356
|
+
// than being re-wrapped as a rename failure.
|
|
357
|
+
await guardWalQuarantine(dbPath, opts.reason, opts.err, poolSidecarLogger);
|
|
352
358
|
try {
|
|
353
359
|
const quarantinePath = await quarantineWalForMissingShadow(dbPath, {
|
|
354
360
|
logger: poolSidecarLogger,
|
|
@@ -396,6 +402,7 @@ async function replayShadowPagesWithWritableOpen(dbPath) {
|
|
|
396
402
|
if (isMissingShadowSidecarError(err)) {
|
|
397
403
|
await tryQuarantineForMissingShadow(dbPath, {
|
|
398
404
|
reason: 'pool writable replay recovery',
|
|
405
|
+
err,
|
|
399
406
|
});
|
|
400
407
|
return;
|
|
401
408
|
}
|
|
@@ -429,6 +436,7 @@ async function openReadOnlyDatabase(dbPath) {
|
|
|
429
436
|
db = undefined;
|
|
430
437
|
await tryQuarantineForMissingShadow(dbPath, {
|
|
431
438
|
reason: 'pool read-only recovery',
|
|
439
|
+
err,
|
|
432
440
|
});
|
|
433
441
|
await preflightLbugSidecars(dbPath, {
|
|
434
442
|
mode: 'read-only',
|
|
@@ -559,6 +567,7 @@ async function doInitLbug(repoId, dbPath) {
|
|
|
559
567
|
}
|
|
560
568
|
}
|
|
561
569
|
if (lastError.message.startsWith('LadybugDB checkpoint sidecar is missing') ||
|
|
570
|
+
lastError.message.startsWith('LadybugDB checkpoint sidecar is present but unreachable') ||
|
|
562
571
|
lastError.message.startsWith('GitNexus could not move the LadybugDB WAL sidecar') ||
|
|
563
572
|
isMissingShadowSidecarError(lastError)) {
|
|
564
573
|
throw lastError;
|
|
@@ -32,6 +32,16 @@ export declare const statIfExists: (filePath: string) => Promise<{
|
|
|
32
32
|
export declare const isMissingShadowSidecarError: (err: unknown) => boolean;
|
|
33
33
|
export declare const isReadOnlyShadowReplayError: (err: unknown) => boolean;
|
|
34
34
|
export declare const shadowSidecarRecoveryMessage: (dbPath: string, err: unknown) => string;
|
|
35
|
+
/**
|
|
36
|
+
* Actionable message for the case where LadybugDB reports a "missing shadow"
|
|
37
|
+
* but `inspectLbugSidecars` finds the `.shadow` PRESENT on disk — the open
|
|
38
|
+
* failed on path reachability or a lock, not a genuinely-missing sidecar (issue
|
|
39
|
+
* #2382 review, S2). Unlike `shadowSidecarRecoveryMessage` it does NOT tell the
|
|
40
|
+
* operator to rebuild the index (the remedy is fixing the lock/path). Keeps the
|
|
41
|
+
* `Original error:` tail so downstream `isMissingShadowSidecarError` recognition
|
|
42
|
+
* still matches the wrapped error.
|
|
43
|
+
*/
|
|
44
|
+
export declare const presentShadowUnreachableMessage: (dbPath: string, err: unknown) => string;
|
|
35
45
|
export declare const isPermissionRenameError: (err: unknown) => boolean;
|
|
36
46
|
/**
|
|
37
47
|
* Classify a failure surfaced by quarantine rename into an actionable user-facing
|
|
@@ -50,6 +60,28 @@ export declare const isPermissionRenameError: (err: unknown) => boolean;
|
|
|
50
60
|
*/
|
|
51
61
|
export declare const renameFailureMessage: (dbPath: string, err: unknown) => string;
|
|
52
62
|
export declare function inspectLbugSidecars(dbPath: string): Promise<LbugSidecarState>;
|
|
63
|
+
/**
|
|
64
|
+
* Reject the WAL-quarantine path when discarding the WAL would be unsafe or
|
|
65
|
+
* wrong. Shared by every reactive missing-shadow recovery consumer — serve (via
|
|
66
|
+
* lbug-adapter's `refuseLargeWalQuarantine`) and the MCP/wiki/augmentation pool
|
|
67
|
+
* (via pool-adapter's `tryQuarantineForMissingShadow`) — so the quarantine
|
|
68
|
+
* safety policy has a single source of truth (issue #2382 review, Finding B).
|
|
69
|
+
*
|
|
70
|
+
* 1. `wal-with-shadow` — the `.shadow` sidecar is PRESENT on disk. A
|
|
71
|
+
* "missing shadow" error alongside a present shadow means the open failed
|
|
72
|
+
* on path reachability or a lock (the #1811 non-ASCII path-garble on
|
|
73
|
+
* Windows), not a genuinely-missing shadow; quarantining would move a live
|
|
74
|
+
* WAL sitting next to its shadow — data loss.
|
|
75
|
+
* 2. `orphan-wal` — the orphan WAL is too large to safely discard
|
|
76
|
+
* (>TINY_ORPHAN_WAL_BYTES); preserve the uncheckpointed pages for explicit
|
|
77
|
+
* operator recovery.
|
|
78
|
+
*
|
|
79
|
+
* Throws `shadowSidecarRecoveryMessage` in either case. Returns silently only
|
|
80
|
+
* when the shadow is absent AND the WAL is absent or tiny — the states where
|
|
81
|
+
* the existing recovery path is safe to proceed. `mode` is a label used only in
|
|
82
|
+
* the warning text (e.g. 'read-only', 'writable', 'pool read-only recovery').
|
|
83
|
+
*/
|
|
84
|
+
export declare const guardWalQuarantine: (dbPath: string, mode: string, triggeringErr: unknown, logger: SidecarRecoveryLogger) => Promise<void>;
|
|
53
85
|
export declare function quarantineWalForMissingShadow(dbPath: string, options: {
|
|
54
86
|
logger: SidecarRecoveryLogger;
|
|
55
87
|
level?: 'debug' | 'info' | 'warn';
|
|
@@ -87,16 +87,47 @@ const warnOnce = (logger, key, message) => {
|
|
|
87
87
|
logger.warn(`${message} (${ordinal(next)} occurrence of this condition)`);
|
|
88
88
|
};
|
|
89
89
|
// LADYBUGDB-CONTRACT: matches @ladybugdb/core ^0.18.0 native error text.
|
|
90
|
-
// When bumping LadybugDB, re-validate this
|
|
90
|
+
// When bumping LadybugDB, re-validate this against the new error format
|
|
91
91
|
// — `git grep "LADYBUGDB-CONTRACT"` enumerates every version-coupled spot.
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
92
|
+
//
|
|
93
|
+
// Two native formats reach here for a genuinely-missing shadow sidecar:
|
|
94
|
+
// POSIX: `Cannot open file <path>.shadow: No such file or directory`
|
|
95
|
+
// Windows: `Cannot open file. path: <path>.shadow - Error 2: <system text>`
|
|
96
|
+
// Windows OS text is localized on non-English installs (issue #2382 was filed
|
|
97
|
+
// from a non-English Windows), so we key on the locale-invariant Win32 code
|
|
98
|
+
// (2 = ERROR_FILE_NOT_FOUND), NOT the English phrase. The code is matched only
|
|
99
|
+
// in the reason AFTER the LAST `.shadow` token (the real failing sidecar; the
|
|
100
|
+
// reason text never contains `.shadow`), so a repo *path* containing e.g.
|
|
101
|
+
// `\error 2\` — even under a `.shadow`-suffixed parent directory — cannot trip
|
|
102
|
+
// it. Deliberate exclusions:
|
|
103
|
+
// - `Error 3` (ERROR_PATH_NOT_FOUND): the #1811 non-ASCII path-garble
|
|
104
|
+
// artifact (see lbug-config.ts) where the shadow is PRESENT on disk;
|
|
105
|
+
// treating it as missing would quarantine a live WAL — data loss.
|
|
106
|
+
// - `Error 5` / `Error 32` / POSIX `Permission denied`: present-but-locked;
|
|
107
|
+
// handled as permission/lock classes, must not quarantine.
|
|
108
|
+
// The quarantine path adds a present-shadow disk check as a belt (see
|
|
109
|
+
// refuseLargeWalQuarantine in lbug-adapter.ts).
|
|
110
|
+
//
|
|
111
|
+
// The Windows branch is derived from the issue #2382 reported string, not a
|
|
112
|
+
// self-produced live crash; unit/consumer tests inject that same string, so
|
|
113
|
+
// GREEN TESTS DO NOT PROVE the byte-exact 0.18.0 Windows format — confirm
|
|
114
|
+
// against a real Windows run before closing #2382.
|
|
97
115
|
export const isMissingShadowSidecarError = (err) => {
|
|
98
116
|
const msg = err instanceof Error ? err.message : String(err);
|
|
99
|
-
|
|
117
|
+
if (!/cannot open file/i.test(msg))
|
|
118
|
+
return false;
|
|
119
|
+
// Anchor on the LAST `.shadow`, not the first: LadybugDB names the failing
|
|
120
|
+
// sidecar as the final `.shadow` token and its reason text (POSIX
|
|
121
|
+
// `: No such file or directory` / Windows ` - Error N: ...`) never contains
|
|
122
|
+
// `.shadow`. Slicing from the last match isolates the true reason, so an
|
|
123
|
+
// earlier `.shadow`-suffixed path segment (e.g. a `branch=subdir` directory
|
|
124
|
+
// like `snap.shadow\`) can't shift the anchor and let a path-embedded
|
|
125
|
+
// `error 2` be read as the Win32 code (issue #2382 review, Finding A).
|
|
126
|
+
const lastShadow = [...msg.matchAll(/\.shadow\b/gi)].at(-1);
|
|
127
|
+
if (lastShadow?.index === undefined)
|
|
128
|
+
return false;
|
|
129
|
+
const reason = msg.slice(lastShadow.index);
|
|
130
|
+
return /no such file or directory/i.test(reason) || /\berror\s+2\b/i.test(reason);
|
|
100
131
|
};
|
|
101
132
|
// LADYBUGDB-CONTRACT: matches @ladybugdb/core ^0.18.0 native error text.
|
|
102
133
|
// When bumping LadybugDB, re-validate this regex against the new error format
|
|
@@ -114,6 +145,24 @@ export const shadowSidecarRecoveryMessage = (dbPath, err) => {
|
|
|
114
145
|
'Rebuild the index with `gitnexus analyze --force <repo-path> --index-only` and restart `gitnexus serve`.' +
|
|
115
146
|
`\n Original error: ${msg.slice(0, 200)}`);
|
|
116
147
|
};
|
|
148
|
+
/**
|
|
149
|
+
* Actionable message for the case where LadybugDB reports a "missing shadow"
|
|
150
|
+
* but `inspectLbugSidecars` finds the `.shadow` PRESENT on disk — the open
|
|
151
|
+
* failed on path reachability or a lock, not a genuinely-missing sidecar (issue
|
|
152
|
+
* #2382 review, S2). Unlike `shadowSidecarRecoveryMessage` it does NOT tell the
|
|
153
|
+
* operator to rebuild the index (the remedy is fixing the lock/path). Keeps the
|
|
154
|
+
* `Original error:` tail so downstream `isMissingShadowSidecarError` recognition
|
|
155
|
+
* still matches the wrapped error.
|
|
156
|
+
*/
|
|
157
|
+
export const presentShadowUnreachableMessage = (dbPath, err) => {
|
|
158
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
159
|
+
return (`LadybugDB checkpoint sidecar is present but unreachable for ${dbPath}. ` +
|
|
160
|
+
'The .shadow file is on disk, so the open likely failed on path reachability or a file lock ' +
|
|
161
|
+
'(antivirus, another process holding a handle, or a non-ASCII path) rather than a missing sidecar. ' +
|
|
162
|
+
'Check filesystem access and locks; only run `gitnexus analyze --force <repo-path> --index-only` ' +
|
|
163
|
+
'if the index is genuinely broken.' +
|
|
164
|
+
`\n Original error: ${msg.slice(0, 200)}`);
|
|
165
|
+
};
|
|
117
166
|
const PERMISSION_RENAME_CODES = new Set(['EACCES', 'EPERM', 'EBUSY']);
|
|
118
167
|
export const isPermissionRenameError = (err) => {
|
|
119
168
|
const code = err?.code;
|
|
@@ -165,6 +214,41 @@ export async function inspectLbugSidecars(dbPath) {
|
|
|
165
214
|
}
|
|
166
215
|
return { kind: 'clean', dbPath };
|
|
167
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Reject the WAL-quarantine path when discarding the WAL would be unsafe or
|
|
219
|
+
* wrong. Shared by every reactive missing-shadow recovery consumer — serve (via
|
|
220
|
+
* lbug-adapter's `refuseLargeWalQuarantine`) and the MCP/wiki/augmentation pool
|
|
221
|
+
* (via pool-adapter's `tryQuarantineForMissingShadow`) — so the quarantine
|
|
222
|
+
* safety policy has a single source of truth (issue #2382 review, Finding B).
|
|
223
|
+
*
|
|
224
|
+
* 1. `wal-with-shadow` — the `.shadow` sidecar is PRESENT on disk. A
|
|
225
|
+
* "missing shadow" error alongside a present shadow means the open failed
|
|
226
|
+
* on path reachability or a lock (the #1811 non-ASCII path-garble on
|
|
227
|
+
* Windows), not a genuinely-missing shadow; quarantining would move a live
|
|
228
|
+
* WAL sitting next to its shadow — data loss.
|
|
229
|
+
* 2. `orphan-wal` — the orphan WAL is too large to safely discard
|
|
230
|
+
* (>TINY_ORPHAN_WAL_BYTES); preserve the uncheckpointed pages for explicit
|
|
231
|
+
* operator recovery.
|
|
232
|
+
*
|
|
233
|
+
* Throws `shadowSidecarRecoveryMessage` in either case. Returns silently only
|
|
234
|
+
* when the shadow is absent AND the WAL is absent or tiny — the states where
|
|
235
|
+
* the existing recovery path is safe to proceed. `mode` is a label used only in
|
|
236
|
+
* the warning text (e.g. 'read-only', 'writable', 'pool read-only recovery').
|
|
237
|
+
*/
|
|
238
|
+
export const guardWalQuarantine = async (dbPath, mode, triggeringErr, logger) => {
|
|
239
|
+
const state = await inspectLbugSidecars(dbPath);
|
|
240
|
+
if (state.kind === 'wal-with-shadow') {
|
|
241
|
+
warnOnce(logger, `${dbPath}:present-shadow-refuse:${mode}`, `GitNexus: refusing to quarantine WAL at ${dbPath}.wal during ${mode} recovery — ` +
|
|
242
|
+
'the .shadow sidecar is present on disk, so the open likely failed on path reachability or a lock ' +
|
|
243
|
+
'rather than a missing shadow. Run `gitnexus analyze --force <repo-path> --index-only` if the index is genuinely broken.');
|
|
244
|
+
throw new Error(presentShadowUnreachableMessage(dbPath, triggeringErr));
|
|
245
|
+
}
|
|
246
|
+
if (state.kind === 'orphan-wal') {
|
|
247
|
+
warnOnce(logger, `${dbPath}:large-wal-refuse:${mode}`, `GitNexus: refusing to quarantine large WAL (${state.walBytes} bytes) at ${dbPath}.wal during ${mode} recovery; ` +
|
|
248
|
+
'manual recovery required — run `gitnexus analyze --force <repo-path> --index-only`.');
|
|
249
|
+
throw new Error(shadowSidecarRecoveryMessage(dbPath, triggeringErr));
|
|
250
|
+
}
|
|
251
|
+
};
|
|
168
252
|
export async function quarantineWalForMissingShadow(dbPath, options) {
|
|
169
253
|
const walPath = `${dbPath}.wal`;
|
|
170
254
|
const quarantinePath = `${walPath}.missing-shadow.${Date.now()}-${Math.random()
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
1
2
|
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
2
4
|
import { createRequire } from 'module';
|
|
3
5
|
const require = createRequire(import.meta.url);
|
|
4
6
|
const packageVersion = (name) => {
|
|
@@ -6,7 +8,31 @@ const packageVersion = (name) => {
|
|
|
6
8
|
return require(`${name}/package.json`).version;
|
|
7
9
|
}
|
|
8
10
|
catch {
|
|
9
|
-
|
|
11
|
+
// Packages whose `exports` map omits ./package.json (e.g. @ladybugdb/core)
|
|
12
|
+
// reject the direct require with ERR_PACKAGE_PATH_NOT_EXPORTED, which made
|
|
13
|
+
// doctor print "LadybugDB: unknown" on every platform (#2374). Resolve the
|
|
14
|
+
// entry point instead and walk up to the package's own package.json.
|
|
15
|
+
try {
|
|
16
|
+
let dir = path.dirname(require.resolve(name));
|
|
17
|
+
// Entry points sit at the package root or a shallow dist/ dir; a few
|
|
18
|
+
// hops always reach the package's own package.json.
|
|
19
|
+
for (let hops = 0; hops < 5; hops++) {
|
|
20
|
+
const candidate = path.join(dir, 'package.json');
|
|
21
|
+
if (fs.existsSync(candidate)) {
|
|
22
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
23
|
+
if (pkg.name === name)
|
|
24
|
+
return pkg.version;
|
|
25
|
+
}
|
|
26
|
+
const parent = path.dirname(dir);
|
|
27
|
+
if (parent === dir)
|
|
28
|
+
break;
|
|
29
|
+
dir = parent;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
10
36
|
}
|
|
11
37
|
};
|
|
12
38
|
const gitnexusVersion = () => {
|
package/dist/core/run-analyze.js
CHANGED
|
@@ -16,7 +16,8 @@ import { resetDegradedParseCounter } from './tree-sitter/safe-parse.js';
|
|
|
16
16
|
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings, deleteNodesForFile, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, deleteAllCallSummaries, deleteAllInjects, queryImporters, loadFTSExtension, } from './lbug/lbug-adapter.js';
|
|
17
17
|
import { createSearchFTSIndexes, initialiseSearchFTSStemmer, verifySearchFTSIndexes, } from './search/fts-indexes.js';
|
|
18
18
|
import { cjkSegmentationModeMismatch, getSearchFTSCjkSegmentation, initialiseSearchFTSCjkSegmentation, } from './search/cjk-segmentation.js';
|
|
19
|
-
import { resolveAnalyzeInstallPolicy } from './lbug/extension-loader.js';
|
|
19
|
+
import { getExtensionCapabilities, resolveAnalyzeInstallPolicy } from './lbug/extension-loader.js';
|
|
20
|
+
import { diagnoseExtensionLoad } from './lbug/extension-load-error.js';
|
|
20
21
|
import { startWalCheckpointDriver, } from './lbug/wal-checkpoint-driver.js';
|
|
21
22
|
import { getStoragePaths, resolveBranchPlacement, saveMeta, loadMeta, ensureGitNexusIgnored, registerRepo, adoptFlatBranchLabel, isReadOnlyFilesystemError, isRepoRegistered, cleanupOldKuzuFiles, reconcileMetadataFiles, isMissingFilesystemError, INDEX_METADATA_FILE, INCREMENTAL_SCHEMA_VERSION, } from '../storage/repo-manager.js';
|
|
22
23
|
import { DEFAULT_PDG_MAX_FUNCTION_LINES } from './ingestion/cfg/collect.js';
|
|
@@ -41,7 +42,11 @@ import { STALE_HASH_SENTINEL } from './lbug/schema.js';
|
|
|
41
42
|
* a full analyze. Kept as a named constant so the env-var/command guidance
|
|
42
43
|
* stays in one place (mirrors the VECTOR message in embedding-pipeline.ts).
|
|
43
44
|
*/
|
|
44
|
-
|
|
45
|
+
// Class-neutral lead, reused for the missing-dependency degrade path (#2383 F2):
|
|
46
|
+
// its remedy already explains that reinstalling will NOT help, so appending the
|
|
47
|
+
// generic "install with network access" tail below would contradict it.
|
|
48
|
+
const FTS_UNAVAILABLE_LEAD = 'FTS extension unavailable; skipping search-index creation.';
|
|
49
|
+
const FTS_UNAVAILABLE_MESSAGE = `${FTS_UNAVAILABLE_LEAD} ` +
|
|
45
50
|
'Full-text/BM25 search will be disabled until the LadybugDB FTS extension is ' +
|
|
46
51
|
'installed once with network access (GITNEXUS_LBUG_EXTENSION_INSTALL=auto) or ' +
|
|
47
52
|
'pre-installed for offline use. Run `gitnexus doctor` for details.';
|
|
@@ -365,9 +370,23 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
365
370
|
policy: resolveAnalyzeInstallPolicy(),
|
|
366
371
|
});
|
|
367
372
|
if (!repairFtsAvailable) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
373
|
+
// Surface the load-side reason (#2374): "not pre-installed" was wrong
|
|
374
|
+
// and doctor never installed anything, so the old message trapped
|
|
375
|
+
// users in a query → repair-fts → doctor loop with no way out.
|
|
376
|
+
const rawFtsReason = getExtensionCapabilities().find((c) => c.name === 'fts')?.reason;
|
|
377
|
+
const ftsReason = rawFtsReason?.replace(/\.$/, '');
|
|
378
|
+
// A missing runtime dependency (Windows error 126, #2374) is not healed
|
|
379
|
+
// by re-installing — the file is already present. Route that class to the
|
|
380
|
+
// classified remedy (install VC++ redist / OpenSSL) instead of the old
|
|
381
|
+
// "retry the network install" text that trapped the user in a loop.
|
|
382
|
+
const { kind, remedy } = diagnoseExtensionLoad(rawFtsReason);
|
|
383
|
+
const remedyTail = kind === 'missing_dependency'
|
|
384
|
+
? ` ${remedy}`
|
|
385
|
+
: '. Retry with network access and GITNEXUS_LBUG_EXTENSION_INSTALL=auto to install it, ' +
|
|
386
|
+
'or pre-install the extension file; run `gitnexus doctor` for live FTS status.';
|
|
387
|
+
throw new Error('Cannot repair FTS indexes: the LadybugDB FTS extension failed to load' +
|
|
388
|
+
(ftsReason ? ` — ${ftsReason}` : '') +
|
|
389
|
+
remedyTail);
|
|
371
390
|
}
|
|
372
391
|
progress('fts', 85, 'Repairing search indexes...');
|
|
373
392
|
await createSearchFTSIndexes({
|
|
@@ -956,7 +975,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
956
975
|
progress('fts', 90, 'Search indexes ready');
|
|
957
976
|
}
|
|
958
977
|
else {
|
|
959
|
-
|
|
978
|
+
// For a missing runtime dependency (#2374) the file is present, so the
|
|
979
|
+
// generic "install it with network access" tail in FTS_UNAVAILABLE_MESSAGE
|
|
980
|
+
// contradicts the remedy's own "reinstalling will NOT help" (#2383 F2). Lead
|
|
981
|
+
// with the class-neutral sentence and append only the classified remedy.
|
|
982
|
+
const ftsReason = getExtensionCapabilities().find((c) => c.name === 'fts')?.reason;
|
|
983
|
+
const { kind, remedy } = diagnoseExtensionLoad(ftsReason);
|
|
984
|
+
log(kind === 'missing_dependency'
|
|
985
|
+
? `${FTS_UNAVAILABLE_LEAD} ${remedy}`
|
|
986
|
+
: FTS_UNAVAILABLE_MESSAGE);
|
|
960
987
|
progress('fts', 90, 'Search indexes skipped (FTS unavailable)');
|
|
961
988
|
}
|
|
962
989
|
// ── Phase 3.5: Re-insert cached embeddings ────────────────────────
|