@optave/codegraph 3.1.0 → 3.1.2
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 +5 -5
- package/grammars/tree-sitter-go.wasm +0 -0
- package/package.json +8 -9
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- package/src/ast-analysis/rules/csharp.js +201 -0
- package/src/ast-analysis/rules/go.js +182 -0
- package/src/ast-analysis/rules/index.js +82 -0
- package/src/ast-analysis/rules/java.js +175 -0
- package/src/ast-analysis/rules/javascript.js +246 -0
- package/src/ast-analysis/rules/php.js +219 -0
- package/src/ast-analysis/rules/python.js +196 -0
- package/src/ast-analysis/rules/ruby.js +204 -0
- package/src/ast-analysis/rules/rust.js +173 -0
- package/src/ast-analysis/shared.js +223 -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 +26 -166
- package/src/audit.js +2 -88
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +82 -172
- package/src/builder.js +48 -184
- package/src/cfg.js +148 -1174
- package/src/check.js +1 -84
- package/src/cli.js +118 -197
- 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 +22 -96
- package/src/complexity.js +234 -1591
- package/src/cycles.js +1 -1
- package/src/dataflow.js +274 -1352
- package/src/db/connection.js +88 -0
- package/src/db/migrations.js +312 -0
- package/src/db/query-builder.js +280 -0
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/cfg.js +83 -0
- package/src/db/repository/cochange.js +41 -0
- package/src/db/repository/complexity.js +15 -0
- package/src/db/repository/dataflow.js +12 -0
- package/src/db/repository/edges.js +259 -0
- package/src/db/repository/embeddings.js +40 -0
- package/src/db/repository/graph-read.js +39 -0
- package/src/db/repository/index.js +42 -0
- package/src/db/repository/nodes.js +236 -0
- package/src/db.js +58 -399
- package/src/embedder.js +158 -174
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +153 -222
- package/src/index.js +53 -16
- package/src/infrastructure/result-formatter.js +21 -0
- package/src/infrastructure/test-filter.js +7 -0
- package/src/kinds.js +50 -0
- package/src/manifesto.js +1 -82
- package/src/mcp.js +37 -20
- package/src/owners.js +127 -182
- package/src/queries-cli.js +866 -0
- package/src/queries.js +1271 -2416
- package/src/sequence.js +179 -223
- package/src/structure.js +211 -269
- package/src/triage.js +117 -212
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { flowData, listEntryPointsData } from '../flow.js';
|
|
2
|
+
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
3
|
+
import { kindIcon } from '../queries.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CLI formatter — text or JSON output.
|
|
7
|
+
*/
|
|
8
|
+
export function flow(name, dbPath, opts = {}) {
|
|
9
|
+
if (opts.list) {
|
|
10
|
+
const data = listEntryPointsData(dbPath, {
|
|
11
|
+
noTests: opts.noTests,
|
|
12
|
+
limit: opts.limit,
|
|
13
|
+
offset: opts.offset,
|
|
14
|
+
});
|
|
15
|
+
if (outputResult(data, 'entries', opts)) return;
|
|
16
|
+
if (data.count === 0) {
|
|
17
|
+
console.log('No entry points found. Run "codegraph build" first.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log(`\nEntry points (${data.count} total):\n`);
|
|
21
|
+
for (const [type, entries] of Object.entries(data.byType)) {
|
|
22
|
+
console.log(` ${type} (${entries.length}):`);
|
|
23
|
+
for (const e of entries) {
|
|
24
|
+
console.log(` [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
|
|
25
|
+
}
|
|
26
|
+
console.log();
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const data = flowData(name, dbPath, opts);
|
|
32
|
+
if (outputResult(data, 'steps', opts)) return;
|
|
33
|
+
|
|
34
|
+
if (!data.entry) {
|
|
35
|
+
console.log(`No matching entry point or function found for "${name}".`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const e = data.entry;
|
|
40
|
+
const typeTag = e.type !== 'exported' ? ` (${e.type})` : '';
|
|
41
|
+
console.log(`\nFlow from: [${kindIcon(e.kind)}] ${e.name}${typeTag} ${e.file}:${e.line}`);
|
|
42
|
+
console.log(
|
|
43
|
+
`Depth: ${data.depth} Reached: ${data.totalReached} nodes Leaves: ${data.leaves.length}`,
|
|
44
|
+
);
|
|
45
|
+
if (data.truncated) {
|
|
46
|
+
console.log(` (truncated at depth ${data.depth})`);
|
|
47
|
+
}
|
|
48
|
+
console.log();
|
|
49
|
+
|
|
50
|
+
if (data.steps.length === 0) {
|
|
51
|
+
console.log(' (leaf node — no callees)');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const step of data.steps) {
|
|
56
|
+
console.log(` depth ${step.depth}:`);
|
|
57
|
+
for (const n of step.nodes) {
|
|
58
|
+
const isLeaf = data.leaves.some((l) => l.name === n.name && l.file === n.file);
|
|
59
|
+
const leafTag = isLeaf ? ' [leaf]' : '';
|
|
60
|
+
console.log(` [${kindIcon(n.kind)}] ${n.name} ${n.file}:${n.line}${leafTag}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (data.cycles.length > 0) {
|
|
65
|
+
console.log('\n Cycles detected:');
|
|
66
|
+
for (const c of data.cycles) {
|
|
67
|
+
console.log(` ${c.from} -> ${c.to} (at depth ${c.depth})`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
2
|
+
import { manifestoData } from '../manifesto.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CLI formatter — prints manifesto results and exits with code 1 on failure.
|
|
6
|
+
*/
|
|
7
|
+
export function manifesto(customDbPath, opts = {}) {
|
|
8
|
+
const data = manifestoData(customDbPath, opts);
|
|
9
|
+
|
|
10
|
+
if (outputResult(data, 'violations', opts)) {
|
|
11
|
+
if (!data.passed) process.exit(1);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.log('\n# Manifesto Rules\n');
|
|
16
|
+
|
|
17
|
+
// Rules table
|
|
18
|
+
console.log(
|
|
19
|
+
` ${'Rule'.padEnd(20)} ${'Level'.padEnd(10)} ${'Status'.padEnd(8)} ${'Warn'.padStart(6)} ${'Fail'.padStart(6)} ${'Violations'.padStart(11)}`,
|
|
20
|
+
);
|
|
21
|
+
console.log(
|
|
22
|
+
` ${'─'.repeat(20)} ${'─'.repeat(10)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(6)} ${'─'.repeat(11)}`,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
for (const rule of data.rules) {
|
|
26
|
+
const warn = rule.thresholds.warn != null ? String(rule.thresholds.warn) : '—';
|
|
27
|
+
const fail = rule.thresholds.fail != null ? String(rule.thresholds.fail) : '—';
|
|
28
|
+
const statusIcon = rule.status === 'pass' ? 'pass' : rule.status === 'warn' ? 'WARN' : 'FAIL';
|
|
29
|
+
console.log(
|
|
30
|
+
` ${rule.name.padEnd(20)} ${rule.level.padEnd(10)} ${statusIcon.padEnd(8)} ${warn.padStart(6)} ${fail.padStart(6)} ${String(rule.violationCount).padStart(11)}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Summary
|
|
35
|
+
const s = data.summary;
|
|
36
|
+
console.log(
|
|
37
|
+
`\n ${s.total} rules | ${s.passed} passed | ${s.warned} warned | ${s.failed} failed | ${s.violationCount} violations`,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Violations detail
|
|
41
|
+
if (data.violations.length > 0) {
|
|
42
|
+
const failViolations = data.violations.filter((v) => v.level === 'fail');
|
|
43
|
+
const warnViolations = data.violations.filter((v) => v.level === 'warn');
|
|
44
|
+
|
|
45
|
+
if (failViolations.length > 0) {
|
|
46
|
+
console.log(`\n## Failures (${failViolations.length})\n`);
|
|
47
|
+
for (const v of failViolations.slice(0, 20)) {
|
|
48
|
+
const loc = v.line ? `${v.file}:${v.line}` : v.file;
|
|
49
|
+
console.log(
|
|
50
|
+
` [FAIL] ${v.rule}: ${v.name} (${v.value}) at ${loc} — threshold ${v.threshold}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (failViolations.length > 20) {
|
|
54
|
+
console.log(` ... and ${failViolations.length - 20} more`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (warnViolations.length > 0) {
|
|
59
|
+
console.log(`\n## Warnings (${warnViolations.length})\n`);
|
|
60
|
+
for (const v of warnViolations.slice(0, 20)) {
|
|
61
|
+
const loc = v.line ? `${v.file}:${v.line}` : v.file;
|
|
62
|
+
console.log(
|
|
63
|
+
` [WARN] ${v.rule}: ${v.name} (${v.value}) at ${loc} — threshold ${v.threshold}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (warnViolations.length > 20) {
|
|
67
|
+
console.log(` ... and ${warnViolations.length - 20} more`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log();
|
|
73
|
+
|
|
74
|
+
if (!data.passed) {
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
2
|
+
import { ownersData } from '../owners.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CLI display function for the `owners` command.
|
|
6
|
+
*/
|
|
7
|
+
export function owners(customDbPath, opts = {}) {
|
|
8
|
+
const data = ownersData(customDbPath, opts);
|
|
9
|
+
if (outputResult(data, null, opts)) return;
|
|
10
|
+
|
|
11
|
+
if (!data.codeownersFile) {
|
|
12
|
+
console.log('No CODEOWNERS file found.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log(`\nCODEOWNERS: ${data.codeownersFile}\n`);
|
|
17
|
+
|
|
18
|
+
const s = data.summary;
|
|
19
|
+
console.log(
|
|
20
|
+
` Coverage: ${s.coveragePercent}% (${s.ownedFiles}/${s.totalFiles} files owned, ${s.ownerCount} owners)\n`,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (s.byOwner.length > 0) {
|
|
24
|
+
console.log(' Owners:\n');
|
|
25
|
+
for (const o of s.byOwner) {
|
|
26
|
+
console.log(` ${o.owner} ${o.fileCount} files`);
|
|
27
|
+
}
|
|
28
|
+
console.log();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (data.files.length > 0 && opts.owner) {
|
|
32
|
+
console.log(` Files owned by ${opts.owner}:\n`);
|
|
33
|
+
for (const f of data.files) {
|
|
34
|
+
console.log(` ${f.file}`);
|
|
35
|
+
}
|
|
36
|
+
console.log();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (data.boundaries.length > 0) {
|
|
40
|
+
console.log(` Cross-owner boundaries: ${data.boundaries.length} edges\n`);
|
|
41
|
+
const shown = data.boundaries.slice(0, 30);
|
|
42
|
+
for (const b of shown) {
|
|
43
|
+
const srcOwner = b.from.owners.join(', ') || '(unowned)';
|
|
44
|
+
const tgtOwner = b.to.owners.join(', ') || '(unowned)';
|
|
45
|
+
console.log(` ${b.from.name} [${srcOwner}] -> ${b.to.name} [${tgtOwner}]`);
|
|
46
|
+
}
|
|
47
|
+
if (data.boundaries.length > 30) {
|
|
48
|
+
console.log(` ... and ${data.boundaries.length - 30} more`);
|
|
49
|
+
}
|
|
50
|
+
console.log();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export all query CLI wrappers from queries-cli.js.
|
|
3
|
+
* This barrel file provides the standard src/commands/ import path.
|
|
4
|
+
*/
|
|
5
|
+
export {
|
|
6
|
+
children,
|
|
7
|
+
context,
|
|
8
|
+
diffImpact,
|
|
9
|
+
explain,
|
|
10
|
+
fileDeps,
|
|
11
|
+
fileExports,
|
|
12
|
+
fnDeps,
|
|
13
|
+
fnImpact,
|
|
14
|
+
impactAnalysis,
|
|
15
|
+
moduleMap,
|
|
16
|
+
queryName,
|
|
17
|
+
roles,
|
|
18
|
+
stats,
|
|
19
|
+
symbolPath,
|
|
20
|
+
where,
|
|
21
|
+
} from '../queries-cli.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
2
|
+
import { kindIcon } from '../queries.js';
|
|
3
|
+
import { sequenceData, sequenceToMermaid } from '../sequence.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CLI entry point — format sequence data as mermaid, JSON, or ndjson.
|
|
7
|
+
*/
|
|
8
|
+
export function sequence(name, dbPath, opts = {}) {
|
|
9
|
+
const data = sequenceData(name, dbPath, opts);
|
|
10
|
+
|
|
11
|
+
if (outputResult(data, 'messages', opts)) return;
|
|
12
|
+
|
|
13
|
+
// Default: mermaid format
|
|
14
|
+
if (!data.entry) {
|
|
15
|
+
console.log(`No matching function found for "${name}".`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const e = data.entry;
|
|
20
|
+
console.log(`\nSequence from: [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
|
|
21
|
+
console.log(`Participants: ${data.participants.length} Messages: ${data.totalMessages}`);
|
|
22
|
+
if (data.truncated) {
|
|
23
|
+
console.log(` (truncated at depth ${data.depth})`);
|
|
24
|
+
}
|
|
25
|
+
console.log();
|
|
26
|
+
|
|
27
|
+
if (data.messages.length === 0) {
|
|
28
|
+
console.log(' (leaf node — no callees)');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(sequenceToMermaid(data));
|
|
33
|
+
}
|
|
@@ -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,9 +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 {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
getCallableNodes,
|
|
6
|
+
getCallEdges,
|
|
7
|
+
getFileNodesAll,
|
|
8
|
+
getImportEdges,
|
|
9
|
+
openReadonlyOrFail,
|
|
10
|
+
} from './db.js';
|
|
11
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
12
|
+
import { paginateResult } from './paginate.js';
|
|
7
13
|
|
|
8
14
|
// ─── Graph Construction ───────────────────────────────────────────────
|
|
9
15
|
|
|
@@ -21,9 +27,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
21
27
|
|
|
22
28
|
if (opts.functions) {
|
|
23
29
|
// Function-level: nodes = function/method/class symbols, edges = calls
|
|
24
|
-
let nodes = db
|
|
25
|
-
.prepare("SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')")
|
|
26
|
-
.all();
|
|
30
|
+
let nodes = getCallableNodes(db);
|
|
27
31
|
if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
28
32
|
|
|
29
33
|
const nodeIds = new Set();
|
|
@@ -33,7 +37,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
33
37
|
nodeIds.add(n.id);
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
const edges = db
|
|
40
|
+
const edges = getCallEdges(db);
|
|
37
41
|
for (const e of edges) {
|
|
38
42
|
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
39
43
|
const src = String(e.source_id);
|
|
@@ -45,7 +49,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
45
49
|
}
|
|
46
50
|
} else {
|
|
47
51
|
// File-level: nodes = files, edges = imports + imports-type (deduplicated, cross-file)
|
|
48
|
-
let nodes = db
|
|
52
|
+
let nodes = getFileNodesAll(db);
|
|
49
53
|
if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
50
54
|
|
|
51
55
|
const nodeIds = new Set();
|
|
@@ -55,9 +59,7 @@ function buildGraphologyGraph(db, opts = {}) {
|
|
|
55
59
|
nodeIds.add(n.id);
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
const edges = db
|
|
59
|
-
.prepare("SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')")
|
|
60
|
-
.all();
|
|
62
|
+
const edges = getImportEdges(db);
|
|
61
63
|
for (const e of edges) {
|
|
62
64
|
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
63
65
|
const src = String(e.source_id);
|
|
@@ -96,12 +98,15 @@ function getDirectory(filePath) {
|
|
|
96
98
|
export function communitiesData(customDbPath, opts = {}) {
|
|
97
99
|
const db = openReadonlyOrFail(customDbPath);
|
|
98
100
|
const resolution = opts.resolution ?? 1.0;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
let graph;
|
|
102
|
+
try {
|
|
103
|
+
graph = buildGraphologyGraph(db, {
|
|
104
|
+
functions: opts.functions,
|
|
105
|
+
noTests: opts.noTests,
|
|
106
|
+
});
|
|
107
|
+
} finally {
|
|
108
|
+
db.close();
|
|
109
|
+
}
|
|
105
110
|
|
|
106
111
|
// Handle empty or trivial graphs
|
|
107
112
|
if (graph.order === 0 || graph.size === 0) {
|
|
@@ -228,82 +233,3 @@ export function communitySummaryForStats(customDbPath, opts = {}) {
|
|
|
228
233
|
const data = communitiesData(customDbPath, { ...opts, drift: true });
|
|
229
234
|
return data.summary;
|
|
230
235
|
}
|
|
231
|
-
|
|
232
|
-
// ─── CLI Display ──────────────────────────────────────────────────────
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* CLI entry point: run community detection and print results.
|
|
236
|
-
*
|
|
237
|
-
* @param {string} [customDbPath]
|
|
238
|
-
* @param {object} [opts]
|
|
239
|
-
*/
|
|
240
|
-
export function communities(customDbPath, opts = {}) {
|
|
241
|
-
const data = communitiesData(customDbPath, opts);
|
|
242
|
-
|
|
243
|
-
if (opts.ndjson) {
|
|
244
|
-
printNdjson(data, 'communities');
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
if (opts.json) {
|
|
248
|
-
console.log(JSON.stringify(data, null, 2));
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (data.summary.communityCount === 0) {
|
|
253
|
-
console.log(
|
|
254
|
-
'\nNo communities detected. The graph may be too small or disconnected.\n' +
|
|
255
|
-
'Run "codegraph build" first to populate the graph.\n',
|
|
256
|
-
);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const mode = opts.functions ? 'Function' : 'File';
|
|
261
|
-
console.log(`\n# ${mode}-Level Communities\n`);
|
|
262
|
-
console.log(
|
|
263
|
-
` ${data.summary.communityCount} communities | ${data.summary.nodeCount} nodes | modularity: ${data.summary.modularity} | drift: ${data.summary.driftScore}%\n`,
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
if (!opts.drift) {
|
|
267
|
-
for (const c of data.communities) {
|
|
268
|
-
const dirs = Object.entries(c.directories)
|
|
269
|
-
.sort((a, b) => b[1] - a[1])
|
|
270
|
-
.map(([d, n]) => `${d} (${n})`)
|
|
271
|
-
.join(', ');
|
|
272
|
-
console.log(` Community ${c.id} (${c.size} members): ${dirs}`);
|
|
273
|
-
if (c.members) {
|
|
274
|
-
const shown = c.members.slice(0, 8);
|
|
275
|
-
for (const m of shown) {
|
|
276
|
-
const kind = m.kind ? ` [${m.kind}]` : '';
|
|
277
|
-
console.log(` - ${m.name}${kind} ${m.file}`);
|
|
278
|
-
}
|
|
279
|
-
if (c.members.length > 8) {
|
|
280
|
-
console.log(` ... and ${c.members.length - 8} more`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Drift analysis
|
|
287
|
-
const d = data.drift;
|
|
288
|
-
if (d.splitCandidates.length > 0 || d.mergeCandidates.length > 0) {
|
|
289
|
-
console.log(`\n# Drift Analysis (score: ${data.summary.driftScore}%)\n`);
|
|
290
|
-
|
|
291
|
-
if (d.splitCandidates.length > 0) {
|
|
292
|
-
console.log(' Split candidates (directories spanning multiple communities):');
|
|
293
|
-
for (const s of d.splitCandidates.slice(0, 10)) {
|
|
294
|
-
console.log(` - ${s.directory} → ${s.communityCount} communities`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (d.mergeCandidates.length > 0) {
|
|
299
|
-
console.log(' Merge candidates (communities spanning multiple directories):');
|
|
300
|
-
for (const m of d.mergeCandidates.slice(0, 10)) {
|
|
301
|
-
console.log(
|
|
302
|
-
` - Community ${m.communityId} (${m.size} members) → ${m.directoryCount} dirs: ${m.directories.join(', ')}`,
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
console.log();
|
|
309
|
-
}
|