@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.
- package/README.md +6 -6
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- package/src/ast-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +13 -140
- package/src/audit.js +2 -87
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +1 -96
- package/src/builder.js +60 -178
- package/src/cfg.js +89 -883
- package/src/check.js +1 -84
- package/src/cli.js +31 -22
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +12 -83
- package/src/complexity.js +43 -357
- package/src/cycles.js +1 -1
- package/src/dataflow.js +12 -665
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/cached-stmt.js +19 -0
- package/src/db/repository/cfg.js +72 -0
- package/src/db/repository/cochange.js +54 -0
- package/src/db/repository/complexity.js +20 -0
- package/src/db/repository/dataflow.js +17 -0
- package/src/db/repository/edges.js +281 -0
- package/src/db/repository/embeddings.js +51 -0
- package/src/db/repository/graph-read.js +59 -0
- package/src/db/repository/index.js +43 -0
- package/src/db/repository/nodes.js +247 -0
- package/src/db.js +40 -1
- package/src/embedder.js +14 -34
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +2 -70
- package/src/index.js +30 -20
- package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
- package/src/kinds.js +1 -0
- package/src/manifesto.js +0 -76
- package/src/native.js +31 -9
- package/src/owners.js +1 -56
- package/src/parser.js +53 -2
- package/src/queries-cli.js +1 -1
- package/src/queries.js +79 -280
- package/src/sequence.js +5 -44
- package/src/structure.js +16 -75
- package/src/triage.js +1 -54
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
- package/src/db/repository.js +0 -134
- /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 {
|
|
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
|
|
1017
|
-
|
|
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
|
|
1408
|
+
const binaryVersion =
|
|
1408
1409
|
typeof native.engineVersion === 'function' ? native.engineVersion() : 'unknown';
|
|
1409
|
-
|
|
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
|
+
}
|