@optave/codegraph 3.1.4 → 3.2.0
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 +29 -72
- package/package.json +10 -8
- package/src/ast-analysis/engine.js +260 -246
- package/src/ast-analysis/shared.js +2 -14
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +4 -7
- package/src/cli/commands/audit.js +11 -11
- package/src/cli/commands/batch.js +6 -5
- package/src/cli/commands/branch-compare.js +1 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/build.js +1 -1
- package/src/cli/commands/cfg.js +5 -8
- package/src/cli/commands/check.js +28 -36
- package/src/cli/commands/children.js +9 -7
- package/src/cli/commands/co-change.js +5 -3
- package/src/cli/commands/communities.js +2 -6
- package/src/cli/commands/complexity.js +5 -3
- package/src/cli/commands/context.js +9 -8
- package/src/cli/commands/cycles.js +12 -8
- package/src/cli/commands/dataflow.js +5 -8
- package/src/cli/commands/deps.js +9 -8
- package/src/cli/commands/diff-impact.js +2 -6
- package/src/cli/commands/embed.js +1 -1
- package/src/cli/commands/export.js +34 -31
- package/src/cli/commands/exports.js +2 -6
- package/src/cli/commands/flow.js +5 -8
- package/src/cli/commands/fn-impact.js +9 -8
- package/src/cli/commands/impact.js +2 -6
- package/src/cli/commands/info.js +2 -2
- package/src/cli/commands/map.js +1 -1
- package/src/cli/commands/mcp.js +1 -1
- package/src/cli/commands/models.js +1 -1
- package/src/cli/commands/owners.js +5 -3
- package/src/cli/commands/path.js +2 -2
- package/src/cli/commands/plot.js +40 -31
- package/src/cli/commands/query.js +9 -8
- package/src/cli/commands/registry.js +2 -2
- package/src/cli/commands/roles.js +5 -8
- package/src/cli/commands/search.js +9 -3
- package/src/cli/commands/sequence.js +5 -8
- package/src/cli/commands/snapshot.js +6 -1
- package/src/cli/commands/stats.js +1 -1
- package/src/cli/commands/structure.js +5 -4
- package/src/cli/commands/triage.js +41 -30
- package/src/cli/commands/watch.js +1 -1
- package/src/cli/commands/where.js +2 -6
- package/src/cli/index.js +11 -5
- package/src/cli/shared/open-graph.js +13 -0
- package/src/cli/shared/options.js +22 -2
- package/src/cli.js +1 -1
- package/src/db/connection.js +140 -11
- package/src/{db.js → db/index.js} +12 -5
- package/src/db/migrations.js +42 -65
- package/src/db/query-builder.js +72 -9
- package/src/db/repository/base.js +1 -1
- package/src/db/repository/graph-read.js +3 -3
- package/src/db/repository/in-memory-repository.js +30 -28
- package/src/db/repository/nodes.js +10 -17
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +392 -0
- package/src/domain/analysis/dependencies.js +395 -0
- package/src/{analysis → domain/analysis}/exports.js +11 -6
- package/src/domain/analysis/impact.js +581 -0
- package/src/domain/analysis/module-map.js +348 -0
- package/src/{analysis → domain/analysis}/roles.js +12 -9
- package/src/{analysis → domain/analysis}/symbol-lookup.js +19 -11
- package/src/{builder → domain/graph/builder}/helpers.js +4 -4
- package/src/{builder → domain/graph/builder}/incremental.js +119 -93
- package/src/domain/graph/builder/pipeline.js +156 -0
- package/src/domain/graph/builder/stages/build-edges.js +376 -0
- package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
- package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
- package/src/{builder → domain/graph/builder}/stages/detect-changes.js +204 -183
- package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
- package/src/domain/graph/builder/stages/insert-nodes.js +203 -0
- package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
- package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
- package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
- package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
- package/src/{cycles.js → domain/graph/cycles.js} +4 -4
- package/src/{journal.js → domain/graph/journal.js} +1 -1
- package/src/{resolve.js → domain/graph/resolve.js} +2 -2
- package/src/{watcher.js → domain/graph/watcher.js} +7 -7
- package/src/{parser.js → domain/parser.js} +24 -15
- package/src/{queries.js → domain/queries.js} +17 -16
- package/src/{embeddings → domain/search}/generator.js +3 -3
- package/src/{embeddings → domain/search}/models.js +2 -2
- package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
- package/src/{embeddings → domain/search}/search/filters.js +9 -5
- package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
- package/src/{embeddings → domain/search}/search/keyword.js +13 -6
- package/src/{embeddings → domain/search}/search/prepare.js +15 -7
- package/src/{embeddings → domain/search}/search/semantic.js +1 -1
- package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +275 -305
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/{ast.js → features/ast.js} +13 -11
- package/src/{audit.js → features/audit.js} +20 -46
- package/src/{batch.js → features/batch.js} +5 -5
- package/src/{boundaries.js → features/boundaries.js} +100 -85
- package/src/{branch-compare.js → features/branch-compare.js} +3 -3
- package/src/{cfg.js → features/cfg.js} +141 -150
- package/src/{check.js → features/check.js} +13 -30
- package/src/{cochange.js → features/cochange.js} +5 -5
- package/src/{communities.js → features/communities.js} +72 -57
- package/src/{complexity.js → features/complexity.js} +154 -143
- package/src/{dataflow.js → features/dataflow.js} +155 -158
- package/src/{export.js → features/export.js} +6 -6
- package/src/{flow.js → features/flow.js} +4 -4
- package/src/{viewer.js → features/graph-enrichment.js} +8 -8
- package/src/{manifesto.js → features/manifesto.js} +15 -12
- package/src/{owners.js → features/owners.js} +6 -5
- package/src/features/sequence.js +300 -0
- package/src/features/shared/find-nodes.js +31 -0
- package/src/{snapshot.js → features/snapshot.js} +3 -3
- package/src/{structure.js → features/structure.js} +139 -108
- package/src/features/triage.js +141 -0
- package/src/graph/builders/dependency.js +33 -14
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.cjs +16 -0
- package/src/index.js +40 -39
- package/src/{native.js → infrastructure/native.js} +1 -1
- package/src/mcp/middleware.js +1 -1
- package/src/mcp/server.js +68 -59
- package/src/mcp/tool-registry.js +15 -2
- package/src/mcp/tools/ast-query.js +1 -1
- package/src/mcp/tools/audit.js +1 -1
- package/src/mcp/tools/batch-query.js +1 -1
- package/src/mcp/tools/branch-compare.js +3 -1
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/cfg.js +1 -1
- package/src/mcp/tools/check.js +3 -3
- package/src/mcp/tools/co-changes.js +1 -1
- package/src/mcp/tools/code-owners.js +1 -1
- package/src/mcp/tools/communities.js +1 -1
- package/src/mcp/tools/complexity.js +1 -1
- package/src/mcp/tools/dataflow.js +2 -2
- package/src/mcp/tools/execution-flow.js +2 -2
- package/src/mcp/tools/export-graph.js +2 -2
- package/src/mcp/tools/find-cycles.js +2 -2
- package/src/mcp/tools/index.js +2 -0
- package/src/mcp/tools/list-repos.js +1 -1
- package/src/mcp/tools/sequence.js +1 -1
- package/src/mcp/tools/structure.js +1 -1
- package/src/mcp/tools/triage.js +2 -2
- package/src/{commands → presentation}/audit.js +2 -2
- package/src/{commands → presentation}/batch.js +1 -1
- package/src/{commands → presentation}/branch-compare.js +2 -2
- package/src/presentation/brief.js +51 -0
- package/src/{commands → presentation}/cfg.js +1 -1
- package/src/{commands → presentation}/check.js +2 -2
- package/src/{commands → presentation}/communities.js +1 -1
- package/src/{commands → presentation}/complexity.js +1 -1
- package/src/{commands → presentation}/dataflow.js +1 -1
- package/src/{commands → presentation}/flow.js +2 -2
- package/src/{commands → presentation}/manifesto.js +1 -1
- package/src/{commands → presentation}/owners.js +1 -1
- package/src/presentation/queries-cli/exports.js +53 -0
- package/src/presentation/queries-cli/impact.js +214 -0
- package/src/presentation/queries-cli/index.js +5 -0
- package/src/presentation/queries-cli/inspect.js +329 -0
- package/src/presentation/queries-cli/overview.js +196 -0
- package/src/presentation/queries-cli/path.js +65 -0
- package/src/presentation/queries-cli.js +27 -0
- package/src/{commands → presentation}/query.js +1 -1
- package/src/presentation/result-formatter.js +126 -3
- package/src/{commands → presentation}/sequence.js +2 -2
- package/src/{commands → presentation}/structure.js +1 -1
- package/src/presentation/table.js +0 -8
- package/src/{commands → presentation}/triage.js +1 -1
- package/src/{constants.js → shared/constants.js} +1 -1
- package/src/shared/file-utils.js +2 -2
- package/src/shared/generators.js +9 -5
- package/src/shared/hierarchy.js +1 -1
- package/src/{kinds.js → shared/kinds.js} +1 -1
- package/src/analysis/context.js +0 -408
- package/src/analysis/dependencies.js +0 -341
- package/src/analysis/impact.js +0 -463
- package/src/analysis/module-map.js +0 -322
- package/src/builder/pipeline.js +0 -130
- package/src/builder/stages/build-edges.js +0 -297
- package/src/builder/stages/insert-nodes.js +0 -195
- package/src/mcp.js +0 -2
- package/src/queries-cli.js +0 -866
- package/src/sequence.js +0 -289
- package/src/triage.js +0 -126
- /package/src/{builder → domain/graph/builder}/context.js +0 -0
- /package/src/{builder.js → domain/graph/builder.js} +0 -0
- /package/src/{embeddings → domain/search}/index.js +0 -0
- /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
- /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
- /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
- /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
- /package/src/{config.js → infrastructure/config.js} +0 -0
- /package/src/{logger.js → infrastructure/logger.js} +0 -0
- /package/src/{registry.js → infrastructure/registry.js} +0 -0
- /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
- /package/src/{commands → presentation}/cochange.js +0 -0
- /package/src/{errors.js → shared/errors.js} +0 -0
- /package/src/{paginate.js → shared/paginate.js} +0 -0
package/src/db/connection.js
CHANGED
|
@@ -1,14 +1,69 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import Database from 'better-sqlite3';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { debug, warn } from '../infrastructure/logger.js';
|
|
6
|
+
import { DbError } from '../shared/errors.js';
|
|
7
|
+
import { Repository } from './repository/base.js';
|
|
8
|
+
import { SqliteRepository } from './repository/sqlite-repository.js';
|
|
9
|
+
|
|
10
|
+
let _cachedRepoRoot; // undefined = not computed, null = not a git repo
|
|
11
|
+
let _cachedRepoRootCwd; // cwd at the time the cache was populated
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Return the git worktree/repo root for the given directory (or cwd).
|
|
15
|
+
* Uses `git rev-parse --show-toplevel` which returns the correct root
|
|
16
|
+
* for both regular repos and git worktrees.
|
|
17
|
+
* Results are cached per-process when called without arguments.
|
|
18
|
+
* The cache is keyed on cwd so it invalidates if the working directory changes
|
|
19
|
+
* (e.g. MCP server serving multiple sessions).
|
|
20
|
+
* @param {string} [fromDir] - Directory to resolve from (defaults to cwd)
|
|
21
|
+
* @returns {string | null} Absolute path to repo root, or null if not in a git repo
|
|
22
|
+
*/
|
|
23
|
+
export function findRepoRoot(fromDir) {
|
|
24
|
+
const dir = fromDir || process.cwd();
|
|
25
|
+
if (!fromDir && _cachedRepoRoot !== undefined && _cachedRepoRootCwd === dir) {
|
|
26
|
+
return _cachedRepoRoot;
|
|
27
|
+
}
|
|
28
|
+
let root = null;
|
|
29
|
+
try {
|
|
30
|
+
const raw = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
31
|
+
cwd: dir,
|
|
32
|
+
encoding: 'utf-8',
|
|
33
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
34
|
+
}).trim();
|
|
35
|
+
// Use realpathSync to resolve symlinks (macOS /var → /private/var) and
|
|
36
|
+
// 8.3 short names (Windows RUNNER~1 → runneradmin) so the ceiling path
|
|
37
|
+
// matches the realpathSync'd dir in findDbPath.
|
|
38
|
+
try {
|
|
39
|
+
root = fs.realpathSync(raw);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
debug(`realpathSync failed for git root "${raw}", using resolve: ${e.message}`);
|
|
42
|
+
root = path.resolve(raw);
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
debug(`git rev-parse failed for "${dir}": ${e.message}`);
|
|
46
|
+
root = null;
|
|
47
|
+
}
|
|
48
|
+
if (!fromDir) {
|
|
49
|
+
_cachedRepoRoot = root;
|
|
50
|
+
_cachedRepoRootCwd = dir;
|
|
51
|
+
}
|
|
52
|
+
return root;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Reset the cached repo root (for testing). */
|
|
56
|
+
export function _resetRepoRootCache() {
|
|
57
|
+
_cachedRepoRoot = undefined;
|
|
58
|
+
_cachedRepoRootCwd = undefined;
|
|
59
|
+
}
|
|
6
60
|
|
|
7
61
|
function isProcessAlive(pid) {
|
|
8
62
|
try {
|
|
9
63
|
process.kill(pid, 0);
|
|
10
64
|
return true;
|
|
11
|
-
} catch {
|
|
65
|
+
} catch (e) {
|
|
66
|
+
debug(`PID ${pid} not alive: ${e.code || e.message}`);
|
|
12
67
|
return false;
|
|
13
68
|
}
|
|
14
69
|
}
|
|
@@ -23,13 +78,13 @@ function acquireAdvisoryLock(dbPath) {
|
|
|
23
78
|
warn(`Another process (PID ${pid}) may be using this database. Proceeding with caution.`);
|
|
24
79
|
}
|
|
25
80
|
}
|
|
26
|
-
} catch {
|
|
27
|
-
|
|
81
|
+
} catch (e) {
|
|
82
|
+
debug(`Advisory lock read failed: ${e.message}`);
|
|
28
83
|
}
|
|
29
84
|
try {
|
|
30
85
|
fs.writeFileSync(lockPath, String(process.pid), 'utf-8');
|
|
31
|
-
} catch {
|
|
32
|
-
|
|
86
|
+
} catch (e) {
|
|
87
|
+
debug(`Advisory lock write failed: ${e.message}`);
|
|
33
88
|
}
|
|
34
89
|
}
|
|
35
90
|
|
|
@@ -39,8 +94,25 @@ function releaseAdvisoryLock(lockPath) {
|
|
|
39
94
|
if (Number(content) === process.pid) {
|
|
40
95
|
fs.unlinkSync(lockPath);
|
|
41
96
|
}
|
|
42
|
-
} catch {
|
|
43
|
-
|
|
97
|
+
} catch (e) {
|
|
98
|
+
debug(`Advisory lock release failed for ${lockPath}: ${e.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if two paths refer to the same directory.
|
|
104
|
+
* Handles Windows 8.3 short names (RUNNER~1 vs runneradmin) and macOS
|
|
105
|
+
* symlinks (/tmp vs /private/tmp) where string comparison fails.
|
|
106
|
+
*/
|
|
107
|
+
function isSameDirectory(a, b) {
|
|
108
|
+
if (path.resolve(a) === path.resolve(b)) return true;
|
|
109
|
+
try {
|
|
110
|
+
const sa = fs.statSync(a);
|
|
111
|
+
const sb = fs.statSync(b);
|
|
112
|
+
return sa.dev === sb.dev && sa.ino === sb.ino;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
debug(`isSameDirectory stat failed: ${e.message}`);
|
|
115
|
+
return false;
|
|
44
116
|
}
|
|
45
117
|
}
|
|
46
118
|
|
|
@@ -62,15 +134,43 @@ export function closeDb(db) {
|
|
|
62
134
|
|
|
63
135
|
export function findDbPath(customPath) {
|
|
64
136
|
if (customPath) return path.resolve(customPath);
|
|
65
|
-
|
|
137
|
+
const rawCeiling = findRepoRoot();
|
|
138
|
+
// Normalize ceiling with realpathSync to resolve 8.3 short names (Windows
|
|
139
|
+
// RUNNER~1 → runneradmin) and symlinks (macOS /var → /private/var).
|
|
140
|
+
// findRepoRoot already applies realpathSync internally, but the git output
|
|
141
|
+
// may still contain short names on some Windows CI environments.
|
|
142
|
+
let ceiling;
|
|
143
|
+
if (rawCeiling) {
|
|
144
|
+
try {
|
|
145
|
+
ceiling = fs.realpathSync(rawCeiling);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
debug(`realpathSync failed for ceiling "${rawCeiling}": ${e.message}`);
|
|
148
|
+
ceiling = rawCeiling;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
ceiling = null;
|
|
152
|
+
}
|
|
153
|
+
// Resolve symlinks (e.g. macOS /var → /private/var) so dir matches ceiling from git
|
|
154
|
+
let dir;
|
|
155
|
+
try {
|
|
156
|
+
dir = fs.realpathSync(process.cwd());
|
|
157
|
+
} catch (e) {
|
|
158
|
+
debug(`realpathSync failed for cwd: ${e.message}`);
|
|
159
|
+
dir = process.cwd();
|
|
160
|
+
}
|
|
66
161
|
while (true) {
|
|
67
162
|
const candidate = path.join(dir, '.codegraph', 'graph.db');
|
|
68
163
|
if (fs.existsSync(candidate)) return candidate;
|
|
164
|
+
if (ceiling && isSameDirectory(dir, ceiling)) {
|
|
165
|
+
debug(`findDbPath: stopped at git ceiling ${ceiling}`);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
69
168
|
const parent = path.dirname(dir);
|
|
70
169
|
if (parent === dir) break;
|
|
71
170
|
dir = parent;
|
|
72
171
|
}
|
|
73
|
-
|
|
172
|
+
const base = ceiling || process.cwd();
|
|
173
|
+
return path.join(base, '.codegraph', 'graph.db');
|
|
74
174
|
}
|
|
75
175
|
|
|
76
176
|
/**
|
|
@@ -86,3 +186,32 @@ export function openReadonlyOrFail(customPath) {
|
|
|
86
186
|
}
|
|
87
187
|
return new Database(dbPath, { readonly: true });
|
|
88
188
|
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Open a Repository from either an injected instance or a DB path.
|
|
192
|
+
*
|
|
193
|
+
* When `opts.repo` is a Repository instance, returns it directly (no DB opened).
|
|
194
|
+
* Otherwise opens a readonly SQLite DB and wraps it in SqliteRepository.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} [customDbPath] - Path to graph.db (ignored when opts.repo is set)
|
|
197
|
+
* @param {object} [opts]
|
|
198
|
+
* @param {Repository} [opts.repo] - Pre-built Repository to use instead of SQLite
|
|
199
|
+
* @returns {{ repo: Repository, close(): void }}
|
|
200
|
+
*/
|
|
201
|
+
export function openRepo(customDbPath, opts = {}) {
|
|
202
|
+
if (opts.repo != null) {
|
|
203
|
+
if (!(opts.repo instanceof Repository)) {
|
|
204
|
+
throw new TypeError(
|
|
205
|
+
`openRepo: opts.repo must be a Repository instance, got ${Object.prototype.toString.call(opts.repo)}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return { repo: opts.repo, close() {} };
|
|
209
|
+
}
|
|
210
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
211
|
+
return {
|
|
212
|
+
repo: new SqliteRepository(db),
|
|
213
|
+
close() {
|
|
214
|
+
db.close();
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
// Barrel re-export — keeps all existing `import { ... } from '
|
|
2
|
-
export {
|
|
3
|
-
|
|
1
|
+
// Barrel re-export — keeps all existing `import { ... } from '…/db/index.js'` working.
|
|
2
|
+
export {
|
|
3
|
+
closeDb,
|
|
4
|
+
findDbPath,
|
|
5
|
+
findRepoRoot,
|
|
6
|
+
openDb,
|
|
7
|
+
openReadonlyOrFail,
|
|
8
|
+
openRepo,
|
|
9
|
+
} from './connection.js';
|
|
10
|
+
export { getBuildMeta, initSchema, MIGRATIONS, setBuildMeta } from './migrations.js';
|
|
4
11
|
export {
|
|
5
12
|
fanInJoinSQL,
|
|
6
13
|
fanOutJoinSQL,
|
|
7
14
|
kindInClause,
|
|
8
15
|
NodeQuery,
|
|
9
16
|
testFilterSQL,
|
|
10
|
-
} from './
|
|
17
|
+
} from './query-builder.js';
|
|
11
18
|
export {
|
|
12
19
|
bulkNodeIdsByFile,
|
|
13
20
|
countCrossFileCallers,
|
|
@@ -60,4 +67,4 @@ export {
|
|
|
60
67
|
Repository,
|
|
61
68
|
SqliteRepository,
|
|
62
69
|
upsertCoChangeMeta,
|
|
63
|
-
} from './
|
|
70
|
+
} from './repository/index.js';
|
package/src/db/migrations.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { debug } from '../logger.js';
|
|
1
|
+
import { debug } from '../infrastructure/logger.js';
|
|
2
2
|
|
|
3
3
|
// ─── Schema Migrations ─────────────────────────────────────────────────
|
|
4
4
|
export const MIGRATIONS = [
|
|
@@ -242,11 +242,23 @@ export const MIGRATIONS = [
|
|
|
242
242
|
},
|
|
243
243
|
];
|
|
244
244
|
|
|
245
|
+
function hasColumn(db, table, column) {
|
|
246
|
+
const cols = db.pragma(`table_info(${table})`);
|
|
247
|
+
return cols.some((c) => c.name === column);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function hasTable(db, table) {
|
|
251
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(table);
|
|
252
|
+
return !!row;
|
|
253
|
+
}
|
|
254
|
+
|
|
245
255
|
export function getBuildMeta(db, key) {
|
|
256
|
+
if (!hasTable(db, 'build_meta')) return null;
|
|
246
257
|
try {
|
|
247
258
|
const row = db.prepare('SELECT value FROM build_meta WHERE key = ?').get(key);
|
|
248
259
|
return row ? row.value : null;
|
|
249
|
-
} catch {
|
|
260
|
+
} catch (e) {
|
|
261
|
+
debug(`getBuildMeta failed for key "${key}": ${e.message}`);
|
|
250
262
|
return null;
|
|
251
263
|
}
|
|
252
264
|
}
|
|
@@ -280,74 +292,39 @@ export function initSchema(db) {
|
|
|
280
292
|
}
|
|
281
293
|
}
|
|
282
294
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
/* already exists */
|
|
292
|
-
}
|
|
293
|
-
try {
|
|
294
|
-
db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0');
|
|
295
|
-
} catch {
|
|
296
|
-
/* already exists */
|
|
297
|
-
}
|
|
298
|
-
try {
|
|
299
|
-
db.exec('ALTER TABLE nodes ADD COLUMN role TEXT');
|
|
300
|
-
} catch {
|
|
301
|
-
/* already exists */
|
|
302
|
-
}
|
|
303
|
-
try {
|
|
295
|
+
// Legacy column compat — add columns that may be missing from pre-migration DBs
|
|
296
|
+
if (hasTable(db, 'nodes')) {
|
|
297
|
+
if (!hasColumn(db, 'nodes', 'end_line')) {
|
|
298
|
+
db.exec('ALTER TABLE nodes ADD COLUMN end_line INTEGER');
|
|
299
|
+
}
|
|
300
|
+
if (!hasColumn(db, 'nodes', 'role')) {
|
|
301
|
+
db.exec('ALTER TABLE nodes ADD COLUMN role TEXT');
|
|
302
|
+
}
|
|
304
303
|
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)');
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
try {
|
|
309
|
-
db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)');
|
|
310
|
-
} catch {
|
|
311
|
-
/* already exists */
|
|
312
|
-
}
|
|
313
|
-
try {
|
|
304
|
+
if (!hasColumn(db, 'nodes', 'parent_id')) {
|
|
305
|
+
db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)');
|
|
306
|
+
}
|
|
314
307
|
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)');
|
|
315
|
-
} catch {
|
|
316
|
-
/* already exists */
|
|
317
|
-
}
|
|
318
|
-
try {
|
|
319
308
|
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)');
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
|
|
330
|
-
} catch {
|
|
331
|
-
/* already exists */
|
|
332
|
-
}
|
|
333
|
-
try {
|
|
334
|
-
db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
|
|
335
|
-
} catch {
|
|
336
|
-
/* already exists */
|
|
337
|
-
}
|
|
338
|
-
try {
|
|
309
|
+
if (!hasColumn(db, 'nodes', 'qualified_name')) {
|
|
310
|
+
db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT');
|
|
311
|
+
}
|
|
312
|
+
if (!hasColumn(db, 'nodes', 'scope')) {
|
|
313
|
+
db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
|
|
314
|
+
}
|
|
315
|
+
if (!hasColumn(db, 'nodes', 'visibility')) {
|
|
316
|
+
db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
|
|
317
|
+
}
|
|
339
318
|
db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL');
|
|
340
|
-
} catch {
|
|
341
|
-
/* nodes table may not exist yet */
|
|
342
|
-
}
|
|
343
|
-
try {
|
|
344
319
|
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
|
|
345
|
-
} catch {
|
|
346
|
-
/* already exists */
|
|
347
|
-
}
|
|
348
|
-
try {
|
|
349
320
|
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
|
|
350
|
-
}
|
|
351
|
-
|
|
321
|
+
}
|
|
322
|
+
if (hasTable(db, 'edges')) {
|
|
323
|
+
if (!hasColumn(db, 'edges', 'confidence')) {
|
|
324
|
+
db.exec('ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0');
|
|
325
|
+
}
|
|
326
|
+
if (!hasColumn(db, 'edges', 'dynamic')) {
|
|
327
|
+
db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0');
|
|
328
|
+
}
|
|
352
329
|
}
|
|
353
330
|
}
|
package/src/db/query-builder.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DbError } from '../errors.js';
|
|
2
|
-
import { EVERY_EDGE_KIND } from '../kinds.js';
|
|
1
|
+
import { DbError } from '../shared/errors.js';
|
|
2
|
+
import { EVERY_EDGE_KIND } from '../shared/kinds.js';
|
|
3
3
|
|
|
4
4
|
// ─── Validation Helpers ─────────────────────────────────────────────
|
|
5
5
|
|
|
@@ -65,6 +65,62 @@ function validateEdgeKind(edgeKind) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// ─── LIKE Escaping ──────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/** Escape LIKE wildcards in a literal string segment. */
|
|
71
|
+
export function escapeLike(s) {
|
|
72
|
+
return s.replace(/[%_\\]/g, '\\$&');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Normalize a file filter value (string, string[], or falsy) into a flat array.
|
|
77
|
+
* Returns an empty array when the input is falsy.
|
|
78
|
+
* @param {string|string[]|undefined|null} file
|
|
79
|
+
* @returns {string[]}
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeFileFilter(file) {
|
|
82
|
+
if (!file) return [];
|
|
83
|
+
return Array.isArray(file) ? file : [file];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a SQL condition + params for a multi-value file LIKE filter.
|
|
88
|
+
* Returns `{ sql: '', params: [] }` when the filter is empty.
|
|
89
|
+
*
|
|
90
|
+
* @param {string|string[]} file - One or more partial file paths
|
|
91
|
+
* @param {string} [column='file'] - The column name to filter on (e.g. 'n.file', 'a.file')
|
|
92
|
+
* @returns {{ sql: string, params: string[] }}
|
|
93
|
+
*/
|
|
94
|
+
export function buildFileConditionSQL(file, column = 'file') {
|
|
95
|
+
validateColumn(column);
|
|
96
|
+
const files = normalizeFileFilter(file);
|
|
97
|
+
if (files.length === 0) return { sql: '', params: [] };
|
|
98
|
+
if (files.length === 1) {
|
|
99
|
+
return {
|
|
100
|
+
sql: ` AND ${column} LIKE ? ESCAPE '\\'`,
|
|
101
|
+
params: [`%${escapeLike(files[0])}%`],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const clauses = files.map(() => `${column} LIKE ? ESCAPE '\\'`);
|
|
105
|
+
return {
|
|
106
|
+
sql: ` AND (${clauses.join(' OR ')})`,
|
|
107
|
+
params: files.map((f) => `%${escapeLike(f)}%`),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Commander option accumulator for repeatable `--file` flag.
|
|
113
|
+
* Use as: `['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile]`
|
|
114
|
+
* @param {string} val - New value from Commander
|
|
115
|
+
* @param {string[]} acc - Accumulated values (undefined on first call)
|
|
116
|
+
* @returns {string[]}
|
|
117
|
+
*/
|
|
118
|
+
export function collectFile(val, acc) {
|
|
119
|
+
acc = acc || [];
|
|
120
|
+
acc.push(val);
|
|
121
|
+
return acc;
|
|
122
|
+
}
|
|
123
|
+
|
|
68
124
|
// ─── Standalone Helpers ──────────────────────────────────────────────
|
|
69
125
|
|
|
70
126
|
/**
|
|
@@ -164,11 +220,18 @@ export class NodeQuery {
|
|
|
164
220
|
return this;
|
|
165
221
|
}
|
|
166
222
|
|
|
167
|
-
/** WHERE n.file LIKE ? (no-op if falsy). */
|
|
223
|
+
/** WHERE n.file LIKE ? (no-op if falsy). Accepts a single string or string[]. */
|
|
168
224
|
fileFilter(file) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
225
|
+
const files = normalizeFileFilter(file);
|
|
226
|
+
if (files.length === 0) return this;
|
|
227
|
+
if (files.length === 1) {
|
|
228
|
+
this.#conditions.push("n.file LIKE ? ESCAPE '\\'");
|
|
229
|
+
this.#params.push(`%${escapeLike(files[0])}%`);
|
|
230
|
+
} else {
|
|
231
|
+
const clauses = files.map(() => "n.file LIKE ? ESCAPE '\\'");
|
|
232
|
+
this.#conditions.push(`(${clauses.join(' OR ')})`);
|
|
233
|
+
this.#params.push(...files.map((f) => `%${escapeLike(f)}%`));
|
|
234
|
+
}
|
|
172
235
|
return this;
|
|
173
236
|
}
|
|
174
237
|
|
|
@@ -188,11 +251,11 @@ export class NodeQuery {
|
|
|
188
251
|
return this;
|
|
189
252
|
}
|
|
190
253
|
|
|
191
|
-
/** WHERE n.name LIKE ? (no-op if falsy). */
|
|
254
|
+
/** WHERE n.name LIKE ? (no-op if falsy). Escapes LIKE wildcards in the value. */
|
|
192
255
|
nameLike(pattern) {
|
|
193
256
|
if (!pattern) return this;
|
|
194
|
-
this.#conditions.push(
|
|
195
|
-
this.#params.push(`%${pattern}%`);
|
|
257
|
+
this.#conditions.push("n.name LIKE ? ESCAPE '\\'");
|
|
258
|
+
this.#params.push(`%${escapeLike(pattern)}%`);
|
|
196
259
|
return this;
|
|
197
260
|
}
|
|
198
261
|
|
|
@@ -163,7 +163,7 @@ export class Repository {
|
|
|
163
163
|
throw new Error('not implemented');
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/** @returns {{ source_id: number, target_id: number }[]} */
|
|
166
|
+
/** @returns {{ source_id: number, target_id: number, confidence: number|null }[]} */
|
|
167
167
|
getCallEdges() {
|
|
168
168
|
throw new Error('not implemented');
|
|
169
169
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CORE_SYMBOL_KINDS } from '../../kinds.js';
|
|
1
|
+
import { CORE_SYMBOL_KINDS } from '../../shared/kinds.js';
|
|
2
2
|
import { cachedStmt } from './cached-stmt.js';
|
|
3
3
|
|
|
4
4
|
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
@@ -25,13 +25,13 @@ export function getCallableNodes(db) {
|
|
|
25
25
|
/**
|
|
26
26
|
* Get all 'calls' edges.
|
|
27
27
|
* @param {object} db
|
|
28
|
-
* @returns {{ source_id: number, target_id: number }[]}
|
|
28
|
+
* @returns {{ source_id: number, target_id: number, confidence: number|null }[]}
|
|
29
29
|
*/
|
|
30
30
|
export function getCallEdges(db) {
|
|
31
31
|
return cachedStmt(
|
|
32
32
|
_getCallEdgesStmt,
|
|
33
33
|
db,
|
|
34
|
-
"SELECT source_id, target_id FROM edges WHERE kind = 'calls'",
|
|
34
|
+
"SELECT source_id, target_id, confidence FROM edges WHERE kind = 'calls'",
|
|
35
35
|
).all();
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import { ConfigError } from '../../errors.js';
|
|
2
|
-
import { CORE_SYMBOL_KINDS, EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
|
|
1
|
+
import { ConfigError } from '../../shared/errors.js';
|
|
2
|
+
import { CORE_SYMBOL_KINDS, EVERY_SYMBOL_KIND, VALID_ROLES } from '../../shared/kinds.js';
|
|
3
|
+
import { escapeLike, normalizeFileFilter } from '../query-builder.js';
|
|
3
4
|
import { Repository } from './base.js';
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
* Escape LIKE special characters so they are treated as literals.
|
|
7
|
-
* Mirrors the `escapeLike` function in `nodes.js`.
|
|
8
|
-
* @param {string} s
|
|
9
|
-
* @returns {string}
|
|
10
|
-
*/
|
|
11
|
-
function escapeLike(s) {
|
|
12
|
-
return s.replace(/[%_\\]/g, '\\$&');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
6
|
/**
|
|
16
7
|
* Convert a SQL LIKE pattern to a RegExp (case-insensitive).
|
|
17
8
|
* Supports `%` (any chars) and `_` (single char).
|
|
@@ -36,6 +27,17 @@ function likeToRegex(pattern) {
|
|
|
36
27
|
return new RegExp(`^${regex}$`, 'i');
|
|
37
28
|
}
|
|
38
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Build a filter predicate for file matching.
|
|
32
|
+
* Accepts string, string[], or falsy. Returns null when no filtering needed.
|
|
33
|
+
*/
|
|
34
|
+
function buildFileFilterFn(file) {
|
|
35
|
+
const files = normalizeFileFilter(file);
|
|
36
|
+
if (files.length === 0) return null;
|
|
37
|
+
const regexes = files.map((f) => likeToRegex(`%${escapeLike(f)}%`));
|
|
38
|
+
return (filePath) => regexes.some((re) => re.test(filePath));
|
|
39
|
+
}
|
|
40
|
+
|
|
39
41
|
/**
|
|
40
42
|
* In-memory Repository implementation backed by Maps.
|
|
41
43
|
* No SQLite dependency — suitable for fast unit tests.
|
|
@@ -130,9 +132,9 @@ export class InMemoryRepository extends Repository {
|
|
|
130
132
|
if (opts.kinds) {
|
|
131
133
|
nodes = nodes.filter((n) => opts.kinds.includes(n.kind));
|
|
132
134
|
}
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
nodes = nodes.filter((n) =>
|
|
135
|
+
{
|
|
136
|
+
const fileFn = buildFileFilterFn(opts.file);
|
|
137
|
+
if (fileFn) nodes = nodes.filter((n) => fileFn(n.file));
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
// Compute fan-in per node
|
|
@@ -206,9 +208,9 @@ export class InMemoryRepository extends Repository {
|
|
|
206
208
|
if (opts.kind) {
|
|
207
209
|
nodes = nodes.filter((n) => n.kind === opts.kind);
|
|
208
210
|
}
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
nodes = nodes.filter((n) =>
|
|
211
|
+
{
|
|
212
|
+
const fileFn = buildFileFilterFn(opts.file);
|
|
213
|
+
if (fileFn) nodes = nodes.filter((n) => fileFn(n.file));
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
@@ -217,9 +219,9 @@ export class InMemoryRepository extends Repository {
|
|
|
217
219
|
findNodeByQualifiedName(qualifiedName, opts = {}) {
|
|
218
220
|
let nodes = [...this.#nodes.values()].filter((n) => n.qualified_name === qualifiedName);
|
|
219
221
|
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
nodes = nodes.filter((n) =>
|
|
222
|
+
{
|
|
223
|
+
const fileFn = buildFileFilterFn(opts.file);
|
|
224
|
+
if (fileFn) nodes = nodes.filter((n) => fileFn(n.file));
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
@@ -257,9 +259,9 @@ export class InMemoryRepository extends Repository {
|
|
|
257
259
|
!n.file.includes('.stories.'),
|
|
258
260
|
);
|
|
259
261
|
}
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
nodes = nodes.filter((n) =>
|
|
262
|
+
{
|
|
263
|
+
const fileFn = buildFileFilterFn(opts.file);
|
|
264
|
+
if (fileFn) nodes = nodes.filter((n) => fileFn(n.file));
|
|
263
265
|
}
|
|
264
266
|
if (opts.role) {
|
|
265
267
|
nodes = nodes.filter((n) => n.role === opts.role);
|
|
@@ -498,7 +500,7 @@ export class InMemoryRepository extends Repository {
|
|
|
498
500
|
getCallEdges() {
|
|
499
501
|
return [...this.#edges.values()]
|
|
500
502
|
.filter((e) => e.kind === 'calls')
|
|
501
|
-
.map((e) => ({ source_id: e.source_id, target_id: e.target_id }));
|
|
503
|
+
.map((e) => ({ source_id: e.source_id, target_id: e.target_id, confidence: e.confidence }));
|
|
502
504
|
}
|
|
503
505
|
|
|
504
506
|
getFileNodesAll() {
|
|
@@ -550,9 +552,9 @@ export class InMemoryRepository extends Repository {
|
|
|
550
552
|
['function', 'method', 'class'].includes(n.kind),
|
|
551
553
|
);
|
|
552
554
|
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
nodes = nodes.filter((n) =>
|
|
555
|
+
{
|
|
556
|
+
const fileFn = buildFileFilterFn(opts.file);
|
|
557
|
+
if (fileFn) nodes = nodes.filter((n) => fileFn(n.file));
|
|
556
558
|
}
|
|
557
559
|
if (opts.pattern) {
|
|
558
560
|
const patternRe = likeToRegex(`%${escapeLike(opts.pattern)}%`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ConfigError } from '../../errors.js';
|
|
2
|
-
import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
|
|
3
|
-
import { NodeQuery } from '../query-builder.js';
|
|
1
|
+
import { ConfigError } from '../../shared/errors.js';
|
|
2
|
+
import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../shared/kinds.js';
|
|
3
|
+
import { buildFileConditionSQL, NodeQuery } from '../query-builder.js';
|
|
4
4
|
import { cachedStmt } from './cached-stmt.js';
|
|
5
5
|
|
|
6
6
|
// ─── Query-builder based lookups (moved from src/db/repository.js) ─────
|
|
@@ -250,11 +250,6 @@ export function findNodeChildren(db, parentId) {
|
|
|
250
250
|
).all(parentId);
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
/** Escape LIKE wildcards in a literal string segment. */
|
|
254
|
-
function escapeLike(s) {
|
|
255
|
-
return s.replace(/[%_\\]/g, '\\$&');
|
|
256
|
-
}
|
|
257
|
-
|
|
258
253
|
/**
|
|
259
254
|
* Find all nodes that belong to a given scope (by scope column).
|
|
260
255
|
* Enables "all methods of class X" without traversing edges.
|
|
@@ -272,10 +267,9 @@ export function findNodesByScope(db, scopeName, opts = {}) {
|
|
|
272
267
|
sql += ' AND kind = ?';
|
|
273
268
|
params.push(opts.kind);
|
|
274
269
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
270
|
+
const fc = buildFileConditionSQL(opts.file, 'file');
|
|
271
|
+
sql += fc.sql;
|
|
272
|
+
params.push(...fc.params);
|
|
279
273
|
sql += ' ORDER BY file, line';
|
|
280
274
|
return db.prepare(sql).all(...params);
|
|
281
275
|
}
|
|
@@ -291,12 +285,11 @@ export function findNodesByScope(db, scopeName, opts = {}) {
|
|
|
291
285
|
* @returns {object[]}
|
|
292
286
|
*/
|
|
293
287
|
export function findNodeByQualifiedName(db, qualifiedName, opts = {}) {
|
|
294
|
-
|
|
288
|
+
const fc = buildFileConditionSQL(opts.file, 'file');
|
|
289
|
+
if (fc.sql) {
|
|
295
290
|
return db
|
|
296
|
-
.prepare(
|
|
297
|
-
|
|
298
|
-
)
|
|
299
|
-
.all(qualifiedName, `%${escapeLike(opts.file)}%`);
|
|
291
|
+
.prepare(`SELECT * FROM nodes WHERE qualified_name = ?${fc.sql} ORDER BY file, line`)
|
|
292
|
+
.all(qualifiedName, ...fc.params);
|
|
300
293
|
}
|
|
301
294
|
return cachedStmt(
|
|
302
295
|
_findNodeByQualifiedNameStmt,
|