@optave/codegraph 3.1.4 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -72
- package/package.json +10 -8
- package/src/ast-analysis/engine.js +260 -246
- package/src/ast-analysis/shared.js +2 -14
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +4 -7
- package/src/cli/commands/audit.js +11 -11
- package/src/cli/commands/batch.js +6 -5
- package/src/cli/commands/branch-compare.js +1 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/build.js +1 -1
- package/src/cli/commands/cfg.js +5 -8
- package/src/cli/commands/check.js +28 -36
- package/src/cli/commands/children.js +9 -7
- package/src/cli/commands/co-change.js +5 -3
- package/src/cli/commands/communities.js +2 -6
- package/src/cli/commands/complexity.js +5 -3
- package/src/cli/commands/context.js +9 -8
- package/src/cli/commands/cycles.js +12 -8
- package/src/cli/commands/dataflow.js +5 -8
- package/src/cli/commands/deps.js +9 -8
- package/src/cli/commands/diff-impact.js +2 -6
- package/src/cli/commands/embed.js +1 -1
- package/src/cli/commands/export.js +34 -31
- package/src/cli/commands/exports.js +2 -6
- package/src/cli/commands/flow.js +5 -8
- package/src/cli/commands/fn-impact.js +9 -8
- package/src/cli/commands/impact.js +2 -6
- package/src/cli/commands/info.js +2 -2
- package/src/cli/commands/map.js +1 -1
- package/src/cli/commands/mcp.js +1 -1
- package/src/cli/commands/models.js +1 -1
- package/src/cli/commands/owners.js +5 -3
- package/src/cli/commands/path.js +2 -2
- package/src/cli/commands/plot.js +40 -31
- package/src/cli/commands/query.js +9 -8
- package/src/cli/commands/registry.js +2 -2
- package/src/cli/commands/roles.js +5 -8
- package/src/cli/commands/search.js +9 -3
- package/src/cli/commands/sequence.js +5 -8
- package/src/cli/commands/snapshot.js +6 -1
- package/src/cli/commands/stats.js +1 -1
- package/src/cli/commands/structure.js +5 -4
- package/src/cli/commands/triage.js +41 -30
- package/src/cli/commands/watch.js +1 -1
- package/src/cli/commands/where.js +2 -6
- package/src/cli/index.js +11 -5
- package/src/cli/shared/open-graph.js +13 -0
- package/src/cli/shared/options.js +22 -2
- package/src/cli.js +1 -1
- package/src/db/connection.js +140 -11
- package/src/{db.js → db/index.js} +12 -5
- package/src/db/migrations.js +42 -65
- package/src/db/query-builder.js +72 -9
- package/src/db/repository/base.js +1 -1
- package/src/db/repository/graph-read.js +3 -3
- package/src/db/repository/in-memory-repository.js +30 -28
- package/src/db/repository/nodes.js +10 -17
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +392 -0
- package/src/domain/analysis/dependencies.js +395 -0
- package/src/{analysis → domain/analysis}/exports.js +11 -6
- package/src/domain/analysis/impact.js +581 -0
- package/src/domain/analysis/module-map.js +348 -0
- package/src/{analysis → domain/analysis}/roles.js +12 -9
- package/src/{analysis → domain/analysis}/symbol-lookup.js +19 -11
- package/src/{builder → domain/graph/builder}/helpers.js +4 -4
- package/src/{builder → domain/graph/builder}/incremental.js +119 -93
- package/src/domain/graph/builder/pipeline.js +156 -0
- package/src/domain/graph/builder/stages/build-edges.js +376 -0
- package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
- package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
- package/src/{builder → domain/graph/builder}/stages/detect-changes.js +204 -183
- package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
- package/src/domain/graph/builder/stages/insert-nodes.js +203 -0
- package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
- package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
- package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
- package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
- package/src/{cycles.js → domain/graph/cycles.js} +4 -4
- package/src/{journal.js → domain/graph/journal.js} +1 -1
- package/src/{resolve.js → domain/graph/resolve.js} +2 -2
- package/src/{watcher.js → domain/graph/watcher.js} +7 -7
- package/src/{parser.js → domain/parser.js} +24 -15
- package/src/{queries.js → domain/queries.js} +17 -16
- package/src/{embeddings → domain/search}/generator.js +3 -3
- package/src/{embeddings → domain/search}/models.js +2 -2
- package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
- package/src/{embeddings → domain/search}/search/filters.js +9 -5
- package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
- package/src/{embeddings → domain/search}/search/keyword.js +13 -6
- package/src/{embeddings → domain/search}/search/prepare.js +15 -7
- package/src/{embeddings → domain/search}/search/semantic.js +1 -1
- package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +275 -305
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/{ast.js → features/ast.js} +13 -11
- package/src/{audit.js → features/audit.js} +20 -46
- package/src/{batch.js → features/batch.js} +5 -5
- package/src/{boundaries.js → features/boundaries.js} +100 -85
- package/src/{branch-compare.js → features/branch-compare.js} +3 -3
- package/src/{cfg.js → features/cfg.js} +141 -150
- package/src/{check.js → features/check.js} +13 -30
- package/src/{cochange.js → features/cochange.js} +5 -5
- package/src/{communities.js → features/communities.js} +72 -57
- package/src/{complexity.js → features/complexity.js} +154 -143
- package/src/{dataflow.js → features/dataflow.js} +155 -158
- package/src/{export.js → features/export.js} +6 -6
- package/src/{flow.js → features/flow.js} +4 -4
- package/src/{viewer.js → features/graph-enrichment.js} +8 -8
- package/src/{manifesto.js → features/manifesto.js} +15 -12
- package/src/{owners.js → features/owners.js} +6 -5
- package/src/features/sequence.js +300 -0
- package/src/features/shared/find-nodes.js +31 -0
- package/src/{snapshot.js → features/snapshot.js} +3 -3
- package/src/{structure.js → features/structure.js} +139 -108
- package/src/features/triage.js +141 -0
- package/src/graph/builders/dependency.js +33 -14
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.cjs +16 -0
- package/src/index.js +40 -39
- package/src/{native.js → infrastructure/native.js} +1 -1
- package/src/mcp/middleware.js +1 -1
- package/src/mcp/server.js +68 -59
- package/src/mcp/tool-registry.js +15 -2
- package/src/mcp/tools/ast-query.js +1 -1
- package/src/mcp/tools/audit.js +1 -1
- package/src/mcp/tools/batch-query.js +1 -1
- package/src/mcp/tools/branch-compare.js +3 -1
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/cfg.js +1 -1
- package/src/mcp/tools/check.js +3 -3
- package/src/mcp/tools/co-changes.js +1 -1
- package/src/mcp/tools/code-owners.js +1 -1
- package/src/mcp/tools/communities.js +1 -1
- package/src/mcp/tools/complexity.js +1 -1
- package/src/mcp/tools/dataflow.js +2 -2
- package/src/mcp/tools/execution-flow.js +2 -2
- package/src/mcp/tools/export-graph.js +2 -2
- package/src/mcp/tools/find-cycles.js +2 -2
- package/src/mcp/tools/index.js +2 -0
- package/src/mcp/tools/list-repos.js +1 -1
- package/src/mcp/tools/sequence.js +1 -1
- package/src/mcp/tools/structure.js +1 -1
- package/src/mcp/tools/triage.js +2 -2
- package/src/{commands → presentation}/audit.js +2 -2
- package/src/{commands → presentation}/batch.js +1 -1
- package/src/{commands → presentation}/branch-compare.js +2 -2
- package/src/presentation/brief.js +51 -0
- package/src/{commands → presentation}/cfg.js +1 -1
- package/src/{commands → presentation}/check.js +2 -2
- package/src/{commands → presentation}/communities.js +1 -1
- package/src/{commands → presentation}/complexity.js +1 -1
- package/src/{commands → presentation}/dataflow.js +1 -1
- package/src/{commands → presentation}/flow.js +2 -2
- package/src/{commands → presentation}/manifesto.js +1 -1
- package/src/{commands → presentation}/owners.js +1 -1
- package/src/presentation/queries-cli/exports.js +53 -0
- package/src/presentation/queries-cli/impact.js +214 -0
- package/src/presentation/queries-cli/index.js +5 -0
- package/src/presentation/queries-cli/inspect.js +329 -0
- package/src/presentation/queries-cli/overview.js +196 -0
- package/src/presentation/queries-cli/path.js +65 -0
- package/src/presentation/queries-cli.js +27 -0
- package/src/{commands → presentation}/query.js +1 -1
- package/src/presentation/result-formatter.js +126 -3
- package/src/{commands → presentation}/sequence.js +2 -2
- package/src/{commands → presentation}/structure.js +1 -1
- package/src/presentation/table.js +0 -8
- package/src/{commands → presentation}/triage.js +1 -1
- package/src/{constants.js → shared/constants.js} +1 -1
- package/src/shared/file-utils.js +2 -2
- package/src/shared/generators.js +9 -5
- package/src/shared/hierarchy.js +1 -1
- package/src/{kinds.js → shared/kinds.js} +1 -1
- package/src/analysis/context.js +0 -408
- package/src/analysis/dependencies.js +0 -341
- package/src/analysis/impact.js +0 -463
- package/src/analysis/module-map.js +0 -322
- package/src/builder/pipeline.js +0 -130
- package/src/builder/stages/build-edges.js +0 -297
- package/src/builder/stages/insert-nodes.js +0 -195
- package/src/mcp.js +0 -2
- package/src/queries-cli.js +0 -866
- package/src/sequence.js +0 -289
- package/src/triage.js +0 -126
- /package/src/{builder → domain/graph/builder}/context.js +0 -0
- /package/src/{builder.js → domain/graph/builder.js} +0 -0
- /package/src/{embeddings → domain/search}/index.js +0 -0
- /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
- /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
- /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
- /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
- /package/src/{config.js → infrastructure/config.js} +0 -0
- /package/src/{logger.js → infrastructure/logger.js} +0 -0
- /package/src/{registry.js → infrastructure/registry.js} +0 -0
- /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
- /package/src/{commands → presentation}/cochange.js +0 -0
- /package/src/{errors.js → shared/errors.js} +0 -0
- /package/src/{paginate.js → shared/paginate.js} +0 -0
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { findCycles } from '../cycles.js';
|
|
3
|
-
import { openReadonlyOrFail, testFilterSQL } from '../db.js';
|
|
4
|
-
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
5
|
-
import { LANGUAGE_REGISTRY } from '../parser.js';
|
|
6
|
-
|
|
7
|
-
export const FALSE_POSITIVE_NAMES = new Set([
|
|
8
|
-
'run',
|
|
9
|
-
'get',
|
|
10
|
-
'set',
|
|
11
|
-
'init',
|
|
12
|
-
'start',
|
|
13
|
-
'handle',
|
|
14
|
-
'main',
|
|
15
|
-
'new',
|
|
16
|
-
'create',
|
|
17
|
-
'update',
|
|
18
|
-
'delete',
|
|
19
|
-
'process',
|
|
20
|
-
'execute',
|
|
21
|
-
'call',
|
|
22
|
-
'apply',
|
|
23
|
-
'setup',
|
|
24
|
-
'render',
|
|
25
|
-
'build',
|
|
26
|
-
'load',
|
|
27
|
-
'save',
|
|
28
|
-
'find',
|
|
29
|
-
'make',
|
|
30
|
-
'open',
|
|
31
|
-
'close',
|
|
32
|
-
'reset',
|
|
33
|
-
'send',
|
|
34
|
-
'read',
|
|
35
|
-
'write',
|
|
36
|
-
]);
|
|
37
|
-
export const FALSE_POSITIVE_CALLER_THRESHOLD = 20;
|
|
38
|
-
|
|
39
|
-
export function moduleMapData(customDbPath, limit = 20, opts = {}) {
|
|
40
|
-
const db = openReadonlyOrFail(customDbPath);
|
|
41
|
-
try {
|
|
42
|
-
const noTests = opts.noTests || false;
|
|
43
|
-
|
|
44
|
-
const testFilter = testFilterSQL('n.file', noTests);
|
|
45
|
-
|
|
46
|
-
const nodes = db
|
|
47
|
-
.prepare(`
|
|
48
|
-
SELECT n.*,
|
|
49
|
-
(SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as out_edges,
|
|
50
|
-
(SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as in_edges
|
|
51
|
-
FROM nodes n
|
|
52
|
-
WHERE n.kind = 'file'
|
|
53
|
-
${testFilter}
|
|
54
|
-
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) DESC
|
|
55
|
-
LIMIT ?
|
|
56
|
-
`)
|
|
57
|
-
.all(limit);
|
|
58
|
-
|
|
59
|
-
const topNodes = nodes.map((n) => ({
|
|
60
|
-
file: n.file,
|
|
61
|
-
dir: path.dirname(n.file) || '.',
|
|
62
|
-
inEdges: n.in_edges,
|
|
63
|
-
outEdges: n.out_edges,
|
|
64
|
-
coupling: n.in_edges + n.out_edges,
|
|
65
|
-
}));
|
|
66
|
-
|
|
67
|
-
const totalNodes = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
|
|
68
|
-
const totalEdges = db.prepare('SELECT COUNT(*) as c FROM edges').get().c;
|
|
69
|
-
const totalFiles = db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get().c;
|
|
70
|
-
|
|
71
|
-
return { limit, topNodes, stats: { totalFiles, totalNodes, totalEdges } };
|
|
72
|
-
} finally {
|
|
73
|
-
db.close();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function statsData(customDbPath, opts = {}) {
|
|
78
|
-
const db = openReadonlyOrFail(customDbPath);
|
|
79
|
-
try {
|
|
80
|
-
const noTests = opts.noTests || false;
|
|
81
|
-
|
|
82
|
-
// Build set of test file IDs for filtering nodes and edges
|
|
83
|
-
let testFileIds = null;
|
|
84
|
-
if (noTests) {
|
|
85
|
-
const allFileNodes = db.prepare("SELECT id, file FROM nodes WHERE kind = 'file'").all();
|
|
86
|
-
testFileIds = new Set();
|
|
87
|
-
const testFiles = new Set();
|
|
88
|
-
for (const n of allFileNodes) {
|
|
89
|
-
if (isTestFile(n.file)) {
|
|
90
|
-
testFileIds.add(n.id);
|
|
91
|
-
testFiles.add(n.file);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Also collect non-file node IDs that belong to test files
|
|
96
|
-
const allNodes = db.prepare('SELECT id, file FROM nodes').all();
|
|
97
|
-
for (const n of allNodes) {
|
|
98
|
-
if (testFiles.has(n.file)) testFileIds.add(n.id);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Node breakdown by kind
|
|
103
|
-
let nodeRows;
|
|
104
|
-
if (noTests) {
|
|
105
|
-
const allNodes = db.prepare('SELECT id, kind, file FROM nodes').all();
|
|
106
|
-
const filtered = allNodes.filter((n) => !testFileIds.has(n.id));
|
|
107
|
-
const counts = {};
|
|
108
|
-
for (const n of filtered) counts[n.kind] = (counts[n.kind] || 0) + 1;
|
|
109
|
-
nodeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
|
|
110
|
-
} else {
|
|
111
|
-
nodeRows = db.prepare('SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind').all();
|
|
112
|
-
}
|
|
113
|
-
const nodesByKind = {};
|
|
114
|
-
let totalNodes = 0;
|
|
115
|
-
for (const r of nodeRows) {
|
|
116
|
-
nodesByKind[r.kind] = r.c;
|
|
117
|
-
totalNodes += r.c;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Edge breakdown by kind
|
|
121
|
-
let edgeRows;
|
|
122
|
-
if (noTests) {
|
|
123
|
-
const allEdges = db.prepare('SELECT source_id, target_id, kind FROM edges').all();
|
|
124
|
-
const filtered = allEdges.filter(
|
|
125
|
-
(e) => !testFileIds.has(e.source_id) && !testFileIds.has(e.target_id),
|
|
126
|
-
);
|
|
127
|
-
const counts = {};
|
|
128
|
-
for (const e of filtered) counts[e.kind] = (counts[e.kind] || 0) + 1;
|
|
129
|
-
edgeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
|
|
130
|
-
} else {
|
|
131
|
-
edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all();
|
|
132
|
-
}
|
|
133
|
-
const edgesByKind = {};
|
|
134
|
-
let totalEdges = 0;
|
|
135
|
-
for (const r of edgeRows) {
|
|
136
|
-
edgesByKind[r.kind] = r.c;
|
|
137
|
-
totalEdges += r.c;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// File/language distribution — map extensions via LANGUAGE_REGISTRY
|
|
141
|
-
const extToLang = new Map();
|
|
142
|
-
for (const entry of LANGUAGE_REGISTRY) {
|
|
143
|
-
for (const ext of entry.extensions) {
|
|
144
|
-
extToLang.set(ext, entry.id);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
let fileNodes = db.prepare("SELECT file FROM nodes WHERE kind = 'file'").all();
|
|
148
|
-
if (noTests) fileNodes = fileNodes.filter((n) => !isTestFile(n.file));
|
|
149
|
-
const byLanguage = {};
|
|
150
|
-
for (const row of fileNodes) {
|
|
151
|
-
const ext = path.extname(row.file).toLowerCase();
|
|
152
|
-
const lang = extToLang.get(ext) || 'other';
|
|
153
|
-
byLanguage[lang] = (byLanguage[lang] || 0) + 1;
|
|
154
|
-
}
|
|
155
|
-
const langCount = Object.keys(byLanguage).length;
|
|
156
|
-
|
|
157
|
-
// Cycles
|
|
158
|
-
const fileCycles = findCycles(db, { fileLevel: true, noTests });
|
|
159
|
-
const fnCycles = findCycles(db, { fileLevel: false, noTests });
|
|
160
|
-
|
|
161
|
-
// Top 5 coupling hotspots (fan-in + fan-out, file nodes)
|
|
162
|
-
const testFilter = testFilterSQL('n.file', noTests);
|
|
163
|
-
const hotspotRows = db
|
|
164
|
-
.prepare(`
|
|
165
|
-
SELECT n.file,
|
|
166
|
-
(SELECT COUNT(*) FROM edges WHERE target_id = n.id) as fan_in,
|
|
167
|
-
(SELECT COUNT(*) FROM edges WHERE source_id = n.id) as fan_out
|
|
168
|
-
FROM nodes n
|
|
169
|
-
WHERE n.kind = 'file' ${testFilter}
|
|
170
|
-
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id)
|
|
171
|
-
+ (SELECT COUNT(*) FROM edges WHERE source_id = n.id) DESC
|
|
172
|
-
`)
|
|
173
|
-
.all();
|
|
174
|
-
const filteredHotspots = noTests ? hotspotRows.filter((r) => !isTestFile(r.file)) : hotspotRows;
|
|
175
|
-
const hotspots = filteredHotspots.slice(0, 5).map((r) => ({
|
|
176
|
-
file: r.file,
|
|
177
|
-
fanIn: r.fan_in,
|
|
178
|
-
fanOut: r.fan_out,
|
|
179
|
-
}));
|
|
180
|
-
|
|
181
|
-
// Embeddings metadata
|
|
182
|
-
let embeddings = null;
|
|
183
|
-
try {
|
|
184
|
-
const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get();
|
|
185
|
-
if (count && count.c > 0) {
|
|
186
|
-
const meta = {};
|
|
187
|
-
const metaRows = db.prepare('SELECT key, value FROM embedding_meta').all();
|
|
188
|
-
for (const r of metaRows) meta[r.key] = r.value;
|
|
189
|
-
embeddings = {
|
|
190
|
-
count: count.c,
|
|
191
|
-
model: meta.model || null,
|
|
192
|
-
dim: meta.dim ? parseInt(meta.dim, 10) : null,
|
|
193
|
-
builtAt: meta.built_at || null,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
} catch {
|
|
197
|
-
/* embeddings table may not exist */
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Graph quality metrics
|
|
201
|
-
const qualityTestFilter = testFilter.replace(/n\.file/g, 'file');
|
|
202
|
-
const totalCallable = db
|
|
203
|
-
.prepare(
|
|
204
|
-
`SELECT COUNT(*) as c FROM nodes WHERE kind IN ('function', 'method') ${qualityTestFilter}`,
|
|
205
|
-
)
|
|
206
|
-
.get().c;
|
|
207
|
-
const callableWithCallers = db
|
|
208
|
-
.prepare(`
|
|
209
|
-
SELECT COUNT(DISTINCT e.target_id) as c FROM edges e
|
|
210
|
-
JOIN nodes n ON e.target_id = n.id
|
|
211
|
-
WHERE e.kind = 'calls' AND n.kind IN ('function', 'method') ${testFilter}
|
|
212
|
-
`)
|
|
213
|
-
.get().c;
|
|
214
|
-
const callerCoverage = totalCallable > 0 ? callableWithCallers / totalCallable : 0;
|
|
215
|
-
|
|
216
|
-
const totalCallEdges = db
|
|
217
|
-
.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls'")
|
|
218
|
-
.get().c;
|
|
219
|
-
const highConfCallEdges = db
|
|
220
|
-
.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls' AND confidence >= 0.7")
|
|
221
|
-
.get().c;
|
|
222
|
-
const callConfidence = totalCallEdges > 0 ? highConfCallEdges / totalCallEdges : 0;
|
|
223
|
-
|
|
224
|
-
// False-positive warnings: generic names with > threshold callers
|
|
225
|
-
const fpRows = db
|
|
226
|
-
.prepare(`
|
|
227
|
-
SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
|
|
228
|
-
FROM nodes n
|
|
229
|
-
LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
|
|
230
|
-
WHERE n.kind IN ('function', 'method')
|
|
231
|
-
GROUP BY n.id
|
|
232
|
-
HAVING caller_count > ?
|
|
233
|
-
ORDER BY caller_count DESC
|
|
234
|
-
`)
|
|
235
|
-
.all(FALSE_POSITIVE_CALLER_THRESHOLD);
|
|
236
|
-
const falsePositiveWarnings = fpRows
|
|
237
|
-
.filter((r) =>
|
|
238
|
-
FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop() : r.name),
|
|
239
|
-
)
|
|
240
|
-
.map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
|
|
241
|
-
|
|
242
|
-
// Edges from suspicious nodes
|
|
243
|
-
let fpEdgeCount = 0;
|
|
244
|
-
for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
|
|
245
|
-
const falsePositiveRatio = totalCallEdges > 0 ? fpEdgeCount / totalCallEdges : 0;
|
|
246
|
-
|
|
247
|
-
const score = Math.round(
|
|
248
|
-
callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20,
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
const quality = {
|
|
252
|
-
score,
|
|
253
|
-
callerCoverage: {
|
|
254
|
-
ratio: callerCoverage,
|
|
255
|
-
covered: callableWithCallers,
|
|
256
|
-
total: totalCallable,
|
|
257
|
-
},
|
|
258
|
-
callConfidence: {
|
|
259
|
-
ratio: callConfidence,
|
|
260
|
-
highConf: highConfCallEdges,
|
|
261
|
-
total: totalCallEdges,
|
|
262
|
-
},
|
|
263
|
-
falsePositiveWarnings,
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
// Role distribution
|
|
267
|
-
let roleRows;
|
|
268
|
-
if (noTests) {
|
|
269
|
-
const allRoleNodes = db.prepare('SELECT role, file FROM nodes WHERE role IS NOT NULL').all();
|
|
270
|
-
const filtered = allRoleNodes.filter((n) => !isTestFile(n.file));
|
|
271
|
-
const counts = {};
|
|
272
|
-
for (const n of filtered) counts[n.role] = (counts[n.role] || 0) + 1;
|
|
273
|
-
roleRows = Object.entries(counts).map(([role, c]) => ({ role, c }));
|
|
274
|
-
} else {
|
|
275
|
-
roleRows = db
|
|
276
|
-
.prepare('SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL GROUP BY role')
|
|
277
|
-
.all();
|
|
278
|
-
}
|
|
279
|
-
const roles = {};
|
|
280
|
-
for (const r of roleRows) roles[r.role] = r.c;
|
|
281
|
-
|
|
282
|
-
// Complexity summary
|
|
283
|
-
let complexity = null;
|
|
284
|
-
try {
|
|
285
|
-
const cRows = db
|
|
286
|
-
.prepare(
|
|
287
|
-
`SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
|
|
288
|
-
FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
|
|
289
|
-
WHERE n.kind IN ('function','method') ${testFilter}`,
|
|
290
|
-
)
|
|
291
|
-
.all();
|
|
292
|
-
if (cRows.length > 0) {
|
|
293
|
-
const miValues = cRows.map((r) => r.maintainability_index || 0);
|
|
294
|
-
complexity = {
|
|
295
|
-
analyzed: cRows.length,
|
|
296
|
-
avgCognitive: +(cRows.reduce((s, r) => s + r.cognitive, 0) / cRows.length).toFixed(1),
|
|
297
|
-
avgCyclomatic: +(cRows.reduce((s, r) => s + r.cyclomatic, 0) / cRows.length).toFixed(1),
|
|
298
|
-
maxCognitive: Math.max(...cRows.map((r) => r.cognitive)),
|
|
299
|
-
maxCyclomatic: Math.max(...cRows.map((r) => r.cyclomatic)),
|
|
300
|
-
avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
|
|
301
|
-
minMI: +Math.min(...miValues).toFixed(1),
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
} catch {
|
|
305
|
-
/* table may not exist in older DBs */
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
nodes: { total: totalNodes, byKind: nodesByKind },
|
|
310
|
-
edges: { total: totalEdges, byKind: edgesByKind },
|
|
311
|
-
files: { total: fileNodes.length, languages: langCount, byLanguage },
|
|
312
|
-
cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
|
|
313
|
-
hotspots,
|
|
314
|
-
embeddings,
|
|
315
|
-
quality,
|
|
316
|
-
roles,
|
|
317
|
-
complexity,
|
|
318
|
-
};
|
|
319
|
-
} finally {
|
|
320
|
-
db.close();
|
|
321
|
-
}
|
|
322
|
-
}
|
package/src/builder/pipeline.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pipeline orchestrator — runs build stages sequentially through a shared PipelineContext.
|
|
3
|
-
*
|
|
4
|
-
* This is the heart of the builder refactor (ROADMAP 3.9): the monolithic buildGraph()
|
|
5
|
-
* is decomposed into independently testable stages that communicate via PipelineContext.
|
|
6
|
-
*/
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { performance } from 'node:perf_hooks';
|
|
9
|
-
import { loadConfig } from '../config.js';
|
|
10
|
-
import { closeDb, getBuildMeta, initSchema, MIGRATIONS, openDb } from '../db.js';
|
|
11
|
-
import { info } from '../logger.js';
|
|
12
|
-
import { getActiveEngine } from '../parser.js';
|
|
13
|
-
import { PipelineContext } from './context.js';
|
|
14
|
-
import { loadPathAliases } from './helpers.js';
|
|
15
|
-
import { buildEdges } from './stages/build-edges.js';
|
|
16
|
-
import { buildStructure } from './stages/build-structure.js';
|
|
17
|
-
// Pipeline stages
|
|
18
|
-
import { collectFiles } from './stages/collect-files.js';
|
|
19
|
-
import { detectChanges } from './stages/detect-changes.js';
|
|
20
|
-
import { finalize } from './stages/finalize.js';
|
|
21
|
-
import { insertNodes } from './stages/insert-nodes.js';
|
|
22
|
-
import { parseFiles } from './stages/parse-files.js';
|
|
23
|
-
import { resolveImports } from './stages/resolve-imports.js';
|
|
24
|
-
import { runAnalyses } from './stages/run-analyses.js';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Build the dependency graph for a codebase.
|
|
28
|
-
*
|
|
29
|
-
* Signature and return value are identical to the original monolithic buildGraph().
|
|
30
|
-
*
|
|
31
|
-
* @param {string} rootDir - Root directory to scan
|
|
32
|
-
* @param {object} [opts] - Build options
|
|
33
|
-
* @returns {Promise<{ phases: object } | undefined>}
|
|
34
|
-
*/
|
|
35
|
-
export async function buildGraph(rootDir, opts = {}) {
|
|
36
|
-
const ctx = new PipelineContext();
|
|
37
|
-
ctx.buildStart = performance.now();
|
|
38
|
-
ctx.opts = opts;
|
|
39
|
-
|
|
40
|
-
// ── Setup (creates DB, loads config, selects engine) ──────────────
|
|
41
|
-
ctx.rootDir = path.resolve(rootDir);
|
|
42
|
-
ctx.dbPath = path.join(ctx.rootDir, '.codegraph', 'graph.db');
|
|
43
|
-
ctx.db = openDb(ctx.dbPath);
|
|
44
|
-
try {
|
|
45
|
-
initSchema(ctx.db);
|
|
46
|
-
|
|
47
|
-
ctx.config = loadConfig(ctx.rootDir);
|
|
48
|
-
ctx.incremental =
|
|
49
|
-
opts.incremental !== false && ctx.config.build && ctx.config.build.incremental !== false;
|
|
50
|
-
|
|
51
|
-
ctx.engineOpts = {
|
|
52
|
-
engine: opts.engine || 'auto',
|
|
53
|
-
dataflow: opts.dataflow !== false,
|
|
54
|
-
ast: opts.ast !== false,
|
|
55
|
-
};
|
|
56
|
-
const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts);
|
|
57
|
-
ctx.engineName = engineName;
|
|
58
|
-
ctx.engineVersion = engineVersion;
|
|
59
|
-
info(`Using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
|
|
60
|
-
|
|
61
|
-
// Engine/schema mismatch detection
|
|
62
|
-
ctx.schemaVersion = MIGRATIONS[MIGRATIONS.length - 1].version;
|
|
63
|
-
ctx.forceFullRebuild = false;
|
|
64
|
-
if (ctx.incremental) {
|
|
65
|
-
const prevEngine = getBuildMeta(ctx.db, 'engine');
|
|
66
|
-
if (prevEngine && prevEngine !== engineName) {
|
|
67
|
-
info(`Engine changed (${prevEngine} → ${engineName}), promoting to full rebuild.`);
|
|
68
|
-
ctx.forceFullRebuild = true;
|
|
69
|
-
}
|
|
70
|
-
const prevSchema = getBuildMeta(ctx.db, 'schema_version');
|
|
71
|
-
if (prevSchema && Number(prevSchema) !== ctx.schemaVersion) {
|
|
72
|
-
info(
|
|
73
|
-
`Schema version changed (${prevSchema} → ${ctx.schemaVersion}), promoting to full rebuild.`,
|
|
74
|
-
);
|
|
75
|
-
ctx.forceFullRebuild = true;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Path aliases
|
|
80
|
-
ctx.aliases = loadPathAliases(ctx.rootDir);
|
|
81
|
-
if (ctx.config.aliases) {
|
|
82
|
-
for (const [key, value] of Object.entries(ctx.config.aliases)) {
|
|
83
|
-
const pattern = key.endsWith('/') ? `${key}*` : key;
|
|
84
|
-
const target = path.resolve(ctx.rootDir, value);
|
|
85
|
-
ctx.aliases.paths[pattern] = [target.endsWith('/') ? `${target}*` : `${target}/*`];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (ctx.aliases.baseUrl || Object.keys(ctx.aliases.paths).length > 0) {
|
|
89
|
-
info(
|
|
90
|
-
`Loaded path aliases: baseUrl=${ctx.aliases.baseUrl || 'none'}, ${Object.keys(ctx.aliases.paths).length} path mappings`,
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
ctx.timing.setupMs = performance.now() - ctx.buildStart;
|
|
95
|
-
|
|
96
|
-
// ── Pipeline stages ─────────────────────────────────────────────
|
|
97
|
-
await collectFiles(ctx);
|
|
98
|
-
await detectChanges(ctx);
|
|
99
|
-
|
|
100
|
-
if (ctx.earlyExit) return;
|
|
101
|
-
|
|
102
|
-
await parseFiles(ctx);
|
|
103
|
-
await insertNodes(ctx);
|
|
104
|
-
await resolveImports(ctx);
|
|
105
|
-
await buildEdges(ctx);
|
|
106
|
-
await buildStructure(ctx);
|
|
107
|
-
await runAnalyses(ctx);
|
|
108
|
-
await finalize(ctx);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
if (!ctx.earlyExit) closeDb(ctx.db);
|
|
111
|
-
throw err;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
phases: {
|
|
116
|
-
setupMs: +ctx.timing.setupMs.toFixed(1),
|
|
117
|
-
parseMs: +(ctx.timing.parseMs ?? 0).toFixed(1),
|
|
118
|
-
insertMs: +(ctx.timing.insertMs ?? 0).toFixed(1),
|
|
119
|
-
resolveMs: +(ctx.timing.resolveMs ?? 0).toFixed(1),
|
|
120
|
-
edgesMs: +(ctx.timing.edgesMs ?? 0).toFixed(1),
|
|
121
|
-
structureMs: +(ctx.timing.structureMs ?? 0).toFixed(1),
|
|
122
|
-
rolesMs: +(ctx.timing.rolesMs ?? 0).toFixed(1),
|
|
123
|
-
astMs: +(ctx.timing.astMs ?? 0).toFixed(1),
|
|
124
|
-
complexityMs: +(ctx.timing.complexityMs ?? 0).toFixed(1),
|
|
125
|
-
...(ctx.timing.cfgMs != null && { cfgMs: +ctx.timing.cfgMs.toFixed(1) }),
|
|
126
|
-
...(ctx.timing.dataflowMs != null && { dataflowMs: +ctx.timing.dataflowMs.toFixed(1) }),
|
|
127
|
-
finalizeMs: +(ctx.timing.finalizeMs ?? 0).toFixed(1),
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|