@optave/codegraph 3.1.1 → 3.1.3
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 +6 -6
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- package/src/ast-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +13 -140
- package/src/audit.js +2 -87
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +1 -96
- package/src/builder.js +60 -178
- package/src/cfg.js +89 -883
- package/src/check.js +1 -84
- package/src/cli.js +31 -22
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +12 -83
- package/src/complexity.js +43 -357
- package/src/cycles.js +1 -1
- package/src/dataflow.js +12 -665
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/cached-stmt.js +19 -0
- package/src/db/repository/cfg.js +72 -0
- package/src/db/repository/cochange.js +54 -0
- package/src/db/repository/complexity.js +20 -0
- package/src/db/repository/dataflow.js +17 -0
- package/src/db/repository/edges.js +281 -0
- package/src/db/repository/embeddings.js +51 -0
- package/src/db/repository/graph-read.js +59 -0
- package/src/db/repository/index.js +43 -0
- package/src/db/repository/nodes.js +247 -0
- package/src/db.js +40 -1
- package/src/embedder.js +14 -34
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +2 -70
- package/src/index.js +30 -20
- package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
- package/src/kinds.js +1 -0
- package/src/manifesto.js +0 -76
- package/src/native.js +31 -9
- package/src/owners.js +1 -56
- package/src/parser.js +53 -2
- package/src/queries-cli.js +1 -1
- package/src/queries.js +79 -280
- package/src/sequence.js +5 -44
- package/src/structure.js +16 -75
- package/src/triage.js +1 -54
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
- package/src/db/repository.js +0 -134
- /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { hotspotsData, moduleBoundariesData, structureData } from '../structure.js';
|
|
3
|
+
|
|
4
|
+
export { structureData, hotspotsData, moduleBoundariesData };
|
|
5
|
+
|
|
6
|
+
export function formatStructure(data) {
|
|
7
|
+
if (data.count === 0) return 'No directory structure found. Run "codegraph build" first.';
|
|
8
|
+
|
|
9
|
+
const lines = [`\nProject structure (${data.count} directories):\n`];
|
|
10
|
+
for (const d of data.directories) {
|
|
11
|
+
const cohStr = d.cohesion !== null ? ` cohesion=${d.cohesion.toFixed(2)}` : '';
|
|
12
|
+
const depth = d.directory.split('/').length - 1;
|
|
13
|
+
const indent = ' '.repeat(depth);
|
|
14
|
+
lines.push(
|
|
15
|
+
`${indent}${d.directory}/ (${d.fileCount} files, ${d.symbolCount} symbols, <-${d.fanIn} ->${d.fanOut}${cohStr})`,
|
|
16
|
+
);
|
|
17
|
+
for (const f of d.files) {
|
|
18
|
+
lines.push(
|
|
19
|
+
`${indent} ${path.basename(f.file)} ${f.lineCount}L ${f.symbolCount}sym <-${f.fanIn} ->${f.fanOut}`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (data.warning) {
|
|
24
|
+
lines.push('');
|
|
25
|
+
lines.push(`⚠ ${data.warning}`);
|
|
26
|
+
}
|
|
27
|
+
return lines.join('\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatHotspots(data) {
|
|
31
|
+
if (data.hotspots.length === 0) return 'No hotspots found. Run "codegraph build" first.';
|
|
32
|
+
|
|
33
|
+
const lines = [`\nHotspots by ${data.metric} (${data.level}-level, top ${data.limit}):\n`];
|
|
34
|
+
let rank = 1;
|
|
35
|
+
for (const h of data.hotspots) {
|
|
36
|
+
const extra =
|
|
37
|
+
h.kind === 'directory'
|
|
38
|
+
? `${h.fileCount} files, cohesion=${h.cohesion !== null ? h.cohesion.toFixed(2) : 'n/a'}`
|
|
39
|
+
: `${h.lineCount || 0}L, ${h.symbolCount || 0} symbols`;
|
|
40
|
+
lines.push(
|
|
41
|
+
` ${String(rank++).padStart(2)}. ${h.name} <-${h.fanIn || 0} ->${h.fanOut || 0} (${extra})`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatModuleBoundaries(data) {
|
|
48
|
+
if (data.count === 0) return `No modules found with cohesion >= ${data.threshold}.`;
|
|
49
|
+
|
|
50
|
+
const lines = [`\nModule boundaries (cohesion >= ${data.threshold}, ${data.count} modules):\n`];
|
|
51
|
+
for (const m of data.modules) {
|
|
52
|
+
lines.push(
|
|
53
|
+
` ${m.directory}/ cohesion=${m.cohesion.toFixed(2)} (${m.fileCount} files, ${m.symbolCount} symbols)`,
|
|
54
|
+
);
|
|
55
|
+
lines.push(` Incoming: ${m.fanIn} edges Outgoing: ${m.fanOut} edges`);
|
|
56
|
+
if (m.files.length > 0) {
|
|
57
|
+
lines.push(
|
|
58
|
+
` Files: ${m.files.slice(0, 5).join(', ')}${m.files.length > 5 ? ` ... +${m.files.length - 5}` : ''}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
lines.push('');
|
|
62
|
+
}
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
2
|
+
import { triageData } from '../triage.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Print triage results to console.
|
|
6
|
+
*/
|
|
7
|
+
export function triage(customDbPath, opts = {}) {
|
|
8
|
+
const data = triageData(customDbPath, opts);
|
|
9
|
+
|
|
10
|
+
if (outputResult(data, 'items', opts)) return;
|
|
11
|
+
|
|
12
|
+
if (data.items.length === 0) {
|
|
13
|
+
if (data.summary.total === 0) {
|
|
14
|
+
console.log('\nNo symbols found. Run "codegraph build" first.\n');
|
|
15
|
+
} else {
|
|
16
|
+
console.log('\nNo symbols match the given filters.\n');
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('\n# Risk Audit Queue\n');
|
|
22
|
+
|
|
23
|
+
console.log(
|
|
24
|
+
` ${'Symbol'.padEnd(35)} ${'File'.padEnd(28)} ${'Role'.padEnd(8)} ${'Score'.padStart(6)} ${'Fan-In'.padStart(7)} ${'Cog'.padStart(4)} ${'Churn'.padStart(6)} ${'MI'.padStart(5)}`,
|
|
25
|
+
);
|
|
26
|
+
console.log(
|
|
27
|
+
` ${'─'.repeat(35)} ${'─'.repeat(28)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(7)} ${'─'.repeat(4)} ${'─'.repeat(6)} ${'─'.repeat(5)}`,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
for (const it of data.items) {
|
|
31
|
+
const name = it.name.length > 33 ? `${it.name.slice(0, 32)}…` : it.name;
|
|
32
|
+
const file = it.file.length > 26 ? `…${it.file.slice(-25)}` : it.file;
|
|
33
|
+
const role = (it.role || '-').padEnd(8);
|
|
34
|
+
const score = it.riskScore.toFixed(2).padStart(6);
|
|
35
|
+
const fanIn = String(it.fanIn).padStart(7);
|
|
36
|
+
const cog = String(it.cognitive).padStart(4);
|
|
37
|
+
const churn = String(it.churn).padStart(6);
|
|
38
|
+
const mi = it.maintainabilityIndex > 0 ? String(it.maintainabilityIndex).padStart(5) : ' -';
|
|
39
|
+
console.log(
|
|
40
|
+
` ${name.padEnd(35)} ${file.padEnd(28)} ${role} ${score} ${fanIn} ${cog} ${churn} ${mi}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const s = data.summary;
|
|
45
|
+
console.log(
|
|
46
|
+
`\n ${s.analyzed} symbols scored (of ${s.total} total) | avg: ${s.avgScore.toFixed(2)} | max: ${s.maxScore.toFixed(2)} | sort: ${opts.sort || 'risk'}`,
|
|
47
|
+
);
|
|
48
|
+
console.log();
|
|
49
|
+
}
|
package/src/communities.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import Graph from 'graphology';
|
|
3
3
|
import louvain from 'graphology-communities-louvain';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getCallableNodes,
|
|
6
|
+
getCallEdges,
|
|
7
|
+
getFileNodesAll,
|
|
8
|
+
getImportEdges,
|
|
9
|
+
openReadonlyOrFail,
|
|
10
|
+
} from './db.js';
|
|
11
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
5
12
|
import { paginateResult } from './paginate.js';
|
|
6
|
-
import { outputResult } from './result-formatter.js';
|
|
7
|
-
import { isTestFile } from './test-filter.js';
|
|
8
13
|
|
|
9
14
|
// ─── Graph Construction ───────────────────────────────────────────────
|
|
10
15
|
|
|
@@ -22,9 +27,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
22
27
|
|
|
23
28
|
if (opts.functions) {
|
|
24
29
|
// Function-level: nodes = function/method/class symbols, edges = calls
|
|
25
|
-
let nodes = db
|
|
26
|
-
.prepare("SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')")
|
|
27
|
-
.all();
|
|
30
|
+
let nodes = getCallableNodes(db);
|
|
28
31
|
if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
29
32
|
|
|
30
33
|
const nodeIds = new Set();
|
|
@@ -34,7 +37,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
34
37
|
nodeIds.add(n.id);
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
const edges = db
|
|
40
|
+
const edges = getCallEdges(db);
|
|
38
41
|
for (const e of edges) {
|
|
39
42
|
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
40
43
|
const src = String(e.source_id);
|
|
@@ -46,7 +49,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
46
49
|
}
|
|
47
50
|
} else {
|
|
48
51
|
// File-level: nodes = files, edges = imports + imports-type (deduplicated, cross-file)
|
|
49
|
-
let nodes = db
|
|
52
|
+
let nodes = getFileNodesAll(db);
|
|
50
53
|
if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
51
54
|
|
|
52
55
|
const nodeIds = new Set();
|
|
@@ -56,9 +59,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
56
59
|
nodeIds.add(n.id);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
const edges = db
|
|
60
|
-
.prepare("SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')")
|
|
61
|
-
.all();
|
|
62
|
+
const edges = getImportEdges(db);
|
|
62
63
|
for (const e of edges) {
|
|
63
64
|
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
64
65
|
const src = String(e.source_id);
|
|
@@ -232,75 +233,3 @@ export function communitySummaryForStats(customDbPath, opts = {}) {
|
|
|
232
233
|
const data = communitiesData(customDbPath, { ...opts, drift: true });
|
|
233
234
|
return data.summary;
|
|
234
235
|
}
|
|
235
|
-
|
|
236
|
-
// ─── CLI Display ──────────────────────────────────────────────────────
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* CLI entry point: run community detection and print results.
|
|
240
|
-
*
|
|
241
|
-
* @param {string} [customDbPath]
|
|
242
|
-
* @param {object} [opts]
|
|
243
|
-
*/
|
|
244
|
-
export function communities(customDbPath, opts = {}) {
|
|
245
|
-
const data = communitiesData(customDbPath, opts);
|
|
246
|
-
|
|
247
|
-
if (outputResult(data, 'communities', opts)) return;
|
|
248
|
-
|
|
249
|
-
if (data.summary.communityCount === 0) {
|
|
250
|
-
console.log(
|
|
251
|
-
'\nNo communities detected. The graph may be too small or disconnected.\n' +
|
|
252
|
-
'Run "codegraph build" first to populate the graph.\n',
|
|
253
|
-
);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const mode = opts.functions ? 'Function' : 'File';
|
|
258
|
-
console.log(`\n# ${mode}-Level Communities\n`);
|
|
259
|
-
console.log(
|
|
260
|
-
` ${data.summary.communityCount} communities | ${data.summary.nodeCount} nodes | modularity: ${data.summary.modularity} | drift: ${data.summary.driftScore}%\n`,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
if (!opts.drift) {
|
|
264
|
-
for (const c of data.communities) {
|
|
265
|
-
const dirs = Object.entries(c.directories)
|
|
266
|
-
.sort((a, b) => b[1] - a[1])
|
|
267
|
-
.map(([d, n]) => `${d} (${n})`)
|
|
268
|
-
.join(', ');
|
|
269
|
-
console.log(` Community ${c.id} (${c.size} members): ${dirs}`);
|
|
270
|
-
if (c.members) {
|
|
271
|
-
const shown = c.members.slice(0, 8);
|
|
272
|
-
for (const m of shown) {
|
|
273
|
-
const kind = m.kind ? ` [${m.kind}]` : '';
|
|
274
|
-
console.log(` - ${m.name}${kind} ${m.file}`);
|
|
275
|
-
}
|
|
276
|
-
if (c.members.length > 8) {
|
|
277
|
-
console.log(` ... and ${c.members.length - 8} more`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Drift analysis
|
|
284
|
-
const d = data.drift;
|
|
285
|
-
if (d.splitCandidates.length > 0 || d.mergeCandidates.length > 0) {
|
|
286
|
-
console.log(`\n# Drift Analysis (score: ${data.summary.driftScore}%)\n`);
|
|
287
|
-
|
|
288
|
-
if (d.splitCandidates.length > 0) {
|
|
289
|
-
console.log(' Split candidates (directories spanning multiple communities):');
|
|
290
|
-
for (const s of d.splitCandidates.slice(0, 10)) {
|
|
291
|
-
console.log(` - ${s.directory} → ${s.communityCount} communities`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (d.mergeCandidates.length > 0) {
|
|
296
|
-
console.log(' Merge candidates (communities spanning multiple directories):');
|
|
297
|
-
for (const m of d.mergeCandidates.slice(0, 10)) {
|
|
298
|
-
console.log(
|
|
299
|
-
` - Community ${m.communityId} (${m.size} members) → ${m.directoryCount} dirs: ${m.directories.join(', ')}`,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
console.log();
|
|
306
|
-
}
|