Skip to content

Commit 4f08082

Browse files
authored
refactor: consolidate CLI by removing 5 redundant commands (#280)
* feat: add normalizeSymbol utility for stable JSON schema Add normalizeSymbol(row, db, hashCache) that returns a consistent 7-field symbol shape (name, kind, file, line, endLine, role, fileHash) across all query and search commands. Update queryNameData, fnDepsData, fnImpactData, explainFunctionImpl, listFunctionsData, rolesData, whereSymbolImpl in queries.js and searchData, multiSearchData, ftsSearchData, hybridSearchData in embedder.js to use normalizeSymbol. Update SQL in listFunctionsData, rolesData, iterListFunctions, iterRoles, _prepareSearch, and ftsSearchData to include end_line and role columns. Export normalizeSymbol from index.js. Add docs/json-schema.md documenting the stable schema. Add 8 unit tests and 7 integration schema conformance tests. Impact: 13 functions changed, 33 affected Impact: 14 functions changed, 42 affected * feat: add expanded edge types — contains, parameter_of, receiver (Phase 2) Build file→definition and parent→child contains edges, parameter_of inverse edges, and receiver edges for method-call dispatch. Add CORE_EDGE_KINDS, STRUCTURAL_EDGE_KINDS, EVERY_EDGE_KIND constants. Exclude structural edges from moduleMapData coupling counts. Scope directory contains-edge cleanup to preserve symbol-level edges. Impact: 3 functions changed, 22 affected * feat: add intraprocedural control flow graph (CFG) construction Add opt-in CFG analysis that builds basic-block control flow graphs from tree-sitter AST for individual functions. Enables complexity-aware impact analysis and opens the path to dataflow (def-use chains). - DB migration v12: cfg_blocks + cfg_edges tables - New src/cfg.js module: CFG_RULES, buildFunctionCFG, buildCFGData, cfgData, cfgToDOT, cfgToMermaid, cfg CLI printer - Builder integration: --cfg flag triggers CFG after complexity pass - CLI: `cfg <name>` command with --format text/dot/mermaid, -j, --ndjson - MCP: cfg tool with name, format, file, kind, pagination props - Exports findFunctionNode from complexity.js for reuse - 24 unit tests + 11 integration tests (35 total) Phase 1: JS/TS/TSX only. Handles if/else, for/while/do-while, switch, try/catch/finally, break/continue (with labels), return/throw. Impact: 27 functions changed, 36 affected * feat: add stored queryable AST nodes (calls, new, string, regex, throw, await) Persist selected AST nodes in a dedicated ast_nodes SQLite table during build, queryable via CLI (codegraph ast), MCP (ast_query), and programmatic API. - DB migration v13: ast_nodes table with indexes on kind, name, file, parent, and (kind,name) - New src/ast.js module: buildAstNodes (extraction), astQueryData/ astQuery (query), AST_NODE_KINDS constant - Builder integration: full-rebuild deletion, incremental cleanup, always-on post-parse extraction (before complexity to preserve _tree) - CLI: codegraph ast [pattern] with -k, -f, -T, -j, --ndjson, --limit, --offset options - MCP: ast_query tool with pattern, kind, file, no_tests, pagination - JS/TS/TSX Phase 1: full AST walk for new/throw/await/string/regex; all languages get call nodes from symbols.calls - Pattern matching uses SQL GLOB with auto-wrapping for substring search - Parent node resolution via narrowest enclosing definition Impact: 12 functions changed, 26 affected * fix: correct misleading comment for break without enclosing loop/switch The comment incorrectly suggested this code path handled break inside switch cases. It actually handles break with no enclosing loop/switch context (invalid syntax) as a no-op. Impact: 2 functions changed, 9 affected * feat: add `exports <file>` command for per-symbol consumer analysis Show all exported symbols of a file with their consumers (who calls each export from other files), re-export detection, and counts. Available as CLI command, MCP tool (file_exports), batch command, and programmatic API (exportsData/fileExports). Impact: 5 functions changed, 8 affected * refactor: consolidate CLI by removing 5 redundant commands - Remove `batch-query` (use `batch where` instead) - Fold `hotspots` into `triage --level file|directory` - Merge `manifesto` into `check` (no args = manifesto, --rules for both) - Replace `explain` with `audit --quick` - Add standalone `path <from> <to>`, deprecate `query --path` All data functions, MCP tools, and programmatic API unchanged. * docs: update all docs to reflect CLI consolidation Update command references across all documentation: - `codegraph explain` → `codegraph audit --quick` - `codegraph hotspots` → `codegraph triage --level` - `codegraph manifesto` → `codegraph check` - Add `codegraph path` references MCP tool references are left unchanged (backward compat). * revert: remove docs changes from CLI consolidation PR Docs updates belong in a separate PR. This reverts commit bd7af72 and the remind-codegraph.sh change from the merge commit. * revert: remove docs changes from CLI consolidation PR Docs updates moved to separate PR #282. * fix: remove unused --limit, --offset, --ndjson from path command * feat: expand node types with parameter, property, constant kinds (Phase 1) Add sub-declaration node extraction to all 9 WASM language extractors, enabling structural queries like "which functions take a Request param?" or "which classes have a userId field?" without reading source code. Schema: migration v11 adds nullable parent_id column with indexes. Builder: insertNode links children to parent via parent_id FK. Extractors: JS/TS, Python, Go, Rust, Java, C#, Ruby, PHP, HCL now emit children arrays for parameters, properties, and constants. Queries: new childrenData() function, children in contextData output. CLI: new `children` command, EVERY_SYMBOL_KIND validation on --kind. MCP: new `symbol_children` tool, extended kind enum on all kind fields. Constants: CORE_SYMBOL_KINDS (10), EXTENDED_SYMBOL_KINDS (3), EVERY_SYMBOL_KIND (13). ALL_SYMBOL_KINDS preserved for backward compat. Native Rust engine: Definition struct gains children field but actual extraction is deferred to Phase 2 — WASM fallback handles new kinds. Impact: 63 functions changed, 62 affected
1 parent aa8fb42 commit 4f08082

File tree

4 files changed

+175
-188
lines changed

4 files changed

+175
-188
lines changed

src/cli.js

Lines changed: 128 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import fs from 'node:fs';
44
import path from 'node:path';
55
import { Command } from 'commander';
66
import { audit } from './audit.js';
7-
import { BATCH_COMMANDS, batch, batchQuery, multiBatchData, splitTargets } from './batch.js';
7+
import { BATCH_COMMANDS, batch, multiBatchData, splitTargets } from './batch.js';
88
import { buildGraph } from './builder.js';
99
import { loadConfig } from './config.js';
1010
import { findCycles, formatCycles } from './cycles.js';
@@ -142,6 +142,7 @@ program
142142
process.exit(1);
143143
}
144144
if (opts.path) {
145+
console.error('Note: "query --path" is deprecated, use "codegraph path <from> <to>" instead');
145146
symbolPath(name, opts.path, opts.db, {
146147
maxDepth: opts.depth ? parseInt(opts.depth, 10) : 10,
147148
edgeKinds: opts.kinds ? opts.kinds.split(',').map((s) => s.trim()) : undefined,
@@ -166,6 +167,36 @@ program
166167
}
167168
});
168169

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,
195+
noTests: resolveNoTests(opts),
196+
json: opts.json,
197+
});
198+
});
199+
169200
program
170201
.command('impact <file>')
171202
.description('Show what depends on this file (transitive)')
@@ -341,43 +372,36 @@ program
341372
});
342373
});
343374

344-
program
345-
.command('explain <target>')
346-
.description('Structural summary of a file or function (no LLM needed)')
347-
.option('-d, --db <path>', 'Path to graph.db')
348-
.option('--depth <n>', 'Recursively explain dependencies up to N levels deep', '0')
349-
.option('-T, --no-tests', 'Exclude test/spec files from results')
350-
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
351-
.option('-j, --json', 'Output as JSON')
352-
.option('--limit <number>', 'Max results to return')
353-
.option('--offset <number>', 'Skip N results (default: 0)')
354-
.option('--ndjson', 'Newline-delimited JSON output')
355-
.action((target, opts) => {
356-
explain(target, opts.db, {
357-
depth: parseInt(opts.depth, 10),
358-
noTests: resolveNoTests(opts),
359-
json: opts.json,
360-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
361-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
362-
ndjson: opts.ndjson,
363-
});
364-
});
365-
366375
program
367376
.command('audit <target>')
368377
.description('Composite report: explain + impact + health metrics per function')
369378
.option('-d, --db <path>', 'Path to graph.db')
370-
.option('--depth <n>', 'Impact analysis depth', '3')
379+
.option('--quick', 'Structural summary only (skip impact analysis and health metrics)')
380+
.option('--depth <n>', 'Impact/explain depth', '3')
371381
.option('-f, --file <path>', 'Scope to file (partial match)')
372382
.option('-k, --kind <kind>', 'Filter by symbol kind')
373383
.option('-T, --no-tests', 'Exclude test/spec files from results')
374384
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
375385
.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)')
376389
.action((target, opts) => {
377390
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
378391
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
379392
process.exit(1);
380393
}
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+
}
381405
audit(target, opts.db, {
382406
depth: parseInt(opts.depth, 10),
383407
file: opts.file,
@@ -443,18 +467,48 @@ program
443467

444468
program
445469
.command('check [ref]')
446-
.description('Run validation predicates against git changes (CI gate)')
470+
.description(
471+
'CI gate: run manifesto rules (no args), diff predicates (with ref/--staged), or both (--rules)',
472+
)
447473
.option('-d, --db <path>', 'Path to graph.db')
448474
.option('--staged', 'Analyze staged changes')
475+
.option('--rules', 'Also run manifesto rules alongside diff predicates')
449476
.option('--cycles', 'Assert no dependency cycles involve changed files')
450477
.option('--blast-radius <n>', 'Assert no function exceeds N transitive callers')
451478
.option('--signatures', 'Assert no function declaration lines were modified')
452479
.option('--boundaries', 'Assert no cross-owner boundary violations')
453480
.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)')
454483
.option('-T, --no-tests', 'Exclude test/spec files from results')
455484
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
456485
.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)')
457489
.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
458512
const { check } = await import('./check.js');
459513
check(opts.db, {
460514
ref,
@@ -467,6 +521,24 @@ program
467521
noTests: resolveNoTests(opts),
468522
json: opts.json,
469523
});
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+
}
470542
});
471543

472544
// ─── New commands ────────────────────────────────────────────────────────
@@ -925,38 +997,6 @@ program
925997
}
926998
});
927999

928-
program
929-
.command('hotspots')
930-
.description(
931-
'Find structural hotspots: files or directories with extreme fan-in, fan-out, or symbol density',
932-
)
933-
.option('-d, --db <path>', 'Path to graph.db')
934-
.option('-n, --limit <number>', 'Number of results', '10')
935-
.option('--metric <metric>', 'fan-in | fan-out | density | coupling', 'fan-in')
936-
.option('--level <level>', 'file | directory', 'file')
937-
.option('-T, --no-tests', 'Exclude test/spec files from results')
938-
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
939-
.option('-j, --json', 'Output as JSON')
940-
.option('--offset <number>', 'Skip N results (default: 0)')
941-
.option('--ndjson', 'Newline-delimited JSON output')
942-
.action(async (opts) => {
943-
const { hotspotsData, formatHotspots } = await import('./structure.js');
944-
const data = hotspotsData(opts.db, {
945-
metric: opts.metric,
946-
level: opts.level,
947-
limit: parseInt(opts.limit, 10),
948-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
949-
noTests: resolveNoTests(opts),
950-
});
951-
if (opts.ndjson) {
952-
printNdjson(data, 'hotspots');
953-
} else if (opts.json) {
954-
console.log(JSON.stringify(data, null, 2));
955-
} else {
956-
console.log(formatHotspots(data));
957-
}
958-
});
959-
9601000
program
9611001
.command('roles')
9621002
.description('Show node role classification: entry, core, utility, adapter, dead, leaf')
@@ -1226,35 +1266,6 @@ program
12261266
});
12271267
});
12281268

1229-
program
1230-
.command('manifesto')
1231-
.description('Evaluate manifesto rules (pass/fail verdicts for code health)')
1232-
.option('-d, --db <path>', 'Path to graph.db')
1233-
.option('-T, --no-tests', 'Exclude test/spec files from results')
1234-
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1235-
.option('-f, --file <path>', 'Scope to file (partial match)')
1236-
.option('-k, --kind <kind>', 'Filter by symbol kind')
1237-
.option('-j, --json', 'Output as JSON')
1238-
.option('--limit <number>', 'Max results to return')
1239-
.option('--offset <number>', 'Skip N results (default: 0)')
1240-
.option('--ndjson', 'Newline-delimited JSON output')
1241-
.action(async (opts) => {
1242-
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1243-
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1244-
process.exit(1);
1245-
}
1246-
const { manifesto } = await import('./manifesto.js');
1247-
manifesto(opts.db, {
1248-
file: opts.file,
1249-
kind: opts.kind,
1250-
noTests: resolveNoTests(opts),
1251-
json: opts.json,
1252-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
1253-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1254-
ndjson: opts.ndjson,
1255-
});
1256-
});
1257-
12581269
program
12591270
.command('communities')
12601271
.description('Detect natural module boundaries using Louvain community detection')
@@ -1289,7 +1300,16 @@ program
12891300
)
12901301
.option('-d, --db <path>', 'Path to graph.db')
12911302
.option('-n, --limit <number>', 'Max results to return', '20')
1292-
.option('--sort <metric>', 'Sort metric: risk | complexity | churn | fan-in | mi', 'risk')
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+
)
12931313
.option('--min-score <score>', 'Only show symbols with risk score >= threshold')
12941314
.option('--role <role>', 'Filter by role (entry, core, utility, adapter, leaf, dead)')
12951315
.option('-f, --file <path>', 'Scope to a specific file (partial match)')
@@ -1301,6 +1321,27 @@ program
13011321
.option('--ndjson', 'Newline-delimited JSON output')
13021322
.option('--weights <json>', 'Custom weights JSON (e.g. \'{"fanIn":1,"complexity":0}\')')
13031323
.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+
13041345
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
13051346
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
13061347
process.exit(1);
@@ -1513,62 +1554,4 @@ program
15131554
}
15141555
});
15151556

1516-
program
1517-
.command('batch-query [targets...]')
1518-
.description(
1519-
`Batch symbol lookup — resolve multiple references in one call.\nDefaults to 'where' command. Accepts comma-separated targets.\nValid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
1520-
)
1521-
.option('-d, --db <path>', 'Path to graph.db')
1522-
.option('-c, --command <cmd>', 'Query command to run (default: where)', 'where')
1523-
.option('--from-file <path>', 'Read targets from file (JSON array or newline-delimited)')
1524-
.option('--stdin', 'Read targets from stdin (JSON array)')
1525-
.option('--depth <n>', 'Traversal depth passed to underlying command')
1526-
.option('-f, --file <path>', 'Scope to file (partial match)')
1527-
.option('-k, --kind <kind>', 'Filter by symbol kind')
1528-
.option('-T, --no-tests', 'Exclude test/spec files from results')
1529-
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1530-
.action(async (positionalTargets, opts) => {
1531-
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1532-
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1533-
process.exit(1);
1534-
}
1535-
1536-
let targets;
1537-
try {
1538-
if (opts.fromFile) {
1539-
const raw = fs.readFileSync(opts.fromFile, 'utf-8').trim();
1540-
if (raw.startsWith('[')) {
1541-
targets = JSON.parse(raw);
1542-
} else {
1543-
targets = raw.split(/\r?\n/).filter(Boolean);
1544-
}
1545-
} else if (opts.stdin) {
1546-
const chunks = [];
1547-
for await (const chunk of process.stdin) chunks.push(chunk);
1548-
const raw = Buffer.concat(chunks).toString('utf-8').trim();
1549-
targets = raw.startsWith('[') ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean);
1550-
} else {
1551-
targets = splitTargets(positionalTargets);
1552-
}
1553-
} catch (err) {
1554-
console.error(`Failed to parse targets: ${err.message}`);
1555-
process.exit(1);
1556-
}
1557-
1558-
if (!targets || targets.length === 0) {
1559-
console.error('No targets provided. Pass targets as arguments, --from-file, or --stdin.');
1560-
process.exit(1);
1561-
}
1562-
1563-
const batchOpts = {
1564-
command: opts.command,
1565-
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
1566-
file: opts.file,
1567-
kind: opts.kind,
1568-
noTests: resolveNoTests(opts),
1569-
};
1570-
1571-
batchQuery(targets, opts.db, batchOpts);
1572-
});
1573-
15741557
program.parse();

src/mcp.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,12 +810,12 @@ export async function startMCPServer(customDbPath, options = {}) {
810810
impactAnalysisData,
811811
moduleMapData,
812812
fileDepsData,
813+
exportsData,
813814
fnDepsData,
814815
fnImpactData,
815816
pathData,
816817
contextData,
817818
childrenData,
818-
exportsData,
819819
explainData,
820820
whereData,
821821
diffImpactData,

0 commit comments

Comments
 (0)