@optave/codegraph 2.5.1 → 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 -49
- 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/builder.js +66 -2
- package/src/check.js +432 -0
- package/src/cli.js +361 -6
- 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/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
|
|
|
@@ -498,6 +608,81 @@ registry
|
|
|
498
608
|
}
|
|
499
609
|
});
|
|
500
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);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
501
686
|
// ─── Embedding commands ─────────────────────────────────────────────────
|
|
502
687
|
|
|
503
688
|
program
|
|
@@ -556,8 +741,16 @@ program
|
|
|
556
741
|
.option('-k, --kind <kind>', 'Filter by kind: function, method, class')
|
|
557
742
|
.option('--file <pattern>', 'Filter by file path pattern')
|
|
558
743
|
.option('--rrf-k <number>', 'RRF k parameter for multi-query ranking', '60')
|
|
744
|
+
.option('--mode <mode>', 'Search mode: hybrid, semantic, keyword (default: hybrid)')
|
|
559
745
|
.option('-j, --json', 'Output as JSON')
|
|
746
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
747
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
560
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
|
+
}
|
|
561
754
|
await search(query, opts.db, {
|
|
562
755
|
limit: parseInt(opts.limit, 10),
|
|
563
756
|
noTests: resolveNoTests(opts),
|
|
@@ -566,6 +759,7 @@ program
|
|
|
566
759
|
kind: opts.kind,
|
|
567
760
|
filePattern: opts.file,
|
|
568
761
|
rrfK: parseInt(opts.rrfK, 10),
|
|
762
|
+
mode: opts.mode,
|
|
569
763
|
json: opts.json,
|
|
570
764
|
});
|
|
571
765
|
});
|
|
@@ -582,6 +776,9 @@ program
|
|
|
582
776
|
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
583
777
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
584
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')
|
|
585
782
|
.action(async (dir, opts) => {
|
|
586
783
|
const { structureData, formatStructure } = await import('./structure.js');
|
|
587
784
|
const data = structureData(opts.db, {
|
|
@@ -590,8 +787,12 @@ program
|
|
|
590
787
|
sort: opts.sort,
|
|
591
788
|
full: opts.full,
|
|
592
789
|
noTests: resolveNoTests(opts),
|
|
790
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
791
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
593
792
|
});
|
|
594
|
-
if (opts.
|
|
793
|
+
if (opts.ndjson) {
|
|
794
|
+
printNdjson(data, 'directories');
|
|
795
|
+
} else if (opts.json) {
|
|
595
796
|
console.log(JSON.stringify(data, null, 2));
|
|
596
797
|
} else {
|
|
597
798
|
console.log(formatStructure(data));
|
|
@@ -610,15 +811,20 @@ program
|
|
|
610
811
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
611
812
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
612
813
|
.option('-j, --json', 'Output as JSON')
|
|
814
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
815
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
613
816
|
.action(async (opts) => {
|
|
614
817
|
const { hotspotsData, formatHotspots } = await import('./structure.js');
|
|
615
818
|
const data = hotspotsData(opts.db, {
|
|
616
819
|
metric: opts.metric,
|
|
617
820
|
level: opts.level,
|
|
618
821
|
limit: parseInt(opts.limit, 10),
|
|
822
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
619
823
|
noTests: resolveNoTests(opts),
|
|
620
824
|
});
|
|
621
|
-
if (opts.
|
|
825
|
+
if (opts.ndjson) {
|
|
826
|
+
printNdjson(data, 'hotspots');
|
|
827
|
+
} else if (opts.json) {
|
|
622
828
|
console.log(JSON.stringify(data, null, 2));
|
|
623
829
|
} else {
|
|
624
830
|
console.log(formatHotspots(data));
|
|
@@ -668,6 +874,8 @@ program
|
|
|
668
874
|
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
669
875
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
670
876
|
.option('-j, --json', 'Output as JSON')
|
|
877
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
878
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
671
879
|
.action(async (file, opts) => {
|
|
672
880
|
const { analyzeCoChanges, coChangeData, coChangeTopData, formatCoChange, formatCoChangeTop } =
|
|
673
881
|
await import('./cochange.js');
|
|
@@ -694,20 +902,25 @@ program
|
|
|
694
902
|
|
|
695
903
|
const queryOpts = {
|
|
696
904
|
limit: parseInt(opts.limit, 10),
|
|
905
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
697
906
|
minJaccard: opts.minJaccard ? parseFloat(opts.minJaccard) : config.coChange?.minJaccard,
|
|
698
907
|
noTests: resolveNoTests(opts),
|
|
699
908
|
};
|
|
700
909
|
|
|
701
910
|
if (file) {
|
|
702
911
|
const data = coChangeData(file, opts.db, queryOpts);
|
|
703
|
-
if (opts.
|
|
912
|
+
if (opts.ndjson) {
|
|
913
|
+
printNdjson(data, 'partners');
|
|
914
|
+
} else if (opts.json) {
|
|
704
915
|
console.log(JSON.stringify(data, null, 2));
|
|
705
916
|
} else {
|
|
706
917
|
console.log(formatCoChange(data));
|
|
707
918
|
}
|
|
708
919
|
} else {
|
|
709
920
|
const data = coChangeTopData(opts.db, queryOpts);
|
|
710
|
-
if (opts.
|
|
921
|
+
if (opts.ndjson) {
|
|
922
|
+
printNdjson(data, 'pairs');
|
|
923
|
+
} else if (opts.json) {
|
|
711
924
|
console.log(JSON.stringify(data, null, 2));
|
|
712
925
|
} else {
|
|
713
926
|
console.log(formatCoChangeTop(data));
|
|
@@ -771,6 +984,8 @@ program
|
|
|
771
984
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
772
985
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
773
986
|
.option('-j, --json', 'Output as JSON')
|
|
987
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
988
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
774
989
|
.action(async (target, opts) => {
|
|
775
990
|
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
776
991
|
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
@@ -780,6 +995,7 @@ program
|
|
|
780
995
|
complexity(opts.db, {
|
|
781
996
|
target,
|
|
782
997
|
limit: parseInt(opts.limit, 10),
|
|
998
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
783
999
|
sort: opts.sort,
|
|
784
1000
|
aboveThreshold: opts.aboveThreshold,
|
|
785
1001
|
health: opts.health,
|
|
@@ -787,6 +1003,7 @@ program
|
|
|
787
1003
|
kind: opts.kind,
|
|
788
1004
|
noTests: resolveNoTests(opts),
|
|
789
1005
|
json: opts.json,
|
|
1006
|
+
ndjson: opts.ndjson,
|
|
790
1007
|
});
|
|
791
1008
|
});
|
|
792
1009
|
|
|
@@ -799,6 +1016,9 @@ program
|
|
|
799
1016
|
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
800
1017
|
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
801
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')
|
|
802
1022
|
.action(async (opts) => {
|
|
803
1023
|
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
804
1024
|
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
@@ -810,6 +1030,9 @@ program
|
|
|
810
1030
|
kind: opts.kind,
|
|
811
1031
|
noTests: resolveNoTests(opts),
|
|
812
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,
|
|
813
1036
|
});
|
|
814
1037
|
});
|
|
815
1038
|
|
|
@@ -823,6 +1046,9 @@ program
|
|
|
823
1046
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
824
1047
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
825
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')
|
|
826
1052
|
.action(async (opts) => {
|
|
827
1053
|
const { communities } = await import('./communities.js');
|
|
828
1054
|
communities(opts.db, {
|
|
@@ -831,6 +1057,84 @@ program
|
|
|
831
1057
|
drift: opts.drift,
|
|
832
1058
|
noTests: resolveNoTests(opts),
|
|
833
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,
|
|
834
1138
|
});
|
|
835
1139
|
});
|
|
836
1140
|
|
|
@@ -927,4 +1231,55 @@ program
|
|
|
927
1231
|
}
|
|
928
1232
|
});
|
|
929
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
|
+
|
|
930
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
|
/**
|
package/src/communities.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import Graph from 'graphology';
|
|
3
3
|
import louvain from 'graphology-communities-louvain';
|
|
4
4
|
import { openReadonlyOrFail } from './db.js';
|
|
5
|
+
import { paginateResult, printNdjson } from './paginate.js';
|
|
5
6
|
import { isTestFile } from './queries.js';
|
|
6
7
|
|
|
7
8
|
// ─── Graph Construction ───────────────────────────────────────────────
|
|
@@ -201,7 +202,7 @@ export function communitiesData(customDbPath, opts = {}) {
|
|
|
201
202
|
|
|
202
203
|
const driftScore = Math.round(((splitRatio + mergeRatio) / 2) * 100);
|
|
203
204
|
|
|
204
|
-
|
|
205
|
+
const base = {
|
|
205
206
|
communities: opts.drift ? [] : communities,
|
|
206
207
|
modularity: +modularity.toFixed(4),
|
|
207
208
|
drift: { splitCandidates, mergeCandidates },
|
|
@@ -212,6 +213,7 @@ export function communitiesData(customDbPath, opts = {}) {
|
|
|
212
213
|
driftScore,
|
|
213
214
|
},
|
|
214
215
|
};
|
|
216
|
+
return paginateResult(base, 'communities', { limit: opts.limit, offset: opts.offset });
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
/**
|
|
@@ -238,6 +240,10 @@ export function communitySummaryForStats(customDbPath, opts = {}) {
|
|
|
238
240
|
export function communities(customDbPath, opts = {}) {
|
|
239
241
|
const data = communitiesData(customDbPath, opts);
|
|
240
242
|
|
|
243
|
+
if (opts.ndjson) {
|
|
244
|
+
printNdjson(data, 'communities');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
241
247
|
if (opts.json) {
|
|
242
248
|
console.log(JSON.stringify(data, null, 2));
|
|
243
249
|
return;
|