@optave/codegraph 3.1.1 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +6 -6
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +365 -0
  4. package/src/ast-analysis/metrics.js +118 -0
  5. package/src/ast-analysis/visitor-utils.js +176 -0
  6. package/src/ast-analysis/visitor.js +162 -0
  7. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  8. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  9. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  10. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  11. package/src/ast.js +13 -140
  12. package/src/audit.js +2 -87
  13. package/src/batch.js +0 -25
  14. package/src/boundaries.js +1 -1
  15. package/src/branch-compare.js +1 -96
  16. package/src/builder.js +60 -178
  17. package/src/cfg.js +89 -883
  18. package/src/check.js +1 -84
  19. package/src/cli.js +31 -22
  20. package/src/cochange.js +1 -39
  21. package/src/commands/audit.js +88 -0
  22. package/src/commands/batch.js +26 -0
  23. package/src/commands/branch-compare.js +97 -0
  24. package/src/commands/cfg.js +55 -0
  25. package/src/commands/check.js +82 -0
  26. package/src/commands/cochange.js +37 -0
  27. package/src/commands/communities.js +69 -0
  28. package/src/commands/complexity.js +77 -0
  29. package/src/commands/dataflow.js +110 -0
  30. package/src/commands/flow.js +70 -0
  31. package/src/commands/manifesto.js +77 -0
  32. package/src/commands/owners.js +52 -0
  33. package/src/commands/query.js +21 -0
  34. package/src/commands/sequence.js +33 -0
  35. package/src/commands/structure.js +64 -0
  36. package/src/commands/triage.js +49 -0
  37. package/src/communities.js +12 -83
  38. package/src/complexity.js +43 -357
  39. package/src/cycles.js +1 -1
  40. package/src/dataflow.js +12 -665
  41. package/src/db/repository/build-stmts.js +104 -0
  42. package/src/db/repository/cached-stmt.js +19 -0
  43. package/src/db/repository/cfg.js +72 -0
  44. package/src/db/repository/cochange.js +54 -0
  45. package/src/db/repository/complexity.js +20 -0
  46. package/src/db/repository/dataflow.js +17 -0
  47. package/src/db/repository/edges.js +281 -0
  48. package/src/db/repository/embeddings.js +51 -0
  49. package/src/db/repository/graph-read.js +59 -0
  50. package/src/db/repository/index.js +43 -0
  51. package/src/db/repository/nodes.js +247 -0
  52. package/src/db.js +40 -1
  53. package/src/embedder.js +14 -34
  54. package/src/export.js +1 -1
  55. package/src/extractors/javascript.js +130 -5
  56. package/src/flow.js +2 -70
  57. package/src/index.js +30 -20
  58. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  59. package/src/kinds.js +1 -0
  60. package/src/manifesto.js +0 -76
  61. package/src/native.js +31 -9
  62. package/src/owners.js +1 -56
  63. package/src/parser.js +53 -2
  64. package/src/queries-cli.js +1 -1
  65. package/src/queries.js +79 -280
  66. package/src/sequence.js +5 -44
  67. package/src/structure.js +16 -75
  68. package/src/triage.js +1 -54
  69. package/src/viewer.js +1 -1
  70. package/src/watcher.js +7 -4
  71. package/src/db/repository.js +0 -134
  72. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
package/src/check.js CHANGED
@@ -4,9 +4,8 @@ import path from 'node:path';
4
4
  import { loadConfig } from './config.js';
5
5
  import { findCycles } from './cycles.js';
6
6
  import { findDbPath, openReadonlyOrFail } from './db.js';
7
+ import { isTestFile } from './infrastructure/test-filter.js';
7
8
  import { matchOwners, parseCodeowners } from './owners.js';
8
- import { outputResult } from './result-formatter.js';
9
- import { isTestFile } from './test-filter.js';
10
9
 
11
10
  // ─── Diff Parser ──────────────────────────────────────────────────────
12
11
 
@@ -348,85 +347,3 @@ export function checkData(customDbPath, opts = {}) {
348
347
  db.close();
349
348
  }
350
349
  }
351
-
352
- // ─── CLI Display ──────────────────────────────────────────────────────
353
-
354
- /**
355
- * CLI formatter — prints check results and exits with code 1 on failure.
356
- */
357
- export function check(customDbPath, opts = {}) {
358
- const data = checkData(customDbPath, opts);
359
-
360
- if (data.error) {
361
- console.error(data.error);
362
- process.exit(1);
363
- }
364
-
365
- if (outputResult(data, null, opts)) {
366
- if (!data.passed) process.exit(1);
367
- return;
368
- }
369
-
370
- console.log('\n# Check Results\n');
371
-
372
- if (data.predicates.length === 0) {
373
- console.log(' No changes detected.\n');
374
- return;
375
- }
376
-
377
- console.log(
378
- ` Changed files: ${data.summary.changedFiles} New files: ${data.summary.newFiles}\n`,
379
- );
380
-
381
- for (const pred of data.predicates) {
382
- const icon = pred.passed ? 'PASS' : 'FAIL';
383
- console.log(` [${icon}] ${pred.name}`);
384
-
385
- if (!pred.passed) {
386
- if (pred.name === 'cycles' && pred.cycles) {
387
- for (const cycle of pred.cycles.slice(0, 10)) {
388
- console.log(` ${cycle.join(' -> ')}`);
389
- }
390
- if (pred.cycles.length > 10) {
391
- console.log(` ... and ${pred.cycles.length - 10} more`);
392
- }
393
- }
394
- if (pred.name === 'blast-radius' && pred.violations) {
395
- for (const v of pred.violations.slice(0, 10)) {
396
- console.log(
397
- ` ${v.name} (${v.kind}) at ${v.file}:${v.line} — ${v.transitiveCallers} callers (max: ${pred.threshold})`,
398
- );
399
- }
400
- if (pred.violations.length > 10) {
401
- console.log(` ... and ${pred.violations.length - 10} more`);
402
- }
403
- }
404
- if (pred.name === 'signatures' && pred.violations) {
405
- for (const v of pred.violations.slice(0, 10)) {
406
- console.log(` ${v.name} (${v.kind}) at ${v.file}:${v.line}`);
407
- }
408
- if (pred.violations.length > 10) {
409
- console.log(` ... and ${pred.violations.length - 10} more`);
410
- }
411
- }
412
- if (pred.name === 'boundaries' && pred.violations) {
413
- for (const v of pred.violations.slice(0, 10)) {
414
- console.log(` ${v.from} -> ${v.to} (${v.edgeKind})`);
415
- }
416
- if (pred.violations.length > 10) {
417
- console.log(` ... and ${pred.violations.length - 10} more`);
418
- }
419
- }
420
- }
421
- if (pred.note) {
422
- console.log(` ${pred.note}`);
423
- }
424
- }
425
-
426
- const s = data.summary;
427
- console.log(`\n ${s.total} predicates | ${s.passed} passed | ${s.failed} failed\n`);
428
-
429
- if (!data.passed) {
430
- process.exit(1);
431
- }
432
- }
package/src/cli.js CHANGED
@@ -3,9 +3,10 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { Command } from 'commander';
6
- import { audit } from './audit.js';
7
- import { BATCH_COMMANDS, batch, multiBatchData, splitTargets } from './batch.js';
6
+ import { BATCH_COMMANDS, multiBatchData, splitTargets } from './batch.js';
8
7
  import { buildGraph } from './builder.js';
8
+ import { audit } from './commands/audit.js';
9
+ import { batch } from './commands/batch.js';
9
10
  import { loadConfig } from './config.js';
10
11
  import { findCycles, formatCycles } from './cycles.js';
11
12
  import { openReadonlyOrFail } from './db.js';
@@ -24,6 +25,7 @@ import {
24
25
  exportMermaid,
25
26
  exportNeo4jCSV,
26
27
  } from './export.js';
28
+ import { outputResult } from './infrastructure/result-formatter.js';
27
29
  import { setVerbose } from './logger.js';
28
30
  import { EVERY_SYMBOL_KIND, VALID_ROLES } from './queries.js';
29
31
  import {
@@ -49,7 +51,6 @@ import {
49
51
  registerRepo,
50
52
  unregisterRepo,
51
53
  } from './registry.js';
52
- import { outputResult } from './result-formatter.js';
53
54
  import { snapshotDelete, snapshotList, snapshotRestore, snapshotSave } from './snapshot.js';
54
55
  import { checkForUpdates, printUpdateNotification } from './update-check.js';
55
56
  import { watchProject } from './watcher.js';
@@ -469,7 +470,7 @@ program
469
470
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
470
471
  process.exit(1);
471
472
  }
472
- const { manifesto } = await import('./manifesto.js');
473
+ const { manifesto } = await import('./commands/manifesto.js');
473
474
  manifesto(opts.db, {
474
475
  file: opts.file,
475
476
  kind: opts.kind,
@@ -483,7 +484,7 @@ program
483
484
  }
484
485
 
485
486
  // Diff predicates mode
486
- const { check } = await import('./check.js');
487
+ const { check } = await import('./commands/check.js');
487
488
  check(opts.db, {
488
489
  ref,
489
490
  staged: opts.staged,
@@ -502,7 +503,7 @@ program
502
503
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
503
504
  process.exit(1);
504
505
  }
505
- const { manifesto } = await import('./manifesto.js');
506
+ const { manifesto } = await import('./commands/manifesto.js');
506
507
  manifesto(opts.db, {
507
508
  file: opts.file,
508
509
  kind: opts.kind,
@@ -952,7 +953,7 @@ program
952
953
  .option('--offset <number>', 'Skip N results (default: 0)')
953
954
  .option('--ndjson', 'Newline-delimited JSON output')
954
955
  .action(async (dir, opts) => {
955
- const { structureData, formatStructure } = await import('./structure.js');
956
+ const { structureData, formatStructure } = await import('./commands/structure.js');
956
957
  const data = structureData(opts.db, {
957
958
  directory: dir,
958
959
  depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
@@ -1013,8 +1014,8 @@ program
1013
1014
  .option('--offset <number>', 'Skip N results (default: 0)')
1014
1015
  .option('--ndjson', 'Newline-delimited JSON output')
1015
1016
  .action(async (file, opts) => {
1016
- const { analyzeCoChanges, coChangeData, coChangeTopData, formatCoChange, formatCoChangeTop } =
1017
- await import('./cochange.js');
1017
+ const { analyzeCoChanges, coChangeData, coChangeTopData } = await import('./cochange.js');
1018
+ const { formatCoChange, formatCoChangeTop } = await import('./commands/cochange.js');
1018
1019
 
1019
1020
  if (opts.analyze) {
1020
1021
  const result = analyzeCoChanges(opts.db, {
@@ -1076,7 +1077,7 @@ QUERY_OPTS(
1076
1077
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1077
1078
  process.exit(1);
1078
1079
  }
1079
- const { flow } = await import('./flow.js');
1080
+ const { flow } = await import('./commands/flow.js');
1080
1081
  flow(name, opts.db, {
1081
1082
  list: opts.list,
1082
1083
  depth: parseInt(opts.depth, 10),
@@ -1106,7 +1107,7 @@ QUERY_OPTS(
1106
1107
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1107
1108
  process.exit(1);
1108
1109
  }
1109
- const { sequence } = await import('./sequence.js');
1110
+ const { sequence } = await import('./commands/sequence.js');
1110
1111
  sequence(name, opts.db, {
1111
1112
  depth: parseInt(opts.depth, 10),
1112
1113
  file: opts.file,
@@ -1134,7 +1135,7 @@ QUERY_OPTS(
1134
1135
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1135
1136
  process.exit(1);
1136
1137
  }
1137
- const { dataflow } = await import('./dataflow.js');
1138
+ const { dataflow } = await import('./commands/dataflow.js');
1138
1139
  dataflow(name, opts.db, {
1139
1140
  file: opts.file,
1140
1141
  kind: opts.kind,
@@ -1157,7 +1158,7 @@ QUERY_OPTS(program.command('cfg <name>').description('Show control flow graph fo
1157
1158
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1158
1159
  process.exit(1);
1159
1160
  }
1160
- const { cfg } = await import('./cfg.js');
1161
+ const { cfg } = await import('./commands/cfg.js');
1161
1162
  cfg(name, opts.db, {
1162
1163
  format: opts.format,
1163
1164
  file: opts.file,
@@ -1194,7 +1195,7 @@ program
1194
1195
  console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1195
1196
  process.exit(1);
1196
1197
  }
1197
- const { complexity } = await import('./complexity.js');
1198
+ const { complexity } = await import('./commands/complexity.js');
1198
1199
  complexity(opts.db, {
1199
1200
  target,
1200
1201
  limit: parseInt(opts.limit, 10),
@@ -1243,7 +1244,7 @@ QUERY_OPTS(
1243
1244
  .option('--resolution <n>', 'Louvain resolution parameter (default 1.0)', '1.0')
1244
1245
  .option('--drift', 'Show only drift analysis')
1245
1246
  .action(async (opts) => {
1246
- const { communities } = await import('./communities.js');
1247
+ const { communities } = await import('./commands/communities.js');
1247
1248
  communities(opts.db, {
1248
1249
  functions: opts.functions,
1249
1250
  resolution: parseFloat(opts.resolution),
@@ -1286,7 +1287,7 @@ program
1286
1287
  .action(async (opts) => {
1287
1288
  if (opts.level === 'file' || opts.level === 'directory') {
1288
1289
  // Delegate to hotspots for file/directory level
1289
- const { hotspotsData, formatHotspots } = await import('./structure.js');
1290
+ const { hotspotsData, formatHotspots } = await import('./commands/structure.js');
1290
1291
  const metric = opts.sort === 'risk' ? 'fan-in' : opts.sort;
1291
1292
  const data = hotspotsData(opts.db, {
1292
1293
  metric,
@@ -1318,7 +1319,7 @@ program
1318
1319
  process.exit(1);
1319
1320
  }
1320
1321
  }
1321
- const { triage } = await import('./triage.js');
1322
+ const { triage } = await import('./commands/triage.js');
1322
1323
  triage(opts.db, {
1323
1324
  limit: parseInt(opts.limit, 10),
1324
1325
  offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
@@ -1346,7 +1347,7 @@ program
1346
1347
  .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1347
1348
  .option('-j, --json', 'Output as JSON')
1348
1349
  .action(async (target, opts) => {
1349
- const { owners } = await import('./owners.js');
1350
+ const { owners } = await import('./commands/owners.js');
1350
1351
  owners(opts.db, {
1351
1352
  owner: opts.owner,
1352
1353
  boundary: opts.boundary,
@@ -1366,7 +1367,7 @@ program
1366
1367
  .option('-j, --json', 'Output as JSON')
1367
1368
  .option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
1368
1369
  .action(async (base, target, opts) => {
1369
- const { branchCompare } = await import('./branch-compare.js');
1370
+ const { branchCompare } = await import('./commands/branch-compare.js');
1370
1371
  await branchCompare(base, target, {
1371
1372
  engine: program.opts().engine,
1372
1373
  depth: parseInt(opts.depth, 10),
@@ -1389,7 +1390,7 @@ program
1389
1390
  .command('info')
1390
1391
  .description('Show codegraph engine info and diagnostics')
1391
1392
  .action(async () => {
1392
- const { isNativeAvailable, loadNative } = await import('./native.js');
1393
+ const { getNativePackageVersion, isNativeAvailable, loadNative } = await import('./native.js');
1393
1394
  const { getActiveEngine } = await import('./parser.js');
1394
1395
 
1395
1396
  const engine = program.opts().engine;
@@ -1404,9 +1405,17 @@ program
1404
1405
  console.log(` Native engine : ${nativeAvailable ? 'available' : 'unavailable'}`);
1405
1406
  if (nativeAvailable) {
1406
1407
  const native = loadNative();
1407
- const nativeVersion =
1408
+ const binaryVersion =
1408
1409
  typeof native.engineVersion === 'function' ? native.engineVersion() : 'unknown';
1409
- console.log(` Native version: ${nativeVersion}`);
1410
+ const pkgVersion = getNativePackageVersion();
1411
+ const knownBinaryVersion = binaryVersion !== 'unknown' ? binaryVersion : null;
1412
+ if (pkgVersion && knownBinaryVersion && pkgVersion !== knownBinaryVersion) {
1413
+ console.log(
1414
+ ` Native version: ${pkgVersion} (binary reports ${knownBinaryVersion} — stale)`,
1415
+ );
1416
+ } else {
1417
+ console.log(` Native version: ${pkgVersion ?? binaryVersion}`);
1418
+ }
1410
1419
  }
1411
1420
  console.log(` Engine flag : --engine ${engine}`);
1412
1421
  console.log(` Active engine : ${activeName}${activeVersion ? ` (v${activeVersion})` : ''}`);
package/src/cochange.js CHANGED
@@ -10,9 +10,9 @@ import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { normalizePath } from './constants.js';
12
12
  import { closeDb, findDbPath, initSchema, openDb, openReadonlyOrFail } from './db.js';
13
+ import { isTestFile } from './infrastructure/test-filter.js';
13
14
  import { warn } from './logger.js';
14
15
  import { paginateResult } from './paginate.js';
15
- import { isTestFile } from './test-filter.js';
16
16
 
17
17
  /**
18
18
  * Scan git history and return parsed commit data.
@@ -419,44 +419,6 @@ export function coChangeForFiles(files, db, opts = {}) {
419
419
  return results;
420
420
  }
421
421
 
422
- /**
423
- * Format co-change data for CLI output (single file).
424
- */
425
- export function formatCoChange(data) {
426
- if (data.error) return data.error;
427
- if (data.partners.length === 0) return `No co-change partners found for ${data.file}`;
428
-
429
- const lines = [`\nCo-change partners for ${data.file}:\n`];
430
- for (const p of data.partners) {
431
- const pct = `${(p.jaccard * 100).toFixed(0)}%`.padStart(4);
432
- const commits = `${p.commitCount} commits`.padStart(12);
433
- lines.push(` ${pct} ${commits} ${p.file}`);
434
- }
435
- if (data.meta?.analyzedAt) {
436
- lines.push(`\n Analyzed: ${data.meta.analyzedAt} | Window: ${data.meta.since || 'all'}`);
437
- }
438
- return lines.join('\n');
439
- }
440
-
441
- /**
442
- * Format top co-change pairs for CLI output (global view).
443
- */
444
- export function formatCoChangeTop(data) {
445
- if (data.error) return data.error;
446
- if (data.pairs.length === 0) return 'No co-change pairs found.';
447
-
448
- const lines = ['\nTop co-change pairs:\n'];
449
- for (const p of data.pairs) {
450
- const pct = `${(p.jaccard * 100).toFixed(0)}%`.padStart(4);
451
- const commits = `${p.commitCount} commits`.padStart(12);
452
- lines.push(` ${pct} ${commits} ${p.fileA} <-> ${p.fileB}`);
453
- }
454
- if (data.meta?.analyzedAt) {
455
- lines.push(`\n Analyzed: ${data.meta.analyzedAt} | Window: ${data.meta.since || 'all'}`);
456
- }
457
- return lines.join('\n');
458
- }
459
-
460
422
  // ─── Internal Helpers ────────────────────────────────────────────────────
461
423
 
462
424
  function resolveCoChangeFile(db, file) {
@@ -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
+ }