gitnexus 1.6.6-rc.156 → 1.6.6-rc.157
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/cli/analyze.js +16 -0
- package/dist/core/ingestion/call-processor.d.ts +2 -13
- package/dist/core/ingestion/call-processor.js +2 -79
- package/dist/core/ingestion/export-detection.d.ts +2 -1
- package/dist/core/ingestion/export-detection.js +2 -1
- package/dist/core/ingestion/filesystem-walker.d.ts +0 -9
- package/dist/core/ingestion/filesystem-walker.js +0 -11
- package/dist/core/ingestion/languages/python/captures.js +4 -3
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +0 -10
- package/dist/core/ingestion/workers/parse-worker.js +16 -2
- package/dist/core/ingestion/workers/worker-pool.js +25 -2
- package/dist/core/lbug/lbug-adapter.js +29 -10
- package/dist/core/lbug/pool-adapter.js +38 -7
- package/dist/core/lbug/query-result-utils.d.ts +19 -0
- package/dist/core/lbug/query-result-utils.js +28 -0
- package/dist/mcp/local/local-backend.js +14 -1
- package/dist/types/pipeline.d.ts +6 -3
- package/package.json +1 -2
- package/dist/core/ingestion/ast-cache.d.ts +0 -26
- package/dist/core/ingestion/ast-cache.js +0 -48
- package/dist/core/ingestion/pipeline-phases/orm-extraction.d.ts +0 -22
- package/dist/core/ingestion/pipeline-phases/orm-extraction.js +0 -92
package/dist/cli/analyze.js
CHANGED
|
@@ -41,6 +41,22 @@ const writeFatalToStderr = (label, err) => {
|
|
|
41
41
|
realStderrWrite(`\n ${label}: ${message}\n`);
|
|
42
42
|
if (isErr && err.stack)
|
|
43
43
|
realStderrWrite(`${err.stack}\n`);
|
|
44
|
+
// Walk and print the `cause` chain. The phase runner wraps the underlying
|
|
45
|
+
// failure as `new Error("Phase 'X' failed: …", { cause })`, so the original
|
|
46
|
+
// error (e.g. a WorkerPoolDispatchError carrying the worker-side stack from
|
|
47
|
+
// #2068) is only reachable via `.cause`. Without this the user sees the
|
|
48
|
+
// wrapper's main-thread stack and never the real frame. `cause.stack` already
|
|
49
|
+
// begins with the cause's message, so we print the stack alone (not message +
|
|
50
|
+
// stack) to avoid repeating it. Depth-bounded so a cyclic `cause` can't loop
|
|
51
|
+
// (the phase runner wraps one level; the bound leaves headroom for future
|
|
52
|
+
// nesting); uses realStderrWrite so the redirected console.error's ANSI
|
|
53
|
+
// clear-line wrapping can't erase it (#1169).
|
|
54
|
+
const MAX_CAUSE_DEPTH = 5;
|
|
55
|
+
let cause = isErr ? err.cause : undefined;
|
|
56
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && cause instanceof Error; depth++) {
|
|
57
|
+
realStderrWrite(`\n Caused by: ${cause.stack ?? cause.message}\n`);
|
|
58
|
+
cause = cause.cause;
|
|
59
|
+
}
|
|
44
60
|
};
|
|
45
61
|
let fatalHandlersInstalled = false;
|
|
46
62
|
/**
|
|
@@ -9,14 +9,12 @@
|
|
|
9
9
|
*
|
|
10
10
|
* - `processRoutesFromExtracted` — CALLS edges from framework routes
|
|
11
11
|
* (e.g. Laravel) to their controller methods.
|
|
12
|
-
* - `processNextjsFetchRoutes` / `
|
|
13
|
-
*
|
|
14
|
-
* Next.js Route nodes.
|
|
12
|
+
* - `processNextjsFetchRoutes` / `extractConsumerAccessedKeys` — FETCHES edges
|
|
13
|
+
* from `fetch()` calls to Next.js Route nodes.
|
|
15
14
|
* - `buildExportedTypeMapFromGraph` — exported symbol → return/declared type
|
|
16
15
|
* map, consumed by the cross-file enrichment pass.
|
|
17
16
|
*/
|
|
18
17
|
import { KnowledgeGraph } from '../graph/types.js';
|
|
19
|
-
import { ASTCache } from './ast-cache.js';
|
|
20
18
|
import type { SemanticModel, SymbolTableReader } from './model/index.js';
|
|
21
19
|
import type { ExtractedRoute, ExtractedFetchCall } from './workers/parse-worker.js';
|
|
22
20
|
/** Per-file resolved type bindings for exported symbols.
|
|
@@ -76,12 +74,3 @@ export declare const extractConsumerAccessedKeys: (content: string) => string[];
|
|
|
76
74
|
*/
|
|
77
75
|
export declare const processNextjsFetchRoutes: (graph: KnowledgeGraph, fetchCalls: ExtractedFetchCall[], routeRegistry: Map<string, string>, // routeURL → handlerFilePath
|
|
78
76
|
consumerContents?: Map<string, string>) => void;
|
|
79
|
-
/**
|
|
80
|
-
* Extract fetch() calls from source files (sequential path).
|
|
81
|
-
* Workers handle this via tree-sitter captures in parse-worker; this function
|
|
82
|
-
* provides the same extraction for the sequential fallback path.
|
|
83
|
-
*/
|
|
84
|
-
export declare const extractFetchCallsFromFiles: (files: {
|
|
85
|
-
path: string;
|
|
86
|
-
content: string;
|
|
87
|
-
}[], astCache: ASTCache) => Promise<ExtractedFetchCall[]>;
|
|
@@ -9,20 +9,13 @@
|
|
|
9
9
|
*
|
|
10
10
|
* - `processRoutesFromExtracted` — CALLS edges from framework routes
|
|
11
11
|
* (e.g. Laravel) to their controller methods.
|
|
12
|
-
* - `processNextjsFetchRoutes` / `
|
|
13
|
-
*
|
|
14
|
-
* Next.js Route nodes.
|
|
12
|
+
* - `processNextjsFetchRoutes` / `extractConsumerAccessedKeys` — FETCHES edges
|
|
13
|
+
* from `fetch()` calls to Next.js Route nodes.
|
|
15
14
|
* - `buildExportedTypeMapFromGraph` — exported symbol → return/declared type
|
|
16
15
|
* map, consumed by the cross-file enrichment pass.
|
|
17
16
|
*/
|
|
18
|
-
import Parser from 'tree-sitter';
|
|
19
|
-
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
20
|
-
import { getProvider } from './languages/index.js';
|
|
21
17
|
import { generateId } from '../../lib/utils.js';
|
|
22
|
-
import { getLanguageFromFilename } from '../../_shared/index.js';
|
|
23
18
|
import { yieldToEventLoop } from './utils/event-loop.js';
|
|
24
|
-
import { parseSourceSafe } from '../tree-sitter/safe-parse.js';
|
|
25
|
-
import { getTreeSitterBufferSize } from './constants.js';
|
|
26
19
|
import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js';
|
|
27
20
|
import { extractReturnTypeName } from './type-extractors/shared.js';
|
|
28
21
|
const MAX_EXPORTS_PER_FILE = 500;
|
|
@@ -408,73 +401,3 @@ consumerContents) => {
|
|
|
408
401
|
}
|
|
409
402
|
}
|
|
410
403
|
};
|
|
411
|
-
/**
|
|
412
|
-
* Extract fetch() calls from source files (sequential path).
|
|
413
|
-
* Workers handle this via tree-sitter captures in parse-worker; this function
|
|
414
|
-
* provides the same extraction for the sequential fallback path.
|
|
415
|
-
*/
|
|
416
|
-
export const extractFetchCallsFromFiles = async (files, astCache) => {
|
|
417
|
-
const parser = await loadParser();
|
|
418
|
-
const result = [];
|
|
419
|
-
for (const file of files) {
|
|
420
|
-
const language = getLanguageFromFilename(file.path);
|
|
421
|
-
if (!language)
|
|
422
|
-
continue;
|
|
423
|
-
if (!isLanguageAvailable(language))
|
|
424
|
-
continue;
|
|
425
|
-
const provider = getProvider(language);
|
|
426
|
-
const queryStr = provider.treeSitterQueries;
|
|
427
|
-
if (!queryStr)
|
|
428
|
-
continue;
|
|
429
|
-
await loadLanguage(language, file.path);
|
|
430
|
-
let tree = astCache.get(file.path);
|
|
431
|
-
if (!tree) {
|
|
432
|
-
const parseContent = provider.preprocessSource?.(file.content, file.path) ?? file.content;
|
|
433
|
-
try {
|
|
434
|
-
tree = parseSourceSafe(parser, parseContent, undefined, {
|
|
435
|
-
bufferSize: getTreeSitterBufferSize(parseContent),
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
catch {
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
astCache.set(file.path, tree);
|
|
442
|
-
}
|
|
443
|
-
let matches;
|
|
444
|
-
try {
|
|
445
|
-
const lang = parser.getLanguage();
|
|
446
|
-
const query = new Parser.Query(lang, queryStr);
|
|
447
|
-
matches = query.matches(tree.rootNode);
|
|
448
|
-
}
|
|
449
|
-
catch {
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
for (const match of matches) {
|
|
453
|
-
const captureMap = {};
|
|
454
|
-
match.captures.forEach((c) => (captureMap[c.name] = c.node));
|
|
455
|
-
if (captureMap['route.fetch']) {
|
|
456
|
-
const urlNode = captureMap['route.url'] ?? captureMap['route.template_url'];
|
|
457
|
-
if (urlNode) {
|
|
458
|
-
result.push({
|
|
459
|
-
filePath: file.path,
|
|
460
|
-
fetchURL: urlNode.text,
|
|
461
|
-
lineNumber: captureMap['route.fetch'].startPosition.row,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
else if (captureMap['http_client'] && captureMap['http_client.url']) {
|
|
466
|
-
const method = captureMap['http_client.method']?.text;
|
|
467
|
-
const url = captureMap['http_client.url'].text;
|
|
468
|
-
const HTTP_CLIENT_ONLY = new Set(['head', 'options', 'request', 'ajax']);
|
|
469
|
-
if (method && HTTP_CLIENT_ONLY.has(method) && url.startsWith('/')) {
|
|
470
|
-
result.push({
|
|
471
|
-
filePath: file.path,
|
|
472
|
-
fetchURL: url,
|
|
473
|
-
lineNumber: captureMap['http_client'].startPosition.row,
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return result;
|
|
480
|
-
};
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Determines whether a symbol (function, class, etc.) is exported/public
|
|
5
5
|
* in its language. This is a pure function — safe for use in worker threads.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Used by the language providers during worker parsing (parse-worker.ts) — the
|
|
8
|
+
* sole parse path. (Sequential parsing was removed.)
|
|
8
9
|
*/
|
|
9
10
|
import { type SyntaxNode } from './utils/ast-helpers.js';
|
|
10
11
|
/** Handler type: given a node and symbol name, return true if the symbol is exported/public. */
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Determines whether a symbol (function, class, etc.) is exported/public
|
|
5
5
|
* in its language. This is a pure function — safe for use in worker threads.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Used by the language providers during worker parsing (parse-worker.ts) — the
|
|
8
|
+
* sole parse path. (Sequential parsing was removed.)
|
|
8
9
|
*/
|
|
9
10
|
import { findSiblingChild } from './utils/ast-helpers.js';
|
|
10
11
|
// ============================================================================
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
export interface FileEntry {
|
|
2
|
-
path: string;
|
|
3
|
-
content: string;
|
|
4
|
-
}
|
|
5
1
|
/** Lightweight entry — path + size from stat, no content in memory */
|
|
6
2
|
export interface ScannedFile {
|
|
7
3
|
path: string;
|
|
@@ -21,8 +17,3 @@ export declare const walkRepositoryPaths: (repoPath: string, onProgress?: (curre
|
|
|
21
17
|
* Returns a Map for O(1) lookup. Silently skips files that fail to read.
|
|
22
18
|
*/
|
|
23
19
|
export declare const readFileContents: (repoPath: string, relativePaths: string[]) => Promise<Map<string, string>>;
|
|
24
|
-
/**
|
|
25
|
-
* Legacy API — scans and reads everything into memory.
|
|
26
|
-
* Used by sequential fallback path only.
|
|
27
|
-
*/
|
|
28
|
-
export declare const walkRepository: (repoPath: string, onProgress?: (current: number, total: number, filePath: string) => void) => Promise<FileEntry[]>;
|
|
@@ -111,14 +111,3 @@ export const readFileContents = async (repoPath, relativePaths) => {
|
|
|
111
111
|
}
|
|
112
112
|
return contents;
|
|
113
113
|
};
|
|
114
|
-
/**
|
|
115
|
-
* Legacy API — scans and reads everything into memory.
|
|
116
|
-
* Used by sequential fallback path only.
|
|
117
|
-
*/
|
|
118
|
-
export const walkRepository = async (repoPath, onProgress) => {
|
|
119
|
-
const scanned = await walkRepositoryPaths(repoPath, onProgress);
|
|
120
|
-
const contents = await readFileContents(repoPath, scanned.map((f) => f.path));
|
|
121
|
-
return scanned
|
|
122
|
-
.filter((f) => contents.has(f.path))
|
|
123
|
-
.map((f) => ({ path: f.path, content: contents.get(f.path) }));
|
|
124
|
-
};
|
|
@@ -26,9 +26,10 @@ import { getTreeSitterBufferSize } from '../../constants.js';
|
|
|
26
26
|
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|
27
27
|
import { pythonFunctionDefinitionLabel } from './simple-hooks.js';
|
|
28
28
|
export function emitPythonScopeCaptures(sourceText, _filePath, cachedTree) {
|
|
29
|
-
// Skip the parse when the caller (
|
|
30
|
-
// produced a Tree for this source
|
|
31
|
-
//
|
|
29
|
+
// Skip the parse when the caller (the scope-resolution orchestrator's
|
|
30
|
+
// `treeCache`) already produced a Tree for this source — empty under
|
|
31
|
+
// worker-pool runs, so cache miss = re-parse. The cachedTree parameter
|
|
32
|
+
// is typed as `unknown` at the
|
|
32
33
|
// contract layer (see `LanguageProvider.emitScopeCaptures`); cast
|
|
33
34
|
// here at the use site.
|
|
34
35
|
let tree = cachedTree;
|
|
@@ -20,7 +20,6 @@ import { fileContentHash, computeChunkHash, loadParseCacheChunk, persistParseCac
|
|
|
20
20
|
import { clearParsedFileStore, persistParsedFileChunk, getDurableParsedFileDir, loadDurableParsedFileIndex, restoreDurableParsedFileShard, } from '../../../storage/parsedfile-store.js';
|
|
21
21
|
import { processRoutesFromExtracted, buildExportedTypeMapFromGraph, } from '../call-processor.js';
|
|
22
22
|
import { createSemanticModel } from '../model/index.js';
|
|
23
|
-
import { createASTCache } from '../ast-cache.js';
|
|
24
23
|
import { getLanguageFromFilename } from '../../../_shared/index.js';
|
|
25
24
|
import { readFileContents } from '../filesystem-walker.js';
|
|
26
25
|
import { isLanguageAvailable } from '../../tree-sitter/parser-loader.js';
|
|
@@ -328,13 +327,6 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
328
327
|
}
|
|
329
328
|
};
|
|
330
329
|
let filesParsedSoFar = 0;
|
|
331
|
-
// Chunk-local tree-sitter cache, cleared between chunks — call / heritage /
|
|
332
|
-
// import processors read it during parse to avoid re-parsing within the same
|
|
333
|
-
// chunk. (The former cross-phase `scopeTreeCache` was only ever populated by
|
|
334
|
-
// the sequential parser, which has been removed; workers can't return native
|
|
335
|
-
// Trees across the MessageChannel, so scope-resolution re-parses as needed.)
|
|
336
|
-
const maxChunkFiles = chunks.reduce((max, c) => Math.max(max, c.length), 0);
|
|
337
|
-
const astCache = createASTCache(maxChunkFiles);
|
|
338
330
|
const exportedTypeMap = new Map();
|
|
339
331
|
const bindingAccumulator = new BindingAccumulator();
|
|
340
332
|
const allFetchCalls = [];
|
|
@@ -494,7 +486,6 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
494
486
|
}
|
|
495
487
|
}
|
|
496
488
|
filesParsedSoFar += chunkFiles.length;
|
|
497
|
-
astCache.clear();
|
|
498
489
|
if (verboseThroughputLog && chunkStartMs !== null) {
|
|
499
490
|
const elapsedMs = Date.now() - chunkStartMs;
|
|
500
491
|
const filesPerSec = elapsedMs > 0 ? (chunkFiles.length * 1000) / elapsedMs : 0;
|
|
@@ -747,7 +738,6 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
747
738
|
// Finalize the accumulator and propagate any fixpoint-inferred exports before
|
|
748
739
|
// `crossFile` disposes it downstream. Wrapped in try/catch so a cleanup
|
|
749
740
|
// failure never masks a real parse error; disposal stays with `crossFile`.
|
|
750
|
-
astCache.clear();
|
|
751
741
|
try {
|
|
752
742
|
bindingAccumulator.finalize();
|
|
753
743
|
const enriched = enrichExportedTypeMap(bindingAccumulator, graph, exportedTypeMap);
|
|
@@ -1888,7 +1888,21 @@ parentPort.on('message', (msg) => {
|
|
|
1888
1888
|
}
|
|
1889
1889
|
}
|
|
1890
1890
|
catch (err) {
|
|
1891
|
-
|
|
1892
|
-
|
|
1891
|
+
// Carry the worker-side stack across the MessageChannel, not just the
|
|
1892
|
+
// message. Without this, an unexpected worker throw (e.g. the minified
|
|
1893
|
+
// `this.#<x> is not a function` family) reaches the operator as a bare
|
|
1894
|
+
// one-liner with no file:line — exactly what made #2068 undebuggable. The
|
|
1895
|
+
// pool embeds `errorStack` into its death/circuit-breaker reason so the
|
|
1896
|
+
// surfaced "Phase 'parse' failed" message points at the real frame (the
|
|
1897
|
+
// stack's first line already carries the error's type + message). We send
|
|
1898
|
+
// primitive fields (not the raw Error) so a non-cloneable `cause` payload
|
|
1899
|
+
// can never turn the report itself into a `messageerror`. `errorStack` is
|
|
1900
|
+
// optional on the wire, so an older pool ignores it.
|
|
1901
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1902
|
+
parentPort.postMessage({
|
|
1903
|
+
type: 'error',
|
|
1904
|
+
error: e.message,
|
|
1905
|
+
errorStack: e.stack,
|
|
1906
|
+
});
|
|
1893
1907
|
}
|
|
1894
1908
|
});
|
|
@@ -356,6 +356,24 @@ function withStderr(worker, message) {
|
|
|
356
356
|
const tail = workerStderrTail(worker);
|
|
357
357
|
return tail ? `${message}. Worker stderr:\n${tail}` : message;
|
|
358
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Build a worker-death reason string that carries the worker-side stack when one
|
|
361
|
+
* is available (#2068). The stack is appended AFTER the `Worker N error: <msg>`
|
|
362
|
+
* prefix so every prefix/substring consumer downstream — recoverAndResume →
|
|
363
|
+
* handleWorkerDeath → the circuit-breaker `WorkerPoolDispatchError` message, and
|
|
364
|
+
* the tests that regex-match those — keeps working unchanged, while the operator
|
|
365
|
+
* now gets the real frame instead of a bare one-liner. The stack's first line is
|
|
366
|
+
* normally the message itself; keeping both is harmless and the indented block
|
|
367
|
+
* scans cleanly in a log. The stack is capped at WORKER_STDERR_TAIL_LIMIT,
|
|
368
|
+
* mirroring the sibling stderr-tail bound, so a pathological error type (or a
|
|
369
|
+
* raised `Error.stackTraceLimit`) can't bloat the death reason. `stack` is
|
|
370
|
+
* `undefined` for an older worker build (or a thrown non-Error), in which case
|
|
371
|
+
* the reason is exactly the prior message-only form.
|
|
372
|
+
*/
|
|
373
|
+
function workerErrorReason(workerIndex, message, stack) {
|
|
374
|
+
const base = `Worker ${workerIndex} error: ${message}`;
|
|
375
|
+
return stack ? `${base}\n worker stack:\n${stack.slice(0, WORKER_STDERR_TAIL_LIMIT)}` : base;
|
|
376
|
+
}
|
|
359
377
|
/**
|
|
360
378
|
* Wait for a freshly-spawned replacement worker to emit the
|
|
361
379
|
* `{type:'ready'}` handshake from `parse-worker.ts` before treating its
|
|
@@ -1338,7 +1356,7 @@ export const createWorkerPool = (workerUrl, poolSize, options) => {
|
|
|
1338
1356
|
else if (msg.type === 'error') {
|
|
1339
1357
|
settled = true;
|
|
1340
1358
|
cleanup();
|
|
1341
|
-
void recoverAndResume(
|
|
1359
|
+
void recoverAndResume(workerErrorReason(workerIndex, msg.error, msg.errorStack), resolveExcludePaths());
|
|
1342
1360
|
}
|
|
1343
1361
|
else if (msg.type === 'result') {
|
|
1344
1362
|
if (!waitingForFlush) {
|
|
@@ -1379,7 +1397,12 @@ export const createWorkerPool = (workerUrl, poolSize, options) => {
|
|
|
1379
1397
|
if (!settled) {
|
|
1380
1398
|
settled = true;
|
|
1381
1399
|
cleanup();
|
|
1382
|
-
|
|
1400
|
+
// The Node 'error' event fires on an UNCAUGHT worker throw (one that
|
|
1401
|
+
// escaped the worker's own try/catch, or an async rejection). Unlike
|
|
1402
|
+
// the `{type:'error'}` message, the event delivers a real Error whose
|
|
1403
|
+
// `.stack` is the worker-side frame — carry it so the surfaced reason
|
|
1404
|
+
// points at the actual failure site, not just `err.message` (#2068).
|
|
1405
|
+
void recoverAndResume(workerErrorReason(workerIndex, err.message, err.stack), resolveExcludePaths());
|
|
1383
1406
|
}
|
|
1384
1407
|
};
|
|
1385
1408
|
const exitHandler = (code) => {
|
|
@@ -7,6 +7,7 @@ import path from 'path';
|
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import crypto from 'crypto';
|
|
9
9
|
import lbug from '@ladybugdb/core';
|
|
10
|
+
import { closeQueryResults } from './query-result-utils.js';
|
|
10
11
|
import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, STALE_HASH_SENTINEL, } from './schema.js';
|
|
11
12
|
import { streamAllCSVsToDisk } from './csv-generator.js';
|
|
12
13
|
import { extensionManager } from './extension-loader.js';
|
|
@@ -153,8 +154,19 @@ const DB_LOCK_RETRY_DELAY_MS = 500;
|
|
|
153
154
|
* analyze` and either already happened or will happen on the next run.
|
|
154
155
|
*/
|
|
155
156
|
export const isReadOnlyDbError = (err) => {
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
// Walk the `cause` chain (bounded) so a wrapped read-only error — e.g. the
|
|
158
|
+
// pool adapter's `new Error('…read-only.', { cause: nativeReadOnlyErr })` —
|
|
159
|
+
// is still detected by callers that only see the wrapper (#2068 follow-up).
|
|
160
|
+
// The same strict regex is re-applied at each level, so a non-read-only
|
|
161
|
+
// chain stays false; the depth bound guards a cyclic `cause`.
|
|
162
|
+
let cur = err;
|
|
163
|
+
for (let depth = 0; depth < 5 && cur != null; depth++) {
|
|
164
|
+
const msg = cur instanceof Error ? cur.message : String(cur);
|
|
165
|
+
if (/read-only database/i.test(msg))
|
|
166
|
+
return true;
|
|
167
|
+
cur = cur instanceof Error ? cur.cause : undefined;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
158
170
|
};
|
|
159
171
|
const isMissingFileError = (err) => {
|
|
160
172
|
const errno = err;
|
|
@@ -301,13 +313,10 @@ const runWithSessionLock = async (operation) => {
|
|
|
301
313
|
}
|
|
302
314
|
};
|
|
303
315
|
const normalizeCopyPath = (filePath) => toNativeSafePath(filePath).replace(/\\/g, '/');
|
|
316
|
+
// Single-result convenience wrapper over the shared best-effort closer
|
|
317
|
+
// (drainQueryResult / readQueryRows close one cursor at a time).
|
|
304
318
|
const closeQueryResult = async (result) => {
|
|
305
|
-
|
|
306
|
-
await result.close();
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
// Best-effort cleanup only.
|
|
310
|
-
}
|
|
319
|
+
await closeQueryResults(result);
|
|
311
320
|
};
|
|
312
321
|
const drainQueryResult = async (queryResult) => {
|
|
313
322
|
const results = Array.isArray(queryResult) ? queryResult : [queryResult];
|
|
@@ -1544,8 +1553,9 @@ export const queryImporters = async (targetFilePath) => {
|
|
|
1544
1553
|
WHERE r.type = 'IMPORTS' AND b.filePath = '${escaped}'
|
|
1545
1554
|
RETURN DISTINCT a.filePath AS importer
|
|
1546
1555
|
`;
|
|
1556
|
+
let queryResult;
|
|
1547
1557
|
try {
|
|
1548
|
-
|
|
1558
|
+
queryResult = await conn.query(cypher);
|
|
1549
1559
|
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
1550
1560
|
const rows = await result.getAll();
|
|
1551
1561
|
const out = [];
|
|
@@ -1559,6 +1569,10 @@ export const queryImporters = async (targetFilePath) => {
|
|
|
1559
1569
|
catch {
|
|
1560
1570
|
return [];
|
|
1561
1571
|
}
|
|
1572
|
+
finally {
|
|
1573
|
+
if (queryResult)
|
|
1574
|
+
await closeQueryResults(queryResult);
|
|
1575
|
+
}
|
|
1562
1576
|
};
|
|
1563
1577
|
/**
|
|
1564
1578
|
* Drop every Community and Process node (and their MEMBER_OF /
|
|
@@ -1573,8 +1587,9 @@ export const deleteAllCommunitiesAndProcesses = async () => {
|
|
|
1573
1587
|
}
|
|
1574
1588
|
let nodesDeleted = 0;
|
|
1575
1589
|
for (const label of ['Community', 'Process']) {
|
|
1590
|
+
let countResult;
|
|
1576
1591
|
try {
|
|
1577
|
-
|
|
1592
|
+
countResult = await conn.query(`MATCH (n:${label}) RETURN count(n) AS cnt`);
|
|
1578
1593
|
const result = Array.isArray(countResult) ? countResult[0] : countResult;
|
|
1579
1594
|
const rows = await result.getAll();
|
|
1580
1595
|
const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0);
|
|
@@ -1586,6 +1601,10 @@ export const deleteAllCommunitiesAndProcesses = async () => {
|
|
|
1586
1601
|
catch {
|
|
1587
1602
|
// Table may not exist yet on a freshly-initialized DB — fine.
|
|
1588
1603
|
}
|
|
1604
|
+
finally {
|
|
1605
|
+
if (countResult)
|
|
1606
|
+
await closeQueryResults(countResult);
|
|
1607
|
+
}
|
|
1589
1608
|
}
|
|
1590
1609
|
return { nodesDeleted };
|
|
1591
1610
|
};
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import fs from 'fs/promises';
|
|
18
18
|
import lbug from '@ladybugdb/core';
|
|
19
19
|
import { isReadOnlyDbError, loadFTSExtension } from './lbug-adapter.js';
|
|
20
|
+
import { closeQueryResults } from './query-result-utils.js';
|
|
20
21
|
import { createLbugDatabase, isWalCorruptionError, toNativeSafePath, WAL_RECOVERY_SUGGESTION, } from './lbug-config.js';
|
|
21
22
|
import { isMissingFsError, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, statIfExists, } from './sidecar-recovery.js';
|
|
22
23
|
const pool = new Map();
|
|
@@ -112,6 +113,18 @@ function closeOne(repoId) {
|
|
|
112
113
|
if (!entry)
|
|
113
114
|
return;
|
|
114
115
|
entry.closed = true;
|
|
116
|
+
// Reject any callers still queued for a connection: the pool is going away
|
|
117
|
+
// (re-init / teardown / LRU eviction), so they must fail fast with an
|
|
118
|
+
// actionable error instead of hanging until WAITER_TIMEOUT_MS and then
|
|
119
|
+
// surfacing a misleading "pool exhausted" (#2068 follow-up). Draining the
|
|
120
|
+
// queue also guarantees checkin() below finds no waiter expecting a
|
|
121
|
+
// connection, so a connection returned after close is simply closed.
|
|
122
|
+
if (entry.waiters.length > 0) {
|
|
123
|
+
const closedErr = new Error(`LadybugDB connection pool closed for repo "${repoId}" (re-init/teardown); retry the query.`);
|
|
124
|
+
for (const waiter of entry.waiters)
|
|
125
|
+
waiter.reject(closedErr);
|
|
126
|
+
entry.waiters.length = 0;
|
|
127
|
+
}
|
|
115
128
|
// Close available connections — fire-and-forget with .catch() to prevent
|
|
116
129
|
// unhandled rejections. Native close() returns Promise<void> but can crash
|
|
117
130
|
// the N-API destructor on macOS/Windows; deferring to process exit lets
|
|
@@ -575,15 +588,21 @@ function checkout(entry) {
|
|
|
575
588
|
}
|
|
576
589
|
// At capacity — queue the caller with a timeout.
|
|
577
590
|
return new Promise((resolve, reject) => {
|
|
578
|
-
const waiter =
|
|
579
|
-
|
|
580
|
-
|
|
591
|
+
const waiter = {
|
|
592
|
+
resolve: (conn) => {
|
|
593
|
+
clearTimeout(timer);
|
|
594
|
+
resolve(conn);
|
|
595
|
+
},
|
|
596
|
+
reject: (err) => {
|
|
597
|
+
clearTimeout(timer);
|
|
598
|
+
reject(err);
|
|
599
|
+
},
|
|
581
600
|
};
|
|
582
601
|
const timer = setTimeout(() => {
|
|
583
602
|
const idx = entry.waiters.indexOf(waiter);
|
|
584
603
|
if (idx !== -1)
|
|
585
604
|
entry.waiters.splice(idx, 1);
|
|
586
|
-
reject(new Error(`Connection pool exhausted: timed out after ${WAITER_TIMEOUT_MS}ms waiting for a free connection`));
|
|
605
|
+
waiter.reject(new Error(`Connection pool exhausted: timed out after ${WAITER_TIMEOUT_MS}ms waiting for a free connection`));
|
|
587
606
|
}, WAITER_TIMEOUT_MS);
|
|
588
607
|
entry.waiters.push(waiter);
|
|
589
608
|
});
|
|
@@ -604,7 +623,7 @@ function checkin(entry, conn) {
|
|
|
604
623
|
if (entry.waiters.length > 0) {
|
|
605
624
|
// Hand directly to the next waiter — no intermediate available state
|
|
606
625
|
const waiter = entry.waiters.shift();
|
|
607
|
-
waiter(conn);
|
|
626
|
+
waiter.resolve(conn);
|
|
608
627
|
}
|
|
609
628
|
else {
|
|
610
629
|
entry.checkedOut--;
|
|
@@ -639,24 +658,36 @@ export const executeParameterized = async (repoId, cypher, params) => {
|
|
|
639
658
|
const conn = await checkout(entry);
|
|
640
659
|
silenceStdout();
|
|
641
660
|
activeQueryCount++;
|
|
661
|
+
let queryResult;
|
|
642
662
|
try {
|
|
643
663
|
const stmt = await withTimeout(conn.prepare(cypher), QUERY_TIMEOUT_MS, 'Prepare');
|
|
644
664
|
if (!stmt.isSuccess()) {
|
|
645
665
|
const errMsg = await stmt.getErrorMessage();
|
|
646
666
|
throw new Error(`Prepare failed: ${errMsg}`);
|
|
647
667
|
}
|
|
648
|
-
|
|
668
|
+
queryResult = await withTimeout(conn.execute(stmt, params), QUERY_TIMEOUT_MS, 'Execute');
|
|
649
669
|
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
650
670
|
const rows = await result.getAll();
|
|
651
671
|
return rows;
|
|
652
672
|
}
|
|
653
673
|
catch (err) {
|
|
654
674
|
if (isReadOnlyDbError(err)) {
|
|
655
|
-
|
|
675
|
+
// Preserve the native error as `cause` so the original frame/message is
|
|
676
|
+
// not lost behind the friendly read-only message (#2068 follow-up).
|
|
677
|
+
throw new Error('Write operations are not allowed. The pool adapter is read-only.', {
|
|
678
|
+
cause: err,
|
|
679
|
+
});
|
|
656
680
|
}
|
|
657
681
|
throw err;
|
|
658
682
|
}
|
|
659
683
|
finally {
|
|
684
|
+
// Close the native QueryResult cursor(s) before returning the connection —
|
|
685
|
+
// getAll() drains rows but does not release the native cursor, so without
|
|
686
|
+
// this the cursor leaks for the connection's lifetime (#2068 follow-up).
|
|
687
|
+
// Best-effort via the shared helper; never masks the query result or a real
|
|
688
|
+
// error.
|
|
689
|
+
if (queryResult)
|
|
690
|
+
await closeQueryResults(queryResult);
|
|
660
691
|
activeQueryCount--;
|
|
661
692
|
restoreStdout();
|
|
662
693
|
checkin(entry, conn);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import lbug from '@ladybugdb/core';
|
|
2
|
+
/**
|
|
3
|
+
* Best-effort close of one or more native `QueryResult` cursors.
|
|
4
|
+
*
|
|
5
|
+
* `result.getAll()` materializes rows into a JS array but does not release the
|
|
6
|
+
* native cursor — leaving it open holds native resources for the connection's
|
|
7
|
+
* lifetime. Both the pooled adapter (`pool-adapter.ts`) and the direct adapter
|
|
8
|
+
* (`lbug-adapter.ts`) must release cursors after reading; this is the single
|
|
9
|
+
* shared implementation so neither re-rolls the array-normalize + swallow loop
|
|
10
|
+
* (a #2068 follow-up de-dup). Lives in its own leaf module so the two adapters
|
|
11
|
+
* depend on it rather than on each other.
|
|
12
|
+
*
|
|
13
|
+
* `conn.execute()` can return either a single `QueryResult` or an array
|
|
14
|
+
* (multi-statement); this normalizes both. Each close is independent and
|
|
15
|
+
* best-effort: a failing or absent `close()` on one cursor never throws and
|
|
16
|
+
* never prevents the others from closing — a cleanup failure must not mask the
|
|
17
|
+
* query result or a real error at the call site.
|
|
18
|
+
*/
|
|
19
|
+
export declare function closeQueryResults(queryResult: lbug.QueryResult | lbug.QueryResult[]): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort close of one or more native `QueryResult` cursors.
|
|
3
|
+
*
|
|
4
|
+
* `result.getAll()` materializes rows into a JS array but does not release the
|
|
5
|
+
* native cursor — leaving it open holds native resources for the connection's
|
|
6
|
+
* lifetime. Both the pooled adapter (`pool-adapter.ts`) and the direct adapter
|
|
7
|
+
* (`lbug-adapter.ts`) must release cursors after reading; this is the single
|
|
8
|
+
* shared implementation so neither re-rolls the array-normalize + swallow loop
|
|
9
|
+
* (a #2068 follow-up de-dup). Lives in its own leaf module so the two adapters
|
|
10
|
+
* depend on it rather than on each other.
|
|
11
|
+
*
|
|
12
|
+
* `conn.execute()` can return either a single `QueryResult` or an array
|
|
13
|
+
* (multi-statement); this normalizes both. Each close is independent and
|
|
14
|
+
* best-effort: a failing or absent `close()` on one cursor never throws and
|
|
15
|
+
* never prevents the others from closing — a cleanup failure must not mask the
|
|
16
|
+
* query result or a real error at the call site.
|
|
17
|
+
*/
|
|
18
|
+
export async function closeQueryResults(queryResult) {
|
|
19
|
+
const results = Array.isArray(queryResult) ? queryResult : [queryResult];
|
|
20
|
+
for (const r of results) {
|
|
21
|
+
try {
|
|
22
|
+
await r?.close();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Best-effort cleanup only.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -143,7 +143,20 @@ function logQueryError(context, err) {
|
|
|
143
143
|
const msg = err instanceof Error ? err.message : String(err);
|
|
144
144
|
logger.error({ context, err: msg }, 'GitNexus query failed');
|
|
145
145
|
}
|
|
146
|
-
const isReadOnlyDbError = (err) =>
|
|
146
|
+
const isReadOnlyDbError = (err) => {
|
|
147
|
+
// Walk the `cause` chain (bounded) so a wrapped read-only error (e.g. the
|
|
148
|
+
// pool adapter's `{ cause }` wrapper) is still detected here — this is the
|
|
149
|
+
// copy the MCP cypher handler uses to surface its curated read-only message
|
|
150
|
+
// (#2068 follow-up). Mirrors lbug-adapter's isReadOnlyDbError.
|
|
151
|
+
let cur = err;
|
|
152
|
+
for (let depth = 0; depth < 5 && cur != null; depth++) {
|
|
153
|
+
const msg = cur instanceof Error ? cur.message : String(cur);
|
|
154
|
+
if (/read-only database/i.test(msg))
|
|
155
|
+
return true;
|
|
156
|
+
cur = cur instanceof Error ? cur.cause : undefined;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
};
|
|
147
160
|
/**
|
|
148
161
|
* Per-query latency telemetry for production aggregation (#553).
|
|
149
162
|
*
|
package/dist/types/pipeline.d.ts
CHANGED
|
@@ -17,9 +17,12 @@ export interface PipelineResult {
|
|
|
17
17
|
*/
|
|
18
18
|
resolutionOutcomes: readonly ResolutionOutcome[];
|
|
19
19
|
/**
|
|
20
|
-
* True if
|
|
21
|
-
* the
|
|
22
|
-
*
|
|
20
|
+
* True if a worker pool was actually constructed for this run. The worker
|
|
21
|
+
* pool is the sole parse path (sequential parsing was removed). False means
|
|
22
|
+
* no pool was needed: either there were no parseable files, or every chunk
|
|
23
|
+
* was a parse-cache hit and the cached worker output was replayed without
|
|
24
|
+
* spawning workers (a warm all-cache-hit run, #2038). Primarily a test
|
|
25
|
+
* affordance so regression suites can prove the pool engaged.
|
|
23
26
|
*/
|
|
24
27
|
usedWorkerPool: boolean;
|
|
25
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.6.6-rc.
|
|
3
|
+
"version": "1.6.6-rc.157",
|
|
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",
|
|
@@ -70,7 +70,6 @@
|
|
|
70
70
|
"ignore": "^7.0.5",
|
|
71
71
|
"js-yaml": "^4.1.1",
|
|
72
72
|
"jsonc-parser": "^3.3.1",
|
|
73
|
-
"lru-cache": "^11.0.0",
|
|
74
73
|
"mnemonist": "^0.40.3",
|
|
75
74
|
"onnxruntime-node": "^1.24.0",
|
|
76
75
|
"pandemonium": "^2.4.0",
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import Parser from 'tree-sitter';
|
|
2
|
-
/**
|
|
3
|
-
* Minimal structural shape consumers need when reading Trees back
|
|
4
|
-
* through a phase-dependency boundary. Declared here so phases that
|
|
5
|
-
* receive ASTCache via `getPhaseOutput<...>` don't hand-roll their
|
|
6
|
-
* own inline structural types that silently drift when ASTCache's
|
|
7
|
-
* contract changes.
|
|
8
|
-
*
|
|
9
|
-
* Typed as `unknown` at the Tree boundary because consumers on the
|
|
10
|
-
* other side of the phase-output map don't share tree-sitter's type
|
|
11
|
-
* graph (e.g. COBOL's standalone processor).
|
|
12
|
-
*/
|
|
13
|
-
export interface ASTCacheReader {
|
|
14
|
-
get(filePath: string): unknown;
|
|
15
|
-
clear(): void;
|
|
16
|
-
}
|
|
17
|
-
export interface ASTCache extends ASTCacheReader {
|
|
18
|
-
get: (filePath: string) => Parser.Tree | undefined;
|
|
19
|
-
set: (filePath: string, tree: Parser.Tree) => void;
|
|
20
|
-
clear: () => void;
|
|
21
|
-
stats: () => {
|
|
22
|
-
size: number;
|
|
23
|
-
maxSize: number;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
export declare const createASTCache: (maxSize?: number) => ASTCache;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { LRUCache } from 'lru-cache';
|
|
2
|
-
import { logger } from '../logger.js';
|
|
3
|
-
export const createASTCache = (maxSize = 50) => {
|
|
4
|
-
const effectiveMax = Math.max(maxSize, 1);
|
|
5
|
-
// Initialize the cache with a 'dispose' handler
|
|
6
|
-
// This is the magic: When an item is evicted (dropped), this runs automatically.
|
|
7
|
-
const cache = new LRUCache({
|
|
8
|
-
max: effectiveMax,
|
|
9
|
-
dispose: (tree) => {
|
|
10
|
-
try {
|
|
11
|
-
// NOTE: web-tree-sitter has tree.delete(); native tree-sitter
|
|
12
|
-
// trees are GC-managed and .delete is absent (no-op here).
|
|
13
|
-
//
|
|
14
|
-
// Single-owner invariant (load-bearing under WASM): a given
|
|
15
|
-
// Parser.Tree reference must live in AT MOST ONE ASTCache
|
|
16
|
-
// that disposes. The parse-phase chunk-local cache clears
|
|
17
|
-
// between chunks; the cross-phase `scopeTreeCache` (also an
|
|
18
|
-
// ASTCache today) holds the same Tree by reference. Under
|
|
19
|
-
// native tree-sitter this is benign (dispose is a no-op).
|
|
20
|
-
// If/when GitNexus adopts web-tree-sitter for sequential
|
|
21
|
-
// parsing, the cross-phase cache must either (a) skip
|
|
22
|
-
// writing Trees that are already owned by a disposing cache,
|
|
23
|
-
// or (b) use tree.copy() per entry. Failing to pick one
|
|
24
|
-
// will hand freed memory to scope-resolution.
|
|
25
|
-
tree.delete?.();
|
|
26
|
-
}
|
|
27
|
-
catch (e) {
|
|
28
|
-
logger.warn({ e }, 'Failed to delete tree from WASM memory');
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
return {
|
|
33
|
-
get: (filePath) => {
|
|
34
|
-
const tree = cache.get(filePath);
|
|
35
|
-
return tree; // Returns undefined if not found
|
|
36
|
-
},
|
|
37
|
-
set: (filePath, tree) => {
|
|
38
|
-
cache.set(filePath, tree);
|
|
39
|
-
},
|
|
40
|
-
clear: () => {
|
|
41
|
-
cache.clear();
|
|
42
|
-
},
|
|
43
|
-
stats: () => ({
|
|
44
|
-
size: cache.size,
|
|
45
|
-
maxSize: effectiveMax,
|
|
46
|
-
}),
|
|
47
|
-
};
|
|
48
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Inline ORM query extraction (sequential fallback path).
|
|
3
|
-
*
|
|
4
|
-
* Extracts Prisma and Supabase query calls from source content using
|
|
5
|
-
* regex patterns. Used by the sequential parse path when workers are
|
|
6
|
-
* not available — the worker path extracts ORM queries via tree-sitter
|
|
7
|
-
* queries instead.
|
|
8
|
-
*
|
|
9
|
-
* @module
|
|
10
|
-
*/
|
|
11
|
-
import type { ExtractedORMQuery } from '../workers/parse-worker.js';
|
|
12
|
-
/**
|
|
13
|
-
* Extract ORM query calls from file content using regex.
|
|
14
|
-
*
|
|
15
|
-
* Fast-path: skips files that don't contain `prisma.` or `supabase.from`.
|
|
16
|
-
* Results are appended to the `out` array (push pattern avoids allocation).
|
|
17
|
-
*
|
|
18
|
-
* @param filePath Relative path of the source file
|
|
19
|
-
* @param content File content string
|
|
20
|
-
* @param out Output array to append extracted queries to
|
|
21
|
-
*/
|
|
22
|
-
export declare function extractORMQueriesInline(filePath: string, content: string, out: ExtractedORMQuery[]): void;
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Inline ORM query extraction (sequential fallback path).
|
|
3
|
-
*
|
|
4
|
-
* Extracts Prisma and Supabase query calls from source content using
|
|
5
|
-
* regex patterns. Used by the sequential parse path when workers are
|
|
6
|
-
* not available — the worker path extracts ORM queries via tree-sitter
|
|
7
|
-
* queries instead.
|
|
8
|
-
*
|
|
9
|
-
* @module
|
|
10
|
-
*/
|
|
11
|
-
// ── Regex patterns ─────────────────────────────────────────────────────────
|
|
12
|
-
/** Matches Prisma client method calls: `prisma.user.findMany(...)` */
|
|
13
|
-
const PRISMA_QUERY_RE = /\bprisma\.(\w+)\.(findMany|findFirst|findUnique|findUniqueOrThrow|findFirstOrThrow|create|createMany|update|updateMany|delete|deleteMany|upsert|count|aggregate|groupBy)\s*\(/g;
|
|
14
|
-
/** Matches Supabase client method calls: `supabase.from('users').select(...)` */
|
|
15
|
-
const SUPABASE_QUERY_RE = /\bsupabase\.from\s*\(\s*['"](\w+)['"]\s*\)\s*\.(select|insert|update|delete|upsert)\s*\(/g;
|
|
16
|
-
// ── Extraction function ───────────────────────────────────────────────────
|
|
17
|
-
/**
|
|
18
|
-
* Extract ORM query calls from file content using regex.
|
|
19
|
-
*
|
|
20
|
-
* Fast-path: skips files that don't contain `prisma.` or `supabase.from`.
|
|
21
|
-
* Results are appended to the `out` array (push pattern avoids allocation).
|
|
22
|
-
*
|
|
23
|
-
* @param filePath Relative path of the source file
|
|
24
|
-
* @param content File content string
|
|
25
|
-
* @param out Output array to append extracted queries to
|
|
26
|
-
*/
|
|
27
|
-
export function extractORMQueriesInline(filePath, content, out) {
|
|
28
|
-
const hasPrisma = content.includes('prisma.');
|
|
29
|
-
const hasSupabase = content.includes('supabase.from');
|
|
30
|
-
if (!hasPrisma && !hasSupabase)
|
|
31
|
-
return;
|
|
32
|
-
// Pre-compute line number offsets to avoid O(n²) substring+split per match
|
|
33
|
-
const lineOffsets = buildLineOffsets(content);
|
|
34
|
-
if (hasPrisma) {
|
|
35
|
-
PRISMA_QUERY_RE.lastIndex = 0;
|
|
36
|
-
let m;
|
|
37
|
-
while ((m = PRISMA_QUERY_RE.exec(content)) !== null) {
|
|
38
|
-
const model = m[1];
|
|
39
|
-
if (model.startsWith('$'))
|
|
40
|
-
continue;
|
|
41
|
-
out.push({
|
|
42
|
-
filePath,
|
|
43
|
-
orm: 'prisma',
|
|
44
|
-
model,
|
|
45
|
-
method: m[2],
|
|
46
|
-
lineNumber: lineNumberAtOffset(lineOffsets, m.index),
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (hasSupabase) {
|
|
51
|
-
SUPABASE_QUERY_RE.lastIndex = 0;
|
|
52
|
-
let m;
|
|
53
|
-
while ((m = SUPABASE_QUERY_RE.exec(content)) !== null) {
|
|
54
|
-
out.push({
|
|
55
|
-
filePath,
|
|
56
|
-
orm: 'supabase',
|
|
57
|
-
model: m[1],
|
|
58
|
-
method: m[2],
|
|
59
|
-
lineNumber: lineNumberAtOffset(lineOffsets, m.index),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// ── Line offset helpers ───────────────────────────────────────────────────
|
|
65
|
-
/** Build an array of byte offsets where each newline occurs (O(n) once). */
|
|
66
|
-
function buildLineOffsets(content) {
|
|
67
|
-
const offsets = [];
|
|
68
|
-
for (let i = 0; i < content.length; i++) {
|
|
69
|
-
if (content[i] === '\n')
|
|
70
|
-
offsets.push(i);
|
|
71
|
-
}
|
|
72
|
-
return offsets;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Binary search for 0-based line number at a given character offset.
|
|
76
|
-
*
|
|
77
|
-
* Returns the number of newlines that occur before `offset` in the content,
|
|
78
|
-
* which is the 0-based line number. When `offset` is beyond the last newline,
|
|
79
|
-
* returns `lineOffsets.length` (i.e., the last line index).
|
|
80
|
-
*/
|
|
81
|
-
function lineNumberAtOffset(lineOffsets, offset) {
|
|
82
|
-
let lo = 0;
|
|
83
|
-
let hi = lineOffsets.length;
|
|
84
|
-
while (lo < hi) {
|
|
85
|
-
const mid = (lo + hi) >>> 1;
|
|
86
|
-
if (lineOffsets[mid] < offset)
|
|
87
|
-
lo = mid + 1;
|
|
88
|
-
else
|
|
89
|
-
hi = mid;
|
|
90
|
-
}
|
|
91
|
-
return lo;
|
|
92
|
-
}
|