@optave/codegraph 2.5.0 → 2.6.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.
- package/README.md +119 -47
- package/package.json +8 -7
- package/src/audit.js +423 -0
- package/src/batch.js +90 -0
- package/src/boundaries.js +346 -0
- package/src/branch-compare.js +568 -0
- package/src/builder.js +66 -2
- package/src/check.js +432 -0
- package/src/cli.js +375 -9
- package/src/cochange.js +5 -2
- package/src/communities.js +7 -1
- package/src/complexity.js +116 -9
- package/src/config.js +10 -0
- package/src/embedder.js +350 -38
- package/src/flow.js +4 -4
- package/src/index.js +28 -1
- package/src/manifesto.js +69 -1
- package/src/mcp.js +347 -19
- package/src/owners.js +359 -0
- package/src/paginate.js +35 -0
- package/src/queries.js +233 -19
- package/src/registry.js +6 -3
- package/src/snapshot.js +149 -0
- package/src/structure.js +5 -2
- package/src/triage.js +273 -0
package/src/cli.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
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 } from './batch.js';
|
|
6
8
|
import { buildGraph } from './builder.js';
|
|
7
9
|
import { loadConfig } from './config.js';
|
|
8
10
|
import { findCycles, formatCycles } from './cycles.js';
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
} from './embedder.js';
|
|
17
19
|
import { exportDOT, exportJSON, exportMermaid } from './export.js';
|
|
18
20
|
import { setVerbose } from './logger.js';
|
|
21
|
+
import { printNdjson } from './paginate.js';
|
|
19
22
|
import {
|
|
20
23
|
ALL_SYMBOL_KINDS,
|
|
21
24
|
context,
|
|
@@ -40,6 +43,7 @@ import {
|
|
|
40
43
|
registerRepo,
|
|
41
44
|
unregisterRepo,
|
|
42
45
|
} from './registry.js';
|
|
46
|
+
import { snapshotDelete, snapshotList, snapshotRestore, snapshotSave } from './snapshot.js';
|
|
43
47
|
import { checkForUpdates, printUpdateNotification } from './update-check.js';
|
|
44
48
|
import { watchProject } from './watcher.js';
|
|
45
49
|
|
|
@@ -83,6 +87,12 @@ function resolveNoTests(opts) {
|
|
|
83
87
|
return config.query?.excludeTests || false;
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
function formatSize(bytes) {
|
|
91
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
92
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
93
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
94
|
+
}
|
|
95
|
+
|
|
86
96
|
program
|
|
87
97
|
.command('build [dir]')
|
|
88
98
|
.description('Parse repo and build graph in .codegraph/graph.db')
|
|
@@ -120,8 +130,17 @@ program
|
|
|
120
130
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
121
131
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
122
132
|
.option('-j, --json', 'Output as JSON')
|
|
133
|
+
.option('--limit <number>', 'Max results to return')
|
|
134
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
135
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
123
136
|
.action((file, opts) => {
|
|
124
|
-
impactAnalysis(file, opts.db, {
|
|
137
|
+
impactAnalysis(file, opts.db, {
|
|
138
|
+
noTests: resolveNoTests(opts),
|
|
139
|
+
json: opts.json,
|
|
140
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
141
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
142
|
+
ndjson: opts.ndjson,
|
|
143
|
+
});
|
|
125
144
|
});
|
|
126
145
|
|
|
127
146
|
program
|
|
@@ -157,8 +176,17 @@ program
|
|
|
157
176
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
158
177
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
159
178
|
.option('-j, --json', 'Output as JSON')
|
|
179
|
+
.option('--limit <number>', 'Max results to return')
|
|
180
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
181
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
160
182
|
.action((file, opts) => {
|
|
161
|
-
fileDeps(file, opts.db, {
|
|
183
|
+
fileDeps(file, opts.db, {
|
|
184
|
+
noTests: resolveNoTests(opts),
|
|
185
|
+
json: opts.json,
|
|
186
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
187
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
188
|
+
ndjson: opts.ndjson,
|
|
189
|
+
});
|
|
162
190
|
});
|
|
163
191
|
|
|
164
192
|
program
|
|
@@ -171,6 +199,9 @@ program
|
|
|
171
199
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
172
200
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
173
201
|
.option('-j, --json', 'Output as JSON')
|
|
202
|
+
.option('--limit <number>', 'Max results to return')
|
|
203
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
204
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
174
205
|
.action((name, opts) => {
|
|
175
206
|
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
176
207
|
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
@@ -182,6 +213,9 @@ program
|
|
|
182
213
|
kind: opts.kind,
|
|
183
214
|
noTests: resolveNoTests(opts),
|
|
184
215
|
json: opts.json,
|
|
216
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
217
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
218
|
+
ndjson: opts.ndjson,
|
|
185
219
|
});
|
|
186
220
|
});
|
|
187
221
|
|
|
@@ -195,6 +229,9 @@ program
|
|
|
195
229
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
196
230
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
197
231
|
.option('-j, --json', 'Output as JSON')
|
|
232
|
+
.option('--limit <number>', 'Max results to return')
|
|
233
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
234
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
198
235
|
.action((name, opts) => {
|
|
199
236
|
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
200
237
|
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
@@ -206,6 +243,9 @@ program
|
|
|
206
243
|
kind: opts.kind,
|
|
207
244
|
noTests: resolveNoTests(opts),
|
|
208
245
|
json: opts.json,
|
|
246
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
247
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
248
|
+
ndjson: opts.ndjson,
|
|
209
249
|
});
|
|
210
250
|
});
|
|
211
251
|
|
|
@@ -251,6 +291,9 @@ program
|
|
|
251
291
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
252
292
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
253
293
|
.option('-j, --json', 'Output as JSON')
|
|
294
|
+
.option('--limit <number>', 'Max results to return')
|
|
295
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
296
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
254
297
|
.action((name, opts) => {
|
|
255
298
|
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
256
299
|
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
@@ -264,6 +307,9 @@ program
|
|
|
264
307
|
noTests: resolveNoTests(opts),
|
|
265
308
|
includeTests: opts.withTestSource,
|
|
266
309
|
json: opts.json,
|
|
310
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
311
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
312
|
+
ndjson: opts.ndjson,
|
|
267
313
|
});
|
|
268
314
|
});
|
|
269
315
|
|
|
@@ -275,11 +321,41 @@ program
|
|
|
275
321
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
276
322
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
277
323
|
.option('-j, --json', 'Output as JSON')
|
|
324
|
+
.option('--limit <number>', 'Max results to return')
|
|
325
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
326
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
278
327
|
.action((target, opts) => {
|
|
279
328
|
explain(target, opts.db, {
|
|
280
329
|
depth: parseInt(opts.depth, 10),
|
|
281
330
|
noTests: resolveNoTests(opts),
|
|
282
331
|
json: opts.json,
|
|
332
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
333
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
334
|
+
ndjson: opts.ndjson,
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
program
|
|
339
|
+
.command('audit <target>')
|
|
340
|
+
.description('Composite report: explain + impact + health metrics per function')
|
|
341
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
342
|
+
.option('--depth <n>', 'Impact analysis depth', '3')
|
|
343
|
+
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
344
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
345
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
346
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
347
|
+
.option('-j, --json', 'Output as JSON')
|
|
348
|
+
.action((target, opts) => {
|
|
349
|
+
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
350
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
audit(target, opts.db, {
|
|
354
|
+
depth: parseInt(opts.depth, 10),
|
|
355
|
+
file: opts.file,
|
|
356
|
+
kind: opts.kind,
|
|
357
|
+
noTests: resolveNoTests(opts),
|
|
358
|
+
json: opts.json,
|
|
283
359
|
});
|
|
284
360
|
});
|
|
285
361
|
|
|
@@ -320,6 +396,9 @@ program
|
|
|
320
396
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
321
397
|
.option('-j, --json', 'Output as JSON')
|
|
322
398
|
.option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
|
|
399
|
+
.option('--limit <number>', 'Max results to return')
|
|
400
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
401
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
323
402
|
.action((ref, opts) => {
|
|
324
403
|
diffImpact(opts.db, {
|
|
325
404
|
ref,
|
|
@@ -328,6 +407,37 @@ program
|
|
|
328
407
|
noTests: resolveNoTests(opts),
|
|
329
408
|
json: opts.json,
|
|
330
409
|
format: opts.format,
|
|
410
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
411
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
412
|
+
ndjson: opts.ndjson,
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
program
|
|
417
|
+
.command('check [ref]')
|
|
418
|
+
.description('Run validation predicates against git changes (CI gate)')
|
|
419
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
420
|
+
.option('--staged', 'Analyze staged changes')
|
|
421
|
+
.option('--cycles', 'Assert no dependency cycles involve changed files')
|
|
422
|
+
.option('--blast-radius <n>', 'Assert no function exceeds N transitive callers')
|
|
423
|
+
.option('--signatures', 'Assert no function declaration lines were modified')
|
|
424
|
+
.option('--boundaries', 'Assert no cross-owner boundary violations')
|
|
425
|
+
.option('--depth <n>', 'Max BFS depth for blast radius (default: 3)')
|
|
426
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
427
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
428
|
+
.option('-j, --json', 'Output as JSON')
|
|
429
|
+
.action(async (ref, opts) => {
|
|
430
|
+
const { check } = await import('./check.js');
|
|
431
|
+
check(opts.db, {
|
|
432
|
+
ref,
|
|
433
|
+
staged: opts.staged,
|
|
434
|
+
cycles: opts.cycles || undefined,
|
|
435
|
+
blastRadius: opts.blastRadius ? parseInt(opts.blastRadius, 10) : undefined,
|
|
436
|
+
signatures: opts.signatures || undefined,
|
|
437
|
+
boundaries: opts.boundaries || undefined,
|
|
438
|
+
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
|
|
439
|
+
noTests: resolveNoTests(opts),
|
|
440
|
+
json: opts.json,
|
|
331
441
|
});
|
|
332
442
|
});
|
|
333
443
|
|
|
@@ -468,6 +578,7 @@ registry
|
|
|
468
578
|
.description('Remove stale registry entries (missing directories or idle beyond TTL)')
|
|
469
579
|
.option('--ttl <days>', 'Days of inactivity before pruning (default: 30)', '30')
|
|
470
580
|
.option('--exclude <names>', 'Comma-separated repo names to preserve from pruning')
|
|
581
|
+
.option('--dry-run', 'Show what would be pruned without removing anything')
|
|
471
582
|
.action((opts) => {
|
|
472
583
|
const excludeNames = opts.exclude
|
|
473
584
|
? opts.exclude
|
|
@@ -475,15 +586,100 @@ registry
|
|
|
475
586
|
.map((s) => s.trim())
|
|
476
587
|
.filter((s) => s.length > 0)
|
|
477
588
|
: [];
|
|
478
|
-
const
|
|
589
|
+
const dryRun = !!opts.dryRun;
|
|
590
|
+
const pruned = pruneRegistry(undefined, parseInt(opts.ttl, 10), excludeNames, dryRun);
|
|
479
591
|
if (pruned.length === 0) {
|
|
480
592
|
console.log('No stale entries found.');
|
|
481
593
|
} else {
|
|
594
|
+
const prefix = dryRun ? 'Would prune' : 'Pruned';
|
|
482
595
|
for (const entry of pruned) {
|
|
483
596
|
const tag = entry.reason === 'expired' ? 'expired' : 'missing';
|
|
484
|
-
console.log(
|
|
597
|
+
console.log(`${prefix} "${entry.name}" (${entry.path}) [${tag}]`);
|
|
598
|
+
}
|
|
599
|
+
if (dryRun) {
|
|
600
|
+
console.log(
|
|
601
|
+
`\nDry run: ${pruned.length} ${pruned.length === 1 ? 'entry' : 'entries'} would be removed.`,
|
|
602
|
+
);
|
|
603
|
+
} else {
|
|
604
|
+
console.log(
|
|
605
|
+
`\nRemoved ${pruned.length} stale ${pruned.length === 1 ? 'entry' : 'entries'}.`,
|
|
606
|
+
);
|
|
485
607
|
}
|
|
486
|
-
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// ─── Snapshot commands ──────────────────────────────────────────────────
|
|
612
|
+
|
|
613
|
+
const snapshot = program
|
|
614
|
+
.command('snapshot')
|
|
615
|
+
.description('Save and restore graph database snapshots');
|
|
616
|
+
|
|
617
|
+
snapshot
|
|
618
|
+
.command('save <name>')
|
|
619
|
+
.description('Save a snapshot of the current graph database')
|
|
620
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
621
|
+
.option('--force', 'Overwrite existing snapshot')
|
|
622
|
+
.action((name, opts) => {
|
|
623
|
+
try {
|
|
624
|
+
const result = snapshotSave(name, { dbPath: opts.db, force: opts.force });
|
|
625
|
+
console.log(`Snapshot saved: ${result.name} (${formatSize(result.size)})`);
|
|
626
|
+
} catch (err) {
|
|
627
|
+
console.error(err.message);
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
snapshot
|
|
633
|
+
.command('restore <name>')
|
|
634
|
+
.description('Restore a snapshot over the current graph database')
|
|
635
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
636
|
+
.action((name, opts) => {
|
|
637
|
+
try {
|
|
638
|
+
snapshotRestore(name, { dbPath: opts.db });
|
|
639
|
+
console.log(`Snapshot "${name}" restored.`);
|
|
640
|
+
} catch (err) {
|
|
641
|
+
console.error(err.message);
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
snapshot
|
|
647
|
+
.command('list')
|
|
648
|
+
.description('List all saved snapshots')
|
|
649
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
650
|
+
.option('-j, --json', 'Output as JSON')
|
|
651
|
+
.action((opts) => {
|
|
652
|
+
try {
|
|
653
|
+
const snapshots = snapshotList({ dbPath: opts.db });
|
|
654
|
+
if (opts.json) {
|
|
655
|
+
console.log(JSON.stringify(snapshots, null, 2));
|
|
656
|
+
} else if (snapshots.length === 0) {
|
|
657
|
+
console.log('No snapshots found.');
|
|
658
|
+
} else {
|
|
659
|
+
console.log(`Snapshots (${snapshots.length}):\n`);
|
|
660
|
+
for (const s of snapshots) {
|
|
661
|
+
console.log(
|
|
662
|
+
` ${s.name.padEnd(30)} ${formatSize(s.size).padStart(10)} ${s.createdAt.toISOString()}`,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
} catch (err) {
|
|
667
|
+
console.error(err.message);
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
snapshot
|
|
673
|
+
.command('delete <name>')
|
|
674
|
+
.description('Delete a saved snapshot')
|
|
675
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
676
|
+
.action((name, opts) => {
|
|
677
|
+
try {
|
|
678
|
+
snapshotDelete(name, { dbPath: opts.db });
|
|
679
|
+
console.log(`Snapshot "${name}" deleted.`);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
console.error(err.message);
|
|
682
|
+
process.exit(1);
|
|
487
683
|
}
|
|
488
684
|
});
|
|
489
685
|
|
|
@@ -545,8 +741,16 @@ program
|
|
|
545
741
|
.option('-k, --kind <kind>', 'Filter by kind: function, method, class')
|
|
546
742
|
.option('--file <pattern>', 'Filter by file path pattern')
|
|
547
743
|
.option('--rrf-k <number>', 'RRF k parameter for multi-query ranking', '60')
|
|
744
|
+
.option('--mode <mode>', 'Search mode: hybrid, semantic, keyword (default: hybrid)')
|
|
548
745
|
.option('-j, --json', 'Output as JSON')
|
|
746
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
747
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
549
748
|
.action(async (query, opts) => {
|
|
749
|
+
const validModes = ['hybrid', 'semantic', 'keyword'];
|
|
750
|
+
if (opts.mode && !validModes.includes(opts.mode)) {
|
|
751
|
+
console.error(`Invalid mode "${opts.mode}". Valid: ${validModes.join(', ')}`);
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
550
754
|
await search(query, opts.db, {
|
|
551
755
|
limit: parseInt(opts.limit, 10),
|
|
552
756
|
noTests: resolveNoTests(opts),
|
|
@@ -555,6 +759,7 @@ program
|
|
|
555
759
|
kind: opts.kind,
|
|
556
760
|
filePattern: opts.file,
|
|
557
761
|
rrfK: parseInt(opts.rrfK, 10),
|
|
762
|
+
mode: opts.mode,
|
|
558
763
|
json: opts.json,
|
|
559
764
|
});
|
|
560
765
|
});
|
|
@@ -571,6 +776,9 @@ program
|
|
|
571
776
|
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
572
777
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
573
778
|
.option('-j, --json', 'Output as JSON')
|
|
779
|
+
.option('--limit <number>', 'Max results to return')
|
|
780
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
781
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
574
782
|
.action(async (dir, opts) => {
|
|
575
783
|
const { structureData, formatStructure } = await import('./structure.js');
|
|
576
784
|
const data = structureData(opts.db, {
|
|
@@ -579,8 +787,12 @@ program
|
|
|
579
787
|
sort: opts.sort,
|
|
580
788
|
full: opts.full,
|
|
581
789
|
noTests: resolveNoTests(opts),
|
|
790
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
791
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
582
792
|
});
|
|
583
|
-
if (opts.
|
|
793
|
+
if (opts.ndjson) {
|
|
794
|
+
printNdjson(data, 'directories');
|
|
795
|
+
} else if (opts.json) {
|
|
584
796
|
console.log(JSON.stringify(data, null, 2));
|
|
585
797
|
} else {
|
|
586
798
|
console.log(formatStructure(data));
|
|
@@ -599,15 +811,20 @@ program
|
|
|
599
811
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
600
812
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
601
813
|
.option('-j, --json', 'Output as JSON')
|
|
814
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
815
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
602
816
|
.action(async (opts) => {
|
|
603
817
|
const { hotspotsData, formatHotspots } = await import('./structure.js');
|
|
604
818
|
const data = hotspotsData(opts.db, {
|
|
605
819
|
metric: opts.metric,
|
|
606
820
|
level: opts.level,
|
|
607
821
|
limit: parseInt(opts.limit, 10),
|
|
822
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
608
823
|
noTests: resolveNoTests(opts),
|
|
609
824
|
});
|
|
610
|
-
if (opts.
|
|
825
|
+
if (opts.ndjson) {
|
|
826
|
+
printNdjson(data, 'hotspots');
|
|
827
|
+
} else if (opts.json) {
|
|
611
828
|
console.log(JSON.stringify(data, null, 2));
|
|
612
829
|
} else {
|
|
613
830
|
console.log(formatHotspots(data));
|
|
@@ -657,6 +874,8 @@ program
|
|
|
657
874
|
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
658
875
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
659
876
|
.option('-j, --json', 'Output as JSON')
|
|
877
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
878
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
660
879
|
.action(async (file, opts) => {
|
|
661
880
|
const { analyzeCoChanges, coChangeData, coChangeTopData, formatCoChange, formatCoChangeTop } =
|
|
662
881
|
await import('./cochange.js');
|
|
@@ -683,20 +902,25 @@ program
|
|
|
683
902
|
|
|
684
903
|
const queryOpts = {
|
|
685
904
|
limit: parseInt(opts.limit, 10),
|
|
905
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
686
906
|
minJaccard: opts.minJaccard ? parseFloat(opts.minJaccard) : config.coChange?.minJaccard,
|
|
687
907
|
noTests: resolveNoTests(opts),
|
|
688
908
|
};
|
|
689
909
|
|
|
690
910
|
if (file) {
|
|
691
911
|
const data = coChangeData(file, opts.db, queryOpts);
|
|
692
|
-
if (opts.
|
|
912
|
+
if (opts.ndjson) {
|
|
913
|
+
printNdjson(data, 'partners');
|
|
914
|
+
} else if (opts.json) {
|
|
693
915
|
console.log(JSON.stringify(data, null, 2));
|
|
694
916
|
} else {
|
|
695
917
|
console.log(formatCoChange(data));
|
|
696
918
|
}
|
|
697
919
|
} else {
|
|
698
920
|
const data = coChangeTopData(opts.db, queryOpts);
|
|
699
|
-
if (opts.
|
|
921
|
+
if (opts.ndjson) {
|
|
922
|
+
printNdjson(data, 'pairs');
|
|
923
|
+
} else if (opts.json) {
|
|
700
924
|
console.log(JSON.stringify(data, null, 2));
|
|
701
925
|
} else {
|
|
702
926
|
console.log(formatCoChangeTop(data));
|
|
@@ -760,6 +984,8 @@ program
|
|
|
760
984
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
761
985
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
762
986
|
.option('-j, --json', 'Output as JSON')
|
|
987
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
988
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
763
989
|
.action(async (target, opts) => {
|
|
764
990
|
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
765
991
|
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
@@ -769,6 +995,7 @@ program
|
|
|
769
995
|
complexity(opts.db, {
|
|
770
996
|
target,
|
|
771
997
|
limit: parseInt(opts.limit, 10),
|
|
998
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
772
999
|
sort: opts.sort,
|
|
773
1000
|
aboveThreshold: opts.aboveThreshold,
|
|
774
1001
|
health: opts.health,
|
|
@@ -776,6 +1003,7 @@ program
|
|
|
776
1003
|
kind: opts.kind,
|
|
777
1004
|
noTests: resolveNoTests(opts),
|
|
778
1005
|
json: opts.json,
|
|
1006
|
+
ndjson: opts.ndjson,
|
|
779
1007
|
});
|
|
780
1008
|
});
|
|
781
1009
|
|
|
@@ -788,6 +1016,9 @@ program
|
|
|
788
1016
|
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
789
1017
|
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
790
1018
|
.option('-j, --json', 'Output as JSON')
|
|
1019
|
+
.option('--limit <number>', 'Max results to return')
|
|
1020
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1021
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
791
1022
|
.action(async (opts) => {
|
|
792
1023
|
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
793
1024
|
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
@@ -799,6 +1030,9 @@ program
|
|
|
799
1030
|
kind: opts.kind,
|
|
800
1031
|
noTests: resolveNoTests(opts),
|
|
801
1032
|
json: opts.json,
|
|
1033
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
1034
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1035
|
+
ndjson: opts.ndjson,
|
|
802
1036
|
});
|
|
803
1037
|
});
|
|
804
1038
|
|
|
@@ -812,6 +1046,9 @@ program
|
|
|
812
1046
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
813
1047
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
814
1048
|
.option('-j, --json', 'Output as JSON')
|
|
1049
|
+
.option('--limit <number>', 'Max results to return')
|
|
1050
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1051
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
815
1052
|
.action(async (opts) => {
|
|
816
1053
|
const { communities } = await import('./communities.js');
|
|
817
1054
|
communities(opts.db, {
|
|
@@ -820,6 +1057,84 @@ program
|
|
|
820
1057
|
drift: opts.drift,
|
|
821
1058
|
noTests: resolveNoTests(opts),
|
|
822
1059
|
json: opts.json,
|
|
1060
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
1061
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1062
|
+
ndjson: opts.ndjson,
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
program
|
|
1067
|
+
.command('triage')
|
|
1068
|
+
.description(
|
|
1069
|
+
'Ranked audit queue by composite risk score (connectivity + complexity + churn + role)',
|
|
1070
|
+
)
|
|
1071
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
1072
|
+
.option('-n, --limit <number>', 'Max results to return', '20')
|
|
1073
|
+
.option('--sort <metric>', 'Sort metric: risk | complexity | churn | fan-in | mi', 'risk')
|
|
1074
|
+
.option('--min-score <score>', 'Only show symbols with risk score >= threshold')
|
|
1075
|
+
.option('--role <role>', 'Filter by role (entry, core, utility, adapter, leaf, dead)')
|
|
1076
|
+
.option('-f, --file <path>', 'Scope to a specific file (partial match)')
|
|
1077
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind (function, method, class)')
|
|
1078
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1079
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1080
|
+
.option('-j, --json', 'Output as JSON')
|
|
1081
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1082
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
1083
|
+
.option('--weights <json>', 'Custom weights JSON (e.g. \'{"fanIn":1,"complexity":0}\')')
|
|
1084
|
+
.action(async (opts) => {
|
|
1085
|
+
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
1086
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
1087
|
+
process.exit(1);
|
|
1088
|
+
}
|
|
1089
|
+
if (opts.role && !VALID_ROLES.includes(opts.role)) {
|
|
1090
|
+
console.error(`Invalid role "${opts.role}". Valid: ${VALID_ROLES.join(', ')}`);
|
|
1091
|
+
process.exit(1);
|
|
1092
|
+
}
|
|
1093
|
+
let weights;
|
|
1094
|
+
if (opts.weights) {
|
|
1095
|
+
try {
|
|
1096
|
+
weights = JSON.parse(opts.weights);
|
|
1097
|
+
} catch {
|
|
1098
|
+
console.error('Invalid --weights JSON');
|
|
1099
|
+
process.exit(1);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
const { triage } = await import('./triage.js');
|
|
1103
|
+
triage(opts.db, {
|
|
1104
|
+
limit: parseInt(opts.limit, 10),
|
|
1105
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1106
|
+
sort: opts.sort,
|
|
1107
|
+
minScore: opts.minScore,
|
|
1108
|
+
role: opts.role,
|
|
1109
|
+
file: opts.file,
|
|
1110
|
+
kind: opts.kind,
|
|
1111
|
+
noTests: resolveNoTests(opts),
|
|
1112
|
+
json: opts.json,
|
|
1113
|
+
ndjson: opts.ndjson,
|
|
1114
|
+
weights,
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
program
|
|
1119
|
+
.command('owners [target]')
|
|
1120
|
+
.description('Show CODEOWNERS mapping for files and functions')
|
|
1121
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
1122
|
+
.option('--owner <owner>', 'Filter to a specific owner')
|
|
1123
|
+
.option('--boundary', 'Show cross-owner boundary edges')
|
|
1124
|
+
.option('-f, --file <path>', 'Scope to a specific file')
|
|
1125
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
1126
|
+
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
1127
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1128
|
+
.option('-j, --json', 'Output as JSON')
|
|
1129
|
+
.action(async (target, opts) => {
|
|
1130
|
+
const { owners } = await import('./owners.js');
|
|
1131
|
+
owners(opts.db, {
|
|
1132
|
+
owner: opts.owner,
|
|
1133
|
+
boundary: opts.boundary,
|
|
1134
|
+
file: opts.file || target,
|
|
1135
|
+
kind: opts.kind,
|
|
1136
|
+
noTests: resolveNoTests(opts),
|
|
1137
|
+
json: opts.json,
|
|
823
1138
|
});
|
|
824
1139
|
});
|
|
825
1140
|
|
|
@@ -916,4 +1231,55 @@ program
|
|
|
916
1231
|
}
|
|
917
1232
|
});
|
|
918
1233
|
|
|
1234
|
+
program
|
|
1235
|
+
.command('batch <command> [targets...]')
|
|
1236
|
+
.description(
|
|
1237
|
+
`Run a query against multiple targets in one call. Output is always JSON.\nValid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
|
|
1238
|
+
)
|
|
1239
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
1240
|
+
.option('--from-file <path>', 'Read targets from file (JSON array or newline-delimited)')
|
|
1241
|
+
.option('--stdin', 'Read targets from stdin (JSON array)')
|
|
1242
|
+
.option('--depth <n>', 'Traversal depth passed to underlying command')
|
|
1243
|
+
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
1244
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
1245
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1246
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1247
|
+
.action(async (command, positionalTargets, opts) => {
|
|
1248
|
+
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
1249
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
1250
|
+
process.exit(1);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
let targets;
|
|
1254
|
+
if (opts.fromFile) {
|
|
1255
|
+
const raw = fs.readFileSync(opts.fromFile, 'utf-8').trim();
|
|
1256
|
+
if (raw.startsWith('[')) {
|
|
1257
|
+
targets = JSON.parse(raw);
|
|
1258
|
+
} else {
|
|
1259
|
+
targets = raw.split(/\r?\n/).filter(Boolean);
|
|
1260
|
+
}
|
|
1261
|
+
} else if (opts.stdin) {
|
|
1262
|
+
const chunks = [];
|
|
1263
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1264
|
+
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
1265
|
+
targets = raw.startsWith('[') ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean);
|
|
1266
|
+
} else {
|
|
1267
|
+
targets = positionalTargets;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (!targets || targets.length === 0) {
|
|
1271
|
+
console.error('No targets provided. Pass targets as arguments, --from-file, or --stdin.');
|
|
1272
|
+
process.exit(1);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const batchOpts = {
|
|
1276
|
+
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
|
|
1277
|
+
file: opts.file,
|
|
1278
|
+
kind: opts.kind,
|
|
1279
|
+
noTests: resolveNoTests(opts),
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
batch(command, targets, opts.db, batchOpts);
|
|
1283
|
+
});
|
|
1284
|
+
|
|
919
1285
|
program.parse();
|
package/src/cochange.js
CHANGED
|
@@ -11,6 +11,7 @@ import path from 'node:path';
|
|
|
11
11
|
import { normalizePath } from './constants.js';
|
|
12
12
|
import { closeDb, findDbPath, initSchema, openDb, openReadonlyOrFail } from './db.js';
|
|
13
13
|
import { warn } from './logger.js';
|
|
14
|
+
import { paginateResult } from './paginate.js';
|
|
14
15
|
import { isTestFile } from './queries.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -313,7 +314,8 @@ export function coChangeData(file, customDbPath, opts = {}) {
|
|
|
313
314
|
const meta = getCoChangeMeta(db);
|
|
314
315
|
closeDb(db);
|
|
315
316
|
|
|
316
|
-
|
|
317
|
+
const base = { file: resolvedFile, partners, meta };
|
|
318
|
+
return paginateResult(base, 'partners', { limit: opts.limit, offset: opts.offset });
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
/**
|
|
@@ -365,7 +367,8 @@ export function coChangeTopData(customDbPath, opts = {}) {
|
|
|
365
367
|
const meta = getCoChangeMeta(db);
|
|
366
368
|
closeDb(db);
|
|
367
369
|
|
|
368
|
-
|
|
370
|
+
const base = { pairs, meta };
|
|
371
|
+
return paginateResult(base, 'pairs', { limit: opts.limit, offset: opts.offset });
|
|
369
372
|
}
|
|
370
373
|
|
|
371
374
|
/**
|