@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.
Files changed (83) hide show
  1. package/README.md +5 -5
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +8 -9
  4. package/src/ast-analysis/engine.js +365 -0
  5. package/src/ast-analysis/metrics.js +118 -0
  6. package/src/ast-analysis/rules/csharp.js +201 -0
  7. package/src/ast-analysis/rules/go.js +182 -0
  8. package/src/ast-analysis/rules/index.js +82 -0
  9. package/src/ast-analysis/rules/java.js +175 -0
  10. package/src/ast-analysis/rules/javascript.js +246 -0
  11. package/src/ast-analysis/rules/php.js +219 -0
  12. package/src/ast-analysis/rules/python.js +196 -0
  13. package/src/ast-analysis/rules/ruby.js +204 -0
  14. package/src/ast-analysis/rules/rust.js +173 -0
  15. package/src/ast-analysis/shared.js +223 -0
  16. package/src/ast-analysis/visitor-utils.js +176 -0
  17. package/src/ast-analysis/visitor.js +162 -0
  18. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  19. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  20. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  21. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  22. package/src/ast.js +26 -166
  23. package/src/audit.js +2 -88
  24. package/src/batch.js +0 -25
  25. package/src/boundaries.js +1 -1
  26. package/src/branch-compare.js +82 -172
  27. package/src/builder.js +48 -184
  28. package/src/cfg.js +148 -1174
  29. package/src/check.js +1 -84
  30. package/src/cli.js +118 -197
  31. package/src/cochange.js +1 -39
  32. package/src/commands/audit.js +88 -0
  33. package/src/commands/batch.js +26 -0
  34. package/src/commands/branch-compare.js +97 -0
  35. package/src/commands/cfg.js +55 -0
  36. package/src/commands/check.js +82 -0
  37. package/src/commands/cochange.js +37 -0
  38. package/src/commands/communities.js +69 -0
  39. package/src/commands/complexity.js +77 -0
  40. package/src/commands/dataflow.js +110 -0
  41. package/src/commands/flow.js +70 -0
  42. package/src/commands/manifesto.js +77 -0
  43. package/src/commands/owners.js +52 -0
  44. package/src/commands/query.js +21 -0
  45. package/src/commands/sequence.js +33 -0
  46. package/src/commands/structure.js +64 -0
  47. package/src/commands/triage.js +49 -0
  48. package/src/communities.js +22 -96
  49. package/src/complexity.js +234 -1591
  50. package/src/cycles.js +1 -1
  51. package/src/dataflow.js +274 -1352
  52. package/src/db/connection.js +88 -0
  53. package/src/db/migrations.js +312 -0
  54. package/src/db/query-builder.js +280 -0
  55. package/src/db/repository/build-stmts.js +104 -0
  56. package/src/db/repository/cfg.js +83 -0
  57. package/src/db/repository/cochange.js +41 -0
  58. package/src/db/repository/complexity.js +15 -0
  59. package/src/db/repository/dataflow.js +12 -0
  60. package/src/db/repository/edges.js +259 -0
  61. package/src/db/repository/embeddings.js +40 -0
  62. package/src/db/repository/graph-read.js +39 -0
  63. package/src/db/repository/index.js +42 -0
  64. package/src/db/repository/nodes.js +236 -0
  65. package/src/db.js +58 -399
  66. package/src/embedder.js +158 -174
  67. package/src/export.js +1 -1
  68. package/src/extractors/javascript.js +130 -5
  69. package/src/flow.js +153 -222
  70. package/src/index.js +53 -16
  71. package/src/infrastructure/result-formatter.js +21 -0
  72. package/src/infrastructure/test-filter.js +7 -0
  73. package/src/kinds.js +50 -0
  74. package/src/manifesto.js +1 -82
  75. package/src/mcp.js +37 -20
  76. package/src/owners.js +127 -182
  77. package/src/queries-cli.js +866 -0
  78. package/src/queries.js +1271 -2416
  79. package/src/sequence.js +179 -223
  80. package/src/structure.js +211 -269
  81. package/src/triage.js +117 -212
  82. package/src/viewer.js +1 -1
  83. 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
+ }
@@ -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 { openReadonlyOrFail } from './db.js';
5
- import { paginateResult, printNdjson } from './paginate.js';
6
- import { isTestFile } from './queries.js';
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.prepare("SELECT source_id, target_id FROM edges WHERE kind = 'calls'").all();
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.prepare("SELECT id, name, file FROM nodes WHERE kind = 'file'").all();
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
- const graph = buildGraphologyGraph(db, {
101
- functions: opts.functions,
102
- noTests: opts.noTests,
103
- });
104
- db.close();
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
- }