@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,88 @@
1
+ import { auditData } from '../audit.js';
2
+ import { outputResult } from '../infrastructure/result-formatter.js';
3
+ import { kindIcon } from '../queries.js';
4
+
5
+ /**
6
+ * CLI formatter for the audit command.
7
+ */
8
+ export function audit(target, customDbPath, opts = {}) {
9
+ const data = auditData(target, customDbPath, opts);
10
+
11
+ if (outputResult(data, null, opts)) return;
12
+
13
+ if (data.functions.length === 0) {
14
+ console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
15
+ return;
16
+ }
17
+
18
+ console.log(`\n# Audit: ${target} (${data.kind})`);
19
+ console.log(` ${data.functions.length} function(s) analyzed\n`);
20
+
21
+ for (const fn of data.functions) {
22
+ const lineRange = fn.endLine ? `${fn.line}-${fn.endLine}` : `${fn.line}`;
23
+ const roleTag = fn.role ? ` [${fn.role}]` : '';
24
+ console.log(`## ${kindIcon(fn.kind)} ${fn.name} (${fn.kind})${roleTag}`);
25
+ console.log(` ${fn.file}:${lineRange}${fn.lineCount ? ` (${fn.lineCount} lines)` : ''}`);
26
+ if (fn.summary) console.log(` ${fn.summary}`);
27
+ if (fn.signature) {
28
+ if (fn.signature.params != null) console.log(` Parameters: (${fn.signature.params})`);
29
+ if (fn.signature.returnType) console.log(` Returns: ${fn.signature.returnType}`);
30
+ }
31
+
32
+ // Health metrics
33
+ if (fn.health.cognitive != null) {
34
+ console.log(`\n Health:`);
35
+ console.log(
36
+ ` Cognitive: ${fn.health.cognitive} Cyclomatic: ${fn.health.cyclomatic} Nesting: ${fn.health.maxNesting}`,
37
+ );
38
+ console.log(` MI: ${fn.health.maintainabilityIndex}`);
39
+ if (fn.health.halstead.volume) {
40
+ console.log(
41
+ ` Halstead: vol=${fn.health.halstead.volume} diff=${fn.health.halstead.difficulty} effort=${fn.health.halstead.effort} bugs=${fn.health.halstead.bugs}`,
42
+ );
43
+ }
44
+ if (fn.health.loc) {
45
+ console.log(
46
+ ` LOC: ${fn.health.loc} SLOC: ${fn.health.sloc} Comments: ${fn.health.commentLines}`,
47
+ );
48
+ }
49
+ }
50
+
51
+ // Threshold breaches
52
+ if (fn.health.thresholdBreaches.length > 0) {
53
+ console.log(`\n Threshold Breaches:`);
54
+ for (const b of fn.health.thresholdBreaches) {
55
+ const icon = b.level === 'fail' ? 'FAIL' : 'WARN';
56
+ console.log(` [${icon}] ${b.metric}: ${b.value} >= ${b.threshold}`);
57
+ }
58
+ }
59
+
60
+ // Impact
61
+ console.log(`\n Impact: ${fn.impact.totalDependents} transitive dependent(s)`);
62
+ for (const [level, nodes] of Object.entries(fn.impact.levels)) {
63
+ console.log(` Level ${level}: ${nodes.map((n) => n.name).join(', ')}`);
64
+ }
65
+
66
+ // Call edges
67
+ if (fn.callees.length > 0) {
68
+ console.log(`\n Calls (${fn.callees.length}):`);
69
+ for (const c of fn.callees) {
70
+ console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
71
+ }
72
+ }
73
+ if (fn.callers.length > 0) {
74
+ console.log(`\n Called by (${fn.callers.length}):`);
75
+ for (const c of fn.callers) {
76
+ console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
77
+ }
78
+ }
79
+ if (fn.relatedTests.length > 0) {
80
+ console.log(`\n Tests (${fn.relatedTests.length}):`);
81
+ for (const t of fn.relatedTests) {
82
+ console.log(` ${t.file}`);
83
+ }
84
+ }
85
+
86
+ console.log();
87
+ }
88
+ }
@@ -0,0 +1,26 @@
1
+ import { batchData, multiBatchData } from '../batch.js';
2
+
3
+ /**
4
+ * CLI wrapper — calls batchData and prints JSON to stdout.
5
+ */
6
+ export function batch(command, targets, customDbPath, opts = {}) {
7
+ const data = batchData(command, targets, customDbPath, opts);
8
+ console.log(JSON.stringify(data, null, 2));
9
+ }
10
+
11
+ /**
12
+ * CLI wrapper for batch-query — detects multi-command mode (objects with .command)
13
+ * or falls back to single-command batchData (default: 'where').
14
+ */
15
+ export function batchQuery(targets, customDbPath, opts = {}) {
16
+ const { command: defaultCommand = 'where', ...rest } = opts;
17
+ const isMulti = targets.length > 0 && typeof targets[0] === 'object' && targets[0].command;
18
+
19
+ let data;
20
+ if (isMulti) {
21
+ data = multiBatchData(targets, customDbPath, rest);
22
+ } else {
23
+ data = batchData(defaultCommand, targets, customDbPath, rest);
24
+ }
25
+ console.log(JSON.stringify(data, null, 2));
26
+ }
@@ -0,0 +1,97 @@
1
+ import { branchCompareData, branchCompareMermaid } from '../branch-compare.js';
2
+ import { outputResult } from '../infrastructure/result-formatter.js';
3
+ import { kindIcon } from '../queries.js';
4
+
5
+ // ─── Text Formatting ────────────────────────────────────────────────────
6
+
7
+ function formatText(data) {
8
+ if (data.error) return `Error: ${data.error}`;
9
+
10
+ const lines = [];
11
+ const shortBase = data.baseSha.slice(0, 7);
12
+ const shortTarget = data.targetSha.slice(0, 7);
13
+
14
+ lines.push(`branch-compare: ${data.baseRef}..${data.targetRef}`);
15
+ lines.push(` Base: ${data.baseRef} (${shortBase})`);
16
+ lines.push(` Target: ${data.targetRef} (${shortTarget})`);
17
+ lines.push(` Files changed: ${data.changedFiles.length}`);
18
+
19
+ if (data.added.length > 0) {
20
+ lines.push('');
21
+ lines.push(` + Added (${data.added.length} symbol${data.added.length !== 1 ? 's' : ''}):`);
22
+ for (const sym of data.added) {
23
+ lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
24
+ }
25
+ }
26
+
27
+ if (data.removed.length > 0) {
28
+ lines.push('');
29
+ lines.push(
30
+ ` - Removed (${data.removed.length} symbol${data.removed.length !== 1 ? 's' : ''}):`,
31
+ );
32
+ for (const sym of data.removed) {
33
+ lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
34
+ if (sym.impact && sym.impact.length > 0) {
35
+ lines.push(
36
+ ` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
37
+ );
38
+ }
39
+ }
40
+ }
41
+
42
+ if (data.changed.length > 0) {
43
+ lines.push('');
44
+ lines.push(
45
+ ` ~ Changed (${data.changed.length} symbol${data.changed.length !== 1 ? 's' : ''}):`,
46
+ );
47
+ for (const sym of data.changed) {
48
+ const parts = [];
49
+ if (sym.changes.lineCount !== 0) {
50
+ parts.push(`lines: ${sym.base.lineCount} -> ${sym.target.lineCount}`);
51
+ }
52
+ if (sym.changes.fanIn !== 0) {
53
+ parts.push(`fan_in: ${sym.base.fanIn} -> ${sym.target.fanIn}`);
54
+ }
55
+ if (sym.changes.fanOut !== 0) {
56
+ parts.push(`fan_out: ${sym.base.fanOut} -> ${sym.target.fanOut}`);
57
+ }
58
+ const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
59
+ lines.push(
60
+ ` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.base.line}${detail}`,
61
+ );
62
+ if (sym.impact && sym.impact.length > 0) {
63
+ lines.push(
64
+ ` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
65
+ );
66
+ }
67
+ }
68
+ }
69
+
70
+ const s = data.summary;
71
+ lines.push('');
72
+ lines.push(
73
+ ` Summary: +${s.added} added, -${s.removed} removed, ~${s.changed} changed` +
74
+ ` -> ${s.totalImpacted} caller${s.totalImpacted !== 1 ? 's' : ''} impacted` +
75
+ (s.filesAffected > 0
76
+ ? ` across ${s.filesAffected} file${s.filesAffected !== 1 ? 's' : ''}`
77
+ : ''),
78
+ );
79
+
80
+ return lines.join('\n');
81
+ }
82
+
83
+ // ─── CLI Display Function ───────────────────────────────────────────────
84
+
85
+ export async function branchCompare(baseRef, targetRef, opts = {}) {
86
+ const data = await branchCompareData(baseRef, targetRef, opts);
87
+
88
+ if (opts.format === 'json') opts = { ...opts, json: true };
89
+ if (outputResult(data, null, opts)) return;
90
+
91
+ if (opts.format === 'mermaid') {
92
+ console.log(branchCompareMermaid(data));
93
+ return;
94
+ }
95
+
96
+ console.log(formatText(data));
97
+ }
@@ -0,0 +1,55 @@
1
+ import { cfgData, cfgToDOT, cfgToMermaid } from '../cfg.js';
2
+ import { outputResult } from '../infrastructure/result-formatter.js';
3
+
4
+ /**
5
+ * CLI display for cfg command.
6
+ */
7
+ export function cfg(name, customDbPath, opts = {}) {
8
+ const data = cfgData(name, customDbPath, opts);
9
+
10
+ if (outputResult(data, 'results', opts)) return;
11
+
12
+ if (data.warning) {
13
+ console.log(`\u26A0 ${data.warning}`);
14
+ return;
15
+ }
16
+ if (data.results.length === 0) {
17
+ console.log(`No symbols matching "${name}".`);
18
+ return;
19
+ }
20
+
21
+ const format = opts.format || 'text';
22
+ if (format === 'dot') {
23
+ console.log(cfgToDOT(data));
24
+ return;
25
+ }
26
+ if (format === 'mermaid') {
27
+ console.log(cfgToMermaid(data));
28
+ return;
29
+ }
30
+
31
+ // Text format
32
+ for (const r of data.results) {
33
+ console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
34
+ console.log('\u2500'.repeat(60));
35
+ console.log(` Blocks: ${r.summary.blockCount} Edges: ${r.summary.edgeCount}`);
36
+
37
+ if (r.blocks.length > 0) {
38
+ console.log('\n Blocks:');
39
+ for (const b of r.blocks) {
40
+ const loc = b.startLine
41
+ ? ` L${b.startLine}${b.endLine && b.endLine !== b.startLine ? `-${b.endLine}` : ''}`
42
+ : '';
43
+ const label = b.label ? ` (${b.label})` : '';
44
+ console.log(` [${b.index}] ${b.type}${label}${loc}`);
45
+ }
46
+ }
47
+
48
+ if (r.edges.length > 0) {
49
+ console.log('\n Edges:');
50
+ for (const e of r.edges) {
51
+ console.log(` B${e.source} \u2192 B${e.target} [${e.kind}]`);
52
+ }
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,82 @@
1
+ import { checkData } from '../check.js';
2
+ import { outputResult } from '../infrastructure/result-formatter.js';
3
+
4
+ /**
5
+ * CLI formatter — prints check results and exits with code 1 on failure.
6
+ */
7
+ export function check(customDbPath, opts = {}) {
8
+ const data = checkData(customDbPath, opts);
9
+
10
+ if (data.error) {
11
+ console.error(data.error);
12
+ process.exit(1);
13
+ }
14
+
15
+ if (outputResult(data, null, opts)) {
16
+ if (!data.passed) process.exit(1);
17
+ return;
18
+ }
19
+
20
+ console.log('\n# Check Results\n');
21
+
22
+ if (data.predicates.length === 0) {
23
+ console.log(' No changes detected.\n');
24
+ return;
25
+ }
26
+
27
+ console.log(
28
+ ` Changed files: ${data.summary.changedFiles} New files: ${data.summary.newFiles}\n`,
29
+ );
30
+
31
+ for (const pred of data.predicates) {
32
+ const icon = pred.passed ? 'PASS' : 'FAIL';
33
+ console.log(` [${icon}] ${pred.name}`);
34
+
35
+ if (!pred.passed) {
36
+ if (pred.name === 'cycles' && pred.cycles) {
37
+ for (const cycle of pred.cycles.slice(0, 10)) {
38
+ console.log(` ${cycle.join(' -> ')}`);
39
+ }
40
+ if (pred.cycles.length > 10) {
41
+ console.log(` ... and ${pred.cycles.length - 10} more`);
42
+ }
43
+ }
44
+ if (pred.name === 'blast-radius' && pred.violations) {
45
+ for (const v of pred.violations.slice(0, 10)) {
46
+ console.log(
47
+ ` ${v.name} (${v.kind}) at ${v.file}:${v.line} — ${v.transitiveCallers} callers (max: ${pred.threshold})`,
48
+ );
49
+ }
50
+ if (pred.violations.length > 10) {
51
+ console.log(` ... and ${pred.violations.length - 10} more`);
52
+ }
53
+ }
54
+ if (pred.name === 'signatures' && pred.violations) {
55
+ for (const v of pred.violations.slice(0, 10)) {
56
+ console.log(` ${v.name} (${v.kind}) at ${v.file}:${v.line}`);
57
+ }
58
+ if (pred.violations.length > 10) {
59
+ console.log(` ... and ${pred.violations.length - 10} more`);
60
+ }
61
+ }
62
+ if (pred.name === 'boundaries' && pred.violations) {
63
+ for (const v of pred.violations.slice(0, 10)) {
64
+ console.log(` ${v.from} -> ${v.to} (${v.edgeKind})`);
65
+ }
66
+ if (pred.violations.length > 10) {
67
+ console.log(` ... and ${pred.violations.length - 10} more`);
68
+ }
69
+ }
70
+ }
71
+ if (pred.note) {
72
+ console.log(` ${pred.note}`);
73
+ }
74
+ }
75
+
76
+ const s = data.summary;
77
+ console.log(`\n ${s.total} predicates | ${s.passed} passed | ${s.failed} failed\n`);
78
+
79
+ if (!data.passed) {
80
+ process.exit(1);
81
+ }
82
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Format co-change data for CLI output (single file).
3
+ */
4
+ export function formatCoChange(data) {
5
+ if (data.error) return data.error;
6
+ if (data.partners.length === 0) return `No co-change partners found for ${data.file}`;
7
+
8
+ const lines = [`\nCo-change partners for ${data.file}:\n`];
9
+ for (const p of data.partners) {
10
+ const pct = `${(p.jaccard * 100).toFixed(0)}%`.padStart(4);
11
+ const commits = `${p.commitCount} commits`.padStart(12);
12
+ lines.push(` ${pct} ${commits} ${p.file}`);
13
+ }
14
+ if (data.meta?.analyzedAt) {
15
+ lines.push(`\n Analyzed: ${data.meta.analyzedAt} | Window: ${data.meta.since || 'all'}`);
16
+ }
17
+ return lines.join('\n');
18
+ }
19
+
20
+ /**
21
+ * Format top co-change pairs for CLI output (global view).
22
+ */
23
+ export function formatCoChangeTop(data) {
24
+ if (data.error) return data.error;
25
+ if (data.pairs.length === 0) return 'No co-change pairs found.';
26
+
27
+ const lines = ['\nTop co-change pairs:\n'];
28
+ for (const p of data.pairs) {
29
+ const pct = `${(p.jaccard * 100).toFixed(0)}%`.padStart(4);
30
+ const commits = `${p.commitCount} commits`.padStart(12);
31
+ lines.push(` ${pct} ${commits} ${p.fileA} <-> ${p.fileB}`);
32
+ }
33
+ if (data.meta?.analyzedAt) {
34
+ lines.push(`\n Analyzed: ${data.meta.analyzedAt} | Window: ${data.meta.since || 'all'}`);
35
+ }
36
+ return lines.join('\n');
37
+ }
@@ -0,0 +1,69 @@
1
+ import { communitiesData } from '../communities.js';
2
+ import { outputResult } from '../infrastructure/result-formatter.js';
3
+
4
+ /**
5
+ * CLI entry point: run community detection and print results.
6
+ */
7
+ export function communities(customDbPath, opts = {}) {
8
+ const data = communitiesData(customDbPath, opts);
9
+
10
+ if (outputResult(data, 'communities', opts)) return;
11
+
12
+ if (data.summary.communityCount === 0) {
13
+ console.log(
14
+ '\nNo communities detected. The graph may be too small or disconnected.\n' +
15
+ 'Run "codegraph build" first to populate the graph.\n',
16
+ );
17
+ return;
18
+ }
19
+
20
+ const mode = opts.functions ? 'Function' : 'File';
21
+ console.log(`\n# ${mode}-Level Communities\n`);
22
+ console.log(
23
+ ` ${data.summary.communityCount} communities | ${data.summary.nodeCount} nodes | modularity: ${data.summary.modularity} | drift: ${data.summary.driftScore}%\n`,
24
+ );
25
+
26
+ if (!opts.drift) {
27
+ for (const c of data.communities) {
28
+ const dirs = Object.entries(c.directories)
29
+ .sort((a, b) => b[1] - a[1])
30
+ .map(([d, n]) => `${d} (${n})`)
31
+ .join(', ');
32
+ console.log(` Community ${c.id} (${c.size} members): ${dirs}`);
33
+ if (c.members) {
34
+ const shown = c.members.slice(0, 8);
35
+ for (const m of shown) {
36
+ const kind = m.kind ? ` [${m.kind}]` : '';
37
+ console.log(` - ${m.name}${kind} ${m.file}`);
38
+ }
39
+ if (c.members.length > 8) {
40
+ console.log(` ... and ${c.members.length - 8} more`);
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ // Drift analysis
47
+ const d = data.drift;
48
+ if (d.splitCandidates.length > 0 || d.mergeCandidates.length > 0) {
49
+ console.log(`\n# Drift Analysis (score: ${data.summary.driftScore}%)\n`);
50
+
51
+ if (d.splitCandidates.length > 0) {
52
+ console.log(' Split candidates (directories spanning multiple communities):');
53
+ for (const s of d.splitCandidates.slice(0, 10)) {
54
+ console.log(` - ${s.directory} → ${s.communityCount} communities`);
55
+ }
56
+ }
57
+
58
+ if (d.mergeCandidates.length > 0) {
59
+ console.log(' Merge candidates (communities spanning multiple directories):');
60
+ for (const m of d.mergeCandidates.slice(0, 10)) {
61
+ console.log(
62
+ ` - Community ${m.communityId} (${m.size} members) → ${m.directoryCount} dirs: ${m.directories.join(', ')}`,
63
+ );
64
+ }
65
+ }
66
+ }
67
+
68
+ console.log();
69
+ }
@@ -0,0 +1,77 @@
1
+ import { complexityData } from '../complexity.js';
2
+ import { outputResult } from '../infrastructure/result-formatter.js';
3
+
4
+ /**
5
+ * Format complexity output for CLI display.
6
+ */
7
+ export function complexity(customDbPath, opts = {}) {
8
+ const data = complexityData(customDbPath, opts);
9
+
10
+ if (outputResult(data, 'functions', opts)) return;
11
+
12
+ if (data.functions.length === 0) {
13
+ if (data.summary === null) {
14
+ if (data.hasGraph) {
15
+ console.log(
16
+ '\nNo complexity data found, but a graph exists. Run "codegraph build --no-incremental" to populate complexity metrics.\n',
17
+ );
18
+ } else {
19
+ console.log(
20
+ '\nNo complexity data found. Run "codegraph build" first to analyze your codebase.\n',
21
+ );
22
+ }
23
+ } else {
24
+ console.log('\nNo functions match the given filters.\n');
25
+ }
26
+ return;
27
+ }
28
+
29
+ const header = opts.aboveThreshold ? 'Functions Above Threshold' : 'Function Complexity';
30
+ console.log(`\n# ${header}\n`);
31
+
32
+ if (opts.health) {
33
+ // Health-focused view with Halstead + MI columns
34
+ console.log(
35
+ ` ${'Function'.padEnd(35)} ${'File'.padEnd(25)} ${'MI'.padStart(5)} ${'Vol'.padStart(7)} ${'Diff'.padStart(6)} ${'Effort'.padStart(9)} ${'Bugs'.padStart(6)} ${'LOC'.padStart(5)} ${'SLOC'.padStart(5)}`,
36
+ );
37
+ console.log(
38
+ ` ${'─'.repeat(35)} ${'─'.repeat(25)} ${'─'.repeat(5)} ${'─'.repeat(7)} ${'─'.repeat(6)} ${'─'.repeat(9)} ${'─'.repeat(6)} ${'─'.repeat(5)} ${'─'.repeat(5)}`,
39
+ );
40
+
41
+ for (const fn of data.functions) {
42
+ const name = fn.name.length > 33 ? `${fn.name.slice(0, 32)}…` : fn.name;
43
+ const file = fn.file.length > 23 ? `…${fn.file.slice(-22)}` : fn.file;
44
+ const miWarn = fn.exceeds?.includes('maintainabilityIndex') ? '!' : ' ';
45
+ console.log(
46
+ ` ${name.padEnd(35)} ${file.padEnd(25)} ${String(fn.maintainabilityIndex).padStart(5)}${miWarn}${String(fn.halstead.volume).padStart(7)} ${String(fn.halstead.difficulty).padStart(6)} ${String(fn.halstead.effort).padStart(9)} ${String(fn.halstead.bugs).padStart(6)} ${String(fn.loc).padStart(5)} ${String(fn.sloc).padStart(5)}`,
47
+ );
48
+ }
49
+ } else {
50
+ // Default view with MI column appended
51
+ console.log(
52
+ ` ${'Function'.padEnd(40)} ${'File'.padEnd(30)} ${'Cog'.padStart(4)} ${'Cyc'.padStart(4)} ${'Nest'.padStart(5)} ${'MI'.padStart(5)}`,
53
+ );
54
+ console.log(
55
+ ` ${'─'.repeat(40)} ${'─'.repeat(30)} ${'─'.repeat(4)} ${'─'.repeat(4)} ${'─'.repeat(5)} ${'─'.repeat(5)}`,
56
+ );
57
+
58
+ for (const fn of data.functions) {
59
+ const name = fn.name.length > 38 ? `${fn.name.slice(0, 37)}…` : fn.name;
60
+ const file = fn.file.length > 28 ? `…${fn.file.slice(-27)}` : fn.file;
61
+ const warn = fn.exceeds ? ' !' : '';
62
+ const mi = fn.maintainabilityIndex > 0 ? String(fn.maintainabilityIndex) : '-';
63
+ console.log(
64
+ ` ${name.padEnd(40)} ${file.padEnd(30)} ${String(fn.cognitive).padStart(4)} ${String(fn.cyclomatic).padStart(4)} ${String(fn.maxNesting).padStart(5)} ${mi.padStart(5)}${warn}`,
65
+ );
66
+ }
67
+ }
68
+
69
+ if (data.summary) {
70
+ const s = data.summary;
71
+ const miPart = s.avgMI != null ? ` | avg MI: ${s.avgMI}` : '';
72
+ console.log(
73
+ `\n ${s.analyzed} functions analyzed | avg cognitive: ${s.avgCognitive} | avg cyclomatic: ${s.avgCyclomatic}${miPart} | ${s.aboveWarn} above threshold`,
74
+ );
75
+ }
76
+ console.log();
77
+ }
@@ -0,0 +1,110 @@
1
+ import { dataflowData, dataflowImpactData } from '../dataflow.js';
2
+ import { outputResult } from '../infrastructure/result-formatter.js';
3
+
4
+ /**
5
+ * CLI display for dataflow command.
6
+ */
7
+ export function dataflow(name, customDbPath, opts = {}) {
8
+ if (opts.impact) {
9
+ return dataflowImpact(name, customDbPath, opts);
10
+ }
11
+
12
+ const data = dataflowData(name, customDbPath, opts);
13
+
14
+ if (outputResult(data, 'results', opts)) return;
15
+
16
+ if (data.warning) {
17
+ console.log(`⚠ ${data.warning}`);
18
+ return;
19
+ }
20
+ if (data.results.length === 0) {
21
+ console.log(`No symbols matching "${name}".`);
22
+ return;
23
+ }
24
+
25
+ for (const r of data.results) {
26
+ console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
27
+ console.log('─'.repeat(60));
28
+
29
+ if (r.flowsTo.length > 0) {
30
+ console.log('\n Data flows TO:');
31
+ for (const f of r.flowsTo) {
32
+ const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
33
+ console.log(` → ${f.target} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
34
+ }
35
+ }
36
+
37
+ if (r.flowsFrom.length > 0) {
38
+ console.log('\n Data flows FROM:');
39
+ for (const f of r.flowsFrom) {
40
+ const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
41
+ console.log(` ← ${f.source} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
42
+ }
43
+ }
44
+
45
+ if (r.returns.length > 0) {
46
+ console.log('\n Return value consumed by:');
47
+ for (const c of r.returns) {
48
+ console.log(` → ${c.consumer} (${c.file}:${c.line}) ${c.expression}`);
49
+ }
50
+ }
51
+
52
+ if (r.returnedBy.length > 0) {
53
+ console.log('\n Uses return value of:');
54
+ for (const p of r.returnedBy) {
55
+ console.log(` ← ${p.producer} (${p.file}:${p.line}) ${p.expression}`);
56
+ }
57
+ }
58
+
59
+ if (r.mutates.length > 0) {
60
+ console.log('\n Mutates:');
61
+ for (const m of r.mutates) {
62
+ console.log(` ✎ ${m.expression} (line ${m.line})`);
63
+ }
64
+ }
65
+
66
+ if (r.mutatedBy.length > 0) {
67
+ console.log('\n Mutated by:');
68
+ for (const m of r.mutatedBy) {
69
+ console.log(` ✎ ${m.source} — ${m.expression} (line ${m.line})`);
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * CLI display for dataflow --impact.
77
+ */
78
+ function dataflowImpact(name, customDbPath, opts = {}) {
79
+ const data = dataflowImpactData(name, customDbPath, {
80
+ noTests: opts.noTests,
81
+ depth: opts.depth ? Number(opts.depth) : 5,
82
+ file: opts.file,
83
+ kind: opts.kind,
84
+ limit: opts.limit,
85
+ offset: opts.offset,
86
+ });
87
+
88
+ if (outputResult(data, 'results', opts)) return;
89
+
90
+ if (data.warning) {
91
+ console.log(`⚠ ${data.warning}`);
92
+ return;
93
+ }
94
+ if (data.results.length === 0) {
95
+ console.log(`No symbols matching "${name}".`);
96
+ return;
97
+ }
98
+
99
+ for (const r of data.results) {
100
+ console.log(
101
+ `\n${r.kind} ${r.name} (${r.file}:${r.line}) — ${r.totalAffected} data-dependent consumer${r.totalAffected !== 1 ? 's' : ''}`,
102
+ );
103
+ for (const [level, items] of Object.entries(r.levels)) {
104
+ console.log(` Level ${level}:`);
105
+ for (const item of items) {
106
+ console.log(` ${item.name} (${item.file}:${item.line})`);
107
+ }
108
+ }
109
+ }
110
+ }