@optave/codegraph 3.1.3 → 3.1.4
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 +17 -19
- package/package.json +10 -7
- package/src/analysis/context.js +408 -0
- package/src/analysis/dependencies.js +341 -0
- package/src/analysis/exports.js +130 -0
- package/src/analysis/impact.js +463 -0
- package/src/analysis/module-map.js +322 -0
- package/src/analysis/roles.js +45 -0
- package/src/analysis/symbol-lookup.js +232 -0
- package/src/ast-analysis/shared.js +5 -4
- package/src/batch.js +2 -1
- package/src/builder/context.js +85 -0
- package/src/builder/helpers.js +218 -0
- package/src/builder/incremental.js +178 -0
- package/src/builder/pipeline.js +130 -0
- package/src/builder/stages/build-edges.js +297 -0
- package/src/builder/stages/build-structure.js +113 -0
- package/src/builder/stages/collect-files.js +44 -0
- package/src/builder/stages/detect-changes.js +413 -0
- package/src/builder/stages/finalize.js +139 -0
- package/src/builder/stages/insert-nodes.js +195 -0
- package/src/builder/stages/parse-files.js +28 -0
- package/src/builder/stages/resolve-imports.js +143 -0
- package/src/builder/stages/run-analyses.js +44 -0
- package/src/builder.js +10 -1485
- package/src/cfg.js +1 -2
- package/src/cli/commands/ast.js +26 -0
- package/src/cli/commands/audit.js +46 -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 +30 -0
- package/src/cli/commands/check.js +79 -0
- package/src/cli/commands/children.js +31 -0
- package/src/cli/commands/co-change.js +65 -0
- package/src/cli/commands/communities.js +23 -0
- package/src/cli/commands/complexity.js +45 -0
- package/src/cli/commands/context.js +34 -0
- package/src/cli/commands/cycles.js +28 -0
- package/src/cli/commands/dataflow.js +32 -0
- package/src/cli/commands/deps.js +16 -0
- package/src/cli/commands/diff-impact.js +30 -0
- package/src/cli/commands/embed.js +30 -0
- package/src/cli/commands/export.js +75 -0
- package/src/cli/commands/exports.js +18 -0
- package/src/cli/commands/flow.js +36 -0
- package/src/cli/commands/fn-impact.js +30 -0
- package/src/cli/commands/impact.js +16 -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 +80 -0
- package/src/cli/commands/query.js +49 -0
- package/src/cli/commands/registry.js +100 -0
- package/src/cli/commands/roles.js +34 -0
- package/src/cli/commands/search.js +42 -0
- package/src/cli/commands/sequence.js +32 -0
- package/src/cli/commands/snapshot.js +61 -0
- package/src/cli/commands/stats.js +15 -0
- package/src/cli/commands/structure.js +32 -0
- package/src/cli/commands/triage.js +78 -0
- package/src/cli/commands/watch.js +12 -0
- package/src/cli/commands/where.js +24 -0
- package/src/cli/index.js +118 -0
- package/src/cli/shared/options.js +39 -0
- package/src/cli/shared/output.js +1 -0
- package/src/cli.js +11 -1522
- package/src/commands/check.js +5 -5
- package/src/commands/manifesto.js +3 -3
- package/src/commands/structure.js +1 -1
- package/src/communities.js +15 -87
- package/src/cycles.js +30 -85
- package/src/dataflow.js +1 -2
- package/src/db/connection.js +4 -4
- package/src/db/migrations.js +41 -0
- package/src/db/query-builder.js +6 -5
- package/src/db/repository/base.js +201 -0
- package/src/db/repository/graph-read.js +5 -2
- package/src/db/repository/in-memory-repository.js +584 -0
- package/src/db/repository/index.js +5 -1
- package/src/db/repository/nodes.js +63 -4
- package/src/db/repository/sqlite-repository.js +219 -0
- package/src/db.js +5 -0
- package/src/embeddings/generator.js +163 -0
- package/src/embeddings/index.js +13 -0
- package/src/embeddings/models.js +218 -0
- package/src/embeddings/search/cli-formatter.js +151 -0
- package/src/embeddings/search/filters.js +46 -0
- package/src/embeddings/search/hybrid.js +121 -0
- package/src/embeddings/search/keyword.js +68 -0
- package/src/embeddings/search/prepare.js +66 -0
- package/src/embeddings/search/semantic.js +145 -0
- package/src/embeddings/stores/fts5.js +27 -0
- package/src/embeddings/stores/sqlite-blob.js +24 -0
- package/src/embeddings/strategies/source.js +14 -0
- package/src/embeddings/strategies/structured.js +43 -0
- package/src/embeddings/strategies/text-utils.js +43 -0
- package/src/errors.js +78 -0
- package/src/export.js +217 -520
- 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 +38 -1
- package/src/extractors/php.js +3 -1
- package/src/extractors/python.js +14 -3
- package/src/extractors/rust.js +3 -1
- 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 +91 -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.js +33 -210
- 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/tool-registry.js +801 -0
- 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 +10 -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/mcp.js +2 -1470
- package/src/native.js +3 -1
- package/src/presentation/colors.js +44 -0
- package/src/presentation/export.js +444 -0
- package/src/presentation/result-formatter.js +21 -0
- package/src/presentation/sequence-renderer.js +43 -0
- package/src/presentation/table.js +47 -0
- package/src/presentation/viewer.js +634 -0
- package/src/queries.js +35 -2276
- package/src/resolve.js +1 -1
- package/src/sequence.js +2 -38
- 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/snapshot.js +6 -5
- package/src/structure.js +15 -40
- package/src/triage.js +20 -72
- package/src/viewer.js +35 -656
- package/src/watcher.js +8 -148
- package/src/embedder.js +0 -1097
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import { ConfigError } from '../../errors.js';
|
|
2
|
+
import { CORE_SYMBOL_KINDS, EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
|
|
3
|
+
import { Repository } from './base.js';
|
|
4
|
+
|
|
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
|
+
/**
|
|
16
|
+
* Convert a SQL LIKE pattern to a RegExp (case-insensitive).
|
|
17
|
+
* Supports `%` (any chars) and `_` (single char).
|
|
18
|
+
* @param {string} pattern
|
|
19
|
+
* @returns {RegExp}
|
|
20
|
+
*/
|
|
21
|
+
function likeToRegex(pattern) {
|
|
22
|
+
let regex = '';
|
|
23
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
24
|
+
const ch = pattern[i];
|
|
25
|
+
if (ch === '\\' && i + 1 < pattern.length) {
|
|
26
|
+
// Escaped literal
|
|
27
|
+
regex += pattern[++i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
} else if (ch === '%') {
|
|
29
|
+
regex += '.*';
|
|
30
|
+
} else if (ch === '_') {
|
|
31
|
+
regex += '.';
|
|
32
|
+
} else {
|
|
33
|
+
regex += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return new RegExp(`^${regex}$`, 'i');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* In-memory Repository implementation backed by Maps.
|
|
41
|
+
* No SQLite dependency — suitable for fast unit tests.
|
|
42
|
+
*/
|
|
43
|
+
export class InMemoryRepository extends Repository {
|
|
44
|
+
#nodes = new Map(); // id → node object
|
|
45
|
+
#edges = new Map(); // id → edge object
|
|
46
|
+
#complexity = new Map(); // node_id → complexity metrics
|
|
47
|
+
#nextNodeId = 1;
|
|
48
|
+
#nextEdgeId = 1;
|
|
49
|
+
|
|
50
|
+
// ── Mutation (test setup only) ────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add a node. Returns the auto-assigned id.
|
|
54
|
+
* @param {object} attrs - { name, kind, file, line, end_line?, parent_id?, exported?, qualified_name?, scope?, visibility?, role? }
|
|
55
|
+
* @returns {number}
|
|
56
|
+
*/
|
|
57
|
+
addNode(attrs) {
|
|
58
|
+
const id = this.#nextNodeId++;
|
|
59
|
+
this.#nodes.set(id, {
|
|
60
|
+
id,
|
|
61
|
+
name: attrs.name,
|
|
62
|
+
kind: attrs.kind,
|
|
63
|
+
file: attrs.file,
|
|
64
|
+
line: attrs.line,
|
|
65
|
+
end_line: attrs.end_line ?? null,
|
|
66
|
+
parent_id: attrs.parent_id ?? null,
|
|
67
|
+
exported: attrs.exported ?? null,
|
|
68
|
+
qualified_name: attrs.qualified_name ?? null,
|
|
69
|
+
scope: attrs.scope ?? null,
|
|
70
|
+
visibility: attrs.visibility ?? null,
|
|
71
|
+
role: attrs.role ?? null,
|
|
72
|
+
});
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Add an edge. Returns the auto-assigned id.
|
|
78
|
+
* @param {object} attrs - { source_id, target_id, kind, confidence?, dynamic? }
|
|
79
|
+
* @returns {number}
|
|
80
|
+
*/
|
|
81
|
+
addEdge(attrs) {
|
|
82
|
+
const id = this.#nextEdgeId++;
|
|
83
|
+
this.#edges.set(id, {
|
|
84
|
+
id,
|
|
85
|
+
source_id: attrs.source_id,
|
|
86
|
+
target_id: attrs.target_id,
|
|
87
|
+
kind: attrs.kind,
|
|
88
|
+
confidence: attrs.confidence ?? null,
|
|
89
|
+
dynamic: attrs.dynamic ?? 0,
|
|
90
|
+
});
|
|
91
|
+
return id;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Add complexity metrics for a node.
|
|
96
|
+
* @param {number} nodeId
|
|
97
|
+
* @param {object} metrics - { cognitive, cyclomatic, max_nesting, maintainability_index?, halstead_volume? }
|
|
98
|
+
*/
|
|
99
|
+
addComplexity(nodeId, metrics) {
|
|
100
|
+
this.#complexity.set(nodeId, {
|
|
101
|
+
cognitive: metrics.cognitive ?? 0,
|
|
102
|
+
cyclomatic: metrics.cyclomatic ?? 0,
|
|
103
|
+
max_nesting: metrics.max_nesting ?? 0,
|
|
104
|
+
maintainability_index: metrics.maintainability_index ?? 0,
|
|
105
|
+
halstead_volume: metrics.halstead_volume ?? 0,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Node lookups ──────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
findNodeById(id) {
|
|
112
|
+
return this.#nodes.get(id) ?? undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
findNodesByFile(file) {
|
|
116
|
+
return [...this.#nodes.values()]
|
|
117
|
+
.filter((n) => n.file === file && n.kind !== 'file')
|
|
118
|
+
.sort((a, b) => a.line - b.line);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
findFileNodes(fileLike) {
|
|
122
|
+
const re = likeToRegex(fileLike);
|
|
123
|
+
return [...this.#nodes.values()].filter((n) => n.kind === 'file' && re.test(n.file));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
findNodesWithFanIn(namePattern, opts = {}) {
|
|
127
|
+
const re = likeToRegex(namePattern);
|
|
128
|
+
let nodes = [...this.#nodes.values()].filter((n) => re.test(n.name));
|
|
129
|
+
|
|
130
|
+
if (opts.kinds) {
|
|
131
|
+
nodes = nodes.filter((n) => opts.kinds.includes(n.kind));
|
|
132
|
+
}
|
|
133
|
+
if (opts.file) {
|
|
134
|
+
const fileRe = likeToRegex(`%${escapeLike(opts.file)}%`);
|
|
135
|
+
nodes = nodes.filter((n) => fileRe.test(n.file));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Compute fan-in per node
|
|
139
|
+
const fanInMap = this.#computeFanIn();
|
|
140
|
+
return nodes.map((n) => ({ ...n, fan_in: fanInMap.get(n.id) ?? 0 }));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
countNodes() {
|
|
144
|
+
return this.#nodes.size;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
countEdges() {
|
|
148
|
+
return this.#edges.size;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
countFiles() {
|
|
152
|
+
const files = new Set();
|
|
153
|
+
for (const n of this.#nodes.values()) {
|
|
154
|
+
files.add(n.file);
|
|
155
|
+
}
|
|
156
|
+
return files.size;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getNodeId(name, kind, file, line) {
|
|
160
|
+
for (const n of this.#nodes.values()) {
|
|
161
|
+
if (n.name === name && n.kind === kind && n.file === file && n.line === line) {
|
|
162
|
+
return n.id;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
getFunctionNodeId(name, file, line) {
|
|
169
|
+
for (const n of this.#nodes.values()) {
|
|
170
|
+
if (
|
|
171
|
+
n.name === name &&
|
|
172
|
+
(n.kind === 'function' || n.kind === 'method') &&
|
|
173
|
+
n.file === file &&
|
|
174
|
+
n.line === line
|
|
175
|
+
) {
|
|
176
|
+
return n.id;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
bulkNodeIdsByFile(file) {
|
|
183
|
+
return [...this.#nodes.values()]
|
|
184
|
+
.filter((n) => n.file === file)
|
|
185
|
+
.map((n) => ({ id: n.id, name: n.name, kind: n.kind, line: n.line }));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
findNodeChildren(parentId) {
|
|
189
|
+
return [...this.#nodes.values()]
|
|
190
|
+
.filter((n) => n.parent_id === parentId)
|
|
191
|
+
.sort((a, b) => a.line - b.line)
|
|
192
|
+
.map((n) => ({
|
|
193
|
+
name: n.name,
|
|
194
|
+
kind: n.kind,
|
|
195
|
+
line: n.line,
|
|
196
|
+
end_line: n.end_line,
|
|
197
|
+
qualified_name: n.qualified_name,
|
|
198
|
+
scope: n.scope,
|
|
199
|
+
visibility: n.visibility,
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
findNodesByScope(scopeName, opts = {}) {
|
|
204
|
+
let nodes = [...this.#nodes.values()].filter((n) => n.scope === scopeName);
|
|
205
|
+
|
|
206
|
+
if (opts.kind) {
|
|
207
|
+
nodes = nodes.filter((n) => n.kind === opts.kind);
|
|
208
|
+
}
|
|
209
|
+
if (opts.file) {
|
|
210
|
+
const fileRe = likeToRegex(`%${escapeLike(opts.file)}%`);
|
|
211
|
+
nodes = nodes.filter((n) => fileRe.test(n.file));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
findNodeByQualifiedName(qualifiedName, opts = {}) {
|
|
218
|
+
let nodes = [...this.#nodes.values()].filter((n) => n.qualified_name === qualifiedName);
|
|
219
|
+
|
|
220
|
+
if (opts.file) {
|
|
221
|
+
const fileRe = likeToRegex(`%${escapeLike(opts.file)}%`);
|
|
222
|
+
nodes = nodes.filter((n) => fileRe.test(n.file));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
listFunctionNodes(opts = {}) {
|
|
229
|
+
return [...this.#iterateFunctionNodesImpl(opts)];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
*iterateFunctionNodes(opts = {}) {
|
|
233
|
+
yield* this.#iterateFunctionNodesImpl(opts);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
findNodesForTriage(opts = {}) {
|
|
237
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
238
|
+
throw new ConfigError(
|
|
239
|
+
`Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (opts.role && !VALID_ROLES.includes(opts.role)) {
|
|
243
|
+
throw new ConfigError(
|
|
244
|
+
`Invalid role: ${opts.role} (expected one of ${VALID_ROLES.join(', ')})`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const kindsToUse = opts.kind ? [opts.kind] : ['function', 'method', 'class'];
|
|
248
|
+
let nodes = [...this.#nodes.values()].filter((n) => kindsToUse.includes(n.kind));
|
|
249
|
+
|
|
250
|
+
if (opts.noTests) {
|
|
251
|
+
nodes = nodes.filter(
|
|
252
|
+
(n) =>
|
|
253
|
+
!n.file.includes('.test.') &&
|
|
254
|
+
!n.file.includes('.spec.') &&
|
|
255
|
+
!n.file.includes('__test__') &&
|
|
256
|
+
!n.file.includes('__tests__') &&
|
|
257
|
+
!n.file.includes('.stories.'),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (opts.file) {
|
|
261
|
+
const fileRe = likeToRegex(`%${escapeLike(opts.file)}%`);
|
|
262
|
+
nodes = nodes.filter((n) => fileRe.test(n.file));
|
|
263
|
+
}
|
|
264
|
+
if (opts.role) {
|
|
265
|
+
nodes = nodes.filter((n) => n.role === opts.role);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const fanInMap = this.#computeFanIn();
|
|
269
|
+
return nodes
|
|
270
|
+
.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)
|
|
271
|
+
.map((n) => {
|
|
272
|
+
const cx = this.#complexity.get(n.id);
|
|
273
|
+
return {
|
|
274
|
+
id: n.id,
|
|
275
|
+
name: n.name,
|
|
276
|
+
kind: n.kind,
|
|
277
|
+
file: n.file,
|
|
278
|
+
line: n.line,
|
|
279
|
+
end_line: n.end_line,
|
|
280
|
+
role: n.role,
|
|
281
|
+
fan_in: fanInMap.get(n.id) ?? 0,
|
|
282
|
+
cognitive: cx?.cognitive ?? 0,
|
|
283
|
+
mi: cx?.maintainability_index ?? 0,
|
|
284
|
+
cyclomatic: cx?.cyclomatic ?? 0,
|
|
285
|
+
max_nesting: cx?.max_nesting ?? 0,
|
|
286
|
+
churn: 0, // no co-change data in-memory
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Edge queries ──────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
findCallees(nodeId) {
|
|
294
|
+
const seen = new Set();
|
|
295
|
+
const results = [];
|
|
296
|
+
for (const e of this.#edges.values()) {
|
|
297
|
+
if (e.source_id === nodeId && e.kind === 'calls' && !seen.has(e.target_id)) {
|
|
298
|
+
seen.add(e.target_id);
|
|
299
|
+
const n = this.#nodes.get(e.target_id);
|
|
300
|
+
if (n)
|
|
301
|
+
results.push({
|
|
302
|
+
id: n.id,
|
|
303
|
+
name: n.name,
|
|
304
|
+
kind: n.kind,
|
|
305
|
+
file: n.file,
|
|
306
|
+
line: n.line,
|
|
307
|
+
end_line: n.end_line,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return results;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
findCallers(nodeId) {
|
|
315
|
+
const results = [];
|
|
316
|
+
for (const e of this.#edges.values()) {
|
|
317
|
+
if (e.target_id === nodeId && e.kind === 'calls') {
|
|
318
|
+
const n = this.#nodes.get(e.source_id);
|
|
319
|
+
if (n) results.push({ id: n.id, name: n.name, kind: n.kind, file: n.file, line: n.line });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return results;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
findDistinctCallers(nodeId) {
|
|
326
|
+
const seen = new Set();
|
|
327
|
+
const results = [];
|
|
328
|
+
for (const e of this.#edges.values()) {
|
|
329
|
+
if (e.target_id === nodeId && e.kind === 'calls' && !seen.has(e.source_id)) {
|
|
330
|
+
seen.add(e.source_id);
|
|
331
|
+
const n = this.#nodes.get(e.source_id);
|
|
332
|
+
if (n) results.push({ id: n.id, name: n.name, kind: n.kind, file: n.file, line: n.line });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return results;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
findAllOutgoingEdges(nodeId) {
|
|
339
|
+
const results = [];
|
|
340
|
+
for (const e of this.#edges.values()) {
|
|
341
|
+
if (e.source_id === nodeId) {
|
|
342
|
+
const n = this.#nodes.get(e.target_id);
|
|
343
|
+
if (n)
|
|
344
|
+
results.push({
|
|
345
|
+
name: n.name,
|
|
346
|
+
kind: n.kind,
|
|
347
|
+
file: n.file,
|
|
348
|
+
line: n.line,
|
|
349
|
+
edge_kind: e.kind,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return results;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
findAllIncomingEdges(nodeId) {
|
|
357
|
+
const results = [];
|
|
358
|
+
for (const e of this.#edges.values()) {
|
|
359
|
+
if (e.target_id === nodeId) {
|
|
360
|
+
const n = this.#nodes.get(e.source_id);
|
|
361
|
+
if (n)
|
|
362
|
+
results.push({
|
|
363
|
+
name: n.name,
|
|
364
|
+
kind: n.kind,
|
|
365
|
+
file: n.file,
|
|
366
|
+
line: n.line,
|
|
367
|
+
edge_kind: e.kind,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return results;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
findCalleeNames(nodeId) {
|
|
375
|
+
const names = new Set();
|
|
376
|
+
for (const e of this.#edges.values()) {
|
|
377
|
+
if (e.source_id === nodeId && e.kind === 'calls') {
|
|
378
|
+
const n = this.#nodes.get(e.target_id);
|
|
379
|
+
if (n) names.add(n.name);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return [...names].sort();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
findCallerNames(nodeId) {
|
|
386
|
+
const names = new Set();
|
|
387
|
+
for (const e of this.#edges.values()) {
|
|
388
|
+
if (e.target_id === nodeId && e.kind === 'calls') {
|
|
389
|
+
const n = this.#nodes.get(e.source_id);
|
|
390
|
+
if (n) names.add(n.name);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return [...names].sort();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
findImportTargets(nodeId) {
|
|
397
|
+
const results = [];
|
|
398
|
+
for (const e of this.#edges.values()) {
|
|
399
|
+
if (e.source_id === nodeId && (e.kind === 'imports' || e.kind === 'imports-type')) {
|
|
400
|
+
const n = this.#nodes.get(e.target_id);
|
|
401
|
+
if (n) results.push({ file: n.file, edge_kind: e.kind });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return results;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
findImportSources(nodeId) {
|
|
408
|
+
const results = [];
|
|
409
|
+
for (const e of this.#edges.values()) {
|
|
410
|
+
if (e.target_id === nodeId && (e.kind === 'imports' || e.kind === 'imports-type')) {
|
|
411
|
+
const n = this.#nodes.get(e.source_id);
|
|
412
|
+
if (n) results.push({ file: n.file, edge_kind: e.kind });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return results;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
findImportDependents(nodeId) {
|
|
419
|
+
const results = [];
|
|
420
|
+
for (const e of this.#edges.values()) {
|
|
421
|
+
if (e.target_id === nodeId && (e.kind === 'imports' || e.kind === 'imports-type')) {
|
|
422
|
+
const n = this.#nodes.get(e.source_id);
|
|
423
|
+
if (n) results.push({ ...n });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return results;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
findCrossFileCallTargets(file) {
|
|
430
|
+
const targets = new Set();
|
|
431
|
+
for (const e of this.#edges.values()) {
|
|
432
|
+
if (e.kind !== 'calls') continue;
|
|
433
|
+
const caller = this.#nodes.get(e.source_id);
|
|
434
|
+
const target = this.#nodes.get(e.target_id);
|
|
435
|
+
if (caller && target && target.file === file && caller.file !== file) {
|
|
436
|
+
targets.add(e.target_id);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return targets;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
countCrossFileCallers(nodeId, file) {
|
|
443
|
+
let count = 0;
|
|
444
|
+
for (const e of this.#edges.values()) {
|
|
445
|
+
if (e.target_id === nodeId && e.kind === 'calls') {
|
|
446
|
+
const caller = this.#nodes.get(e.source_id);
|
|
447
|
+
if (caller && caller.file !== file) count++;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return count;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
getClassHierarchy(classNodeId) {
|
|
454
|
+
const ancestors = new Set();
|
|
455
|
+
const queue = [classNodeId];
|
|
456
|
+
while (queue.length > 0) {
|
|
457
|
+
const current = queue.shift();
|
|
458
|
+
for (const e of this.#edges.values()) {
|
|
459
|
+
if (e.source_id === current && e.kind === 'extends') {
|
|
460
|
+
const target = this.#nodes.get(e.target_id);
|
|
461
|
+
if (target && !ancestors.has(target.id)) {
|
|
462
|
+
ancestors.add(target.id);
|
|
463
|
+
queue.push(target.id);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return ancestors;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
findIntraFileCallEdges(file) {
|
|
472
|
+
const results = [];
|
|
473
|
+
for (const e of this.#edges.values()) {
|
|
474
|
+
if (e.kind !== 'calls') continue;
|
|
475
|
+
const caller = this.#nodes.get(e.source_id);
|
|
476
|
+
const callee = this.#nodes.get(e.target_id);
|
|
477
|
+
if (caller && callee && caller.file === file && callee.file === file) {
|
|
478
|
+
results.push({ caller_name: caller.name, callee_name: callee.name });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const lineByName = new Map();
|
|
482
|
+
for (const n of this.#nodes.values()) {
|
|
483
|
+
if (n.file === file) lineByName.set(n.name, n.line);
|
|
484
|
+
}
|
|
485
|
+
return results.sort((a, b) => {
|
|
486
|
+
return (lineByName.get(a.caller_name) ?? 0) - (lineByName.get(b.caller_name) ?? 0);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── Graph-read queries ────────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
getCallableNodes() {
|
|
493
|
+
return [...this.#nodes.values()]
|
|
494
|
+
.filter((n) => CORE_SYMBOL_KINDS.includes(n.kind))
|
|
495
|
+
.map((n) => ({ id: n.id, name: n.name, kind: n.kind, file: n.file }));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
getCallEdges() {
|
|
499
|
+
return [...this.#edges.values()]
|
|
500
|
+
.filter((e) => e.kind === 'calls')
|
|
501
|
+
.map((e) => ({ source_id: e.source_id, target_id: e.target_id }));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
getFileNodesAll() {
|
|
505
|
+
return [...this.#nodes.values()]
|
|
506
|
+
.filter((n) => n.kind === 'file')
|
|
507
|
+
.map((n) => ({ id: n.id, name: n.name, file: n.file }));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
getImportEdges() {
|
|
511
|
+
return [...this.#edges.values()]
|
|
512
|
+
.filter((e) => e.kind === 'imports' || e.kind === 'imports-type')
|
|
513
|
+
.map((e) => ({ source_id: e.source_id, target_id: e.target_id }));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── Optional table checks ─────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
hasCfgTables() {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
hasEmbeddings() {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
hasDataflowTable() {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
getComplexityForNode(nodeId) {
|
|
531
|
+
return this.#complexity.get(nodeId);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Private helpers ───────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
/** Compute fan-in (incoming 'calls' edge count) for all nodes. */
|
|
537
|
+
#computeFanIn() {
|
|
538
|
+
const fanIn = new Map();
|
|
539
|
+
for (const e of this.#edges.values()) {
|
|
540
|
+
if (e.kind === 'calls') {
|
|
541
|
+
fanIn.set(e.target_id, (fanIn.get(e.target_id) ?? 0) + 1);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return fanIn;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Internal generator for function/method/class listing with filters. */
|
|
548
|
+
*#iterateFunctionNodesImpl(opts = {}) {
|
|
549
|
+
let nodes = [...this.#nodes.values()].filter((n) =>
|
|
550
|
+
['function', 'method', 'class'].includes(n.kind),
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
if (opts.file) {
|
|
554
|
+
const fileRe = likeToRegex(`%${escapeLike(opts.file)}%`);
|
|
555
|
+
nodes = nodes.filter((n) => fileRe.test(n.file));
|
|
556
|
+
}
|
|
557
|
+
if (opts.pattern) {
|
|
558
|
+
const patternRe = likeToRegex(`%${escapeLike(opts.pattern)}%`);
|
|
559
|
+
nodes = nodes.filter((n) => patternRe.test(n.name));
|
|
560
|
+
}
|
|
561
|
+
if (opts.noTests) {
|
|
562
|
+
nodes = nodes.filter(
|
|
563
|
+
(n) =>
|
|
564
|
+
!n.file.includes('.test.') &&
|
|
565
|
+
!n.file.includes('.spec.') &&
|
|
566
|
+
!n.file.includes('__test__') &&
|
|
567
|
+
!n.file.includes('__tests__') &&
|
|
568
|
+
!n.file.includes('.stories.'),
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
573
|
+
for (const n of nodes) {
|
|
574
|
+
yield {
|
|
575
|
+
name: n.name,
|
|
576
|
+
kind: n.kind,
|
|
577
|
+
file: n.file,
|
|
578
|
+
line: n.line,
|
|
579
|
+
end_line: n.end_line,
|
|
580
|
+
role: n.role,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Barrel re-export for repository/ modules.
|
|
2
2
|
|
|
3
|
+
export { Repository } from './base.js';
|
|
3
4
|
export { purgeFileData, purgeFilesData } from './build-stmts.js';
|
|
4
5
|
export { cachedStmt } from './cached-stmt.js';
|
|
5
6
|
export { deleteCfgForNode, getCfgBlocks, getCfgEdges, hasCfgTables } from './cfg.js';
|
|
6
7
|
export { getCoChangeMeta, hasCoChanges, upsertCoChangeMeta } from './cochange.js';
|
|
7
|
-
|
|
8
8
|
export { getComplexityForNode } from './complexity.js';
|
|
9
9
|
export { hasDataflowTable } from './dataflow.js';
|
|
10
10
|
export {
|
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
} from './edges.js';
|
|
26
26
|
export { getEmbeddingCount, getEmbeddingMeta, hasEmbeddings } from './embeddings.js';
|
|
27
27
|
export { getCallableNodes, getCallEdges, getFileNodesAll, getImportEdges } from './graph-read.js';
|
|
28
|
+
export { InMemoryRepository } from './in-memory-repository.js';
|
|
28
29
|
export {
|
|
29
30
|
bulkNodeIdsByFile,
|
|
30
31
|
countEdges,
|
|
@@ -32,8 +33,10 @@ export {
|
|
|
32
33
|
countNodes,
|
|
33
34
|
findFileNodes,
|
|
34
35
|
findNodeById,
|
|
36
|
+
findNodeByQualifiedName,
|
|
35
37
|
findNodeChildren,
|
|
36
38
|
findNodesByFile,
|
|
39
|
+
findNodesByScope,
|
|
37
40
|
findNodesForTriage,
|
|
38
41
|
findNodesWithFanIn,
|
|
39
42
|
getFunctionNodeId,
|
|
@@ -41,3 +44,4 @@ export {
|
|
|
41
44
|
iterateFunctionNodes,
|
|
42
45
|
listFunctionNodes,
|
|
43
46
|
} from './nodes.js';
|
|
47
|
+
export { SqliteRepository } from './sqlite-repository.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConfigError } from '../../errors.js';
|
|
1
2
|
import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
|
|
2
3
|
import { NodeQuery } from '../query-builder.js';
|
|
3
4
|
import { cachedStmt } from './cached-stmt.js';
|
|
@@ -37,10 +38,12 @@ export function findNodesWithFanIn(db, namePattern, opts = {}) {
|
|
|
37
38
|
*/
|
|
38
39
|
export function findNodesForTriage(db, opts = {}) {
|
|
39
40
|
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
40
|
-
throw new
|
|
41
|
+
throw new ConfigError(
|
|
42
|
+
`Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`,
|
|
43
|
+
);
|
|
41
44
|
}
|
|
42
45
|
if (opts.role && !VALID_ROLES.includes(opts.role)) {
|
|
43
|
-
throw new
|
|
46
|
+
throw new ConfigError(`Invalid role: ${opts.role} (expected one of ${VALID_ROLES.join(', ')})`);
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
const kindsToUse = opts.kind ? [opts.kind] : ['function', 'method', 'class'];
|
|
@@ -113,6 +116,7 @@ const _getNodeIdStmt = new WeakMap();
|
|
|
113
116
|
const _getFunctionNodeIdStmt = new WeakMap();
|
|
114
117
|
const _bulkNodeIdsByFileStmt = new WeakMap();
|
|
115
118
|
const _findNodeChildrenStmt = new WeakMap();
|
|
119
|
+
const _findNodeByQualifiedNameStmt = new WeakMap();
|
|
116
120
|
|
|
117
121
|
/**
|
|
118
122
|
* Count total nodes.
|
|
@@ -236,12 +240,67 @@ export function bulkNodeIdsByFile(db, file) {
|
|
|
236
240
|
* Find child nodes (parameters, properties, constants) of a parent.
|
|
237
241
|
* @param {object} db
|
|
238
242
|
* @param {number} parentId
|
|
239
|
-
* @returns {{ name: string, kind: string, line: number, end_line: number|null }[]}
|
|
243
|
+
* @returns {{ name: string, kind: string, line: number, end_line: number|null, qualified_name: string|null, scope: string|null, visibility: string|null }[]}
|
|
240
244
|
*/
|
|
241
245
|
export function findNodeChildren(db, parentId) {
|
|
242
246
|
return cachedStmt(
|
|
243
247
|
_findNodeChildrenStmt,
|
|
244
248
|
db,
|
|
245
|
-
'SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line',
|
|
249
|
+
'SELECT name, kind, line, end_line, qualified_name, scope, visibility FROM nodes WHERE parent_id = ? ORDER BY line',
|
|
246
250
|
).all(parentId);
|
|
247
251
|
}
|
|
252
|
+
|
|
253
|
+
/** Escape LIKE wildcards in a literal string segment. */
|
|
254
|
+
function escapeLike(s) {
|
|
255
|
+
return s.replace(/[%_\\]/g, '\\$&');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Find all nodes that belong to a given scope (by scope column).
|
|
260
|
+
* Enables "all methods of class X" without traversing edges.
|
|
261
|
+
* @param {object} db
|
|
262
|
+
* @param {string} scopeName - The scope to search for (e.g., class name)
|
|
263
|
+
* @param {object} [opts]
|
|
264
|
+
* @param {string} [opts.kind] - Filter by node kind
|
|
265
|
+
* @param {string} [opts.file] - Filter by file path (LIKE match)
|
|
266
|
+
* @returns {object[]}
|
|
267
|
+
*/
|
|
268
|
+
export function findNodesByScope(db, scopeName, opts = {}) {
|
|
269
|
+
let sql = 'SELECT * FROM nodes WHERE scope = ?';
|
|
270
|
+
const params = [scopeName];
|
|
271
|
+
if (opts.kind) {
|
|
272
|
+
sql += ' AND kind = ?';
|
|
273
|
+
params.push(opts.kind);
|
|
274
|
+
}
|
|
275
|
+
if (opts.file) {
|
|
276
|
+
sql += " AND file LIKE ? ESCAPE '\\'";
|
|
277
|
+
params.push(`%${escapeLike(opts.file)}%`);
|
|
278
|
+
}
|
|
279
|
+
sql += ' ORDER BY file, line';
|
|
280
|
+
return db.prepare(sql).all(...params);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Find nodes by qualified name. Returns all matches since the same
|
|
285
|
+
* qualified_name can exist in different files (e.g., two classes named
|
|
286
|
+
* `DateHelper.format` in separate modules). Pass `opts.file` to narrow.
|
|
287
|
+
* @param {object} db
|
|
288
|
+
* @param {string} qualifiedName - e.g., 'DateHelper.format'
|
|
289
|
+
* @param {object} [opts]
|
|
290
|
+
* @param {string} [opts.file] - Filter by file path (LIKE match)
|
|
291
|
+
* @returns {object[]}
|
|
292
|
+
*/
|
|
293
|
+
export function findNodeByQualifiedName(db, qualifiedName, opts = {}) {
|
|
294
|
+
if (opts.file) {
|
|
295
|
+
return db
|
|
296
|
+
.prepare(
|
|
297
|
+
"SELECT * FROM nodes WHERE qualified_name = ? AND file LIKE ? ESCAPE '\\' ORDER BY file, line",
|
|
298
|
+
)
|
|
299
|
+
.all(qualifiedName, `%${escapeLike(opts.file)}%`);
|
|
300
|
+
}
|
|
301
|
+
return cachedStmt(
|
|
302
|
+
_findNodeByQualifiedNameStmt,
|
|
303
|
+
db,
|
|
304
|
+
'SELECT * FROM nodes WHERE qualified_name = ? ORDER BY file, line',
|
|
305
|
+
).all(qualifiedName);
|
|
306
|
+
}
|