@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
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { openReadonlyOrFail, testFilterSQL } from '../../db/index.js';
|
|
3
|
+
import { debug } from '../../infrastructure/logger.js';
|
|
4
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
5
|
+
import { findCycles } from '../graph/cycles.js';
|
|
6
|
+
import { LANGUAGE_REGISTRY } from '../parser.js';
|
|
7
|
+
|
|
8
|
+
export const FALSE_POSITIVE_NAMES = new Set([
|
|
9
|
+
'run',
|
|
10
|
+
'get',
|
|
11
|
+
'set',
|
|
12
|
+
'init',
|
|
13
|
+
'start',
|
|
14
|
+
'handle',
|
|
15
|
+
'main',
|
|
16
|
+
'new',
|
|
17
|
+
'create',
|
|
18
|
+
'update',
|
|
19
|
+
'delete',
|
|
20
|
+
'process',
|
|
21
|
+
'execute',
|
|
22
|
+
'call',
|
|
23
|
+
'apply',
|
|
24
|
+
'setup',
|
|
25
|
+
'render',
|
|
26
|
+
'build',
|
|
27
|
+
'load',
|
|
28
|
+
'save',
|
|
29
|
+
'find',
|
|
30
|
+
'make',
|
|
31
|
+
'open',
|
|
32
|
+
'close',
|
|
33
|
+
'reset',
|
|
34
|
+
'send',
|
|
35
|
+
'read',
|
|
36
|
+
'write',
|
|
37
|
+
]);
|
|
38
|
+
export const FALSE_POSITIVE_CALLER_THRESHOLD = 20;
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Section helpers
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function buildTestFileIds(db) {
|
|
45
|
+
const allFileNodes = db.prepare("SELECT id, file FROM nodes WHERE kind = 'file'").all();
|
|
46
|
+
const testFileIds = new Set();
|
|
47
|
+
const testFiles = new Set();
|
|
48
|
+
for (const n of allFileNodes) {
|
|
49
|
+
if (isTestFile(n.file)) {
|
|
50
|
+
testFileIds.add(n.id);
|
|
51
|
+
testFiles.add(n.file);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const allNodes = db.prepare('SELECT id, file FROM nodes').all();
|
|
55
|
+
for (const n of allNodes) {
|
|
56
|
+
if (testFiles.has(n.file)) testFileIds.add(n.id);
|
|
57
|
+
}
|
|
58
|
+
return testFileIds;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function countNodesByKind(db, testFileIds) {
|
|
62
|
+
let nodeRows;
|
|
63
|
+
if (testFileIds) {
|
|
64
|
+
const allNodes = db.prepare('SELECT id, kind, file FROM nodes').all();
|
|
65
|
+
const filtered = allNodes.filter((n) => !testFileIds.has(n.id));
|
|
66
|
+
const counts = {};
|
|
67
|
+
for (const n of filtered) counts[n.kind] = (counts[n.kind] || 0) + 1;
|
|
68
|
+
nodeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
|
|
69
|
+
} else {
|
|
70
|
+
nodeRows = db.prepare('SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind').all();
|
|
71
|
+
}
|
|
72
|
+
const byKind = {};
|
|
73
|
+
let total = 0;
|
|
74
|
+
for (const r of nodeRows) {
|
|
75
|
+
byKind[r.kind] = r.c;
|
|
76
|
+
total += r.c;
|
|
77
|
+
}
|
|
78
|
+
return { total, byKind };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function countEdgesByKind(db, testFileIds) {
|
|
82
|
+
let edgeRows;
|
|
83
|
+
if (testFileIds) {
|
|
84
|
+
const allEdges = db.prepare('SELECT source_id, target_id, kind FROM edges').all();
|
|
85
|
+
const filtered = allEdges.filter(
|
|
86
|
+
(e) => !testFileIds.has(e.source_id) && !testFileIds.has(e.target_id),
|
|
87
|
+
);
|
|
88
|
+
const counts = {};
|
|
89
|
+
for (const e of filtered) counts[e.kind] = (counts[e.kind] || 0) + 1;
|
|
90
|
+
edgeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
|
|
91
|
+
} else {
|
|
92
|
+
edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all();
|
|
93
|
+
}
|
|
94
|
+
const byKind = {};
|
|
95
|
+
let total = 0;
|
|
96
|
+
for (const r of edgeRows) {
|
|
97
|
+
byKind[r.kind] = r.c;
|
|
98
|
+
total += r.c;
|
|
99
|
+
}
|
|
100
|
+
return { total, byKind };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function countFilesByLanguage(db, noTests) {
|
|
104
|
+
const extToLang = new Map();
|
|
105
|
+
for (const entry of LANGUAGE_REGISTRY) {
|
|
106
|
+
for (const ext of entry.extensions) {
|
|
107
|
+
extToLang.set(ext, entry.id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
let fileNodes = db.prepare("SELECT file FROM nodes WHERE kind = 'file'").all();
|
|
111
|
+
if (noTests) fileNodes = fileNodes.filter((n) => !isTestFile(n.file));
|
|
112
|
+
const byLanguage = {};
|
|
113
|
+
for (const row of fileNodes) {
|
|
114
|
+
const ext = path.extname(row.file).toLowerCase();
|
|
115
|
+
const lang = extToLang.get(ext) || 'other';
|
|
116
|
+
byLanguage[lang] = (byLanguage[lang] || 0) + 1;
|
|
117
|
+
}
|
|
118
|
+
return { total: fileNodes.length, languages: Object.keys(byLanguage).length, byLanguage };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function findHotspots(db, noTests, limit) {
|
|
122
|
+
const testFilter = testFilterSQL('n.file', noTests);
|
|
123
|
+
const hotspotRows = db
|
|
124
|
+
.prepare(`
|
|
125
|
+
SELECT n.file,
|
|
126
|
+
(SELECT COUNT(*) FROM edges WHERE target_id = n.id) as fan_in,
|
|
127
|
+
(SELECT COUNT(*) FROM edges WHERE source_id = n.id) as fan_out
|
|
128
|
+
FROM nodes n
|
|
129
|
+
WHERE n.kind = 'file' ${testFilter}
|
|
130
|
+
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id)
|
|
131
|
+
+ (SELECT COUNT(*) FROM edges WHERE source_id = n.id) DESC
|
|
132
|
+
`)
|
|
133
|
+
.all();
|
|
134
|
+
const filtered = noTests ? hotspotRows.filter((r) => !isTestFile(r.file)) : hotspotRows;
|
|
135
|
+
return filtered.slice(0, limit).map((r) => ({
|
|
136
|
+
file: r.file,
|
|
137
|
+
fanIn: r.fan_in,
|
|
138
|
+
fanOut: r.fan_out,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getEmbeddingsInfo(db) {
|
|
143
|
+
try {
|
|
144
|
+
const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get();
|
|
145
|
+
if (count && count.c > 0) {
|
|
146
|
+
const meta = {};
|
|
147
|
+
const metaRows = db.prepare('SELECT key, value FROM embedding_meta').all();
|
|
148
|
+
for (const r of metaRows) meta[r.key] = r.value;
|
|
149
|
+
return {
|
|
150
|
+
count: count.c,
|
|
151
|
+
model: meta.model || null,
|
|
152
|
+
dim: meta.dim ? parseInt(meta.dim, 10) : null,
|
|
153
|
+
builtAt: meta.built_at || null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
debug(`embeddings lookup skipped: ${e.message}`);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function computeQualityMetrics(db, testFilter) {
|
|
163
|
+
const qualityTestFilter = testFilter.replace(/n\.file/g, 'file');
|
|
164
|
+
|
|
165
|
+
const totalCallable = db
|
|
166
|
+
.prepare(
|
|
167
|
+
`SELECT COUNT(*) as c FROM nodes WHERE kind IN ('function', 'method') ${qualityTestFilter}`,
|
|
168
|
+
)
|
|
169
|
+
.get().c;
|
|
170
|
+
const callableWithCallers = db
|
|
171
|
+
.prepare(`
|
|
172
|
+
SELECT COUNT(DISTINCT e.target_id) as c FROM edges e
|
|
173
|
+
JOIN nodes n ON e.target_id = n.id
|
|
174
|
+
WHERE e.kind = 'calls' AND n.kind IN ('function', 'method') ${testFilter}
|
|
175
|
+
`)
|
|
176
|
+
.get().c;
|
|
177
|
+
const callerCoverage = totalCallable > 0 ? callableWithCallers / totalCallable : 0;
|
|
178
|
+
|
|
179
|
+
const totalCallEdges = db.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls'").get().c;
|
|
180
|
+
const highConfCallEdges = db
|
|
181
|
+
.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls' AND confidence >= 0.7")
|
|
182
|
+
.get().c;
|
|
183
|
+
const callConfidence = totalCallEdges > 0 ? highConfCallEdges / totalCallEdges : 0;
|
|
184
|
+
|
|
185
|
+
const fpRows = db
|
|
186
|
+
.prepare(`
|
|
187
|
+
SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
|
|
188
|
+
FROM nodes n
|
|
189
|
+
LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
|
|
190
|
+
WHERE n.kind IN ('function', 'method')
|
|
191
|
+
GROUP BY n.id
|
|
192
|
+
HAVING caller_count > ?
|
|
193
|
+
ORDER BY caller_count DESC
|
|
194
|
+
`)
|
|
195
|
+
.all(FALSE_POSITIVE_CALLER_THRESHOLD);
|
|
196
|
+
const falsePositiveWarnings = fpRows
|
|
197
|
+
.filter((r) =>
|
|
198
|
+
FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop() : r.name),
|
|
199
|
+
)
|
|
200
|
+
.map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
|
|
201
|
+
|
|
202
|
+
let fpEdgeCount = 0;
|
|
203
|
+
for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
|
|
204
|
+
const falsePositiveRatio = totalCallEdges > 0 ? fpEdgeCount / totalCallEdges : 0;
|
|
205
|
+
|
|
206
|
+
const score = Math.round(
|
|
207
|
+
callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
score,
|
|
212
|
+
callerCoverage: {
|
|
213
|
+
ratio: callerCoverage,
|
|
214
|
+
covered: callableWithCallers,
|
|
215
|
+
total: totalCallable,
|
|
216
|
+
},
|
|
217
|
+
callConfidence: {
|
|
218
|
+
ratio: callConfidence,
|
|
219
|
+
highConf: highConfCallEdges,
|
|
220
|
+
total: totalCallEdges,
|
|
221
|
+
},
|
|
222
|
+
falsePositiveWarnings,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function countRoles(db, noTests) {
|
|
227
|
+
let roleRows;
|
|
228
|
+
if (noTests) {
|
|
229
|
+
const allRoleNodes = db.prepare('SELECT role, file FROM nodes WHERE role IS NOT NULL').all();
|
|
230
|
+
const filtered = allRoleNodes.filter((n) => !isTestFile(n.file));
|
|
231
|
+
const counts = {};
|
|
232
|
+
for (const n of filtered) counts[n.role] = (counts[n.role] || 0) + 1;
|
|
233
|
+
roleRows = Object.entries(counts).map(([role, c]) => ({ role, c }));
|
|
234
|
+
} else {
|
|
235
|
+
roleRows = db
|
|
236
|
+
.prepare('SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL GROUP BY role')
|
|
237
|
+
.all();
|
|
238
|
+
}
|
|
239
|
+
const roles = {};
|
|
240
|
+
for (const r of roleRows) roles[r.role] = r.c;
|
|
241
|
+
return roles;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getComplexitySummary(db, testFilter) {
|
|
245
|
+
try {
|
|
246
|
+
const cRows = db
|
|
247
|
+
.prepare(
|
|
248
|
+
`SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
|
|
249
|
+
FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
|
|
250
|
+
WHERE n.kind IN ('function','method') ${testFilter}`,
|
|
251
|
+
)
|
|
252
|
+
.all();
|
|
253
|
+
if (cRows.length > 0) {
|
|
254
|
+
const miValues = cRows.map((r) => r.maintainability_index || 0);
|
|
255
|
+
return {
|
|
256
|
+
analyzed: cRows.length,
|
|
257
|
+
avgCognitive: +(cRows.reduce((s, r) => s + r.cognitive, 0) / cRows.length).toFixed(1),
|
|
258
|
+
avgCyclomatic: +(cRows.reduce((s, r) => s + r.cyclomatic, 0) / cRows.length).toFixed(1),
|
|
259
|
+
maxCognitive: Math.max(...cRows.map((r) => r.cognitive)),
|
|
260
|
+
maxCyclomatic: Math.max(...cRows.map((r) => r.cyclomatic)),
|
|
261
|
+
avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
|
|
262
|
+
minMI: +Math.min(...miValues).toFixed(1),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
} catch (e) {
|
|
266
|
+
debug(`complexity summary skipped: ${e.message}`);
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Public API
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
export function moduleMapData(customDbPath, limit = 20, opts = {}) {
|
|
276
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
277
|
+
try {
|
|
278
|
+
const noTests = opts.noTests || false;
|
|
279
|
+
|
|
280
|
+
const testFilter = testFilterSQL('n.file', noTests);
|
|
281
|
+
|
|
282
|
+
const nodes = db
|
|
283
|
+
.prepare(`
|
|
284
|
+
SELECT n.*,
|
|
285
|
+
(SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as out_edges,
|
|
286
|
+
(SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as in_edges
|
|
287
|
+
FROM nodes n
|
|
288
|
+
WHERE n.kind = 'file'
|
|
289
|
+
${testFilter}
|
|
290
|
+
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) DESC
|
|
291
|
+
LIMIT ?
|
|
292
|
+
`)
|
|
293
|
+
.all(limit);
|
|
294
|
+
|
|
295
|
+
const topNodes = nodes.map((n) => ({
|
|
296
|
+
file: n.file,
|
|
297
|
+
dir: path.dirname(n.file) || '.',
|
|
298
|
+
inEdges: n.in_edges,
|
|
299
|
+
outEdges: n.out_edges,
|
|
300
|
+
coupling: n.in_edges + n.out_edges,
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
const totalNodes = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
|
|
304
|
+
const totalEdges = db.prepare('SELECT COUNT(*) as c FROM edges').get().c;
|
|
305
|
+
const totalFiles = db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get().c;
|
|
306
|
+
|
|
307
|
+
return { limit, topNodes, stats: { totalFiles, totalNodes, totalEdges } };
|
|
308
|
+
} finally {
|
|
309
|
+
db.close();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function statsData(customDbPath, opts = {}) {
|
|
314
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
315
|
+
try {
|
|
316
|
+
const noTests = opts.noTests || false;
|
|
317
|
+
const testFilter = testFilterSQL('n.file', noTests);
|
|
318
|
+
|
|
319
|
+
const testFileIds = noTests ? buildTestFileIds(db) : null;
|
|
320
|
+
|
|
321
|
+
const { total: totalNodes, byKind: nodesByKind } = countNodesByKind(db, testFileIds);
|
|
322
|
+
const { total: totalEdges, byKind: edgesByKind } = countEdgesByKind(db, testFileIds);
|
|
323
|
+
const files = countFilesByLanguage(db, noTests);
|
|
324
|
+
|
|
325
|
+
const fileCycles = findCycles(db, { fileLevel: true, noTests });
|
|
326
|
+
const fnCycles = findCycles(db, { fileLevel: false, noTests });
|
|
327
|
+
|
|
328
|
+
const hotspots = findHotspots(db, noTests, 5);
|
|
329
|
+
const embeddings = getEmbeddingsInfo(db);
|
|
330
|
+
const quality = computeQualityMetrics(db, testFilter);
|
|
331
|
+
const roles = countRoles(db, noTests);
|
|
332
|
+
const complexity = getComplexitySummary(db, testFilter);
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
nodes: { total: totalNodes, byKind: nodesByKind },
|
|
336
|
+
edges: { total: totalEdges, byKind: edgesByKind },
|
|
337
|
+
files,
|
|
338
|
+
cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
|
|
339
|
+
hotspots,
|
|
340
|
+
embeddings,
|
|
341
|
+
quality,
|
|
342
|
+
roles,
|
|
343
|
+
complexity,
|
|
344
|
+
};
|
|
345
|
+
} finally {
|
|
346
|
+
db.close();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { openReadonlyOrFail } from '
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { normalizeSymbol } from '
|
|
1
|
+
import { openReadonlyOrFail } from '../../db/index.js';
|
|
2
|
+
import { buildFileConditionSQL } from '../../db/query-builder.js';
|
|
3
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
4
|
+
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
5
|
+
import { paginateResult } from '../../shared/paginate.js';
|
|
5
6
|
|
|
6
7
|
export function rolesData(customDbPath, opts = {}) {
|
|
7
8
|
const db = openReadonlyOrFail(customDbPath);
|
|
8
9
|
try {
|
|
9
10
|
const noTests = opts.noTests || false;
|
|
10
11
|
const filterRole = opts.role || null;
|
|
11
|
-
const filterFile = opts.file || null;
|
|
12
|
-
|
|
13
12
|
const conditions = ['role IS NOT NULL'];
|
|
14
13
|
const params = [];
|
|
15
14
|
|
|
@@ -17,9 +16,13 @@ export function rolesData(customDbPath, opts = {}) {
|
|
|
17
16
|
conditions.push('role = ?');
|
|
18
17
|
params.push(filterRole);
|
|
19
18
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
{
|
|
20
|
+
const fc = buildFileConditionSQL(opts.file, 'file');
|
|
21
|
+
if (fc.sql) {
|
|
22
|
+
// Strip leading ' AND ' since we're using conditions array
|
|
23
|
+
conditions.push(fc.sql.replace(/^ AND /, ''));
|
|
24
|
+
params.push(...fc.params);
|
|
25
|
+
}
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
let rows = db
|
|
@@ -12,22 +12,29 @@ import {
|
|
|
12
12
|
findNodesWithFanIn,
|
|
13
13
|
listFunctionNodes,
|
|
14
14
|
openReadonlyOrFail,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
15
|
+
Repository,
|
|
16
|
+
} from '../../db/index.js';
|
|
17
|
+
import { debug } from '../../infrastructure/logger.js';
|
|
18
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
19
|
+
import { EVERY_SYMBOL_KIND } from '../../shared/kinds.js';
|
|
20
|
+
import { getFileHash, normalizeSymbol } from '../../shared/normalize.js';
|
|
21
|
+
import { paginateResult } from '../../shared/paginate.js';
|
|
20
22
|
|
|
21
|
-
const FUNCTION_KINDS = ['function', 'method', 'class'];
|
|
23
|
+
const FUNCTION_KINDS = ['function', 'method', 'class', 'constant'];
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Find nodes matching a name query, ranked by relevance.
|
|
25
27
|
* Scoring: exact=100, prefix=60, word-boundary=40, substring=10, plus fan-in tiebreaker.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} dbOrRepo - A better-sqlite3 Database or a Repository instance
|
|
26
30
|
*/
|
|
27
|
-
export function findMatchingNodes(
|
|
31
|
+
export function findMatchingNodes(dbOrRepo, name, opts = {}) {
|
|
28
32
|
const kinds = opts.kind ? [opts.kind] : opts.kinds?.length ? opts.kinds : FUNCTION_KINDS;
|
|
29
33
|
|
|
30
|
-
const
|
|
34
|
+
const isRepo = dbOrRepo instanceof Repository;
|
|
35
|
+
const rows = isRepo
|
|
36
|
+
? dbOrRepo.findNodesWithFanIn(`%${name}%`, { kinds, file: opts.file })
|
|
37
|
+
: findNodesWithFanIn(dbOrRepo, `%${name}%`, { kinds, file: opts.file });
|
|
31
38
|
|
|
32
39
|
const nodes = opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
|
|
33
40
|
|
|
@@ -103,12 +110,12 @@ export function queryNameData(name, customDbPath, opts = {}) {
|
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
function whereSymbolImpl(db, target, noTests) {
|
|
106
|
-
const placeholders =
|
|
113
|
+
const placeholders = EVERY_SYMBOL_KIND.map(() => '?').join(', ');
|
|
107
114
|
let nodes = db
|
|
108
115
|
.prepare(
|
|
109
116
|
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN (${placeholders}) ORDER BY file, line`,
|
|
110
117
|
)
|
|
111
|
-
.all(`%${target}%`, ...
|
|
118
|
+
.all(`%${target}%`, ...EVERY_SYMBOL_KIND);
|
|
112
119
|
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
113
120
|
|
|
114
121
|
const hc = new Map();
|
|
@@ -200,7 +207,8 @@ export function childrenData(name, customDbPath, opts = {}) {
|
|
|
200
207
|
let children;
|
|
201
208
|
try {
|
|
202
209
|
children = findNodeChildren(db, node.id);
|
|
203
|
-
} catch {
|
|
210
|
+
} catch (e) {
|
|
211
|
+
debug(`findNodeChildren failed for node ${node.id}: ${e.message}`);
|
|
204
212
|
children = [];
|
|
205
213
|
}
|
|
206
214
|
if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file));
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { purgeFilesData } from '../../../db/index.js';
|
|
10
|
+
import { warn } from '../../../infrastructure/logger.js';
|
|
11
|
+
import { EXTENSIONS, IGNORE_DIRS } from '../../../shared/constants.js';
|
|
12
12
|
|
|
13
13
|
export const BUILTIN_RECEIVERS = new Set([
|
|
14
14
|
'console',
|
|
@@ -179,7 +179,7 @@ export function purgeFilesFromGraph(db, files, options = {}) {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/** Batch INSERT chunk size for multi-value INSERTs. */
|
|
182
|
-
|
|
182
|
+
const BATCH_CHUNK = 200;
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
185
|
* Batch-insert node rows via multi-value INSERT statements.
|