gitnexus 1.6.10-rc.220 → 1.6.10-rc.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/ignore-service.d.ts +2 -0
- package/dist/config/ignore-service.js +29 -7
- package/dist/core/group/extractors/python-workspace-extractor.js +5 -2
- package/dist/core/ingestion/filesystem-walker.js +18 -2
- package/dist/core/ingestion/import-resolvers/node-workspace-packages.js +4 -3
- package/dist/core/ingestion/languages/typescript/tsconfig.js +4 -3
- package/dist/core/lbug/pool-adapter.d.ts +9 -0
- package/dist/core/lbug/pool-adapter.js +154 -14
- package/dist/mcp/local/local-backend.d.ts +5 -5
- package/dist/mcp/local/local-backend.js +43 -34
- package/package.json +1 -1
|
@@ -3,6 +3,8 @@ import type { Path } from 'path-scurry';
|
|
|
3
3
|
export declare const shouldIgnorePath: (filePath: string) => boolean;
|
|
4
4
|
/** Check if a directory name is in the hardcoded ignore list */
|
|
5
5
|
export declare const isHardcodedIgnoredDirectory: (name: string) => boolean;
|
|
6
|
+
/** Apply directory ignore rules that depend on repository-relative depth. */
|
|
7
|
+
export declare const isHardcodedIgnoredDirectoryAtPath: (repoRoot: string, directoryPath: string) => boolean;
|
|
6
8
|
/**
|
|
7
9
|
* Load .gitignore and .gitnexusignore rules from the repo root.
|
|
8
10
|
* Returns an `ignore` instance with all patterns, or null if no files found.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ignore from 'ignore';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
2
3
|
import fs from 'fs/promises';
|
|
3
4
|
import nodePath from 'path';
|
|
4
5
|
import { logger } from '../core/logger.js';
|
|
@@ -27,12 +28,15 @@ const DEFAULT_IGNORE_LIST = new Set([
|
|
|
27
28
|
// 'packages' removed - commonly used for monorepo source code (lerna, pnpm, yarn workspaces)
|
|
28
29
|
'venv',
|
|
29
30
|
'.venv',
|
|
30
|
-
'env',
|
|
31
31
|
'.env',
|
|
32
|
+
// Bare `env/` can be application source or a Python virtual environment.
|
|
33
|
+
// Path-aware rules below prune it at the root and wherever pyvenv.cfg marks
|
|
34
|
+
// a virtual environment, while preserving ordinary nested source folders.
|
|
32
35
|
'__pycache__',
|
|
33
36
|
'.pytest_cache',
|
|
34
37
|
'.mypy_cache',
|
|
35
38
|
'site-packages',
|
|
39
|
+
'dist-packages',
|
|
36
40
|
'.tox',
|
|
37
41
|
'eggs',
|
|
38
42
|
'.eggs',
|
|
@@ -78,8 +82,9 @@ const DEFAULT_IGNORE_LIST = new Set([
|
|
|
78
82
|
'.temp',
|
|
79
83
|
// Generated/Compiled
|
|
80
84
|
'.generated',
|
|
81
|
-
'generated',
|
|
82
85
|
'auto-generated',
|
|
86
|
+
// Bare `generated/` can contain tracked source-of-truth code. Build output
|
|
87
|
+
// remains covered by .gitignore/.gitnexusignore and the unambiguous names.
|
|
83
88
|
'monaco-workers', // Monaco editor web-worker bundles generated for browser runtime
|
|
84
89
|
'.terraform',
|
|
85
90
|
'.serverless',
|
|
@@ -95,6 +100,11 @@ const DEFAULT_IGNORE_LIST = new Set([
|
|
|
95
100
|
'snapshots', // Jest snapshots
|
|
96
101
|
'__snapshots__',
|
|
97
102
|
]);
|
|
103
|
+
// Ambiguous names that conventionally denote generated artifacts only at the
|
|
104
|
+
// repository root. Nested directories with these names are frequently source
|
|
105
|
+
// modules (for example apps/web/src/env or packages/api/generated).
|
|
106
|
+
const ROOT_ARTIFACT_DIRECTORIES = new Set(['env', 'generated']);
|
|
107
|
+
const isRootArtifactDirectory = (relativePath, name) => !relativePath.includes('/') && ROOT_ARTIFACT_DIRECTORIES.has(name);
|
|
98
108
|
const IGNORED_EXTENSIONS = new Set([
|
|
99
109
|
// Images
|
|
100
110
|
'.png',
|
|
@@ -264,6 +274,9 @@ export const shouldIgnorePath = (filePath) => {
|
|
|
264
274
|
const parts = normalizedPath.split('/');
|
|
265
275
|
const fileName = parts[parts.length - 1];
|
|
266
276
|
const fileNameLower = fileName.toLowerCase();
|
|
277
|
+
if (parts.length > 0 && isRootArtifactDirectory(parts[0], parts[0])) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
267
280
|
// Laravel compiles Blade templates into generated PHP cache files under
|
|
268
281
|
// storage/framework/views. Source templates live in resources/views and are
|
|
269
282
|
// handled separately; compiled cache should not become source-of-truth. Keep
|
|
@@ -299,9 +312,7 @@ export const shouldIgnorePath = (filePath) => {
|
|
|
299
312
|
// Ignore files that look like generated/bundled code
|
|
300
313
|
if (fileNameLower.includes('.bundle.') ||
|
|
301
314
|
fileNameLower.includes('.chunk.') ||
|
|
302
|
-
fileNameLower.includes('.generated.')
|
|
303
|
-
fileNameLower.endsWith('.d.ts')) {
|
|
304
|
-
// TypeScript declaration files
|
|
315
|
+
fileNameLower.includes('.generated.')) {
|
|
305
316
|
return true;
|
|
306
317
|
}
|
|
307
318
|
return false;
|
|
@@ -310,6 +321,16 @@ export const shouldIgnorePath = (filePath) => {
|
|
|
310
321
|
export const isHardcodedIgnoredDirectory = (name) => {
|
|
311
322
|
return DEFAULT_IGNORE_LIST.has(name);
|
|
312
323
|
};
|
|
324
|
+
/** Apply directory ignore rules that depend on repository-relative depth. */
|
|
325
|
+
export const isHardcodedIgnoredDirectoryAtPath = (repoRoot, directoryPath) => {
|
|
326
|
+
const name = nodePath.basename(directoryPath);
|
|
327
|
+
if (isHardcodedIgnoredDirectory(name))
|
|
328
|
+
return true;
|
|
329
|
+
const relative = nodePath.relative(repoRoot, directoryPath).replace(/\\/g, '/');
|
|
330
|
+
if (isRootArtifactDirectory(relative, name))
|
|
331
|
+
return true;
|
|
332
|
+
return name === 'env' && existsSync(nodePath.join(directoryPath, 'pyvenv.cfg'));
|
|
333
|
+
};
|
|
313
334
|
export const loadIgnoreRules = async (repoPath, options) => {
|
|
314
335
|
const ig = ignore();
|
|
315
336
|
let hasRules = false;
|
|
@@ -450,9 +471,10 @@ export const createIgnoreFilter = async (repoPath, options) => {
|
|
|
450
471
|
// blocks descent into `__tests__/generated/`.
|
|
451
472
|
if (ig && rel && hasExplicitUnignore(ig, rel) && !ig.ignores(rel + '/'))
|
|
452
473
|
return false;
|
|
453
|
-
// Hardcoded
|
|
454
|
-
if (
|
|
474
|
+
// Hardcoded and path-aware rules prune whole trees before glob walks them.
|
|
475
|
+
if (rel && isHardcodedIgnoredDirectoryAtPath(repoPath, nodePath.join(repoPath, rel))) {
|
|
455
476
|
return true;
|
|
477
|
+
}
|
|
456
478
|
// Check against .gitignore / .gitnexusignore patterns.
|
|
457
479
|
// Since childrenIgnored is only called for directories, always test with
|
|
458
480
|
// a trailing slash. This ensures directory-only negation patterns (e.g.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js';
|
|
3
|
+
import { shouldIgnorePath, loadIgnoreRules, isHardcodedIgnoredDirectoryAtPath, } from '../../../config/ignore-service.js';
|
|
4
4
|
import { logger } from '../../logger.js';
|
|
5
5
|
async function parsePythonManifest(repoPath) {
|
|
6
6
|
const pyprojectPath = path.join(repoPath, 'pyproject.toml');
|
|
@@ -124,11 +124,14 @@ async function findPythonFiles(repoPath) {
|
|
|
124
124
|
for (const entry of entries) {
|
|
125
125
|
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
126
126
|
if (entry.isDirectory()) {
|
|
127
|
+
const childPath = path.join(dir, entry.name);
|
|
127
128
|
if (shouldIgnorePath(childRel))
|
|
128
129
|
continue;
|
|
130
|
+
if (isHardcodedIgnoredDirectoryAtPath(repoPath, childPath))
|
|
131
|
+
continue;
|
|
129
132
|
if (ig && ig.ignores(childRel + '/'))
|
|
130
133
|
continue;
|
|
131
|
-
await walk(
|
|
134
|
+
await walk(childPath, childRel);
|
|
132
135
|
}
|
|
133
136
|
else if (entry.name.endsWith('.py')) {
|
|
134
137
|
if (shouldIgnorePath(childRel))
|
|
@@ -8,6 +8,20 @@ import { mapConcurrent } from '../../lib/utils.js';
|
|
|
8
8
|
import { logger } from '../logger.js';
|
|
9
9
|
const READ_CONCURRENCY = 32;
|
|
10
10
|
const ANALYZE_PROGRESS_ACTIVE_ENV = 'GITNEXUS_ANALYZE_PROGRESS_ACTIVE';
|
|
11
|
+
const DECLARATION_COMPANION_SUFFIXES = [
|
|
12
|
+
{ declaration: '.d.ts', implementations: ['.ts', '.tsx'] },
|
|
13
|
+
{ declaration: '.d.mts', implementations: ['.mts'] },
|
|
14
|
+
{ declaration: '.d.cts', implementations: ['.cts'] },
|
|
15
|
+
];
|
|
16
|
+
const hasImplementationSibling = (declarationPath, scannedPaths) => {
|
|
17
|
+
const companion = DECLARATION_COMPANION_SUFFIXES.find(({ declaration }) => declarationPath.endsWith(declaration));
|
|
18
|
+
if (!companion)
|
|
19
|
+
return false;
|
|
20
|
+
// Keep standalone declarations. Only suppress declaration output that sits
|
|
21
|
+
// beside an implementation with the corresponding module suffix.
|
|
22
|
+
const stem = declarationPath.slice(0, -companion.declaration.length);
|
|
23
|
+
return companion.implementations.some((suffix) => scannedPaths.has(`${stem}${suffix}`));
|
|
24
|
+
};
|
|
11
25
|
const warnLargeFileSkip = (message) => {
|
|
12
26
|
if (process.env[ANALYZE_PROGRESS_ACTIVE_ENV] === '1') {
|
|
13
27
|
// analyze.ts routes console.warn through the progress bar logger while
|
|
@@ -61,10 +75,12 @@ export const walkRepositoryPaths = async (repoPath, onProgress) => {
|
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
77
|
}
|
|
78
|
+
const scannedPaths = new Set(entries.map((entry) => entry.path));
|
|
79
|
+
const deduplicatedEntries = entries.filter((entry) => !hasImplementationSibling(entry.path, scannedPaths));
|
|
64
80
|
// Filesystem/glob traversal order is not stable across filesystems or repeated
|
|
65
81
|
// scans. Canonicalize once at the scan boundary so every downstream phase sees
|
|
66
82
|
// the same repository order.
|
|
67
|
-
|
|
83
|
+
deduplicatedEntries.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
68
84
|
if (skippedLarge > 0) {
|
|
69
85
|
const isDefault = maxFileSizeBytes === DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
70
86
|
const isOverrideUnset = !process.env.GITNEXUS_MAX_FILE_SIZE;
|
|
@@ -93,7 +109,7 @@ export const walkRepositoryPaths = async (repoPath, onProgress) => {
|
|
|
93
109
|
warnLargeFileSkip(` Set GITNEXUS_MAX_FILE_SIZE=<KB> to include files above the default cap.`);
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
|
-
return
|
|
112
|
+
return deduplicatedEntries;
|
|
97
113
|
};
|
|
98
114
|
/**
|
|
99
115
|
* Phase 2: Read file contents for a specific set of relative paths.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import fs from 'fs/promises';
|
|
18
18
|
import path from 'path';
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
|
-
import {
|
|
20
|
+
import { isHardcodedIgnoredDirectoryAtPath } from '../../../config/ignore-service.js';
|
|
21
21
|
import { logger } from '../../logger.js';
|
|
22
22
|
import { resolveFile } from '../languages/typescript/file-candidates.js';
|
|
23
23
|
// `js-yaml` is CJS; the rest of this repository reaches it the same way
|
|
@@ -285,10 +285,11 @@ export async function loadNodeWorkspacePackages(repoRoot) {
|
|
|
285
285
|
}
|
|
286
286
|
for (const entry of entries) {
|
|
287
287
|
if (entry.isDirectory()) {
|
|
288
|
-
|
|
288
|
+
const childDir = path.join(dir, entry.name);
|
|
289
|
+
if (isHardcodedIgnoredDirectoryAtPath(repoRoot, childDir))
|
|
289
290
|
continue;
|
|
290
291
|
if (depth < SCAN_MAX_DEPTH) {
|
|
291
|
-
queue.push({ dir:
|
|
292
|
+
queue.push({ dir: childDir, depth: depth + 1 });
|
|
292
293
|
}
|
|
293
294
|
continue;
|
|
294
295
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import fs from 'fs/promises';
|
|
23
23
|
import path from 'path';
|
|
24
|
-
import {
|
|
24
|
+
import { isHardcodedIgnoredDirectoryAtPath } from '../../../../config/ignore-service.js';
|
|
25
25
|
import { logger } from '../../../logger.js';
|
|
26
26
|
const SCAN_MAX_DIRS = 20_000;
|
|
27
27
|
const SCAN_MAX_DEPTH = 24;
|
|
@@ -247,10 +247,11 @@ async function findTsconfigFiles(repoRoot) {
|
|
|
247
247
|
}
|
|
248
248
|
for (const entry of entries) {
|
|
249
249
|
if (entry.isDirectory()) {
|
|
250
|
-
|
|
250
|
+
const childDir = path.join(dir, entry.name);
|
|
251
|
+
if (isHardcodedIgnoredDirectoryAtPath(repoRoot, childDir))
|
|
251
252
|
continue;
|
|
252
253
|
if (depth < SCAN_MAX_DEPTH)
|
|
253
|
-
queue.push({ dir:
|
|
254
|
+
queue.push({ dir: childDir, depth: depth + 1 });
|
|
254
255
|
continue;
|
|
255
256
|
}
|
|
256
257
|
if (!entry.isFile())
|
|
@@ -122,6 +122,15 @@ export declare const initLbug: (repoId: string, dbPath: string) => Promise<boole
|
|
|
122
122
|
* repoId already injected it), the existing entry is reused.
|
|
123
123
|
*/
|
|
124
124
|
export declare function initLbugWithDb(repoId: string, existingDb: lbug.Database, dbPath: string): Promise<void>;
|
|
125
|
+
/**
|
|
126
|
+
* Lazily load VECTOR for a semantic query.
|
|
127
|
+
*
|
|
128
|
+
* Exact graph reads never call this function, so opening their read pool does
|
|
129
|
+
* not probe or warn about an optional extension they do not use. The promise
|
|
130
|
+
* lives on SharedDB because extension scope is per Database, and also joins
|
|
131
|
+
* concurrent first semantic requests onto one LOAD attempt.
|
|
132
|
+
*/
|
|
133
|
+
export declare function ensureVectorExtension(repoId: string): Promise<boolean>;
|
|
125
134
|
export declare const executeQuery: (repoId: string, cypher: string) => Promise<any[]>;
|
|
126
135
|
/**
|
|
127
136
|
* Execute a parameterized query on a specific repo's connection pool.
|
|
@@ -265,6 +265,7 @@ function closeOne(repoId) {
|
|
|
265
265
|
shared.refCount = 0;
|
|
266
266
|
shared.ftsLoaded = false;
|
|
267
267
|
shared.vectorLoaded = false;
|
|
268
|
+
shared.vectorLoadPromise = undefined;
|
|
268
269
|
}
|
|
269
270
|
else {
|
|
270
271
|
shared.db.close().catch(() => { });
|
|
@@ -692,13 +693,6 @@ async function doInitLbug(repoId, dbPath) {
|
|
|
692
693
|
if (!shared.ftsLoaded) {
|
|
693
694
|
shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
|
|
694
695
|
}
|
|
695
|
-
// VECTOR too — extension load scope is per-Database, so this one load
|
|
696
|
-
// makes QUERY_VECTOR_INDEX legal on every pooled connection. Same
|
|
697
|
-
// load-only contract as FTS above; on failure the semantic-query lane
|
|
698
|
-
// falls back to the exact scan with its own diagnostic (#2623 follow-up).
|
|
699
|
-
if (!shared.vectorLoaded) {
|
|
700
|
-
shared.vectorLoaded = await loadVectorExtension(available[0], { policy: 'load-only' });
|
|
701
|
-
}
|
|
702
696
|
// Register pool entry only after all connections are pre-warmed and FTS is
|
|
703
697
|
// loaded. Concurrent executeQuery calls see either "not initialized"
|
|
704
698
|
// (and throw cleanly) or a fully ready pool — never a half-built one.
|
|
@@ -761,11 +755,6 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
|
|
|
761
755
|
if (!shared.ftsLoaded) {
|
|
762
756
|
shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
|
|
763
757
|
}
|
|
764
|
-
// VECTOR too — same per-Database scope and load-only contract as the
|
|
765
|
-
// doInitLbug site above (#2623 follow-up).
|
|
766
|
-
if (!shared.vectorLoaded) {
|
|
767
|
-
shared.vectorLoaded = await loadVectorExtension(available[0], { policy: 'load-only' });
|
|
768
|
-
}
|
|
769
758
|
pool.set(repoId, {
|
|
770
759
|
db: existingDb,
|
|
771
760
|
available,
|
|
@@ -780,6 +769,143 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
|
|
|
780
769
|
ensureIdleTimer();
|
|
781
770
|
traceRss('init', repoId);
|
|
782
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Lazily load VECTOR for a semantic query.
|
|
774
|
+
*
|
|
775
|
+
* Exact graph reads never call this function, so opening their read pool does
|
|
776
|
+
* not probe or warn about an optional extension they do not use. The promise
|
|
777
|
+
* lives on SharedDB because extension scope is per Database, and also joins
|
|
778
|
+
* concurrent first semantic requests onto one LOAD attempt.
|
|
779
|
+
*/
|
|
780
|
+
export async function ensureVectorExtension(repoId) {
|
|
781
|
+
const entry = pool.get(repoId);
|
|
782
|
+
if (!entry) {
|
|
783
|
+
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
784
|
+
}
|
|
785
|
+
const shared = dbCache.get(entry.dbPath);
|
|
786
|
+
if (!shared) {
|
|
787
|
+
throw new Error(`LadybugDB shared handle is unavailable for repo "${repoId}".`);
|
|
788
|
+
}
|
|
789
|
+
if (shared.vectorLoaded)
|
|
790
|
+
return true;
|
|
791
|
+
if (shared.vectorLoadPromise)
|
|
792
|
+
return shared.vectorLoadPromise;
|
|
793
|
+
const loadAttempt = (async () => {
|
|
794
|
+
const conn = await checkout(entry);
|
|
795
|
+
try {
|
|
796
|
+
const loaded = await loadVectorExtension(conn, { policy: 'load-only' });
|
|
797
|
+
shared.vectorLoaded = loaded;
|
|
798
|
+
return loaded;
|
|
799
|
+
}
|
|
800
|
+
finally {
|
|
801
|
+
checkin(entry, conn);
|
|
802
|
+
}
|
|
803
|
+
})();
|
|
804
|
+
const cachedAttempt = loadAttempt.catch((err) => {
|
|
805
|
+
// A transient checkout/load failure must not poison this Database for the
|
|
806
|
+
// rest of its lifetime. Keep resolved false cached, but let a later
|
|
807
|
+
// semantic request retry a rejected attempt.
|
|
808
|
+
if (shared.vectorLoadPromise === cachedAttempt) {
|
|
809
|
+
shared.vectorLoadPromise = undefined;
|
|
810
|
+
}
|
|
811
|
+
throw err;
|
|
812
|
+
});
|
|
813
|
+
shared.vectorLoadPromise = cachedAttempt;
|
|
814
|
+
return shared.vectorLoadPromise;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Detect an actual VECTOR procedure call without treating source text stored in
|
|
818
|
+
* Cypher literals or comments as executable syntax.
|
|
819
|
+
*/
|
|
820
|
+
function callsVectorIndex(cypher) {
|
|
821
|
+
if (!/QUERY_VECTOR_INDEX/i.test(cypher))
|
|
822
|
+
return false;
|
|
823
|
+
let code = '';
|
|
824
|
+
let state = 'code';
|
|
825
|
+
let backtickIdentifier = '';
|
|
826
|
+
for (let i = 0; i < cypher.length; i++) {
|
|
827
|
+
const ch = cypher[i];
|
|
828
|
+
const next = cypher[i + 1];
|
|
829
|
+
if (state === 'code') {
|
|
830
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
831
|
+
state = ch === "'" ? 'single' : ch === '"' ? 'double' : 'backtick';
|
|
832
|
+
if (state === 'backtick')
|
|
833
|
+
backtickIdentifier = '';
|
|
834
|
+
code += ' ';
|
|
835
|
+
}
|
|
836
|
+
else if (ch === '/' && next === '/') {
|
|
837
|
+
state = 'line-comment';
|
|
838
|
+
code += ' ';
|
|
839
|
+
i++;
|
|
840
|
+
}
|
|
841
|
+
else if (ch === '/' && next === '*') {
|
|
842
|
+
state = 'block-comment';
|
|
843
|
+
code += ' ';
|
|
844
|
+
i++;
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
code += ch;
|
|
848
|
+
}
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (state === 'line-comment') {
|
|
852
|
+
if (ch === '\n' || ch === '\r') {
|
|
853
|
+
state = 'code';
|
|
854
|
+
code += ch;
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
code += ' ';
|
|
858
|
+
}
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (state === 'block-comment') {
|
|
862
|
+
if (ch === '*' && next === '/') {
|
|
863
|
+
state = 'code';
|
|
864
|
+
code += ' ';
|
|
865
|
+
i++;
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
code += ch === '\n' || ch === '\r' ? ch : ' ';
|
|
869
|
+
}
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (state === 'backtick') {
|
|
873
|
+
if (ch === '`' && next === '`') {
|
|
874
|
+
backtickIdentifier += '`';
|
|
875
|
+
code += ' ';
|
|
876
|
+
i++;
|
|
877
|
+
}
|
|
878
|
+
else if (ch === '`') {
|
|
879
|
+
state = 'code';
|
|
880
|
+
code +=
|
|
881
|
+
backtickIdentifier.toUpperCase() === 'QUERY_VECTOR_INDEX' ? 'QUERY_VECTOR_INDEX' : ' ';
|
|
882
|
+
}
|
|
883
|
+
else if (ch === '\\' && next !== undefined) {
|
|
884
|
+
backtickIdentifier += next;
|
|
885
|
+
code += ' ';
|
|
886
|
+
i++;
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
backtickIdentifier += ch;
|
|
890
|
+
code += ch === '\n' || ch === '\r' ? ch : ' ';
|
|
891
|
+
}
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (ch === '\\') {
|
|
895
|
+
code += ' ';
|
|
896
|
+
if (next !== undefined) {
|
|
897
|
+
code += next === '\n' || next === '\r' ? next : ' ';
|
|
898
|
+
i++;
|
|
899
|
+
}
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const closesLiteral = (state === 'single' && ch === "'") || (state === 'double' && ch === '"');
|
|
903
|
+
if (closesLiteral)
|
|
904
|
+
state = 'code';
|
|
905
|
+
code += ch === '\n' || ch === '\r' ? ch : ' ';
|
|
906
|
+
}
|
|
907
|
+
return /\bCALL\s+QUERY_VECTOR_INDEX\s*\(/i.test(code);
|
|
908
|
+
}
|
|
783
909
|
/**
|
|
784
910
|
* Checkout a connection from the pool.
|
|
785
911
|
* Returns an available connection, or creates a new one if under the cap.
|
|
@@ -870,12 +996,26 @@ export const executeParameterized = async (repoId, cypher, params) => {
|
|
|
870
996
|
// a query built by splicing a caller-sized list names itself even when the
|
|
871
997
|
// repo is not initialized. Never throws (#2915).
|
|
872
998
|
warnIfQueryTextUnbounded(cypher, `pool executeParameterized (repo "${repoId}")`, (message) => poolSidecarLogger.warn(message));
|
|
873
|
-
|
|
999
|
+
let entry = pool.get(repoId);
|
|
874
1000
|
if (!entry) {
|
|
875
1001
|
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
876
1002
|
}
|
|
877
|
-
|
|
1003
|
+
// Exact reads must not pay for VECTOR, but an explicit raw vector procedure
|
|
1004
|
+
// call is a semantic read. Preflight before taking the query connection:
|
|
1005
|
+
// ensureVectorExtension performs its own checkout, so holding one here could
|
|
1006
|
+
// make a saturated pool wait for a connection that every caller is holding.
|
|
1007
|
+
// A load rejection must not replace the query's own diagnostic.
|
|
1008
|
+
if (callsVectorIndex(cypher)) {
|
|
1009
|
+
await ensureVectorExtension(repoId).catch(() => false);
|
|
1010
|
+
// The preflight suspends, so close/re-init may replace the pool entry.
|
|
1011
|
+
// Re-read it before checkout to avoid querying through a stale handle.
|
|
1012
|
+
entry = pool.get(repoId);
|
|
1013
|
+
if (!entry) {
|
|
1014
|
+
throw new Error(`LadybugDB connection pool closed for repo "${repoId}" (re-init/teardown); retry the query.`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
878
1017
|
const conn = await checkout(entry);
|
|
1018
|
+
entry.lastUsed = Date.now();
|
|
879
1019
|
silenceStdout();
|
|
880
1020
|
activeQueryCount++;
|
|
881
1021
|
let queryResult;
|
|
@@ -256,12 +256,12 @@ export declare class LocalBackend {
|
|
|
256
256
|
*/
|
|
257
257
|
private warnedSiblingDrift;
|
|
258
258
|
/**
|
|
259
|
-
* One-shot stderr
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* stderr noisy per DoD §2.8.
|
|
259
|
+
* One-shot stderr guards for distinct VECTOR load and index-query failures.
|
|
260
|
+
* Keeping them separate preserves both diagnostics across semanticSearch calls
|
|
261
|
+
* without repeating either on hot paths.
|
|
263
262
|
*/
|
|
264
|
-
private
|
|
263
|
+
private warnedVectorLoadFailed;
|
|
264
|
+
private warnedVectorQueryFailed;
|
|
265
265
|
/**
|
|
266
266
|
* One-shot warning when a pruned or Node-unloadable optional embedding stack
|
|
267
267
|
* (#2370/#2372) forces semantic search to fall back to BM25 — so the
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { createHash } from 'crypto';
|
|
11
|
-
import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, statDbIdentity, dbIdentityChanged, } from '../../core/lbug/pool-adapter.js';
|
|
11
|
+
import { initLbug, executeQuery, executeParameterized, ensureVectorExtension, closeLbug, isLbugReady, statDbIdentity, dbIdentityChanged, } from '../../core/lbug/pool-adapter.js';
|
|
12
12
|
import { queryClassBeanMetadata } from './bean-metadata.js';
|
|
13
13
|
import { querySpringAopMetadata } from './aop-metadata.js';
|
|
14
14
|
import { queryConvexDispatchMetadata } from './convex-metadata.js';
|
|
@@ -783,12 +783,12 @@ export class LocalBackend {
|
|
|
783
783
|
*/
|
|
784
784
|
warnedSiblingDrift = new Set();
|
|
785
785
|
/**
|
|
786
|
-
* One-shot stderr
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
* stderr noisy per DoD §2.8.
|
|
786
|
+
* One-shot stderr guards for distinct VECTOR load and index-query failures.
|
|
787
|
+
* Keeping them separate preserves both diagnostics across semanticSearch calls
|
|
788
|
+
* without repeating either on hot paths.
|
|
790
789
|
*/
|
|
791
|
-
|
|
790
|
+
warnedVectorLoadFailed = false;
|
|
791
|
+
warnedVectorQueryFailed = false;
|
|
792
792
|
/**
|
|
793
793
|
* One-shot warning when a pruned or Node-unloadable optional embedding stack
|
|
794
794
|
* (#2370/#2372) forces semantic search to fall back to BM25 — so the
|
|
@@ -2518,17 +2518,25 @@ export class LocalBackend {
|
|
|
2518
2518
|
this.lastQueryEmbeddingDims.set(repo.lbugPath, dims);
|
|
2519
2519
|
const queryVecStr = `[${queryVec.join(',')}]`;
|
|
2520
2520
|
const maxDistance = getVectorMaxDistance(DEFAULT_MCP_VECTOR_MAX_DISTANCE);
|
|
2521
|
-
let
|
|
2522
|
-
// Always TRY the vector lane — no platform gate. LadybugDB ships the
|
|
2523
|
-
// VECTOR extension for every supported platform, Windows included
|
|
2524
|
-
// (#2623 follow-up; the old `platform !== 'win32'` gate was stale), so
|
|
2525
|
-
// whether the index is queryable is a per-machine runtime fact. The
|
|
2526
|
-
// catch below is the fallback: any failure (extension unloadable, index
|
|
2527
|
-
// absent, older DB) degrades to the exact scan with a once-per-backend
|
|
2528
|
-
// diagnostic instead of being silently swallowed.
|
|
2521
|
+
let vectorReady = false;
|
|
2529
2522
|
try {
|
|
2530
|
-
|
|
2531
|
-
|
|
2523
|
+
vectorReady = await ensureVectorExtension(repo.lbugPath);
|
|
2524
|
+
}
|
|
2525
|
+
catch (err) {
|
|
2526
|
+
if (!this.warnedVectorLoadFailed) {
|
|
2527
|
+
this.warnedVectorLoadFailed = true;
|
|
2528
|
+
logger.warn({ err }, 'GitNexus [query:vector]: vector extension load failed; using exact scan fallback');
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
let bestChunks = new Map();
|
|
2532
|
+
// Try the vector lane only after its lazy load succeeds. An unavailable
|
|
2533
|
+
// extension is already reported by ExtensionManager; an index/query
|
|
2534
|
+
// failure below gets its own once-per-backend diagnostic before the exact
|
|
2535
|
+
// scan fallback.
|
|
2536
|
+
if (vectorReady) {
|
|
2537
|
+
try {
|
|
2538
|
+
bestChunks = await collectBestChunks(limit, async (fetchLimit) => {
|
|
2539
|
+
const vectorQuery = `
|
|
2532
2540
|
CALL QUERY_VECTOR_INDEX('${EMBEDDING_TABLE_NAME}', '${EMBEDDING_INDEX_NAME}',
|
|
2533
2541
|
CAST(${queryVecStr} AS FLOAT[${dims}]), ${fetchLimit})
|
|
2534
2542
|
YIELD node AS emb, distance
|
|
@@ -2538,24 +2546,25 @@ export class LocalBackend {
|
|
|
2538
2546
|
emb.startLine AS startLine, emb.endLine AS endLine, distance
|
|
2539
2547
|
ORDER BY distance
|
|
2540
2548
|
`;
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2549
|
+
const embResults = await executeQuery(repo.lbugPath, vectorQuery);
|
|
2550
|
+
return embResults.map((row) => ({
|
|
2551
|
+
nodeId: row.nodeId ?? row[0],
|
|
2552
|
+
chunkIndex: row.chunkIndex ?? row[1] ?? 0,
|
|
2553
|
+
startLine: row.startLine ?? row[2] ?? 0,
|
|
2554
|
+
endLine: row.endLine ?? row[3] ?? 0,
|
|
2555
|
+
distance: row.distance ?? row[4],
|
|
2556
|
+
}));
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
catch (err) {
|
|
2560
|
+
bestChunks = new Map();
|
|
2561
|
+
if (!this.warnedVectorQueryFailed) {
|
|
2562
|
+
// Rare diagnostic: surface why semantic search fell back to the
|
|
2563
|
+
// exact scan. Emitted once per `LocalBackend` instance lifetime to
|
|
2564
|
+
// avoid noisy stderr on hot semantic-search paths (DoD §2.8).
|
|
2565
|
+
this.warnedVectorQueryFailed = true;
|
|
2566
|
+
logger.warn({ err }, 'GitNexus [query:vector]: vector index query failed; using exact scan fallback');
|
|
2567
|
+
}
|
|
2559
2568
|
}
|
|
2560
2569
|
}
|
|
2561
2570
|
if (bestChunks.size === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.6.10-rc.
|
|
3
|
+
"version": "1.6.10-rc.222",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|