@optave/codegraph 3.1.3 → 3.1.5
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 +38 -84
- package/package.json +13 -8
- package/src/ast-analysis/engine.js +32 -12
- package/src/ast-analysis/shared.js +6 -5
- package/src/cli/commands/ast.js +22 -0
- package/src/cli/commands/audit.js +45 -0
- package/src/cli/commands/batch.js +68 -0
- package/src/cli/commands/branch-compare.js +21 -0
- package/src/cli/commands/build.js +26 -0
- package/src/cli/commands/cfg.js +26 -0
- package/src/cli/commands/check.js +74 -0
- package/src/cli/commands/children.js +28 -0
- package/src/cli/commands/co-change.js +67 -0
- package/src/cli/commands/communities.js +19 -0
- package/src/cli/commands/complexity.js +46 -0
- package/src/cli/commands/context.js +30 -0
- package/src/cli/commands/cycles.js +32 -0
- package/src/cli/commands/dataflow.js +28 -0
- package/src/cli/commands/deps.js +12 -0
- package/src/cli/commands/diff-impact.js +26 -0
- package/src/cli/commands/embed.js +30 -0
- package/src/cli/commands/export.js +78 -0
- package/src/cli/commands/exports.js +14 -0
- package/src/cli/commands/flow.js +32 -0
- package/src/cli/commands/fn-impact.js +26 -0
- package/src/cli/commands/impact.js +12 -0
- package/src/cli/commands/info.js +76 -0
- package/src/cli/commands/map.js +19 -0
- package/src/cli/commands/mcp.js +18 -0
- package/src/cli/commands/models.js +19 -0
- package/src/cli/commands/owners.js +25 -0
- package/src/cli/commands/path.js +36 -0
- package/src/cli/commands/plot.js +89 -0
- package/src/cli/commands/query.js +45 -0
- package/src/cli/commands/registry.js +100 -0
- package/src/cli/commands/roles.js +30 -0
- package/src/cli/commands/search.js +42 -0
- package/src/cli/commands/sequence.js +28 -0
- package/src/cli/commands/snapshot.js +66 -0
- package/src/cli/commands/stats.js +15 -0
- package/src/cli/commands/structure.js +33 -0
- package/src/cli/commands/triage.js +78 -0
- package/src/cli/commands/watch.js +12 -0
- package/src/cli/commands/where.js +20 -0
- package/src/cli/index.js +124 -0
- package/src/cli/shared/open-graph.js +13 -0
- package/src/cli/shared/options.js +59 -0
- package/src/cli/shared/output.js +1 -0
- package/src/cli.js +11 -1522
- package/src/db/connection.js +130 -7
- package/src/{db.js → db/index.js} +17 -5
- package/src/db/migrations.js +42 -1
- package/src/db/query-builder.js +20 -12
- package/src/db/repository/base.js +201 -0
- package/src/db/repository/graph-read.js +7 -4
- package/src/db/repository/in-memory-repository.js +575 -0
- package/src/db/repository/index.js +5 -1
- package/src/db/repository/nodes.js +60 -6
- package/src/db/repository/sqlite-repository.js +219 -0
- package/src/domain/analysis/context.js +408 -0
- package/src/domain/analysis/dependencies.js +341 -0
- package/src/domain/analysis/exports.js +134 -0
- package/src/domain/analysis/impact.js +466 -0
- package/src/domain/analysis/module-map.js +322 -0
- package/src/domain/analysis/roles.js +45 -0
- package/src/domain/analysis/symbol-lookup.js +238 -0
- package/src/domain/graph/builder/context.js +85 -0
- package/src/domain/graph/builder/helpers.js +218 -0
- package/src/domain/graph/builder/incremental.js +178 -0
- package/src/domain/graph/builder/pipeline.js +130 -0
- package/src/domain/graph/builder/stages/build-edges.js +297 -0
- package/src/domain/graph/builder/stages/build-structure.js +113 -0
- package/src/domain/graph/builder/stages/collect-files.js +44 -0
- package/src/domain/graph/builder/stages/detect-changes.js +413 -0
- package/src/domain/graph/builder/stages/finalize.js +139 -0
- package/src/domain/graph/builder/stages/insert-nodes.js +195 -0
- package/src/domain/graph/builder/stages/parse-files.js +28 -0
- package/src/domain/graph/builder/stages/resolve-imports.js +143 -0
- package/src/domain/graph/builder/stages/run-analyses.js +44 -0
- package/src/domain/graph/builder.js +11 -0
- package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
- package/src/domain/graph/cycles.js +82 -0
- package/src/{journal.js → domain/graph/journal.js} +1 -1
- package/src/{resolve.js → domain/graph/resolve.js} +3 -3
- package/src/{watcher.js → domain/graph/watcher.js} +10 -150
- package/src/{parser.js → domain/parser.js} +5 -5
- package/src/domain/queries.js +48 -0
- package/src/domain/search/generator.js +163 -0
- package/src/domain/search/index.js +13 -0
- package/src/domain/search/models.js +218 -0
- package/src/domain/search/search/cli-formatter.js +151 -0
- package/src/domain/search/search/filters.js +46 -0
- package/src/domain/search/search/hybrid.js +121 -0
- package/src/domain/search/search/keyword.js +68 -0
- package/src/domain/search/search/prepare.js +66 -0
- package/src/domain/search/search/semantic.js +145 -0
- package/src/domain/search/stores/fts5.js +27 -0
- package/src/domain/search/stores/sqlite-blob.js +24 -0
- package/src/domain/search/strategies/source.js +14 -0
- package/src/domain/search/strategies/structured.js +43 -0
- package/src/domain/search/strategies/text-utils.js +43 -0
- package/src/extractors/csharp.js +10 -2
- package/src/extractors/go.js +3 -1
- package/src/extractors/helpers.js +71 -0
- package/src/extractors/java.js +9 -2
- package/src/extractors/javascript.js +39 -2
- package/src/extractors/php.js +3 -1
- package/src/extractors/python.js +14 -3
- package/src/extractors/rust.js +3 -1
- package/src/{ast.js → features/ast.js} +8 -8
- package/src/{audit.js → features/audit.js} +16 -44
- package/src/{batch.js → features/batch.js} +6 -5
- package/src/{boundaries.js → features/boundaries.js} +2 -2
- package/src/{branch-compare.js → features/branch-compare.js} +3 -3
- package/src/{cfg.js → features/cfg.js} +11 -12
- 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} +18 -90
- package/src/{complexity.js → features/complexity.js} +13 -13
- package/src/{dataflow.js → features/dataflow.js} +12 -13
- package/src/features/export.js +378 -0
- package/src/{flow.js → features/flow.js} +4 -4
- package/src/features/graph-enrichment.js +327 -0
- package/src/{manifesto.js → features/manifesto.js} +6 -6
- package/src/{owners.js → features/owners.js} +2 -2
- package/src/{sequence.js → features/sequence.js} +16 -52
- package/src/{snapshot.js → features/snapshot.js} +8 -7
- package/src/{structure.js → features/structure.js} +20 -45
- package/src/{triage.js → features/triage.js} +27 -79
- package/src/graph/algorithms/bfs.js +49 -0
- package/src/graph/algorithms/centrality.js +16 -0
- package/src/graph/algorithms/index.js +5 -0
- package/src/graph/algorithms/louvain.js +26 -0
- package/src/graph/algorithms/shortest-path.js +41 -0
- package/src/graph/algorithms/tarjan.js +49 -0
- package/src/graph/builders/dependency.js +110 -0
- package/src/graph/builders/index.js +3 -0
- package/src/graph/builders/structure.js +40 -0
- package/src/graph/builders/temporal.js +33 -0
- package/src/graph/classifiers/index.js +2 -0
- package/src/graph/classifiers/risk.js +85 -0
- package/src/graph/classifiers/roles.js +64 -0
- package/src/graph/index.js +13 -0
- package/src/graph/model.js +230 -0
- package/src/index.cjs +16 -0
- package/src/index.js +42 -219
- package/src/{native.js → infrastructure/native.js} +3 -1
- package/src/infrastructure/result-formatter.js +2 -21
- package/src/mcp/index.js +2 -0
- package/src/mcp/middleware.js +26 -0
- package/src/mcp/server.js +128 -0
- package/src/{mcp.js → mcp/tool-registry.js} +6 -675
- package/src/mcp/tools/ast-query.js +14 -0
- package/src/mcp/tools/audit.js +21 -0
- package/src/mcp/tools/batch-query.js +11 -0
- package/src/mcp/tools/branch-compare.js +12 -0
- package/src/mcp/tools/cfg.js +21 -0
- package/src/mcp/tools/check.js +43 -0
- package/src/mcp/tools/co-changes.js +20 -0
- package/src/mcp/tools/code-owners.js +12 -0
- package/src/mcp/tools/communities.js +15 -0
- package/src/mcp/tools/complexity.js +18 -0
- package/src/mcp/tools/context.js +17 -0
- package/src/mcp/tools/dataflow.js +26 -0
- package/src/mcp/tools/diff-impact.js +24 -0
- package/src/mcp/tools/execution-flow.js +26 -0
- package/src/mcp/tools/export-graph.js +57 -0
- package/src/mcp/tools/file-deps.js +12 -0
- package/src/mcp/tools/file-exports.js +13 -0
- package/src/mcp/tools/find-cycles.js +15 -0
- package/src/mcp/tools/fn-impact.js +15 -0
- package/src/mcp/tools/impact-analysis.js +12 -0
- package/src/mcp/tools/index.js +71 -0
- package/src/mcp/tools/list-functions.js +14 -0
- package/src/mcp/tools/list-repos.js +11 -0
- package/src/mcp/tools/module-map.js +6 -0
- package/src/mcp/tools/node-roles.js +14 -0
- package/src/mcp/tools/path.js +12 -0
- package/src/mcp/tools/query.js +30 -0
- package/src/mcp/tools/semantic-search.js +65 -0
- package/src/mcp/tools/sequence.js +17 -0
- package/src/mcp/tools/structure.js +15 -0
- package/src/mcp/tools/symbol-children.js +14 -0
- package/src/mcp/tools/triage.js +35 -0
- package/src/mcp/tools/where.js +13 -0
- 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/{commands → presentation}/cfg.js +1 -1
- package/src/{commands → presentation}/check.js +6 -6
- package/src/presentation/colors.js +44 -0
- 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/presentation/export.js +444 -0
- package/src/{commands → presentation}/flow.js +2 -2
- package/src/{commands → presentation}/manifesto.js +4 -4
- package/src/{commands → presentation}/owners.js +1 -1
- package/src/presentation/queries-cli/exports.js +46 -0
- package/src/presentation/queries-cli/impact.js +198 -0
- package/src/presentation/queries-cli/index.js +5 -0
- package/src/presentation/queries-cli/inspect.js +334 -0
- package/src/presentation/queries-cli/overview.js +197 -0
- package/src/presentation/queries-cli/path.js +58 -0
- package/src/presentation/queries-cli.js +27 -0
- package/src/{commands → presentation}/query.js +1 -1
- package/src/presentation/result-formatter.js +144 -0
- package/src/presentation/sequence-renderer.js +43 -0
- package/src/{commands → presentation}/sequence.js +2 -2
- package/src/{commands → presentation}/structure.js +2 -2
- package/src/presentation/table.js +47 -0
- package/src/{commands → presentation}/triage.js +1 -1
- package/src/{viewer.js → presentation/viewer.js} +68 -382
- package/src/{constants.js → shared/constants.js} +1 -1
- package/src/shared/errors.js +78 -0
- package/src/shared/file-utils.js +153 -0
- package/src/shared/generators.js +125 -0
- package/src/shared/hierarchy.js +27 -0
- package/src/shared/normalize.js +59 -0
- package/src/builder.js +0 -1486
- package/src/cycles.js +0 -137
- package/src/embedder.js +0 -1097
- package/src/export.js +0 -681
- package/src/queries-cli.js +0 -866
- package/src/queries.js +0 -2289
- /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/{kinds.js → shared/kinds.js} +0 -0
- /package/src/{paginate.js → shared/paginate.js} +0 -0
package/src/db/connection.js
CHANGED
|
@@ -1,7 +1,60 @@
|
|
|
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 { warn } from '../logger.js';
|
|
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 {
|
|
41
|
+
root = path.resolve(raw);
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
root = null;
|
|
45
|
+
}
|
|
46
|
+
if (!fromDir) {
|
|
47
|
+
_cachedRepoRoot = root;
|
|
48
|
+
_cachedRepoRootCwd = dir;
|
|
49
|
+
}
|
|
50
|
+
return root;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Reset the cached repo root (for testing). */
|
|
54
|
+
export function _resetRepoRootCache() {
|
|
55
|
+
_cachedRepoRoot = undefined;
|
|
56
|
+
_cachedRepoRootCwd = undefined;
|
|
57
|
+
}
|
|
5
58
|
|
|
6
59
|
function isProcessAlive(pid) {
|
|
7
60
|
try {
|
|
@@ -43,6 +96,22 @@ function releaseAdvisoryLock(lockPath) {
|
|
|
43
96
|
}
|
|
44
97
|
}
|
|
45
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Check if two paths refer to the same directory.
|
|
101
|
+
* Handles Windows 8.3 short names (RUNNER~1 vs runneradmin) and macOS
|
|
102
|
+
* symlinks (/tmp vs /private/tmp) where string comparison fails.
|
|
103
|
+
*/
|
|
104
|
+
function isSameDirectory(a, b) {
|
|
105
|
+
if (path.resolve(a) === path.resolve(b)) return true;
|
|
106
|
+
try {
|
|
107
|
+
const sa = fs.statSync(a);
|
|
108
|
+
const sb = fs.statSync(b);
|
|
109
|
+
return sa.dev === sb.dev && sa.ino === sb.ino;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
46
115
|
export function openDb(dbPath) {
|
|
47
116
|
const dir = path.dirname(dbPath);
|
|
48
117
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
@@ -61,15 +130,41 @@ export function closeDb(db) {
|
|
|
61
130
|
|
|
62
131
|
export function findDbPath(customPath) {
|
|
63
132
|
if (customPath) return path.resolve(customPath);
|
|
64
|
-
|
|
133
|
+
const rawCeiling = findRepoRoot();
|
|
134
|
+
// Normalize ceiling with realpathSync to resolve 8.3 short names (Windows
|
|
135
|
+
// RUNNER~1 → runneradmin) and symlinks (macOS /var → /private/var).
|
|
136
|
+
// findRepoRoot already applies realpathSync internally, but the git output
|
|
137
|
+
// may still contain short names on some Windows CI environments.
|
|
138
|
+
let ceiling;
|
|
139
|
+
if (rawCeiling) {
|
|
140
|
+
try {
|
|
141
|
+
ceiling = fs.realpathSync(rawCeiling);
|
|
142
|
+
} catch {
|
|
143
|
+
ceiling = rawCeiling;
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
ceiling = null;
|
|
147
|
+
}
|
|
148
|
+
// Resolve symlinks (e.g. macOS /var → /private/var) so dir matches ceiling from git
|
|
149
|
+
let dir;
|
|
150
|
+
try {
|
|
151
|
+
dir = fs.realpathSync(process.cwd());
|
|
152
|
+
} catch {
|
|
153
|
+
dir = process.cwd();
|
|
154
|
+
}
|
|
65
155
|
while (true) {
|
|
66
156
|
const candidate = path.join(dir, '.codegraph', 'graph.db');
|
|
67
157
|
if (fs.existsSync(candidate)) return candidate;
|
|
158
|
+
if (ceiling && isSameDirectory(dir, ceiling)) {
|
|
159
|
+
debug(`findDbPath: stopped at git ceiling ${ceiling}`);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
68
162
|
const parent = path.dirname(dir);
|
|
69
163
|
if (parent === dir) break;
|
|
70
164
|
dir = parent;
|
|
71
165
|
}
|
|
72
|
-
|
|
166
|
+
const base = ceiling || process.cwd();
|
|
167
|
+
return path.join(base, '.codegraph', 'graph.db');
|
|
73
168
|
}
|
|
74
169
|
|
|
75
170
|
/**
|
|
@@ -78,11 +173,39 @@ export function findDbPath(customPath) {
|
|
|
78
173
|
export function openReadonlyOrFail(customPath) {
|
|
79
174
|
const dbPath = findDbPath(customPath);
|
|
80
175
|
if (!fs.existsSync(dbPath)) {
|
|
81
|
-
|
|
82
|
-
`No codegraph database found at ${dbPath}.\
|
|
83
|
-
|
|
176
|
+
throw new DbError(
|
|
177
|
+
`No codegraph database found at ${dbPath}.\nRun "codegraph build" first to analyze your codebase.`,
|
|
178
|
+
{ file: dbPath },
|
|
84
179
|
);
|
|
85
|
-
process.exit(1);
|
|
86
180
|
}
|
|
87
181
|
return new Database(dbPath, { readonly: true });
|
|
88
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Open a Repository from either an injected instance or a DB path.
|
|
186
|
+
*
|
|
187
|
+
* When `opts.repo` is a Repository instance, returns it directly (no DB opened).
|
|
188
|
+
* Otherwise opens a readonly SQLite DB and wraps it in SqliteRepository.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} [customDbPath] - Path to graph.db (ignored when opts.repo is set)
|
|
191
|
+
* @param {object} [opts]
|
|
192
|
+
* @param {Repository} [opts.repo] - Pre-built Repository to use instead of SQLite
|
|
193
|
+
* @returns {{ repo: Repository, close(): void }}
|
|
194
|
+
*/
|
|
195
|
+
export function openRepo(customDbPath, opts = {}) {
|
|
196
|
+
if (opts.repo != null) {
|
|
197
|
+
if (!(opts.repo instanceof Repository)) {
|
|
198
|
+
throw new TypeError(
|
|
199
|
+
`openRepo: opts.repo must be a Repository instance, got ${Object.prototype.toString.call(opts.repo)}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return { repo: opts.repo, close() {} };
|
|
203
|
+
}
|
|
204
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
205
|
+
return {
|
|
206
|
+
repo: new SqliteRepository(db),
|
|
207
|
+
close() {
|
|
208
|
+
db.close();
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -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,
|
|
@@ -29,8 +36,10 @@ export {
|
|
|
29
36
|
findImportTargets,
|
|
30
37
|
findIntraFileCallEdges,
|
|
31
38
|
findNodeById,
|
|
39
|
+
findNodeByQualifiedName,
|
|
32
40
|
findNodeChildren,
|
|
33
41
|
findNodesByFile,
|
|
42
|
+
findNodesByScope,
|
|
34
43
|
findNodesForTriage,
|
|
35
44
|
findNodesWithFanIn,
|
|
36
45
|
getCallableNodes,
|
|
@@ -50,9 +59,12 @@ export {
|
|
|
50
59
|
hasCoChanges,
|
|
51
60
|
hasDataflowTable,
|
|
52
61
|
hasEmbeddings,
|
|
62
|
+
InMemoryRepository,
|
|
53
63
|
iterateFunctionNodes,
|
|
54
64
|
listFunctionNodes,
|
|
55
65
|
purgeFileData,
|
|
56
66
|
purgeFilesData,
|
|
67
|
+
Repository,
|
|
68
|
+
SqliteRepository,
|
|
57
69
|
upsertCoChangeMeta,
|
|
58
|
-
} 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 = [
|
|
@@ -229,6 +229,17 @@ export const MIGRATIONS = [
|
|
|
229
229
|
CREATE INDEX IF NOT EXISTS idx_nodes_exported ON nodes(exported);
|
|
230
230
|
`,
|
|
231
231
|
},
|
|
232
|
+
{
|
|
233
|
+
version: 15,
|
|
234
|
+
up: `
|
|
235
|
+
ALTER TABLE nodes ADD COLUMN qualified_name TEXT;
|
|
236
|
+
ALTER TABLE nodes ADD COLUMN scope TEXT;
|
|
237
|
+
ALTER TABLE nodes ADD COLUMN visibility TEXT;
|
|
238
|
+
UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL;
|
|
239
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name);
|
|
240
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope);
|
|
241
|
+
`,
|
|
242
|
+
},
|
|
232
243
|
];
|
|
233
244
|
|
|
234
245
|
export function getBuildMeta(db, key) {
|
|
@@ -309,4 +320,34 @@ export function initSchema(db) {
|
|
|
309
320
|
} catch {
|
|
310
321
|
/* already exists */
|
|
311
322
|
}
|
|
323
|
+
try {
|
|
324
|
+
db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT');
|
|
325
|
+
} catch {
|
|
326
|
+
/* already exists */
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
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 {
|
|
339
|
+
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
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
|
|
345
|
+
} catch {
|
|
346
|
+
/* already exists */
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
|
|
350
|
+
} catch {
|
|
351
|
+
/* already exists */
|
|
352
|
+
}
|
|
312
353
|
}
|
package/src/db/query-builder.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DbError } from '../shared/errors.js';
|
|
2
|
+
import { EVERY_EDGE_KIND } from '../shared/kinds.js';
|
|
2
3
|
|
|
3
4
|
// ─── Validation Helpers ─────────────────────────────────────────────
|
|
4
5
|
|
|
@@ -12,13 +13,13 @@ const SAFE_SELECT_TOKEN_RE =
|
|
|
12
13
|
|
|
13
14
|
function validateAlias(alias) {
|
|
14
15
|
if (!SAFE_ALIAS_RE.test(alias)) {
|
|
15
|
-
throw new
|
|
16
|
+
throw new DbError(`Invalid SQL alias: ${alias}`);
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function validateColumn(column) {
|
|
20
21
|
if (!SAFE_COLUMN_RE.test(column)) {
|
|
21
|
-
throw new
|
|
22
|
+
throw new DbError(`Invalid SQL column: ${column}`);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -26,7 +27,7 @@ function validateOrderBy(clause) {
|
|
|
26
27
|
const terms = clause.split(',').map((t) => t.trim());
|
|
27
28
|
for (const term of terms) {
|
|
28
29
|
if (!SAFE_ORDER_TERM_RE.test(term)) {
|
|
29
|
-
throw new
|
|
30
|
+
throw new DbError(`Invalid ORDER BY term: ${term}`);
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -51,19 +52,26 @@ function validateSelectCols(cols) {
|
|
|
51
52
|
const tokens = splitTopLevelCommas(cols);
|
|
52
53
|
for (const token of tokens) {
|
|
53
54
|
if (!SAFE_SELECT_TOKEN_RE.test(token)) {
|
|
54
|
-
throw new
|
|
55
|
+
throw new DbError(`Invalid SELECT expression: ${token}`);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
function validateEdgeKind(edgeKind) {
|
|
60
61
|
if (!EVERY_EDGE_KIND.includes(edgeKind)) {
|
|
61
|
-
throw new
|
|
62
|
+
throw new DbError(
|
|
62
63
|
`Invalid edge kind: ${edgeKind} (expected one of ${EVERY_EDGE_KIND.join(', ')})`,
|
|
63
64
|
);
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
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
|
+
|
|
67
75
|
// ─── Standalone Helpers ──────────────────────────────────────────────
|
|
68
76
|
|
|
69
77
|
/**
|
|
@@ -163,11 +171,11 @@ export class NodeQuery {
|
|
|
163
171
|
return this;
|
|
164
172
|
}
|
|
165
173
|
|
|
166
|
-
/** WHERE n.file LIKE ? (no-op if falsy). */
|
|
174
|
+
/** WHERE n.file LIKE ? (no-op if falsy). Escapes LIKE wildcards in the value. */
|
|
167
175
|
fileFilter(file) {
|
|
168
176
|
if (!file) return this;
|
|
169
|
-
this.#conditions.push(
|
|
170
|
-
this.#params.push(`%${file}%`);
|
|
177
|
+
this.#conditions.push("n.file LIKE ? ESCAPE '\\'");
|
|
178
|
+
this.#params.push(`%${escapeLike(file)}%`);
|
|
171
179
|
return this;
|
|
172
180
|
}
|
|
173
181
|
|
|
@@ -187,11 +195,11 @@ export class NodeQuery {
|
|
|
187
195
|
return this;
|
|
188
196
|
}
|
|
189
197
|
|
|
190
|
-
/** WHERE n.name LIKE ? (no-op if falsy). */
|
|
198
|
+
/** WHERE n.name LIKE ? (no-op if falsy). Escapes LIKE wildcards in the value. */
|
|
191
199
|
nameLike(pattern) {
|
|
192
200
|
if (!pattern) return this;
|
|
193
|
-
this.#conditions.push(
|
|
194
|
-
this.#params.push(`%${pattern}%`);
|
|
201
|
+
this.#conditions.push("n.name LIKE ? ESCAPE '\\'");
|
|
202
|
+
this.#params.push(`%${escapeLike(pattern)}%`);
|
|
195
203
|
return this;
|
|
196
204
|
}
|
|
197
205
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract Repository base class.
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for all graph data access. Every method throws
|
|
5
|
+
* "not implemented" by default — concrete subclasses override what they support.
|
|
6
|
+
*/
|
|
7
|
+
export class Repository {
|
|
8
|
+
// ── Node lookups ────────────────────────────────────────────────────
|
|
9
|
+
/** @param {number} id @returns {object|undefined} */
|
|
10
|
+
findNodeById(_id) {
|
|
11
|
+
throw new Error('not implemented');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @param {string} file @returns {object[]} */
|
|
15
|
+
findNodesByFile(_file) {
|
|
16
|
+
throw new Error('not implemented');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @param {string} fileLike @returns {object[]} */
|
|
20
|
+
findFileNodes(_fileLike) {
|
|
21
|
+
throw new Error('not implemented');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @param {string} namePattern @param {object} [opts] @returns {object[]} */
|
|
25
|
+
findNodesWithFanIn(_namePattern, _opts) {
|
|
26
|
+
throw new Error('not implemented');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @returns {number} */
|
|
30
|
+
countNodes() {
|
|
31
|
+
throw new Error('not implemented');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @returns {number} */
|
|
35
|
+
countEdges() {
|
|
36
|
+
throw new Error('not implemented');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @returns {number} */
|
|
40
|
+
countFiles() {
|
|
41
|
+
throw new Error('not implemented');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {string} name @param {string} kind @param {string} file @param {number} line @returns {number|undefined} */
|
|
45
|
+
getNodeId(_name, _kind, _file, _line) {
|
|
46
|
+
throw new Error('not implemented');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {string} name @param {string} file @param {number} line @returns {number|undefined} */
|
|
50
|
+
getFunctionNodeId(_name, _file, _line) {
|
|
51
|
+
throw new Error('not implemented');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {string} file @returns {{ id: number, name: string, kind: string, line: number }[]} */
|
|
55
|
+
bulkNodeIdsByFile(_file) {
|
|
56
|
+
throw new Error('not implemented');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @param {number} parentId @returns {object[]} */
|
|
60
|
+
findNodeChildren(_parentId) {
|
|
61
|
+
throw new Error('not implemented');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {string} scopeName @param {object} [opts] @returns {object[]} */
|
|
65
|
+
findNodesByScope(_scopeName, _opts) {
|
|
66
|
+
throw new Error('not implemented');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** @param {string} qualifiedName @param {object} [opts] @returns {object[]} */
|
|
70
|
+
findNodeByQualifiedName(_qualifiedName, _opts) {
|
|
71
|
+
throw new Error('not implemented');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @param {object} [opts] @returns {object[]} */
|
|
75
|
+
listFunctionNodes(_opts) {
|
|
76
|
+
throw new Error('not implemented');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {object} [opts] @returns {IterableIterator} */
|
|
80
|
+
iterateFunctionNodes(_opts) {
|
|
81
|
+
throw new Error('not implemented');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @param {object} [opts] @returns {object[]} */
|
|
85
|
+
findNodesForTriage(_opts) {
|
|
86
|
+
throw new Error('not implemented');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Edge queries ────────────────────────────────────────────────────
|
|
90
|
+
/** @param {number} nodeId @returns {object[]} */
|
|
91
|
+
findCallees(_nodeId) {
|
|
92
|
+
throw new Error('not implemented');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @param {number} nodeId @returns {object[]} */
|
|
96
|
+
findCallers(_nodeId) {
|
|
97
|
+
throw new Error('not implemented');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @param {number} nodeId @returns {object[]} */
|
|
101
|
+
findDistinctCallers(_nodeId) {
|
|
102
|
+
throw new Error('not implemented');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @param {number} nodeId @returns {object[]} */
|
|
106
|
+
findAllOutgoingEdges(_nodeId) {
|
|
107
|
+
throw new Error('not implemented');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {number} nodeId @returns {object[]} */
|
|
111
|
+
findAllIncomingEdges(_nodeId) {
|
|
112
|
+
throw new Error('not implemented');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @param {number} nodeId @returns {string[]} */
|
|
116
|
+
findCalleeNames(_nodeId) {
|
|
117
|
+
throw new Error('not implemented');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** @param {number} nodeId @returns {string[]} */
|
|
121
|
+
findCallerNames(_nodeId) {
|
|
122
|
+
throw new Error('not implemented');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @param {number} nodeId @returns {{ file: string, edge_kind: string }[]} */
|
|
126
|
+
findImportTargets(_nodeId) {
|
|
127
|
+
throw new Error('not implemented');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @param {number} nodeId @returns {{ file: string, edge_kind: string }[]} */
|
|
131
|
+
findImportSources(_nodeId) {
|
|
132
|
+
throw new Error('not implemented');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @param {number} nodeId @returns {object[]} */
|
|
136
|
+
findImportDependents(_nodeId) {
|
|
137
|
+
throw new Error('not implemented');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @param {string} file @returns {Set<number>} */
|
|
141
|
+
findCrossFileCallTargets(_file) {
|
|
142
|
+
throw new Error('not implemented');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** @param {number} nodeId @param {string} file @returns {number} */
|
|
146
|
+
countCrossFileCallers(_nodeId, _file) {
|
|
147
|
+
throw new Error('not implemented');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @param {number} classNodeId @returns {Set<number>} */
|
|
151
|
+
getClassHierarchy(_classNodeId) {
|
|
152
|
+
throw new Error('not implemented');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @param {string} file @returns {{ caller_name: string, callee_name: string }[]} */
|
|
156
|
+
findIntraFileCallEdges(_file) {
|
|
157
|
+
throw new Error('not implemented');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Graph-read queries ──────────────────────────────────────────────
|
|
161
|
+
/** @returns {{ id: number, name: string, kind: string, file: string }[]} */
|
|
162
|
+
getCallableNodes() {
|
|
163
|
+
throw new Error('not implemented');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** @returns {{ source_id: number, target_id: number, confidence: number|null }[]} */
|
|
167
|
+
getCallEdges() {
|
|
168
|
+
throw new Error('not implemented');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @returns {{ id: number, name: string, file: string }[]} */
|
|
172
|
+
getFileNodesAll() {
|
|
173
|
+
throw new Error('not implemented');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @returns {{ source_id: number, target_id: number }[]} */
|
|
177
|
+
getImportEdges() {
|
|
178
|
+
throw new Error('not implemented');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Optional table checks (default: false/undefined) ────────────────
|
|
182
|
+
/** @returns {boolean} */
|
|
183
|
+
hasCfgTables() {
|
|
184
|
+
throw new Error('not implemented');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @returns {boolean} */
|
|
188
|
+
hasEmbeddings() {
|
|
189
|
+
throw new Error('not implemented');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** @returns {boolean} */
|
|
193
|
+
hasDataflowTable() {
|
|
194
|
+
throw new Error('not implemented');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** @param {number} nodeId @returns {object|undefined} */
|
|
198
|
+
getComplexityForNode(_nodeId) {
|
|
199
|
+
throw new Error('not implemented');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CORE_SYMBOL_KINDS } from '../../shared/kinds.js';
|
|
1
2
|
import { cachedStmt } from './cached-stmt.js';
|
|
2
3
|
|
|
3
4
|
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
@@ -6,8 +7,10 @@ const _getCallEdgesStmt = new WeakMap();
|
|
|
6
7
|
const _getFileNodesAllStmt = new WeakMap();
|
|
7
8
|
const _getImportEdgesStmt = new WeakMap();
|
|
8
9
|
|
|
10
|
+
const CALLABLE_KINDS_SQL = CORE_SYMBOL_KINDS.map((k) => `'${k}'`).join(',');
|
|
11
|
+
|
|
9
12
|
/**
|
|
10
|
-
* Get callable nodes (
|
|
13
|
+
* Get callable nodes (all core symbol kinds) for graph construction.
|
|
11
14
|
* @param {object} db
|
|
12
15
|
* @returns {{ id: number, name: string, kind: string, file: string }[]}
|
|
13
16
|
*/
|
|
@@ -15,20 +18,20 @@ export function getCallableNodes(db) {
|
|
|
15
18
|
return cachedStmt(
|
|
16
19
|
_getCallableNodesStmt,
|
|
17
20
|
db,
|
|
18
|
-
|
|
21
|
+
`SELECT id, name, kind, file FROM nodes WHERE kind IN (${CALLABLE_KINDS_SQL})`,
|
|
19
22
|
).all();
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* Get all 'calls' edges.
|
|
24
27
|
* @param {object} db
|
|
25
|
-
* @returns {{ source_id: number, target_id: number }[]}
|
|
28
|
+
* @returns {{ source_id: number, target_id: number, confidence: number|null }[]}
|
|
26
29
|
*/
|
|
27
30
|
export function getCallEdges(db) {
|
|
28
31
|
return cachedStmt(
|
|
29
32
|
_getCallEdgesStmt,
|
|
30
33
|
db,
|
|
31
|
-
"SELECT source_id, target_id FROM edges WHERE kind = 'calls'",
|
|
34
|
+
"SELECT source_id, target_id, confidence FROM edges WHERE kind = 'calls'",
|
|
32
35
|
).all();
|
|
33
36
|
}
|
|
34
37
|
|