@optave/codegraph 3.1.4 → 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 +26 -70
- package/package.json +10 -8
- package/src/ast-analysis/engine.js +32 -12
- package/src/ast-analysis/shared.js +2 -2
- package/src/cli/commands/ast.js +2 -6
- package/src/cli/commands/audit.js +9 -10
- package/src/cli/commands/batch.js +4 -4
- package/src/cli/commands/branch-compare.js +1 -1
- package/src/cli/commands/build.js +1 -1
- package/src/cli/commands/cfg.js +3 -7
- package/src/cli/commands/check.js +12 -17
- package/src/cli/commands/children.js +3 -6
- package/src/cli/commands/co-change.js +5 -3
- package/src/cli/commands/communities.js +2 -6
- package/src/cli/commands/complexity.js +3 -2
- package/src/cli/commands/context.js +3 -7
- package/src/cli/commands/cycles.js +12 -8
- package/src/cli/commands/dataflow.js +3 -7
- package/src/cli/commands/deps.js +2 -6
- 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 +3 -7
- package/src/cli/commands/fn-impact.js +3 -7
- 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 +1 -1
- package/src/cli/commands/path.js +2 -2
- package/src/cli/commands/plot.js +40 -31
- package/src/cli/commands/query.js +3 -7
- package/src/cli/commands/registry.js +2 -2
- package/src/cli/commands/roles.js +3 -7
- package/src/cli/commands/search.js +1 -1
- package/src/cli/commands/sequence.js +3 -7
- 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 +4 -4
- 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 +127 -4
- package/src/{db.js → db/index.js} +12 -5
- package/src/db/migrations.js +1 -1
- package/src/db/query-builder.js +15 -8
- 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 +4 -13
- package/src/db/repository/nodes.js +3 -8
- package/src/{analysis → domain/analysis}/context.js +6 -6
- package/src/{analysis → domain/analysis}/dependencies.js +5 -5
- package/src/{analysis → domain/analysis}/exports.js +8 -4
- package/src/{analysis → domain/analysis}/impact.js +61 -58
- package/src/{analysis → domain/analysis}/module-map.js +3 -3
- package/src/{analysis → domain/analysis}/roles.js +4 -4
- package/src/{analysis → domain/analysis}/symbol-lookup.js +13 -7
- package/src/{builder → domain/graph/builder}/helpers.js +3 -3
- package/src/{builder → domain/graph/builder}/incremental.js +3 -3
- package/src/{builder → domain/graph/builder}/pipeline.js +4 -4
- package/src/{builder → domain/graph/builder}/stages/build-edges.js +2 -2
- 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 +6 -6
- package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
- package/src/{builder → domain/graph/builder}/stages/insert-nodes.js +1 -1
- 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} +5 -5
- package/src/{parser.js → domain/parser.js} +5 -5
- package/src/{queries.js → domain/queries.js} +16 -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/hybrid.js +1 -1
- package/src/{embeddings → domain/search}/search/keyword.js +1 -1
- package/src/{embeddings → domain/search}/search/prepare.js +2 -2
- package/src/{embeddings → domain/search}/search/semantic.js +1 -1
- package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
- package/src/extractors/javascript.js +1 -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} +5 -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} +10 -10
- 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} +7 -7
- package/src/{complexity.js → features/complexity.js} +13 -13
- package/src/{dataflow.js → features/dataflow.js} +11 -11
- package/src/{export.js → features/export.js} +3 -3
- package/src/{flow.js → features/flow.js} +4 -4
- package/src/{viewer.js → features/graph-enrichment.js} +6 -6
- 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} +15 -15
- package/src/{snapshot.js → features/snapshot.js} +3 -3
- package/src/{structure.js → features/structure.js} +7 -7
- package/src/{triage.js → features/triage.js} +8 -8
- package/src/graph/builders/dependency.js +33 -14
- package/src/index.cjs +16 -0
- package/src/index.js +39 -39
- package/src/{native.js → infrastructure/native.js} +1 -1
- package/src/mcp/middleware.js +1 -1
- package/src/mcp/server.js +5 -5
- package/src/mcp/tool-registry.js +2 -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/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/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/{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 +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 +126 -3
- package/src/{commands → presentation}/sequence.js +2 -2
- package/src/{commands → presentation}/structure.js +1 -1
- 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 +2 -2
- package/src/shared/hierarchy.js +1 -1
- package/src/mcp.js +0 -2
- package/src/queries-cli.js +0 -866
- /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}/search/filters.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/{kinds.js → shared/kinds.js} +0 -0
- /package/src/{paginate.js → shared/paginate.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadConfig } from '../../config.js';
|
|
1
|
+
import { loadConfig } from '../../infrastructure/config.js';
|
|
2
2
|
|
|
3
3
|
const config = loadConfig(process.cwd());
|
|
4
4
|
|
|
@@ -15,7 +15,9 @@ export function applyQueryOpts(cmd) {
|
|
|
15
15
|
.option('-j, --json', 'Output as JSON')
|
|
16
16
|
.option('--limit <number>', 'Max results to return')
|
|
17
17
|
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
18
|
-
.option('--ndjson', 'Newline-delimited JSON output')
|
|
18
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
19
|
+
.option('--table', 'Output as aligned table')
|
|
20
|
+
.option('--csv', 'Output as CSV');
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -30,6 +32,24 @@ export function resolveNoTests(opts) {
|
|
|
30
32
|
return config.query?.excludeTests || false;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Extract the common query option fields shared by most analysis commands.
|
|
37
|
+
*
|
|
38
|
+
* Spreads cleanly into per-command option objects:
|
|
39
|
+
* `{ ...resolveQueryOpts(opts), depth: parseInt(opts.depth, 10) }`
|
|
40
|
+
*/
|
|
41
|
+
export function resolveQueryOpts(opts) {
|
|
42
|
+
return {
|
|
43
|
+
noTests: resolveNoTests(opts),
|
|
44
|
+
json: opts.json,
|
|
45
|
+
ndjson: opts.ndjson,
|
|
46
|
+
table: opts.table,
|
|
47
|
+
csv: opts.csv,
|
|
48
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
49
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
export function formatSize(bytes) {
|
|
34
54
|
if (bytes < 1024) return `${bytes} B`;
|
|
35
55
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
package/src/cli.js
CHANGED
package/src/db/connection.js
CHANGED
|
@@ -1,8 +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 {
|
|
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 {
|
|
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
|
+
}
|
|
6
58
|
|
|
7
59
|
function isProcessAlive(pid) {
|
|
8
60
|
try {
|
|
@@ -44,6 +96,22 @@ function releaseAdvisoryLock(lockPath) {
|
|
|
44
96
|
}
|
|
45
97
|
}
|
|
46
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
|
+
|
|
47
115
|
export function openDb(dbPath) {
|
|
48
116
|
const dir = path.dirname(dbPath);
|
|
49
117
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
@@ -62,15 +130,41 @@ export function closeDb(db) {
|
|
|
62
130
|
|
|
63
131
|
export function findDbPath(customPath) {
|
|
64
132
|
if (customPath) return path.resolve(customPath);
|
|
65
|
-
|
|
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
|
+
}
|
|
66
155
|
while (true) {
|
|
67
156
|
const candidate = path.join(dir, '.codegraph', 'graph.db');
|
|
68
157
|
if (fs.existsSync(candidate)) return candidate;
|
|
158
|
+
if (ceiling && isSameDirectory(dir, ceiling)) {
|
|
159
|
+
debug(`findDbPath: stopped at git ceiling ${ceiling}`);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
69
162
|
const parent = path.dirname(dir);
|
|
70
163
|
if (parent === dir) break;
|
|
71
164
|
dir = parent;
|
|
72
165
|
}
|
|
73
|
-
|
|
166
|
+
const base = ceiling || process.cwd();
|
|
167
|
+
return path.join(base, '.codegraph', 'graph.db');
|
|
74
168
|
}
|
|
75
169
|
|
|
76
170
|
/**
|
|
@@ -86,3 +180,32 @@ export function openReadonlyOrFail(customPath) {
|
|
|
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,
|
|
@@ -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
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,13 @@ 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
|
+
|
|
68
75
|
// ─── Standalone Helpers ──────────────────────────────────────────────
|
|
69
76
|
|
|
70
77
|
/**
|
|
@@ -164,11 +171,11 @@ export class NodeQuery {
|
|
|
164
171
|
return this;
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
/** WHERE n.file LIKE ? (no-op if falsy). */
|
|
174
|
+
/** WHERE n.file LIKE ? (no-op if falsy). Escapes LIKE wildcards in the value. */
|
|
168
175
|
fileFilter(file) {
|
|
169
176
|
if (!file) return this;
|
|
170
|
-
this.#conditions.push(
|
|
171
|
-
this.#params.push(`%${file}%`);
|
|
177
|
+
this.#conditions.push("n.file LIKE ? ESCAPE '\\'");
|
|
178
|
+
this.#params.push(`%${escapeLike(file)}%`);
|
|
172
179
|
return this;
|
|
173
180
|
}
|
|
174
181
|
|
|
@@ -188,11 +195,11 @@ export class NodeQuery {
|
|
|
188
195
|
return this;
|
|
189
196
|
}
|
|
190
197
|
|
|
191
|
-
/** WHERE n.name LIKE ? (no-op if falsy). */
|
|
198
|
+
/** WHERE n.name LIKE ? (no-op if falsy). Escapes LIKE wildcards in the value. */
|
|
192
199
|
nameLike(pattern) {
|
|
193
200
|
if (!pattern) return this;
|
|
194
|
-
this.#conditions.push(
|
|
195
|
-
this.#params.push(`%${pattern}%`);
|
|
201
|
+
this.#conditions.push("n.name LIKE ? ESCAPE '\\'");
|
|
202
|
+
this.#params.push(`%${escapeLike(pattern)}%`);
|
|
196
203
|
return this;
|
|
197
204
|
}
|
|
198
205
|
|
|
@@ -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 } 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).
|
|
@@ -498,7 +489,7 @@ export class InMemoryRepository extends Repository {
|
|
|
498
489
|
getCallEdges() {
|
|
499
490
|
return [...this.#edges.values()]
|
|
500
491
|
.filter((e) => e.kind === 'calls')
|
|
501
|
-
.map((e) => ({ source_id: e.source_id, target_id: e.target_id }));
|
|
492
|
+
.map((e) => ({ source_id: e.source_id, target_id: e.target_id, confidence: e.confidence }));
|
|
502
493
|
}
|
|
503
494
|
|
|
504
495
|
getFileNodesAll() {
|
|
@@ -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 { escapeLike, 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.
|
|
@@ -12,18 +12,18 @@ import {
|
|
|
12
12
|
findNodesByFile,
|
|
13
13
|
getComplexityForNode,
|
|
14
14
|
openReadonlyOrFail,
|
|
15
|
-
} from '
|
|
16
|
-
import { isTestFile } from '
|
|
17
|
-
import { paginateResult } from '../paginate.js';
|
|
15
|
+
} from '../../db/index.js';
|
|
16
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
18
17
|
import {
|
|
19
18
|
createFileLinesReader,
|
|
20
19
|
extractSignature,
|
|
21
20
|
extractSummary,
|
|
22
21
|
isFileLikeTarget,
|
|
23
22
|
readSourceRange,
|
|
24
|
-
} from '
|
|
25
|
-
import { resolveMethodViaHierarchy } from '
|
|
26
|
-
import { normalizeSymbol } from '
|
|
23
|
+
} from '../../shared/file-utils.js';
|
|
24
|
+
import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
|
|
25
|
+
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
26
|
+
import { paginateResult } from '../../shared/paginate.js';
|
|
27
27
|
import { findMatchingNodes } from './symbol-lookup.js';
|
|
28
28
|
|
|
29
29
|
function explainFileImpl(db, target, getFileLines) {
|
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
findImportTargets,
|
|
7
7
|
findNodesByFile,
|
|
8
8
|
openReadonlyOrFail,
|
|
9
|
-
} from '
|
|
10
|
-
import { isTestFile } from '
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
9
|
+
} from '../../db/index.js';
|
|
10
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
11
|
+
import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
|
|
12
|
+
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
13
|
+
import { paginateResult } from '../../shared/paginate.js';
|
|
14
14
|
import { findMatchingNodes } from './symbol-lookup.js';
|
|
15
15
|
|
|
16
16
|
export function fileDepsData(file, customDbPath, opts = {}) {
|
|
@@ -5,10 +5,14 @@ import {
|
|
|
5
5
|
findFileNodes,
|
|
6
6
|
findNodesByFile,
|
|
7
7
|
openReadonlyOrFail,
|
|
8
|
-
} from '
|
|
9
|
-
import { isTestFile } from '
|
|
10
|
-
import {
|
|
11
|
-
|
|
8
|
+
} from '../../db/index.js';
|
|
9
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
10
|
+
import {
|
|
11
|
+
createFileLinesReader,
|
|
12
|
+
extractSignature,
|
|
13
|
+
extractSummary,
|
|
14
|
+
} from '../../shared/file-utils.js';
|
|
15
|
+
import { paginateResult } from '../../shared/paginate.js';
|
|
12
16
|
|
|
13
17
|
export function exportsData(file, customDbPath, opts = {}) {
|
|
14
18
|
const db = openReadonlyOrFail(customDbPath);
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { evaluateBoundaries } from '../boundaries.js';
|
|
5
|
-
import { coChangeForFiles } from '../cochange.js';
|
|
6
|
-
import { loadConfig } from '../config.js';
|
|
7
4
|
import {
|
|
8
5
|
findDbPath,
|
|
9
6
|
findDistinctCallers,
|
|
@@ -11,13 +8,52 @@ import {
|
|
|
11
8
|
findImportDependents,
|
|
12
9
|
findNodeById,
|
|
13
10
|
openReadonlyOrFail,
|
|
14
|
-
} from '
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
11
|
+
} from '../../db/index.js';
|
|
12
|
+
import { evaluateBoundaries } from '../../features/boundaries.js';
|
|
13
|
+
import { coChangeForFiles } from '../../features/cochange.js';
|
|
14
|
+
import { ownersForFiles } from '../../features/owners.js';
|
|
15
|
+
import { loadConfig } from '../../infrastructure/config.js';
|
|
16
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
17
|
+
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
18
|
+
import { paginateResult } from '../../shared/paginate.js';
|
|
19
19
|
import { findMatchingNodes } from './symbol-lookup.js';
|
|
20
20
|
|
|
21
|
+
// ─── Shared BFS: transitive callers ────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* BFS traversal to find transitive callers of a node.
|
|
25
|
+
*
|
|
26
|
+
* @param {import('better-sqlite3').Database} db - Open read-only SQLite database handle (not a Repository)
|
|
27
|
+
* @param {number} startId - Starting node ID
|
|
28
|
+
* @param {{ noTests?: boolean, maxDepth?: number, onVisit?: (caller: object, parentId: number, depth: number) => void }} options
|
|
29
|
+
* @returns {{ totalDependents: number, levels: Record<number, Array<{name:string, kind:string, file:string, line:number}>> }}
|
|
30
|
+
*/
|
|
31
|
+
export function bfsTransitiveCallers(db, startId, { noTests = false, maxDepth = 3, onVisit } = {}) {
|
|
32
|
+
const visited = new Set([startId]);
|
|
33
|
+
const levels = {};
|
|
34
|
+
let frontier = [startId];
|
|
35
|
+
|
|
36
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
37
|
+
const nextFrontier = [];
|
|
38
|
+
for (const fid of frontier) {
|
|
39
|
+
const callers = findDistinctCallers(db, fid);
|
|
40
|
+
for (const c of callers) {
|
|
41
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
42
|
+
visited.add(c.id);
|
|
43
|
+
nextFrontier.push(c.id);
|
|
44
|
+
if (!levels[d]) levels[d] = [];
|
|
45
|
+
levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
|
|
46
|
+
if (onVisit) onVisit(c, fid, d);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
frontier = nextFrontier;
|
|
51
|
+
if (frontier.length === 0) break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { totalDependents: visited.size - 1, levels };
|
|
55
|
+
}
|
|
56
|
+
|
|
21
57
|
export function impactAnalysisData(file, customDbPath, opts = {}) {
|
|
22
58
|
const db = openReadonlyOrFail(customDbPath);
|
|
23
59
|
try {
|
|
@@ -82,31 +118,11 @@ export function fnImpactData(name, customDbPath, opts = {}) {
|
|
|
82
118
|
}
|
|
83
119
|
|
|
84
120
|
const results = nodes.map((node) => {
|
|
85
|
-
const
|
|
86
|
-
const levels = {};
|
|
87
|
-
let frontier = [node.id];
|
|
88
|
-
|
|
89
|
-
for (let d = 1; d <= maxDepth; d++) {
|
|
90
|
-
const nextFrontier = [];
|
|
91
|
-
for (const fid of frontier) {
|
|
92
|
-
const callers = findDistinctCallers(db, fid);
|
|
93
|
-
for (const c of callers) {
|
|
94
|
-
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
95
|
-
visited.add(c.id);
|
|
96
|
-
nextFrontier.push(c.id);
|
|
97
|
-
if (!levels[d]) levels[d] = [];
|
|
98
|
-
levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
frontier = nextFrontier;
|
|
103
|
-
if (frontier.length === 0) break;
|
|
104
|
-
}
|
|
105
|
-
|
|
121
|
+
const { levels, totalDependents } = bfsTransitiveCallers(db, node.id, { noTests, maxDepth });
|
|
106
122
|
return {
|
|
107
123
|
...normalizeSymbol(node, db, hc),
|
|
108
124
|
levels,
|
|
109
|
-
totalDependents
|
|
125
|
+
totalDependents,
|
|
110
126
|
};
|
|
111
127
|
});
|
|
112
128
|
|
|
@@ -232,40 +248,27 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
232
248
|
|
|
233
249
|
const allAffected = new Set();
|
|
234
250
|
const functionResults = affectedFunctions.map((fn) => {
|
|
235
|
-
const visited = new Set([fn.id]);
|
|
236
|
-
let frontier = [fn.id];
|
|
237
|
-
let totalCallers = 0;
|
|
238
|
-
const levels = {};
|
|
239
251
|
const edges = [];
|
|
240
252
|
const idToKey = new Map();
|
|
241
253
|
idToKey.set(fn.id, `${fn.file}::${fn.name}:${fn.line}`);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
|
|
255
|
-
edges.push({ from: idToKey.get(fid), to: callerKey });
|
|
256
|
-
totalCallers++;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
frontier = nextFrontier;
|
|
261
|
-
if (frontier.length === 0) break;
|
|
262
|
-
}
|
|
254
|
+
|
|
255
|
+
const { levels, totalDependents } = bfsTransitiveCallers(db, fn.id, {
|
|
256
|
+
noTests,
|
|
257
|
+
maxDepth,
|
|
258
|
+
onVisit(c, parentId) {
|
|
259
|
+
allAffected.add(`${c.file}:${c.name}`);
|
|
260
|
+
const callerKey = `${c.file}::${c.name}:${c.line}`;
|
|
261
|
+
idToKey.set(c.id, callerKey);
|
|
262
|
+
edges.push({ from: idToKey.get(parentId), to: callerKey });
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
263
266
|
return {
|
|
264
267
|
name: fn.name,
|
|
265
268
|
kind: fn.kind,
|
|
266
269
|
file: fn.file,
|
|
267
270
|
line: fn.line,
|
|
268
|
-
transitiveCallers:
|
|
271
|
+
transitiveCallers: totalDependents,
|
|
269
272
|
levels,
|
|
270
273
|
edges,
|
|
271
274
|
};
|
|
@@ -310,8 +313,8 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
310
313
|
let boundaryViolations = [];
|
|
311
314
|
let boundaryViolationCount = 0;
|
|
312
315
|
try {
|
|
313
|
-
const
|
|
314
|
-
const boundaryConfig =
|
|
316
|
+
const cfg = opts.config || loadConfig(repoRoot);
|
|
317
|
+
const boundaryConfig = cfg.manifesto?.boundaries;
|
|
315
318
|
if (boundaryConfig) {
|
|
316
319
|
const result = evaluateBoundaries(db, boundaryConfig, {
|
|
317
320
|
scopeFiles: [...changedRanges.keys()],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { openReadonlyOrFail, testFilterSQL } from '../../db/index.js';
|
|
3
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
4
|
+
import { findCycles } from '../graph/cycles.js';
|
|
5
5
|
import { LANGUAGE_REGISTRY } from '../parser.js';
|
|
6
6
|
|
|
7
7
|
export const FALSE_POSITIVE_NAMES = new Set([
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { openReadonlyOrFail } from '
|
|
2
|
-
import { isTestFile } from '
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { openReadonlyOrFail } from '../../db/index.js';
|
|
2
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
3
|
+
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
4
|
+
import { paginateResult } from '../../shared/paginate.js';
|
|
5
5
|
|
|
6
6
|
export function rolesData(customDbPath, opts = {}) {
|
|
7
7
|
const db = openReadonlyOrFail(customDbPath);
|