@optave/codegraph 3.1.5 → 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.
Files changed (91) hide show
  1. package/README.md +3 -2
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +252 -258
  4. package/src/ast-analysis/shared.js +0 -12
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +2 -1
  9. package/src/cli/commands/audit.js +2 -1
  10. package/src/cli/commands/batch.js +2 -1
  11. package/src/cli/commands/brief.js +12 -0
  12. package/src/cli/commands/cfg.js +2 -1
  13. package/src/cli/commands/check.js +20 -23
  14. package/src/cli/commands/children.js +6 -1
  15. package/src/cli/commands/complexity.js +2 -1
  16. package/src/cli/commands/context.js +6 -1
  17. package/src/cli/commands/dataflow.js +2 -1
  18. package/src/cli/commands/deps.js +8 -3
  19. package/src/cli/commands/flow.js +2 -1
  20. package/src/cli/commands/fn-impact.js +6 -1
  21. package/src/cli/commands/owners.js +4 -2
  22. package/src/cli/commands/query.js +6 -1
  23. package/src/cli/commands/roles.js +2 -1
  24. package/src/cli/commands/search.js +8 -2
  25. package/src/cli/commands/sequence.js +2 -1
  26. package/src/cli/commands/triage.js +38 -27
  27. package/src/db/connection.js +18 -12
  28. package/src/db/migrations.js +41 -64
  29. package/src/db/query-builder.js +60 -4
  30. package/src/db/repository/in-memory-repository.js +27 -16
  31. package/src/db/repository/nodes.js +8 -10
  32. package/src/domain/analysis/brief.js +155 -0
  33. package/src/domain/analysis/context.js +174 -190
  34. package/src/domain/analysis/dependencies.js +200 -146
  35. package/src/domain/analysis/exports.js +3 -2
  36. package/src/domain/analysis/impact.js +267 -152
  37. package/src/domain/analysis/module-map.js +247 -221
  38. package/src/domain/analysis/roles.js +8 -5
  39. package/src/domain/analysis/symbol-lookup.js +7 -5
  40. package/src/domain/graph/builder/helpers.js +1 -1
  41. package/src/domain/graph/builder/incremental.js +116 -90
  42. package/src/domain/graph/builder/pipeline.js +106 -80
  43. package/src/domain/graph/builder/stages/build-edges.js +318 -239
  44. package/src/domain/graph/builder/stages/detect-changes.js +198 -177
  45. package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
  46. package/src/domain/graph/watcher.js +2 -2
  47. package/src/domain/parser.js +20 -11
  48. package/src/domain/queries.js +1 -0
  49. package/src/domain/search/search/filters.js +9 -5
  50. package/src/domain/search/search/keyword.js +12 -5
  51. package/src/domain/search/search/prepare.js +13 -5
  52. package/src/extractors/csharp.js +224 -207
  53. package/src/extractors/go.js +176 -172
  54. package/src/extractors/hcl.js +94 -78
  55. package/src/extractors/java.js +213 -207
  56. package/src/extractors/javascript.js +274 -304
  57. package/src/extractors/php.js +234 -221
  58. package/src/extractors/python.js +252 -250
  59. package/src/extractors/ruby.js +192 -185
  60. package/src/extractors/rust.js +182 -167
  61. package/src/features/ast.js +5 -3
  62. package/src/features/audit.js +4 -2
  63. package/src/features/boundaries.js +98 -83
  64. package/src/features/cfg.js +134 -143
  65. package/src/features/communities.js +68 -53
  66. package/src/features/complexity.js +143 -132
  67. package/src/features/dataflow.js +146 -149
  68. package/src/features/export.js +3 -3
  69. package/src/features/graph-enrichment.js +2 -2
  70. package/src/features/manifesto.js +9 -6
  71. package/src/features/owners.js +4 -3
  72. package/src/features/sequence.js +152 -141
  73. package/src/features/shared/find-nodes.js +31 -0
  74. package/src/features/structure.js +130 -99
  75. package/src/features/triage.js +83 -68
  76. package/src/graph/classifiers/risk.js +3 -2
  77. package/src/graph/classifiers/roles.js +6 -3
  78. package/src/index.js +1 -0
  79. package/src/mcp/server.js +65 -56
  80. package/src/mcp/tool-registry.js +13 -0
  81. package/src/mcp/tools/brief.js +8 -0
  82. package/src/mcp/tools/index.js +2 -0
  83. package/src/presentation/brief.js +51 -0
  84. package/src/presentation/queries-cli/exports.js +21 -14
  85. package/src/presentation/queries-cli/impact.js +55 -39
  86. package/src/presentation/queries-cli/inspect.js +184 -189
  87. package/src/presentation/queries-cli/overview.js +57 -58
  88. package/src/presentation/queries-cli/path.js +36 -29
  89. package/src/presentation/table.js +0 -8
  90. package/src/shared/generators.js +7 -3
  91. package/src/shared/kinds.js +1 -1
@@ -96,96 +96,7 @@ export function context(name, customDbPath, opts = {}) {
96
96
  }
97
97
 
98
98
  for (const r of data.results) {
99
- const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
100
- const roleTag = r.role ? ` [${r.role}]` : '';
101
- console.log(`\n# ${r.name} (${r.kind})${roleTag} — ${r.file}:${lineRange}\n`);
102
-
103
- // Signature
104
- if (r.signature) {
105
- console.log('## Type/Shape Info');
106
- if (r.signature.params != null) console.log(` Parameters: (${r.signature.params})`);
107
- if (r.signature.returnType) console.log(` Returns: ${r.signature.returnType}`);
108
- console.log();
109
- }
110
-
111
- // Children
112
- if (r.children && r.children.length > 0) {
113
- console.log(`## Children (${r.children.length})`);
114
- for (const c of r.children) {
115
- console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
116
- }
117
- console.log();
118
- }
119
-
120
- // Complexity
121
- if (r.complexity) {
122
- const cx = r.complexity;
123
- const miPart = cx.maintainabilityIndex ? ` | MI: ${cx.maintainabilityIndex}` : '';
124
- console.log('## Complexity');
125
- console.log(
126
- ` Cognitive: ${cx.cognitive} | Cyclomatic: ${cx.cyclomatic} | Max Nesting: ${cx.maxNesting}${miPart}`,
127
- );
128
- console.log();
129
- }
130
-
131
- // Source
132
- if (r.source) {
133
- console.log('## Source');
134
- for (const line of r.source.split('\n')) {
135
- console.log(` ${line}`);
136
- }
137
- console.log();
138
- }
139
-
140
- // Callees
141
- if (r.callees.length > 0) {
142
- console.log(`## Direct Dependencies (${r.callees.length})`);
143
- for (const c of r.callees) {
144
- const summary = c.summary ? ` — ${c.summary}` : '';
145
- console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${summary}`);
146
- if (c.source) {
147
- for (const line of c.source.split('\n').slice(0, 10)) {
148
- console.log(` | ${line}`);
149
- }
150
- }
151
- }
152
- console.log();
153
- }
154
-
155
- // Callers
156
- if (r.callers.length > 0) {
157
- console.log(`## Callers (${r.callers.length})`);
158
- for (const c of r.callers) {
159
- const via = c.viaHierarchy ? ` (via ${c.viaHierarchy})` : '';
160
- console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${via}`);
161
- }
162
- console.log();
163
- }
164
-
165
- // Related tests
166
- if (r.relatedTests.length > 0) {
167
- console.log('## Related Tests');
168
- for (const t of r.relatedTests) {
169
- console.log(` ${t.file} — ${t.testCount} tests`);
170
- for (const tn of t.testNames) {
171
- console.log(` - ${tn}`);
172
- }
173
- if (t.source) {
174
- console.log(' Source:');
175
- for (const line of t.source.split('\n').slice(0, 20)) {
176
- console.log(` | ${line}`);
177
- }
178
- }
179
- }
180
- console.log();
181
- }
182
-
183
- if (r.callees.length === 0 && r.callers.length === 0 && r.relatedTests.length === 0) {
184
- console.log(
185
- ' (no call edges or tests found — may be invoked dynamically or via re-exports)',
186
- );
187
- console.log();
188
- }
99
+ renderContextResult(r);
189
100
  }
190
101
  }
191
102
 
@@ -209,126 +120,210 @@ export function children(name, customDbPath, opts = {}) {
209
120
  }
210
121
  }
211
122
 
212
- export function explain(target, customDbPath, opts = {}) {
213
- const data = explainData(target, customDbPath, opts);
214
- if (outputResult(data, 'results', opts)) return;
123
+ function renderContextResult(r) {
124
+ const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
125
+ const roleTag = r.role ? ` [${r.role}]` : '';
126
+ console.log(`\n# ${r.name} (${r.kind})${roleTag} — ${r.file}:${lineRange}\n`);
215
127
 
216
- if (data.results.length === 0) {
217
- console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
218
- return;
128
+ if (r.signature) {
129
+ console.log('## Type/Shape Info');
130
+ if (r.signature.params != null) console.log(` Parameters: (${r.signature.params})`);
131
+ if (r.signature.returnType) console.log(` Returns: ${r.signature.returnType}`);
132
+ console.log();
219
133
  }
220
134
 
221
- if (data.kind === 'file') {
222
- for (const r of data.results) {
223
- const publicCount = r.publicApi.length;
224
- const internalCount = r.internal.length;
225
- const lineInfo = r.lineCount ? `${r.lineCount} lines, ` : '';
226
- console.log(`\n# ${r.file}`);
227
- console.log(
228
- ` ${lineInfo}${r.symbolCount} symbols (${publicCount} exported, ${internalCount} internal)`,
229
- );
135
+ if (r.children && r.children.length > 0) {
136
+ console.log(`## Children (${r.children.length})`);
137
+ for (const c of r.children) {
138
+ console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
139
+ }
140
+ console.log();
141
+ }
230
142
 
231
- if (r.imports.length > 0) {
232
- console.log(` Imports: ${r.imports.map((i) => i.file).join(', ')}`);
233
- }
234
- if (r.importedBy.length > 0) {
235
- console.log(` Imported by: ${r.importedBy.map((i) => i.file).join(', ')}`);
236
- }
143
+ if (r.complexity) {
144
+ const cx = r.complexity;
145
+ const miPart = cx.maintainabilityIndex ? ` | MI: ${cx.maintainabilityIndex}` : '';
146
+ console.log('## Complexity');
147
+ console.log(
148
+ ` Cognitive: ${cx.cognitive} | Cyclomatic: ${cx.cyclomatic} | Max Nesting: ${cx.maxNesting}${miPart}`,
149
+ );
150
+ console.log();
151
+ }
237
152
 
238
- if (r.publicApi.length > 0) {
239
- console.log(`\n## Exported`);
240
- for (const s of r.publicApi) {
241
- const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
242
- const roleTag = s.role ? ` [${s.role}]` : '';
243
- const summary = s.summary ? ` -- ${s.summary}` : '';
244
- console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
245
- }
246
- }
153
+ if (r.source) {
154
+ console.log('## Source');
155
+ for (const line of r.source.split('\n')) {
156
+ console.log(` ${line}`);
157
+ }
158
+ console.log();
159
+ }
247
160
 
248
- if (r.internal.length > 0) {
249
- console.log(`\n## Internal`);
250
- for (const s of r.internal) {
251
- const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
252
- const roleTag = s.role ? ` [${s.role}]` : '';
253
- const summary = s.summary ? ` -- ${s.summary}` : '';
254
- console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
161
+ if (r.callees.length > 0) {
162
+ console.log(`## Direct Dependencies (${r.callees.length})`);
163
+ for (const c of r.callees) {
164
+ const summary = c.summary ? ` ${c.summary}` : '';
165
+ console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${summary}`);
166
+ if (c.source) {
167
+ for (const line of c.source.split('\n').slice(0, 10)) {
168
+ console.log(` | ${line}`);
255
169
  }
256
170
  }
171
+ }
172
+ console.log();
173
+ }
257
174
 
258
- if (r.dataFlow.length > 0) {
259
- console.log(`\n## Data Flow`);
260
- for (const df of r.dataFlow) {
261
- console.log(` ${df.caller} -> ${df.callees.join(', ')}`);
262
- }
263
- }
264
- console.log();
175
+ if (r.callers.length > 0) {
176
+ console.log(`## Callers (${r.callers.length})`);
177
+ for (const c of r.callers) {
178
+ const via = c.viaHierarchy ? ` (via ${c.viaHierarchy})` : '';
179
+ console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${via}`);
265
180
  }
266
- } else {
267
- function printFunctionExplain(r, indent = '') {
268
- const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
269
- const lineInfo = r.lineCount ? `${r.lineCount} lines` : '';
270
- const summaryPart = r.summary ? ` | ${r.summary}` : '';
271
- const roleTag = r.role ? ` [${r.role}]` : '';
272
- const depthLevel = r._depth || 0;
273
- const heading = depthLevel === 0 ? '#' : '##'.padEnd(depthLevel + 2, '#');
274
- console.log(`\n${indent}${heading} ${r.name} (${r.kind})${roleTag} ${r.file}:${lineRange}`);
275
- if (lineInfo || r.summary) {
276
- console.log(`${indent} ${lineInfo}${summaryPart}`);
277
- }
278
- if (r.signature) {
279
- if (r.signature.params != null)
280
- console.log(`${indent} Parameters: (${r.signature.params})`);
281
- if (r.signature.returnType) console.log(`${indent} Returns: ${r.signature.returnType}`);
282
- }
181
+ console.log();
182
+ }
283
183
 
284
- if (r.complexity) {
285
- const cx = r.complexity;
286
- const miPart = cx.maintainabilityIndex ? ` MI=${cx.maintainabilityIndex}` : '';
287
- console.log(
288
- `${indent} Complexity: cognitive=${cx.cognitive} cyclomatic=${cx.cyclomatic} nesting=${cx.maxNesting}${miPart}`,
289
- );
184
+ if (r.relatedTests.length > 0) {
185
+ console.log('## Related Tests');
186
+ for (const t of r.relatedTests) {
187
+ console.log(` ${t.file} — ${t.testCount} tests`);
188
+ for (const tn of t.testNames) {
189
+ console.log(` - ${tn}`);
290
190
  }
291
-
292
- if (r.callees.length > 0) {
293
- console.log(`\n${indent} Calls (${r.callees.length}):`);
294
- for (const c of r.callees) {
295
- console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
191
+ if (t.source) {
192
+ console.log(' Source:');
193
+ for (const line of t.source.split('\n').slice(0, 20)) {
194
+ console.log(` | ${line}`);
296
195
  }
297
196
  }
197
+ }
198
+ console.log();
199
+ }
298
200
 
299
- if (r.callers.length > 0) {
300
- console.log(`\n${indent} Called by (${r.callers.length}):`);
301
- for (const c of r.callers) {
302
- console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
303
- }
304
- }
201
+ if (r.callees.length === 0 && r.callers.length === 0 && r.relatedTests.length === 0) {
202
+ console.log(' (no call edges or tests found — may be invoked dynamically or via re-exports)');
203
+ console.log();
204
+ }
205
+ }
305
206
 
306
- if (r.relatedTests.length > 0) {
307
- const label = r.relatedTests.length === 1 ? 'file' : 'files';
308
- console.log(`\n${indent} Tests (${r.relatedTests.length} ${label}):`);
309
- for (const t of r.relatedTests) {
310
- console.log(`${indent} ${t.file}`);
311
- }
312
- }
207
+ function renderFileExplain(r) {
208
+ const publicCount = r.publicApi.length;
209
+ const internalCount = r.internal.length;
210
+ const lineInfo = r.lineCount ? `${r.lineCount} lines, ` : '';
211
+ console.log(`\n# ${r.file}`);
212
+ console.log(
213
+ ` ${lineInfo}${r.symbolCount} symbols (${publicCount} exported, ${internalCount} internal)`,
214
+ );
215
+
216
+ if (r.imports.length > 0) {
217
+ console.log(` Imports: ${r.imports.map((i) => i.file).join(', ')}`);
218
+ }
219
+ if (r.importedBy.length > 0) {
220
+ console.log(` Imported by: ${r.importedBy.map((i) => i.file).join(', ')}`);
221
+ }
313
222
 
314
- if (r.callees.length === 0 && r.callers.length === 0) {
315
- console.log(
316
- `${indent} (no call edges found -- may be invoked dynamically or via re-exports)`,
317
- );
318
- }
223
+ if (r.publicApi.length > 0) {
224
+ console.log(`\n## Exported`);
225
+ for (const s of r.publicApi) {
226
+ const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
227
+ const roleTag = s.role ? ` [${s.role}]` : '';
228
+ const summary = s.summary ? ` -- ${s.summary}` : '';
229
+ console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
230
+ }
231
+ }
319
232
 
320
- // Render recursive dependency details
321
- if (r.depDetails && r.depDetails.length > 0) {
322
- console.log(`\n${indent} --- Dependencies (depth ${depthLevel + 1}) ---`);
323
- for (const dep of r.depDetails) {
324
- printFunctionExplain(dep, `${indent} `);
325
- }
326
- }
327
- console.log();
233
+ if (r.internal.length > 0) {
234
+ console.log(`\n## Internal`);
235
+ for (const s of r.internal) {
236
+ const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
237
+ const roleTag = s.role ? ` [${s.role}]` : '';
238
+ const summary = s.summary ? ` -- ${s.summary}` : '';
239
+ console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
240
+ }
241
+ }
242
+
243
+ if (r.dataFlow.length > 0) {
244
+ console.log(`\n## Data Flow`);
245
+ for (const df of r.dataFlow) {
246
+ console.log(` ${df.caller} -> ${df.callees.join(', ')}`);
247
+ }
248
+ }
249
+ console.log();
250
+ }
251
+
252
+ function renderFunctionExplain(r, indent = '') {
253
+ const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
254
+ const lineInfo = r.lineCount ? `${r.lineCount} lines` : '';
255
+ const summaryPart = r.summary ? ` | ${r.summary}` : '';
256
+ const roleTag = r.role ? ` [${r.role}]` : '';
257
+ const depthLevel = r._depth || 0;
258
+ const heading = depthLevel === 0 ? '#' : '##'.padEnd(depthLevel + 2, '#');
259
+ console.log(`\n${indent}${heading} ${r.name} (${r.kind})${roleTag} ${r.file}:${lineRange}`);
260
+ if (lineInfo || r.summary) {
261
+ console.log(`${indent} ${lineInfo}${summaryPart}`);
262
+ }
263
+ if (r.signature) {
264
+ if (r.signature.params != null) console.log(`${indent} Parameters: (${r.signature.params})`);
265
+ if (r.signature.returnType) console.log(`${indent} Returns: ${r.signature.returnType}`);
266
+ }
267
+
268
+ if (r.complexity) {
269
+ const cx = r.complexity;
270
+ const miPart = cx.maintainabilityIndex ? ` MI=${cx.maintainabilityIndex}` : '';
271
+ console.log(
272
+ `${indent} Complexity: cognitive=${cx.cognitive} cyclomatic=${cx.cyclomatic} nesting=${cx.maxNesting}${miPart}`,
273
+ );
274
+ }
275
+
276
+ if (r.callees.length > 0) {
277
+ console.log(`\n${indent} Calls (${r.callees.length}):`);
278
+ for (const c of r.callees) {
279
+ console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
280
+ }
281
+ }
282
+
283
+ if (r.callers.length > 0) {
284
+ console.log(`\n${indent} Called by (${r.callers.length}):`);
285
+ for (const c of r.callers) {
286
+ console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
287
+ }
288
+ }
289
+
290
+ if (r.relatedTests.length > 0) {
291
+ const label = r.relatedTests.length === 1 ? 'file' : 'files';
292
+ console.log(`\n${indent} Tests (${r.relatedTests.length} ${label}):`);
293
+ for (const t of r.relatedTests) {
294
+ console.log(`${indent} ${t.file}`);
328
295
  }
296
+ }
297
+
298
+ if (r.callees.length === 0 && r.callers.length === 0) {
299
+ console.log(`${indent} (no call edges found -- may be invoked dynamically or via re-exports)`);
300
+ }
329
301
 
302
+ if (r.depDetails && r.depDetails.length > 0) {
303
+ console.log(`\n${indent} --- Dependencies (depth ${depthLevel + 1}) ---`);
304
+ for (const dep of r.depDetails) {
305
+ renderFunctionExplain(dep, `${indent} `);
306
+ }
307
+ }
308
+ console.log();
309
+ }
310
+
311
+ export function explain(target, customDbPath, opts = {}) {
312
+ const data = explainData(target, customDbPath, opts);
313
+ if (outputResult(data, 'results', opts)) return;
314
+
315
+ if (data.results.length === 0) {
316
+ console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
317
+ return;
318
+ }
319
+
320
+ if (data.kind === 'file') {
321
+ for (const r of data.results) {
322
+ renderFileExplain(r);
323
+ }
324
+ } else {
330
325
  for (const r of data.results) {
331
- printFunctionExplain(r);
326
+ renderFunctionExplain(r);
332
327
  }
333
328
  }
334
329
  }
@@ -2,64 +2,42 @@ import path from 'node:path';
2
2
  import { kindIcon, moduleMapData, rolesData, statsData } from '../../domain/queries.js';
3
3
  import { outputResult } from '../../infrastructure/result-formatter.js';
4
4
 
5
- export async function stats(customDbPath, opts = {}) {
6
- const data = statsData(customDbPath, { noTests: opts.noTests });
7
-
8
- // Community detection summary (async import for lazy-loading)
9
- try {
10
- const { communitySummaryForStats } = await import('../../features/communities.js');
11
- data.communities = communitySummaryForStats(customDbPath, { noTests: opts.noTests });
12
- } catch {
13
- /* graphology may not be available */
14
- }
15
-
16
- if (outputResult(data, null, opts)) return;
17
-
18
- // Human-readable output
19
- console.log('\n# Codegraph Stats\n');
20
-
21
- // Nodes
22
- console.log(`Nodes: ${data.nodes.total} total`);
23
- const kindEntries = Object.entries(data.nodes.byKind).sort((a, b) => b[1] - a[1]);
24
- const kindParts = kindEntries.map(([k, v]) => `${k} ${v}`);
25
- for (let i = 0; i < kindParts.length; i += 3) {
26
- const row = kindParts
5
+ function printCountGrid(entries, padWidth) {
6
+ const parts = entries.map(([k, v]) => `${k} ${v}`);
7
+ for (let i = 0; i < parts.length; i += 3) {
8
+ const row = parts
27
9
  .slice(i, i + 3)
28
- .map((p) => p.padEnd(18))
10
+ .map((p) => p.padEnd(padWidth))
29
11
  .join('');
30
12
  console.log(` ${row}`);
31
13
  }
14
+ }
32
15
 
33
- // Edges
16
+ function printNodes(data) {
17
+ console.log(`Nodes: ${data.nodes.total} total`);
18
+ const kindEntries = Object.entries(data.nodes.byKind).sort((a, b) => b[1] - a[1]);
19
+ printCountGrid(kindEntries, 18);
20
+ }
21
+
22
+ function printEdges(data) {
34
23
  console.log(`\nEdges: ${data.edges.total} total`);
35
24
  const edgeEntries = Object.entries(data.edges.byKind).sort((a, b) => b[1] - a[1]);
36
- const edgeParts = edgeEntries.map(([k, v]) => `${k} ${v}`);
37
- for (let i = 0; i < edgeParts.length; i += 3) {
38
- const row = edgeParts
39
- .slice(i, i + 3)
40
- .map((p) => p.padEnd(18))
41
- .join('');
42
- console.log(` ${row}`);
43
- }
25
+ printCountGrid(edgeEntries, 18);
26
+ }
44
27
 
45
- // Files
28
+ function printFiles(data) {
46
29
  console.log(`\nFiles: ${data.files.total} (${data.files.languages} languages)`);
47
30
  const langEntries = Object.entries(data.files.byLanguage).sort((a, b) => b[1] - a[1]);
48
- const langParts = langEntries.map(([k, v]) => `${k} ${v}`);
49
- for (let i = 0; i < langParts.length; i += 3) {
50
- const row = langParts
51
- .slice(i, i + 3)
52
- .map((p) => p.padEnd(18))
53
- .join('');
54
- console.log(` ${row}`);
55
- }
31
+ printCountGrid(langEntries, 18);
32
+ }
56
33
 
57
- // Cycles
34
+ function printCycles(data) {
58
35
  console.log(
59
36
  `\nCycles: ${data.cycles.fileLevel} file-level, ${data.cycles.functionLevel} function-level`,
60
37
  );
38
+ }
61
39
 
62
- // Hotspots
40
+ function printHotspots(data) {
63
41
  if (data.hotspots.length > 0) {
64
42
  console.log(`\nTop ${data.hotspots.length} coupling hotspots:`);
65
43
  for (let i = 0; i < data.hotspots.length; i++) {
@@ -69,8 +47,9 @@ export async function stats(customDbPath, opts = {}) {
69
47
  );
70
48
  }
71
49
  }
50
+ }
72
51
 
73
- // Embeddings
52
+ function printEmbeddings(data) {
74
53
  if (data.embeddings) {
75
54
  const e = data.embeddings;
76
55
  console.log(
@@ -79,8 +58,9 @@ export async function stats(customDbPath, opts = {}) {
79
58
  } else {
80
59
  console.log('\nEmbeddings: not built');
81
60
  }
61
+ }
82
62
 
83
- // Quality
63
+ function printQuality(data) {
84
64
  if (data.quality) {
85
65
  const q = data.quality;
86
66
  const cc = q.callerCoverage;
@@ -99,24 +79,18 @@ export async function stats(customDbPath, opts = {}) {
99
79
  }
100
80
  }
101
81
  }
82
+ }
102
83
 
103
- // Roles
84
+ function printRoles(data) {
104
85
  if (data.roles && Object.keys(data.roles).length > 0) {
105
86
  const total = Object.values(data.roles).reduce((a, b) => a + b, 0);
106
87
  console.log(`\nRoles: ${total} classified symbols`);
107
- const roleParts = Object.entries(data.roles)
108
- .sort((a, b) => b[1] - a[1])
109
- .map(([k, v]) => `${k} ${v}`);
110
- for (let i = 0; i < roleParts.length; i += 3) {
111
- const row = roleParts
112
- .slice(i, i + 3)
113
- .map((p) => p.padEnd(18))
114
- .join('');
115
- console.log(` ${row}`);
116
- }
88
+ const roleEntries = Object.entries(data.roles).sort((a, b) => b[1] - a[1]);
89
+ printCountGrid(roleEntries, 18);
117
90
  }
91
+ }
118
92
 
119
- // Complexity
93
+ function printComplexity(data) {
120
94
  if (data.complexity) {
121
95
  const cx = data.complexity;
122
96
  const miPart = cx.avgMI != null ? ` | avg MI: ${cx.avgMI} | min MI: ${cx.minMI}` : '';
@@ -124,15 +98,40 @@ export async function stats(customDbPath, opts = {}) {
124
98
  `\nComplexity: ${cx.analyzed} functions | avg cognitive: ${cx.avgCognitive} | avg cyclomatic: ${cx.avgCyclomatic} | max cognitive: ${cx.maxCognitive}${miPart}`,
125
99
  );
126
100
  }
101
+ }
127
102
 
128
- // Communities
103
+ function printCommunities(data) {
129
104
  if (data.communities) {
130
105
  const cm = data.communities;
131
106
  console.log(
132
107
  `\nCommunities: ${cm.communityCount} detected | modularity: ${cm.modularity} | drift: ${cm.driftScore}%`,
133
108
  );
134
109
  }
110
+ }
111
+
112
+ export async function stats(customDbPath, opts = {}) {
113
+ const data = statsData(customDbPath, { noTests: opts.noTests });
114
+
115
+ try {
116
+ const { communitySummaryForStats } = await import('../../features/communities.js');
117
+ data.communities = communitySummaryForStats(customDbPath, { noTests: opts.noTests });
118
+ } catch {
119
+ /* graphology may not be available */
120
+ }
135
121
 
122
+ if (outputResult(data, null, opts)) return;
123
+
124
+ console.log('\n# Codegraph Stats\n');
125
+ printNodes(data);
126
+ printEdges(data);
127
+ printFiles(data);
128
+ printCycles(data);
129
+ printHotspots(data);
130
+ printEmbeddings(data);
131
+ printQuality(data);
132
+ printRoles(data);
133
+ printComplexity(data);
134
+ printCommunities(data);
136
135
  console.log();
137
136
  }
138
137
 
@@ -1,6 +1,40 @@
1
1
  import { kindIcon, pathData } from '../../domain/queries.js';
2
2
  import { outputResult } from '../../infrastructure/result-formatter.js';
3
3
 
4
+ function printNotFound(from, to, data) {
5
+ const dir = data.reverse ? 'reverse ' : '';
6
+ console.log(`No ${dir}path from "${from}" to "${to}" within ${data.maxDepth} hops.`);
7
+ if (data.fromCandidates.length > 1) {
8
+ console.log(
9
+ `\n "${from}" matched ${data.fromCandidates.length} symbols — using top match: ${data.fromCandidates[0].name} (${data.fromCandidates[0].file}:${data.fromCandidates[0].line})`,
10
+ );
11
+ }
12
+ if (data.toCandidates.length > 1) {
13
+ console.log(
14
+ ` "${to}" matched ${data.toCandidates.length} symbols — using top match: ${data.toCandidates[0].name} (${data.toCandidates[0].file}:${data.toCandidates[0].line})`,
15
+ );
16
+ }
17
+ }
18
+
19
+ function printPathSteps(data) {
20
+ for (let i = 0; i < data.path.length; i++) {
21
+ const n = data.path[i];
22
+ const indent = ' '.repeat(i + 1);
23
+ if (i === 0) {
24
+ console.log(`${indent}${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`);
25
+ } else {
26
+ console.log(
27
+ `${indent}--[${n.edgeKind}]--> ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`,
28
+ );
29
+ }
30
+ }
31
+ if (data.alternateCount > 0) {
32
+ console.log(
33
+ `\n (${data.alternateCount} alternate shortest ${data.alternateCount === 1 ? 'path' : 'paths'} at same depth)`,
34
+ );
35
+ }
36
+ }
37
+
4
38
  export function symbolPath(from, to, customDbPath, opts = {}) {
5
39
  const data = pathData(from, to, customDbPath, opts);
6
40
  if (outputResult(data, null, opts)) return;
@@ -11,18 +45,7 @@ export function symbolPath(from, to, customDbPath, opts = {}) {
11
45
  }
12
46
 
13
47
  if (!data.found) {
14
- const dir = data.reverse ? 'reverse ' : '';
15
- console.log(`No ${dir}path from "${from}" to "${to}" within ${data.maxDepth} hops.`);
16
- if (data.fromCandidates.length > 1) {
17
- console.log(
18
- `\n "${from}" matched ${data.fromCandidates.length} symbols — using top match: ${data.fromCandidates[0].name} (${data.fromCandidates[0].file}:${data.fromCandidates[0].line})`,
19
- );
20
- }
21
- if (data.toCandidates.length > 1) {
22
- console.log(
23
- ` "${to}" matched ${data.toCandidates.length} symbols — using top match: ${data.toCandidates[0].name} (${data.toCandidates[0].file}:${data.toCandidates[0].line})`,
24
- );
25
- }
48
+ printNotFound(from, to, data);
26
49
  return;
27
50
  }
28
51
 
@@ -37,22 +60,6 @@ export function symbolPath(from, to, customDbPath, opts = {}) {
37
60
  console.log(
38
61
  `\nPath from ${from} to ${to} (${data.hops} ${data.hops === 1 ? 'hop' : 'hops'})${dir}:\n`,
39
62
  );
40
- for (let i = 0; i < data.path.length; i++) {
41
- const n = data.path[i];
42
- const indent = ' '.repeat(i + 1);
43
- if (i === 0) {
44
- console.log(`${indent}${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`);
45
- } else {
46
- console.log(
47
- `${indent}--[${n.edgeKind}]--> ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`,
48
- );
49
- }
50
- }
51
-
52
- if (data.alternateCount > 0) {
53
- console.log(
54
- `\n (${data.alternateCount} alternate shortest ${data.alternateCount === 1 ? 'path' : 'paths'} at same depth)`,
55
- );
56
- }
63
+ printPathSteps(data);
57
64
  console.log();
58
65
  }