@optave/codegraph 2.5.1 → 3.0.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 +216 -89
- package/package.json +8 -7
- package/src/ast.js +392 -0
- package/src/audit.js +423 -0
- package/src/batch.js +180 -0
- package/src/boundaries.js +346 -0
- package/src/builder.js +375 -92
- package/src/cfg.js +1451 -0
- package/src/change-journal.js +130 -0
- package/src/check.js +432 -0
- package/src/cli.js +734 -107
- package/src/cochange.js +5 -2
- package/src/communities.js +7 -1
- package/src/complexity.js +124 -17
- package/src/config.js +10 -0
- package/src/dataflow.js +1187 -0
- package/src/db.js +96 -0
- package/src/embedder.js +359 -47
- package/src/export.js +305 -0
- package/src/extractors/csharp.js +64 -1
- package/src/extractors/go.js +66 -1
- package/src/extractors/hcl.js +22 -0
- package/src/extractors/java.js +61 -1
- package/src/extractors/javascript.js +142 -0
- package/src/extractors/php.js +79 -0
- package/src/extractors/python.js +134 -0
- package/src/extractors/ruby.js +89 -0
- package/src/extractors/rust.js +71 -1
- package/src/flow.js +4 -4
- package/src/index.js +78 -3
- package/src/manifesto.js +69 -1
- package/src/mcp.js +702 -193
- package/src/owners.js +359 -0
- package/src/paginate.js +37 -2
- package/src/parser.js +8 -0
- package/src/queries.js +590 -50
- package/src/snapshot.js +149 -0
- package/src/structure.js +9 -3
- package/src/triage.js +273 -0
- package/src/viewer.js +948 -0
- package/src/watcher.js +36 -1
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, multiBatchData, splitTargets } 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';
|
|
@@ -14,19 +16,28 @@ import {
|
|
|
14
16
|
MODELS,
|
|
15
17
|
search,
|
|
16
18
|
} from './embedder.js';
|
|
17
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
exportDOT,
|
|
21
|
+
exportGraphML,
|
|
22
|
+
exportGraphSON,
|
|
23
|
+
exportJSON,
|
|
24
|
+
exportMermaid,
|
|
25
|
+
exportNeo4jCSV,
|
|
26
|
+
} from './export.js';
|
|
18
27
|
import { setVerbose } from './logger.js';
|
|
28
|
+
import { printNdjson } from './paginate.js';
|
|
19
29
|
import {
|
|
20
|
-
|
|
30
|
+
children,
|
|
21
31
|
context,
|
|
22
32
|
diffImpact,
|
|
33
|
+
EVERY_SYMBOL_KIND,
|
|
23
34
|
explain,
|
|
24
35
|
fileDeps,
|
|
36
|
+
fileExports,
|
|
25
37
|
fnDeps,
|
|
26
38
|
fnImpact,
|
|
27
39
|
impactAnalysis,
|
|
28
40
|
moduleMap,
|
|
29
|
-
queryName,
|
|
30
41
|
roles,
|
|
31
42
|
stats,
|
|
32
43
|
symbolPath,
|
|
@@ -40,6 +51,7 @@ import {
|
|
|
40
51
|
registerRepo,
|
|
41
52
|
unregisterRepo,
|
|
42
53
|
} from './registry.js';
|
|
54
|
+
import { snapshotDelete, snapshotList, snapshotRestore, snapshotSave } from './snapshot.js';
|
|
43
55
|
import { checkForUpdates, printUpdateNotification } from './update-check.js';
|
|
44
56
|
import { watchProject } from './watcher.js';
|
|
45
57
|
|
|
@@ -83,20 +95,41 @@ function resolveNoTests(opts) {
|
|
|
83
95
|
return config.query?.excludeTests || false;
|
|
84
96
|
}
|
|
85
97
|
|
|
98
|
+
function formatSize(bytes) {
|
|
99
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
100
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
101
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
102
|
+
}
|
|
103
|
+
|
|
86
104
|
program
|
|
87
105
|
.command('build [dir]')
|
|
88
106
|
.description('Parse repo and build graph in .codegraph/graph.db')
|
|
89
107
|
.option('--no-incremental', 'Force full rebuild (ignore file hashes)')
|
|
108
|
+
.option('--dataflow', 'Extract data flow edges (flows_to, returns, mutates)')
|
|
109
|
+
.option('--cfg', 'Build intraprocedural control flow graphs')
|
|
90
110
|
.action(async (dir, opts) => {
|
|
91
111
|
const root = path.resolve(dir || '.');
|
|
92
112
|
const engine = program.opts().engine;
|
|
93
|
-
await buildGraph(root, {
|
|
113
|
+
await buildGraph(root, {
|
|
114
|
+
incremental: opts.incremental,
|
|
115
|
+
engine,
|
|
116
|
+
dataflow: opts.dataflow,
|
|
117
|
+
cfg: opts.cfg,
|
|
118
|
+
});
|
|
94
119
|
});
|
|
95
120
|
|
|
96
121
|
program
|
|
97
122
|
.command('query <name>')
|
|
98
|
-
.description('
|
|
123
|
+
.description('Function-level dependency chain or shortest path between symbols')
|
|
99
124
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
125
|
+
.option('--depth <n>', 'Transitive caller depth', '3')
|
|
126
|
+
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
|
|
127
|
+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
128
|
+
.option('--path <to>', 'Path mode: find shortest path to <to>')
|
|
129
|
+
.option('--kinds <kinds>', 'Path mode: comma-separated edge kinds to follow (default: calls)')
|
|
130
|
+
.option('--reverse', 'Path mode: follow edges backward')
|
|
131
|
+
.option('--from-file <path>', 'Path mode: disambiguate source symbol by file')
|
|
132
|
+
.option('--to-file <path>', 'Path mode: disambiguate target symbol by file')
|
|
100
133
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
101
134
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
102
135
|
.option('-j, --json', 'Output as JSON')
|
|
@@ -104,12 +137,63 @@ program
|
|
|
104
137
|
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
105
138
|
.option('--ndjson', 'Newline-delimited JSON output')
|
|
106
139
|
.action((name, opts) => {
|
|
107
|
-
|
|
140
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
141
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
if (opts.path) {
|
|
145
|
+
console.error('Note: "query --path" is deprecated, use "codegraph path <from> <to>" instead');
|
|
146
|
+
symbolPath(name, opts.path, opts.db, {
|
|
147
|
+
maxDepth: opts.depth ? parseInt(opts.depth, 10) : 10,
|
|
148
|
+
edgeKinds: opts.kinds ? opts.kinds.split(',').map((s) => s.trim()) : undefined,
|
|
149
|
+
reverse: opts.reverse,
|
|
150
|
+
fromFile: opts.fromFile,
|
|
151
|
+
toFile: opts.toFile,
|
|
152
|
+
kind: opts.kind,
|
|
153
|
+
noTests: resolveNoTests(opts),
|
|
154
|
+
json: opts.json,
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
fnDeps(name, opts.db, {
|
|
158
|
+
depth: parseInt(opts.depth, 10),
|
|
159
|
+
file: opts.file,
|
|
160
|
+
kind: opts.kind,
|
|
161
|
+
noTests: resolveNoTests(opts),
|
|
162
|
+
json: opts.json,
|
|
163
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
164
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
165
|
+
ndjson: opts.ndjson,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
program
|
|
171
|
+
.command('path <from> <to>')
|
|
172
|
+
.description('Find shortest path between two symbols')
|
|
173
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
174
|
+
.option('--reverse', 'Follow edges backward')
|
|
175
|
+
.option('--kinds <kinds>', 'Comma-separated edge kinds to follow (default: calls)')
|
|
176
|
+
.option('--from-file <path>', 'Disambiguate source symbol by file')
|
|
177
|
+
.option('--to-file <path>', 'Disambiguate target symbol by file')
|
|
178
|
+
.option('--depth <n>', 'Max traversal depth', '10')
|
|
179
|
+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
180
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
181
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
182
|
+
.option('-j, --json', 'Output as JSON')
|
|
183
|
+
.action((from, to, opts) => {
|
|
184
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
185
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
symbolPath(from, to, opts.db, {
|
|
189
|
+
maxDepth: opts.depth ? parseInt(opts.depth, 10) : 10,
|
|
190
|
+
edgeKinds: opts.kinds ? opts.kinds.split(',').map((s) => s.trim()) : undefined,
|
|
191
|
+
reverse: opts.reverse,
|
|
192
|
+
fromFile: opts.fromFile,
|
|
193
|
+
toFile: opts.toFile,
|
|
194
|
+
kind: opts.kind,
|
|
108
195
|
noTests: resolveNoTests(opts),
|
|
109
196
|
json: opts.json,
|
|
110
|
-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
111
|
-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
112
|
-
ndjson: opts.ndjson,
|
|
113
197
|
});
|
|
114
198
|
});
|
|
115
199
|
|
|
@@ -120,8 +204,17 @@ program
|
|
|
120
204
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
121
205
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
122
206
|
.option('-j, --json', 'Output as JSON')
|
|
207
|
+
.option('--limit <number>', 'Max results to return')
|
|
208
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
209
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
123
210
|
.action((file, opts) => {
|
|
124
|
-
impactAnalysis(file, opts.db, {
|
|
211
|
+
impactAnalysis(file, opts.db, {
|
|
212
|
+
noTests: resolveNoTests(opts),
|
|
213
|
+
json: opts.json,
|
|
214
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
215
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
216
|
+
ndjson: opts.ndjson,
|
|
217
|
+
});
|
|
125
218
|
});
|
|
126
219
|
|
|
127
220
|
program
|
|
@@ -157,31 +250,36 @@ program
|
|
|
157
250
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
158
251
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
159
252
|
.option('-j, --json', 'Output as JSON')
|
|
253
|
+
.option('--limit <number>', 'Max results to return')
|
|
254
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
255
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
160
256
|
.action((file, opts) => {
|
|
161
|
-
fileDeps(file, opts.db, {
|
|
257
|
+
fileDeps(file, opts.db, {
|
|
258
|
+
noTests: resolveNoTests(opts),
|
|
259
|
+
json: opts.json,
|
|
260
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
261
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
262
|
+
ndjson: opts.ndjson,
|
|
263
|
+
});
|
|
162
264
|
});
|
|
163
265
|
|
|
164
266
|
program
|
|
165
|
-
.command('
|
|
166
|
-
.description('
|
|
267
|
+
.command('exports <file>')
|
|
268
|
+
.description('Show exported symbols with per-symbol consumers (who calls each export)')
|
|
167
269
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
168
|
-
.option('--depth <n>', 'Transitive caller depth', '3')
|
|
169
|
-
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
|
|
170
|
-
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
171
270
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
172
271
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
173
272
|
.option('-j, --json', 'Output as JSON')
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
fnDeps(name, opts.db, {
|
|
180
|
-
depth: parseInt(opts.depth, 10),
|
|
181
|
-
file: opts.file,
|
|
182
|
-
kind: opts.kind,
|
|
273
|
+
.option('--limit <number>', 'Max results to return')
|
|
274
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
275
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
276
|
+
.action((file, opts) => {
|
|
277
|
+
fileExports(file, opts.db, {
|
|
183
278
|
noTests: resolveNoTests(opts),
|
|
184
279
|
json: opts.json,
|
|
280
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
281
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
282
|
+
ndjson: opts.ndjson,
|
|
185
283
|
});
|
|
186
284
|
});
|
|
187
285
|
|
|
@@ -195,9 +293,12 @@ program
|
|
|
195
293
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
196
294
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
197
295
|
.option('-j, --json', 'Output as JSON')
|
|
296
|
+
.option('--limit <number>', 'Max results to return')
|
|
297
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
298
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
198
299
|
.action((name, opts) => {
|
|
199
|
-
if (opts.kind && !
|
|
200
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${
|
|
300
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
301
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
201
302
|
process.exit(1);
|
|
202
303
|
}
|
|
203
304
|
fnImpact(name, opts.db, {
|
|
@@ -206,78 +307,105 @@ program
|
|
|
206
307
|
kind: opts.kind,
|
|
207
308
|
noTests: resolveNoTests(opts),
|
|
208
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,
|
|
209
313
|
});
|
|
210
314
|
});
|
|
211
315
|
|
|
212
316
|
program
|
|
213
|
-
.command('
|
|
214
|
-
.description('
|
|
317
|
+
.command('context <name>')
|
|
318
|
+
.description('Full context for a function: source, deps, callers, tests, signature')
|
|
215
319
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
216
|
-
.option('--
|
|
217
|
-
.option('--
|
|
218
|
-
.option('--
|
|
219
|
-
.option('--
|
|
220
|
-
.option('--
|
|
221
|
-
.option('-k, --kind <kind>', 'Filter both symbols by kind')
|
|
320
|
+
.option('--depth <n>', 'Include callee source up to N levels deep', '0')
|
|
321
|
+
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
|
|
322
|
+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
323
|
+
.option('--no-source', 'Metadata only (skip source extraction)')
|
|
324
|
+
.option('--with-test-source', 'Include test source code')
|
|
222
325
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
223
326
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
224
327
|
.option('-j, --json', 'Output as JSON')
|
|
225
|
-
.
|
|
226
|
-
|
|
227
|
-
|
|
328
|
+
.option('--limit <number>', 'Max results to return')
|
|
329
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
330
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
331
|
+
.action((name, opts) => {
|
|
332
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
333
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
228
334
|
process.exit(1);
|
|
229
335
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
reverse: opts.reverse,
|
|
234
|
-
fromFile: opts.fromFile,
|
|
235
|
-
toFile: opts.toFile,
|
|
336
|
+
context(name, opts.db, {
|
|
337
|
+
depth: parseInt(opts.depth, 10),
|
|
338
|
+
file: opts.file,
|
|
236
339
|
kind: opts.kind,
|
|
340
|
+
noSource: !opts.source,
|
|
237
341
|
noTests: resolveNoTests(opts),
|
|
342
|
+
includeTests: opts.withTestSource,
|
|
238
343
|
json: opts.json,
|
|
344
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
345
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
346
|
+
ndjson: opts.ndjson,
|
|
239
347
|
});
|
|
240
348
|
});
|
|
241
349
|
|
|
242
350
|
program
|
|
243
|
-
.command('
|
|
244
|
-
.description('
|
|
351
|
+
.command('children <name>')
|
|
352
|
+
.description('List parameters, properties, and constants of a symbol')
|
|
245
353
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
246
|
-
.option('--
|
|
247
|
-
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
|
|
354
|
+
.option('-f, --file <path>', 'Scope search to symbols in this file (partial match)')
|
|
248
355
|
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
249
|
-
.option('--no-source', 'Metadata only (skip source extraction)')
|
|
250
|
-
.option('--with-test-source', 'Include test source code')
|
|
251
356
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
252
|
-
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
253
357
|
.option('-j, --json', 'Output as JSON')
|
|
358
|
+
.option('--limit <number>', 'Max results to return')
|
|
359
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
254
360
|
.action((name, opts) => {
|
|
255
|
-
if (opts.kind && !
|
|
256
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${
|
|
361
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
362
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
257
363
|
process.exit(1);
|
|
258
364
|
}
|
|
259
|
-
|
|
260
|
-
depth: parseInt(opts.depth, 10),
|
|
365
|
+
children(name, opts.db, {
|
|
261
366
|
file: opts.file,
|
|
262
367
|
kind: opts.kind,
|
|
263
|
-
noSource: !opts.source,
|
|
264
368
|
noTests: resolveNoTests(opts),
|
|
265
|
-
includeTests: opts.withTestSource,
|
|
266
369
|
json: opts.json,
|
|
370
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
371
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
267
372
|
});
|
|
268
373
|
});
|
|
269
374
|
|
|
270
375
|
program
|
|
271
|
-
.command('
|
|
272
|
-
.description('
|
|
376
|
+
.command('audit <target>')
|
|
377
|
+
.description('Composite report: explain + impact + health metrics per function')
|
|
273
378
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
274
|
-
.option('--
|
|
379
|
+
.option('--quick', 'Structural summary only (skip impact analysis and health metrics)')
|
|
380
|
+
.option('--depth <n>', 'Impact/explain depth', '3')
|
|
381
|
+
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
382
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
275
383
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
276
384
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
277
385
|
.option('-j, --json', 'Output as JSON')
|
|
386
|
+
.option('--limit <number>', 'Max results to return (quick mode)')
|
|
387
|
+
.option('--offset <number>', 'Skip N results (quick mode)')
|
|
388
|
+
.option('--ndjson', 'Newline-delimited JSON output (quick mode)')
|
|
278
389
|
.action((target, opts) => {
|
|
279
|
-
|
|
390
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
391
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
if (opts.quick) {
|
|
395
|
+
explain(target, opts.db, {
|
|
396
|
+
depth: parseInt(opts.depth, 10),
|
|
397
|
+
noTests: resolveNoTests(opts),
|
|
398
|
+
json: opts.json,
|
|
399
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
400
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
401
|
+
ndjson: opts.ndjson,
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
audit(target, opts.db, {
|
|
280
406
|
depth: parseInt(opts.depth, 10),
|
|
407
|
+
file: opts.file,
|
|
408
|
+
kind: opts.kind,
|
|
281
409
|
noTests: resolveNoTests(opts),
|
|
282
410
|
json: opts.json,
|
|
283
411
|
});
|
|
@@ -320,6 +448,9 @@ program
|
|
|
320
448
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
321
449
|
.option('-j, --json', 'Output as JSON')
|
|
322
450
|
.option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
|
|
451
|
+
.option('--limit <number>', 'Max results to return')
|
|
452
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
453
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
323
454
|
.action((ref, opts) => {
|
|
324
455
|
diffImpact(opts.db, {
|
|
325
456
|
ref,
|
|
@@ -328,16 +459,99 @@ program
|
|
|
328
459
|
noTests: resolveNoTests(opts),
|
|
329
460
|
json: opts.json,
|
|
330
461
|
format: opts.format,
|
|
462
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
463
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
464
|
+
ndjson: opts.ndjson,
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
program
|
|
469
|
+
.command('check [ref]')
|
|
470
|
+
.description(
|
|
471
|
+
'CI gate: run manifesto rules (no args), diff predicates (with ref/--staged), or both (--rules)',
|
|
472
|
+
)
|
|
473
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
474
|
+
.option('--staged', 'Analyze staged changes')
|
|
475
|
+
.option('--rules', 'Also run manifesto rules alongside diff predicates')
|
|
476
|
+
.option('--cycles', 'Assert no dependency cycles involve changed files')
|
|
477
|
+
.option('--blast-radius <n>', 'Assert no function exceeds N transitive callers')
|
|
478
|
+
.option('--signatures', 'Assert no function declaration lines were modified')
|
|
479
|
+
.option('--boundaries', 'Assert no cross-owner boundary violations')
|
|
480
|
+
.option('--depth <n>', 'Max BFS depth for blast radius (default: 3)')
|
|
481
|
+
.option('-f, --file <path>', 'Scope to file (partial match, manifesto mode)')
|
|
482
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind (manifesto mode)')
|
|
483
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
484
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
485
|
+
.option('-j, --json', 'Output as JSON')
|
|
486
|
+
.option('--limit <number>', 'Max results to return (manifesto mode)')
|
|
487
|
+
.option('--offset <number>', 'Skip N results (manifesto mode)')
|
|
488
|
+
.option('--ndjson', 'Newline-delimited JSON output (manifesto mode)')
|
|
489
|
+
.action(async (ref, opts) => {
|
|
490
|
+
const isDiffMode = ref || opts.staged;
|
|
491
|
+
|
|
492
|
+
if (!isDiffMode && !opts.rules) {
|
|
493
|
+
// No ref, no --staged → run manifesto rules on whole codebase
|
|
494
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
495
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
const { manifesto } = await import('./manifesto.js');
|
|
499
|
+
manifesto(opts.db, {
|
|
500
|
+
file: opts.file,
|
|
501
|
+
kind: opts.kind,
|
|
502
|
+
noTests: resolveNoTests(opts),
|
|
503
|
+
json: opts.json,
|
|
504
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
505
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
506
|
+
ndjson: opts.ndjson,
|
|
507
|
+
});
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Diff predicates mode
|
|
512
|
+
const { check } = await import('./check.js');
|
|
513
|
+
check(opts.db, {
|
|
514
|
+
ref,
|
|
515
|
+
staged: opts.staged,
|
|
516
|
+
cycles: opts.cycles || undefined,
|
|
517
|
+
blastRadius: opts.blastRadius ? parseInt(opts.blastRadius, 10) : undefined,
|
|
518
|
+
signatures: opts.signatures || undefined,
|
|
519
|
+
boundaries: opts.boundaries || undefined,
|
|
520
|
+
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
|
|
521
|
+
noTests: resolveNoTests(opts),
|
|
522
|
+
json: opts.json,
|
|
331
523
|
});
|
|
524
|
+
|
|
525
|
+
// If --rules, also run manifesto after diff predicates
|
|
526
|
+
if (opts.rules) {
|
|
527
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
528
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
const { manifesto } = await import('./manifesto.js');
|
|
532
|
+
manifesto(opts.db, {
|
|
533
|
+
file: opts.file,
|
|
534
|
+
kind: opts.kind,
|
|
535
|
+
noTests: resolveNoTests(opts),
|
|
536
|
+
json: opts.json,
|
|
537
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
538
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
539
|
+
ndjson: opts.ndjson,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
332
542
|
});
|
|
333
543
|
|
|
334
544
|
// ─── New commands ────────────────────────────────────────────────────────
|
|
335
545
|
|
|
336
546
|
program
|
|
337
547
|
.command('export')
|
|
338
|
-
.description('Export dependency graph as DOT
|
|
548
|
+
.description('Export dependency graph as DOT, Mermaid, JSON, GraphML, GraphSON, or Neo4j CSV')
|
|
339
549
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
340
|
-
.option(
|
|
550
|
+
.option(
|
|
551
|
+
'-f, --format <format>',
|
|
552
|
+
'Output format: dot, mermaid, json, graphml, graphson, neo4j',
|
|
553
|
+
'dot',
|
|
554
|
+
)
|
|
341
555
|
.option('--functions', 'Function-level graph instead of file-level')
|
|
342
556
|
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
343
557
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
@@ -361,6 +575,25 @@ program
|
|
|
361
575
|
case 'json':
|
|
362
576
|
output = JSON.stringify(exportJSON(db, exportOpts), null, 2);
|
|
363
577
|
break;
|
|
578
|
+
case 'graphml':
|
|
579
|
+
output = exportGraphML(db, exportOpts);
|
|
580
|
+
break;
|
|
581
|
+
case 'graphson':
|
|
582
|
+
output = JSON.stringify(exportGraphSON(db, exportOpts), null, 2);
|
|
583
|
+
break;
|
|
584
|
+
case 'neo4j': {
|
|
585
|
+
const csv = exportNeo4jCSV(db, exportOpts);
|
|
586
|
+
if (opts.output) {
|
|
587
|
+
const base = opts.output.replace(/\.[^.]+$/, '') || opts.output;
|
|
588
|
+
fs.writeFileSync(`${base}-nodes.csv`, csv.nodes, 'utf-8');
|
|
589
|
+
fs.writeFileSync(`${base}-relationships.csv`, csv.relationships, 'utf-8');
|
|
590
|
+
db.close();
|
|
591
|
+
console.log(`Exported to ${base}-nodes.csv and ${base}-relationships.csv`);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
output = `--- nodes.csv ---\n${csv.nodes}\n\n--- relationships.csv ---\n${csv.relationships}`;
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
364
597
|
default:
|
|
365
598
|
output = exportDOT(db, exportOpts);
|
|
366
599
|
break;
|
|
@@ -376,6 +609,81 @@ program
|
|
|
376
609
|
}
|
|
377
610
|
});
|
|
378
611
|
|
|
612
|
+
program
|
|
613
|
+
.command('plot')
|
|
614
|
+
.description('Generate an interactive HTML dependency graph viewer')
|
|
615
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
616
|
+
.option('--functions', 'Function-level graph instead of file-level')
|
|
617
|
+
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
618
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
619
|
+
.option('--min-confidence <score>', 'Minimum edge confidence threshold (default: 0.5)', '0.5')
|
|
620
|
+
.option('-o, --output <file>', 'Write HTML to file')
|
|
621
|
+
.option('-c, --config <path>', 'Path to .plotDotCfg config file')
|
|
622
|
+
.option('--no-open', 'Do not open in browser')
|
|
623
|
+
.option('--cluster <mode>', 'Cluster nodes: none | community | directory')
|
|
624
|
+
.option('--overlay <list>', 'Comma-separated overlays: complexity,risk')
|
|
625
|
+
.option('--seed <strategy>', 'Seed strategy: all | top-fanin | entry')
|
|
626
|
+
.option('--seed-count <n>', 'Number of seed nodes (default: 30)')
|
|
627
|
+
.option('--size-by <metric>', 'Size nodes by: uniform | fan-in | fan-out | complexity')
|
|
628
|
+
.option('--color-by <mode>', 'Color nodes by: kind | role | community | complexity')
|
|
629
|
+
.action(async (opts) => {
|
|
630
|
+
const { generatePlotHTML, loadPlotConfig } = await import('./viewer.js');
|
|
631
|
+
const os = await import('node:os');
|
|
632
|
+
const db = openReadonlyOrFail(opts.db);
|
|
633
|
+
|
|
634
|
+
let plotCfg;
|
|
635
|
+
if (opts.config) {
|
|
636
|
+
try {
|
|
637
|
+
plotCfg = JSON.parse(fs.readFileSync(opts.config, 'utf-8'));
|
|
638
|
+
} catch (e) {
|
|
639
|
+
console.error(`Failed to load config: ${e.message}`);
|
|
640
|
+
db.close();
|
|
641
|
+
process.exitCode = 1;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
plotCfg = loadPlotConfig(process.cwd());
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Merge CLI flags into config
|
|
649
|
+
if (opts.cluster) plotCfg.clusterBy = opts.cluster;
|
|
650
|
+
if (opts.colorBy) plotCfg.colorBy = opts.colorBy;
|
|
651
|
+
if (opts.sizeBy) plotCfg.sizeBy = opts.sizeBy;
|
|
652
|
+
if (opts.seed) plotCfg.seedStrategy = opts.seed;
|
|
653
|
+
if (opts.seedCount) plotCfg.seedCount = parseInt(opts.seedCount, 10);
|
|
654
|
+
if (opts.overlay) {
|
|
655
|
+
const parts = opts.overlay.split(',').map((s) => s.trim());
|
|
656
|
+
if (!plotCfg.overlays) plotCfg.overlays = {};
|
|
657
|
+
if (parts.includes('complexity')) plotCfg.overlays.complexity = true;
|
|
658
|
+
if (parts.includes('risk')) plotCfg.overlays.risk = true;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const html = generatePlotHTML(db, {
|
|
662
|
+
fileLevel: !opts.functions,
|
|
663
|
+
noTests: resolveNoTests(opts),
|
|
664
|
+
minConfidence: parseFloat(opts.minConfidence),
|
|
665
|
+
config: plotCfg,
|
|
666
|
+
});
|
|
667
|
+
db.close();
|
|
668
|
+
|
|
669
|
+
const outPath = opts.output || path.join(os.tmpdir(), `codegraph-plot-${Date.now()}.html`);
|
|
670
|
+
fs.writeFileSync(outPath, html, 'utf-8');
|
|
671
|
+
console.log(`Plot written to ${outPath}`);
|
|
672
|
+
|
|
673
|
+
if (opts.open !== false) {
|
|
674
|
+
const { execFile } = await import('node:child_process');
|
|
675
|
+
const args =
|
|
676
|
+
process.platform === 'win32'
|
|
677
|
+
? ['cmd', ['/c', 'start', '', outPath]]
|
|
678
|
+
: process.platform === 'darwin'
|
|
679
|
+
? ['open', [outPath]]
|
|
680
|
+
: ['xdg-open', [outPath]];
|
|
681
|
+
execFile(args[0], args[1], (err) => {
|
|
682
|
+
if (err) console.error('Could not open browser:', err.message);
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
379
687
|
program
|
|
380
688
|
.command('cycles')
|
|
381
689
|
.description('Detect circular dependencies in the codebase')
|
|
@@ -498,6 +806,81 @@ registry
|
|
|
498
806
|
}
|
|
499
807
|
});
|
|
500
808
|
|
|
809
|
+
// ─── Snapshot commands ──────────────────────────────────────────────────
|
|
810
|
+
|
|
811
|
+
const snapshot = program
|
|
812
|
+
.command('snapshot')
|
|
813
|
+
.description('Save and restore graph database snapshots');
|
|
814
|
+
|
|
815
|
+
snapshot
|
|
816
|
+
.command('save <name>')
|
|
817
|
+
.description('Save a snapshot of the current graph database')
|
|
818
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
819
|
+
.option('--force', 'Overwrite existing snapshot')
|
|
820
|
+
.action((name, opts) => {
|
|
821
|
+
try {
|
|
822
|
+
const result = snapshotSave(name, { dbPath: opts.db, force: opts.force });
|
|
823
|
+
console.log(`Snapshot saved: ${result.name} (${formatSize(result.size)})`);
|
|
824
|
+
} catch (err) {
|
|
825
|
+
console.error(err.message);
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
snapshot
|
|
831
|
+
.command('restore <name>')
|
|
832
|
+
.description('Restore a snapshot over the current graph database')
|
|
833
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
834
|
+
.action((name, opts) => {
|
|
835
|
+
try {
|
|
836
|
+
snapshotRestore(name, { dbPath: opts.db });
|
|
837
|
+
console.log(`Snapshot "${name}" restored.`);
|
|
838
|
+
} catch (err) {
|
|
839
|
+
console.error(err.message);
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
snapshot
|
|
845
|
+
.command('list')
|
|
846
|
+
.description('List all saved snapshots')
|
|
847
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
848
|
+
.option('-j, --json', 'Output as JSON')
|
|
849
|
+
.action((opts) => {
|
|
850
|
+
try {
|
|
851
|
+
const snapshots = snapshotList({ dbPath: opts.db });
|
|
852
|
+
if (opts.json) {
|
|
853
|
+
console.log(JSON.stringify(snapshots, null, 2));
|
|
854
|
+
} else if (snapshots.length === 0) {
|
|
855
|
+
console.log('No snapshots found.');
|
|
856
|
+
} else {
|
|
857
|
+
console.log(`Snapshots (${snapshots.length}):\n`);
|
|
858
|
+
for (const s of snapshots) {
|
|
859
|
+
console.log(
|
|
860
|
+
` ${s.name.padEnd(30)} ${formatSize(s.size).padStart(10)} ${s.createdAt.toISOString()}`,
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
} catch (err) {
|
|
865
|
+
console.error(err.message);
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
snapshot
|
|
871
|
+
.command('delete <name>')
|
|
872
|
+
.description('Delete a saved snapshot')
|
|
873
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
874
|
+
.action((name, opts) => {
|
|
875
|
+
try {
|
|
876
|
+
snapshotDelete(name, { dbPath: opts.db });
|
|
877
|
+
console.log(`Snapshot "${name}" deleted.`);
|
|
878
|
+
} catch (err) {
|
|
879
|
+
console.error(err.message);
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
501
884
|
// ─── Embedding commands ─────────────────────────────────────────────────
|
|
502
885
|
|
|
503
886
|
program
|
|
@@ -556,8 +939,16 @@ program
|
|
|
556
939
|
.option('-k, --kind <kind>', 'Filter by kind: function, method, class')
|
|
557
940
|
.option('--file <pattern>', 'Filter by file path pattern')
|
|
558
941
|
.option('--rrf-k <number>', 'RRF k parameter for multi-query ranking', '60')
|
|
942
|
+
.option('--mode <mode>', 'Search mode: hybrid, semantic, keyword (default: hybrid)')
|
|
559
943
|
.option('-j, --json', 'Output as JSON')
|
|
944
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
945
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
560
946
|
.action(async (query, opts) => {
|
|
947
|
+
const validModes = ['hybrid', 'semantic', 'keyword'];
|
|
948
|
+
if (opts.mode && !validModes.includes(opts.mode)) {
|
|
949
|
+
console.error(`Invalid mode "${opts.mode}". Valid: ${validModes.join(', ')}`);
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
561
952
|
await search(query, opts.db, {
|
|
562
953
|
limit: parseInt(opts.limit, 10),
|
|
563
954
|
noTests: resolveNoTests(opts),
|
|
@@ -566,6 +957,7 @@ program
|
|
|
566
957
|
kind: opts.kind,
|
|
567
958
|
filePattern: opts.file,
|
|
568
959
|
rrfK: parseInt(opts.rrfK, 10),
|
|
960
|
+
mode: opts.mode,
|
|
569
961
|
json: opts.json,
|
|
570
962
|
});
|
|
571
963
|
});
|
|
@@ -582,6 +974,9 @@ program
|
|
|
582
974
|
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
583
975
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
584
976
|
.option('-j, --json', 'Output as JSON')
|
|
977
|
+
.option('--limit <number>', 'Max results to return')
|
|
978
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
979
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
585
980
|
.action(async (dir, opts) => {
|
|
586
981
|
const { structureData, formatStructure } = await import('./structure.js');
|
|
587
982
|
const data = structureData(opts.db, {
|
|
@@ -590,41 +985,18 @@ program
|
|
|
590
985
|
sort: opts.sort,
|
|
591
986
|
full: opts.full,
|
|
592
987
|
noTests: resolveNoTests(opts),
|
|
988
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
989
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
593
990
|
});
|
|
594
|
-
if (opts.
|
|
991
|
+
if (opts.ndjson) {
|
|
992
|
+
printNdjson(data, 'directories');
|
|
993
|
+
} else if (opts.json) {
|
|
595
994
|
console.log(JSON.stringify(data, null, 2));
|
|
596
995
|
} else {
|
|
597
996
|
console.log(formatStructure(data));
|
|
598
997
|
}
|
|
599
998
|
});
|
|
600
999
|
|
|
601
|
-
program
|
|
602
|
-
.command('hotspots')
|
|
603
|
-
.description(
|
|
604
|
-
'Find structural hotspots: files or directories with extreme fan-in, fan-out, or symbol density',
|
|
605
|
-
)
|
|
606
|
-
.option('-d, --db <path>', 'Path to graph.db')
|
|
607
|
-
.option('-n, --limit <number>', 'Number of results', '10')
|
|
608
|
-
.option('--metric <metric>', 'fan-in | fan-out | density | coupling', 'fan-in')
|
|
609
|
-
.option('--level <level>', 'file | directory', 'file')
|
|
610
|
-
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
611
|
-
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
612
|
-
.option('-j, --json', 'Output as JSON')
|
|
613
|
-
.action(async (opts) => {
|
|
614
|
-
const { hotspotsData, formatHotspots } = await import('./structure.js');
|
|
615
|
-
const data = hotspotsData(opts.db, {
|
|
616
|
-
metric: opts.metric,
|
|
617
|
-
level: opts.level,
|
|
618
|
-
limit: parseInt(opts.limit, 10),
|
|
619
|
-
noTests: resolveNoTests(opts),
|
|
620
|
-
});
|
|
621
|
-
if (opts.json) {
|
|
622
|
-
console.log(JSON.stringify(data, null, 2));
|
|
623
|
-
} else {
|
|
624
|
-
console.log(formatHotspots(data));
|
|
625
|
-
}
|
|
626
|
-
});
|
|
627
|
-
|
|
628
1000
|
program
|
|
629
1001
|
.command('roles')
|
|
630
1002
|
.description('Show node role classification: entry, core, utility, adapter, dead, leaf')
|
|
@@ -668,6 +1040,8 @@ program
|
|
|
668
1040
|
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
669
1041
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
670
1042
|
.option('-j, --json', 'Output as JSON')
|
|
1043
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1044
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
671
1045
|
.action(async (file, opts) => {
|
|
672
1046
|
const { analyzeCoChanges, coChangeData, coChangeTopData, formatCoChange, formatCoChangeTop } =
|
|
673
1047
|
await import('./cochange.js');
|
|
@@ -694,20 +1068,25 @@ program
|
|
|
694
1068
|
|
|
695
1069
|
const queryOpts = {
|
|
696
1070
|
limit: parseInt(opts.limit, 10),
|
|
1071
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
697
1072
|
minJaccard: opts.minJaccard ? parseFloat(opts.minJaccard) : config.coChange?.minJaccard,
|
|
698
1073
|
noTests: resolveNoTests(opts),
|
|
699
1074
|
};
|
|
700
1075
|
|
|
701
1076
|
if (file) {
|
|
702
1077
|
const data = coChangeData(file, opts.db, queryOpts);
|
|
703
|
-
if (opts.
|
|
1078
|
+
if (opts.ndjson) {
|
|
1079
|
+
printNdjson(data, 'partners');
|
|
1080
|
+
} else if (opts.json) {
|
|
704
1081
|
console.log(JSON.stringify(data, null, 2));
|
|
705
1082
|
} else {
|
|
706
1083
|
console.log(formatCoChange(data));
|
|
707
1084
|
}
|
|
708
1085
|
} else {
|
|
709
1086
|
const data = coChangeTopData(opts.db, queryOpts);
|
|
710
|
-
if (opts.
|
|
1087
|
+
if (opts.ndjson) {
|
|
1088
|
+
printNdjson(data, 'pairs');
|
|
1089
|
+
} else if (opts.json) {
|
|
711
1090
|
console.log(JSON.stringify(data, null, 2));
|
|
712
1091
|
} else {
|
|
713
1092
|
console.log(formatCoChangeTop(data));
|
|
@@ -736,8 +1115,8 @@ program
|
|
|
736
1115
|
console.error('Provide a function/entry point name or use --list to see all entry points.');
|
|
737
1116
|
process.exit(1);
|
|
738
1117
|
}
|
|
739
|
-
if (opts.kind && !
|
|
740
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${
|
|
1118
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1119
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
741
1120
|
process.exit(1);
|
|
742
1121
|
}
|
|
743
1122
|
const { flow } = await import('./flow.js');
|
|
@@ -754,6 +1133,70 @@ program
|
|
|
754
1133
|
});
|
|
755
1134
|
});
|
|
756
1135
|
|
|
1136
|
+
program
|
|
1137
|
+
.command('dataflow <name>')
|
|
1138
|
+
.description('Show data flow for a function: parameters, return consumers, mutations')
|
|
1139
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
1140
|
+
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
1141
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
1142
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1143
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1144
|
+
.option('-j, --json', 'Output as JSON')
|
|
1145
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
1146
|
+
.option('--limit <number>', 'Max results to return')
|
|
1147
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1148
|
+
.option('--impact', 'Show data-dependent blast radius')
|
|
1149
|
+
.option('--depth <n>', 'Max traversal depth', '5')
|
|
1150
|
+
.action(async (name, opts) => {
|
|
1151
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1152
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
const { dataflow } = await import('./dataflow.js');
|
|
1156
|
+
dataflow(name, opts.db, {
|
|
1157
|
+
file: opts.file,
|
|
1158
|
+
kind: opts.kind,
|
|
1159
|
+
noTests: resolveNoTests(opts),
|
|
1160
|
+
json: opts.json,
|
|
1161
|
+
ndjson: opts.ndjson,
|
|
1162
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
1163
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1164
|
+
impact: opts.impact,
|
|
1165
|
+
depth: opts.depth,
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
program
|
|
1170
|
+
.command('cfg <name>')
|
|
1171
|
+
.description('Show control flow graph for a function')
|
|
1172
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
1173
|
+
.option('--format <fmt>', 'Output format: text, dot, mermaid', 'text')
|
|
1174
|
+
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
1175
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
1176
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1177
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1178
|
+
.option('-j, --json', 'Output as JSON')
|
|
1179
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
1180
|
+
.option('--limit <number>', 'Max results to return')
|
|
1181
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1182
|
+
.action(async (name, opts) => {
|
|
1183
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1184
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
1185
|
+
process.exit(1);
|
|
1186
|
+
}
|
|
1187
|
+
const { cfg } = await import('./cfg.js');
|
|
1188
|
+
cfg(name, opts.db, {
|
|
1189
|
+
format: opts.format,
|
|
1190
|
+
file: opts.file,
|
|
1191
|
+
kind: opts.kind,
|
|
1192
|
+
noTests: resolveNoTests(opts),
|
|
1193
|
+
json: opts.json,
|
|
1194
|
+
ndjson: opts.ndjson,
|
|
1195
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
1196
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
|
|
757
1200
|
program
|
|
758
1201
|
.command('complexity [target]')
|
|
759
1202
|
.description('Show per-function complexity metrics (cognitive, cyclomatic, nesting depth, MI)')
|
|
@@ -771,15 +1214,18 @@ program
|
|
|
771
1214
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
772
1215
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
773
1216
|
.option('-j, --json', 'Output as JSON')
|
|
1217
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1218
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
774
1219
|
.action(async (target, opts) => {
|
|
775
|
-
if (opts.kind && !
|
|
776
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${
|
|
1220
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1221
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
777
1222
|
process.exit(1);
|
|
778
1223
|
}
|
|
779
1224
|
const { complexity } = await import('./complexity.js');
|
|
780
1225
|
complexity(opts.db, {
|
|
781
1226
|
target,
|
|
782
1227
|
limit: parseInt(opts.limit, 10),
|
|
1228
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
783
1229
|
sort: opts.sort,
|
|
784
1230
|
aboveThreshold: opts.aboveThreshold,
|
|
785
1231
|
health: opts.health,
|
|
@@ -787,29 +1233,36 @@ program
|
|
|
787
1233
|
kind: opts.kind,
|
|
788
1234
|
noTests: resolveNoTests(opts),
|
|
789
1235
|
json: opts.json,
|
|
1236
|
+
ndjson: opts.ndjson,
|
|
790
1237
|
});
|
|
791
1238
|
});
|
|
792
1239
|
|
|
793
1240
|
program
|
|
794
|
-
.command('
|
|
795
|
-
.description('
|
|
1241
|
+
.command('ast [pattern]')
|
|
1242
|
+
.description('Search stored AST nodes (calls, new, string, regex, throw, await) by pattern')
|
|
796
1243
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
1244
|
+
.option('-k, --kind <kind>', 'Filter by AST node kind (call, new, string, regex, throw, await)')
|
|
1245
|
+
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
797
1246
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
798
1247
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
799
|
-
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
800
|
-
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
801
1248
|
.option('-j, --json', 'Output as JSON')
|
|
802
|
-
.
|
|
803
|
-
|
|
804
|
-
|
|
1249
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
1250
|
+
.option('--limit <number>', 'Max results to return')
|
|
1251
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1252
|
+
.action(async (pattern, opts) => {
|
|
1253
|
+
const { AST_NODE_KINDS, astQuery } = await import('./ast.js');
|
|
1254
|
+
if (opts.kind && !AST_NODE_KINDS.includes(opts.kind)) {
|
|
1255
|
+
console.error(`Invalid AST kind "${opts.kind}". Valid: ${AST_NODE_KINDS.join(', ')}`);
|
|
805
1256
|
process.exit(1);
|
|
806
1257
|
}
|
|
807
|
-
|
|
808
|
-
manifesto(opts.db, {
|
|
809
|
-
file: opts.file,
|
|
1258
|
+
astQuery(pattern, opts.db, {
|
|
810
1259
|
kind: opts.kind,
|
|
1260
|
+
file: opts.file,
|
|
811
1261
|
noTests: resolveNoTests(opts),
|
|
812
1262
|
json: opts.json,
|
|
1263
|
+
ndjson: opts.ndjson,
|
|
1264
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
1265
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
813
1266
|
});
|
|
814
1267
|
});
|
|
815
1268
|
|
|
@@ -823,6 +1276,9 @@ program
|
|
|
823
1276
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
824
1277
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
825
1278
|
.option('-j, --json', 'Output as JSON')
|
|
1279
|
+
.option('--limit <number>', 'Max results to return')
|
|
1280
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1281
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
826
1282
|
.action(async (opts) => {
|
|
827
1283
|
const { communities } = await import('./communities.js');
|
|
828
1284
|
communities(opts.db, {
|
|
@@ -831,6 +1287,114 @@ program
|
|
|
831
1287
|
drift: opts.drift,
|
|
832
1288
|
noTests: resolveNoTests(opts),
|
|
833
1289
|
json: opts.json,
|
|
1290
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
1291
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1292
|
+
ndjson: opts.ndjson,
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
program
|
|
1297
|
+
.command('triage')
|
|
1298
|
+
.description(
|
|
1299
|
+
'Ranked audit queue by composite risk score (connectivity + complexity + churn + role)',
|
|
1300
|
+
)
|
|
1301
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
1302
|
+
.option('-n, --limit <number>', 'Max results to return', '20')
|
|
1303
|
+
.option(
|
|
1304
|
+
'--level <level>',
|
|
1305
|
+
'Granularity: function (default) | file | directory. File/directory level shows hotspots',
|
|
1306
|
+
'function',
|
|
1307
|
+
)
|
|
1308
|
+
.option(
|
|
1309
|
+
'--sort <metric>',
|
|
1310
|
+
'Sort metric: risk | complexity | churn | fan-in | mi (function level); fan-in | fan-out | density | coupling (file/directory level)',
|
|
1311
|
+
'risk',
|
|
1312
|
+
)
|
|
1313
|
+
.option('--min-score <score>', 'Only show symbols with risk score >= threshold')
|
|
1314
|
+
.option('--role <role>', 'Filter by role (entry, core, utility, adapter, leaf, dead)')
|
|
1315
|
+
.option('-f, --file <path>', 'Scope to a specific file (partial match)')
|
|
1316
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind (function, method, class)')
|
|
1317
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1318
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1319
|
+
.option('-j, --json', 'Output as JSON')
|
|
1320
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1321
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
1322
|
+
.option('--weights <json>', 'Custom weights JSON (e.g. \'{"fanIn":1,"complexity":0}\')')
|
|
1323
|
+
.action(async (opts) => {
|
|
1324
|
+
if (opts.level === 'file' || opts.level === 'directory') {
|
|
1325
|
+
// Delegate to hotspots for file/directory level
|
|
1326
|
+
const { hotspotsData, formatHotspots } = await import('./structure.js');
|
|
1327
|
+
const metric = opts.sort === 'risk' ? 'fan-in' : opts.sort;
|
|
1328
|
+
const data = hotspotsData(opts.db, {
|
|
1329
|
+
metric,
|
|
1330
|
+
level: opts.level,
|
|
1331
|
+
limit: parseInt(opts.limit, 10),
|
|
1332
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1333
|
+
noTests: resolveNoTests(opts),
|
|
1334
|
+
});
|
|
1335
|
+
if (opts.ndjson) {
|
|
1336
|
+
printNdjson(data, 'hotspots');
|
|
1337
|
+
} else if (opts.json) {
|
|
1338
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1339
|
+
} else {
|
|
1340
|
+
console.log(formatHotspots(data));
|
|
1341
|
+
}
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1346
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
1347
|
+
process.exit(1);
|
|
1348
|
+
}
|
|
1349
|
+
if (opts.role && !VALID_ROLES.includes(opts.role)) {
|
|
1350
|
+
console.error(`Invalid role "${opts.role}". Valid: ${VALID_ROLES.join(', ')}`);
|
|
1351
|
+
process.exit(1);
|
|
1352
|
+
}
|
|
1353
|
+
let weights;
|
|
1354
|
+
if (opts.weights) {
|
|
1355
|
+
try {
|
|
1356
|
+
weights = JSON.parse(opts.weights);
|
|
1357
|
+
} catch {
|
|
1358
|
+
console.error('Invalid --weights JSON');
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
const { triage } = await import('./triage.js');
|
|
1363
|
+
triage(opts.db, {
|
|
1364
|
+
limit: parseInt(opts.limit, 10),
|
|
1365
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1366
|
+
sort: opts.sort,
|
|
1367
|
+
minScore: opts.minScore,
|
|
1368
|
+
role: opts.role,
|
|
1369
|
+
file: opts.file,
|
|
1370
|
+
kind: opts.kind,
|
|
1371
|
+
noTests: resolveNoTests(opts),
|
|
1372
|
+
json: opts.json,
|
|
1373
|
+
ndjson: opts.ndjson,
|
|
1374
|
+
weights,
|
|
1375
|
+
});
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
program
|
|
1379
|
+
.command('owners [target]')
|
|
1380
|
+
.description('Show CODEOWNERS mapping for files and functions')
|
|
1381
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
1382
|
+
.option('--owner <owner>', 'Filter to a specific owner')
|
|
1383
|
+
.option('--boundary', 'Show cross-owner boundary edges')
|
|
1384
|
+
.option('-f, --file <path>', 'Scope to a specific file')
|
|
1385
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
1386
|
+
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
1387
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1388
|
+
.option('-j, --json', 'Output as JSON')
|
|
1389
|
+
.action(async (target, opts) => {
|
|
1390
|
+
const { owners } = await import('./owners.js');
|
|
1391
|
+
owners(opts.db, {
|
|
1392
|
+
owner: opts.owner,
|
|
1393
|
+
boundary: opts.boundary,
|
|
1394
|
+
file: opts.file || target,
|
|
1395
|
+
kind: opts.kind,
|
|
1396
|
+
noTests: resolveNoTests(opts),
|
|
1397
|
+
json: opts.json,
|
|
834
1398
|
});
|
|
835
1399
|
});
|
|
836
1400
|
|
|
@@ -927,4 +1491,67 @@ program
|
|
|
927
1491
|
}
|
|
928
1492
|
});
|
|
929
1493
|
|
|
1494
|
+
program
|
|
1495
|
+
.command('batch <command> [targets...]')
|
|
1496
|
+
.description(
|
|
1497
|
+
`Run a query against multiple targets in one call. Output is always JSON.\nValid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
|
|
1498
|
+
)
|
|
1499
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
1500
|
+
.option('--from-file <path>', 'Read targets from file (JSON array or newline-delimited)')
|
|
1501
|
+
.option('--stdin', 'Read targets from stdin (JSON array)')
|
|
1502
|
+
.option('--depth <n>', 'Traversal depth passed to underlying command')
|
|
1503
|
+
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
1504
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
1505
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1506
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1507
|
+
.action(async (command, positionalTargets, opts) => {
|
|
1508
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1509
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
1510
|
+
process.exit(1);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
let targets;
|
|
1514
|
+
try {
|
|
1515
|
+
if (opts.fromFile) {
|
|
1516
|
+
const raw = fs.readFileSync(opts.fromFile, 'utf-8').trim();
|
|
1517
|
+
if (raw.startsWith('[')) {
|
|
1518
|
+
targets = JSON.parse(raw);
|
|
1519
|
+
} else {
|
|
1520
|
+
targets = raw.split(/\r?\n/).filter(Boolean);
|
|
1521
|
+
}
|
|
1522
|
+
} else if (opts.stdin) {
|
|
1523
|
+
const chunks = [];
|
|
1524
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1525
|
+
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
1526
|
+
targets = raw.startsWith('[') ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean);
|
|
1527
|
+
} else {
|
|
1528
|
+
targets = splitTargets(positionalTargets);
|
|
1529
|
+
}
|
|
1530
|
+
} catch (err) {
|
|
1531
|
+
console.error(`Failed to parse targets: ${err.message}`);
|
|
1532
|
+
process.exit(1);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
if (!targets || targets.length === 0) {
|
|
1536
|
+
console.error('No targets provided. Pass targets as arguments, --from-file, or --stdin.');
|
|
1537
|
+
process.exit(1);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
const batchOpts = {
|
|
1541
|
+
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
|
|
1542
|
+
file: opts.file,
|
|
1543
|
+
kind: opts.kind,
|
|
1544
|
+
noTests: resolveNoTests(opts),
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
// Multi-command mode: items from --from-file / --stdin may be objects with { command, target }
|
|
1548
|
+
const isMulti = targets.length > 0 && typeof targets[0] === 'object' && targets[0].command;
|
|
1549
|
+
if (isMulti) {
|
|
1550
|
+
const data = multiBatchData(targets, opts.db, batchOpts);
|
|
1551
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1552
|
+
} else {
|
|
1553
|
+
batch(command, targets, opts.db, batchOpts);
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
|
|
930
1557
|
program.parse();
|