Skip to content

Commit ff72655

Browse files
feat: add codegraph explain <file|function> command
Structural summary of a file or function entirely from the graph DB — no LLM or API key needed. Composes symbols, edges, imports, and call chains into a single digestible output. File mode (e.g. `codegraph explain src/builder.js`): - Public vs internal API split (based on cross-file callers) - Imports / imported-by - Intra-file data flow - Line count (from node_metrics with MAX(end_line) fallback) Function mode (e.g. `codegraph explain buildGraph`): - Callees, callers, related test files - Summary + signature extraction - Line count and range Also exposed as MCP tool (`explain`) and programmatic API (`explainData`).
1 parent 00f091c commit ff72655

6 files changed

Lines changed: 541 additions & 16 deletions

File tree

src/cli.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import { buildEmbeddings, MODELS, search } from './embedder.js';
1111
import { exportDOT, exportJSON, exportMermaid } from './export.js';
1212
import { setVerbose } from './logger.js';
1313
import {
14+
ALL_SYMBOL_KINDS,
1415
context,
1516
diffImpact,
17+
explain,
1618
fileDeps,
1719
fnDeps,
1820
fnImpact,
@@ -106,11 +108,19 @@ program
106108
.description('Function-level dependencies: callers, callees, and transitive call chain')
107109
.option('-d, --db <path>', 'Path to graph.db')
108110
.option('--depth <n>', 'Transitive caller depth', '3')
111+
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
112+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
109113
.option('-T, --no-tests', 'Exclude test/spec files from results')
110114
.option('-j, --json', 'Output as JSON')
111115
.action((name, opts) => {
116+
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
117+
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
118+
process.exit(1);
119+
}
112120
fnDeps(name, opts.db, {
113121
depth: parseInt(opts.depth, 10),
122+
file: opts.file,
123+
kind: opts.kind,
114124
noTests: !opts.tests,
115125
json: opts.json,
116126
});
@@ -121,11 +131,19 @@ program
121131
.description('Function-level impact: what functions break if this one changes')
122132
.option('-d, --db <path>', 'Path to graph.db')
123133
.option('--depth <n>', 'Max transitive depth', '5')
134+
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
135+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
124136
.option('-T, --no-tests', 'Exclude test/spec files from results')
125137
.option('-j, --json', 'Output as JSON')
126138
.action((name, opts) => {
139+
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
140+
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
141+
process.exit(1);
142+
}
127143
fnImpact(name, opts.db, {
128144
depth: parseInt(opts.depth, 10),
145+
file: opts.file,
146+
kind: opts.kind,
129147
noTests: !opts.tests,
130148
json: opts.json,
131149
});
@@ -136,20 +154,38 @@ program
136154
.description('Full context for a function: source, deps, callers, tests, signature')
137155
.option('-d, --db <path>', 'Path to graph.db')
138156
.option('--depth <n>', 'Include callee source up to N levels deep', '0')
157+
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
158+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
139159
.option('--no-source', 'Metadata only (skip source extraction)')
140160
.option('--include-tests', 'Include test source code')
141161
.option('-T, --no-tests', 'Exclude test files from callers')
142162
.option('-j, --json', 'Output as JSON')
143163
.action((name, opts) => {
164+
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
165+
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
166+
process.exit(1);
167+
}
144168
context(name, opts.db, {
145169
depth: parseInt(opts.depth, 10),
170+
file: opts.file,
171+
kind: opts.kind,
146172
noSource: !opts.source,
147173
noTests: !opts.tests,
148174
includeTests: opts.includeTests,
149175
json: opts.json,
150176
});
151177
});
152178

179+
program
180+
.command('explain <target>')
181+
.description('Structural summary of a file or function (no LLM needed)')
182+
.option('-d, --db <path>', 'Path to graph.db')
183+
.option('-T, --no-tests', 'Exclude test/spec files')
184+
.option('-j, --json', 'Output as JSON')
185+
.action((target, opts) => {
186+
explain(target, opts.db, { noTests: !opts.tests, json: opts.json });
187+
});
188+
153189
program
154190
.command('diff-impact [ref]')
155191
.description('Show impact of git changes (unstaged, staged, or vs a ref)')

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export { getActiveEngine, parseFileAuto, parseFilesAuto } from './parser.js';
4040
export {
4141
contextData,
4242
diffImpactData,
43+
explainData,
4344
fileDepsData,
4445
fnDepsData,
4546
fnImpactData,

src/mcp.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { createRequire } from 'node:module';
99
import { findCycles } from './cycles.js';
1010
import { findDbPath } from './db.js';
11+
import { ALL_SYMBOL_KINDS } from './queries.js';
1112

1213
const REPO_PROP = {
1314
repo: {
@@ -81,6 +82,15 @@ const BASE_TOOLS = [
8182
properties: {
8283
name: { type: 'string', description: 'Function/method/class name (partial match)' },
8384
depth: { type: 'number', description: 'Transitive caller depth', default: 3 },
85+
file: {
86+
type: 'string',
87+
description: 'Scope search to functions in this file (partial match)',
88+
},
89+
kind: {
90+
type: 'string',
91+
enum: ALL_SYMBOL_KINDS,
92+
description: 'Filter to a specific symbol kind',
93+
},
8494
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
8595
},
8696
required: ['name'],
@@ -95,6 +105,15 @@ const BASE_TOOLS = [
95105
properties: {
96106
name: { type: 'string', description: 'Function/method/class name (partial match)' },
97107
depth: { type: 'number', description: 'Max traversal depth', default: 5 },
108+
file: {
109+
type: 'string',
110+
description: 'Scope search to functions in this file (partial match)',
111+
},
112+
kind: {
113+
type: 'string',
114+
enum: ALL_SYMBOL_KINDS,
115+
description: 'Filter to a specific symbol kind',
116+
},
98117
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
99118
},
100119
required: ['name'],
@@ -113,6 +132,15 @@ const BASE_TOOLS = [
113132
description: 'Include callee source up to N levels deep (0=no source, 1=direct)',
114133
default: 0,
115134
},
135+
file: {
136+
type: 'string',
137+
description: 'Scope search to functions in this file (partial match)',
138+
},
139+
kind: {
140+
type: 'string',
141+
enum: ALL_SYMBOL_KINDS,
142+
description: 'Filter to a specific symbol kind',
143+
},
116144
no_source: {
117145
type: 'boolean',
118146
description: 'Skip source extraction (metadata only)',
@@ -128,6 +156,19 @@ const BASE_TOOLS = [
128156
required: ['name'],
129157
},
130158
},
159+
{
160+
name: 'explain',
161+
description:
162+
'Structural summary of a file or function: public/internal API, data flow, dependencies. No LLM needed.',
163+
inputSchema: {
164+
type: 'object',
165+
properties: {
166+
target: { type: 'string', description: 'File path or function name' },
167+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
168+
},
169+
required: ['target'],
170+
},
171+
},
131172
{
132173
name: 'diff_impact',
133174
description: 'Analyze git diff to find which functions changed and their transitive callers',
@@ -299,6 +340,7 @@ export async function startMCPServer(customDbPath, options = {}) {
299340
fnDepsData,
300341
fnImpactData,
301342
contextData,
343+
explainData,
302344
diffImpactData,
303345
listFunctionsData,
304346
} = await import('./queries.js');
@@ -368,23 +410,32 @@ export async function startMCPServer(customDbPath, options = {}) {
368410
case 'fn_deps':
369411
result = fnDepsData(args.name, dbPath, {
370412
depth: args.depth,
413+
file: args.file,
414+
kind: args.kind,
371415
noTests: args.no_tests,
372416
});
373417
break;
374418
case 'fn_impact':
375419
result = fnImpactData(args.name, dbPath, {
376420
depth: args.depth,
421+
file: args.file,
422+
kind: args.kind,
377423
noTests: args.no_tests,
378424
});
379425
break;
380426
case 'context':
381427
result = contextData(args.name, dbPath, {
382428
depth: args.depth,
429+
file: args.file,
430+
kind: args.kind,
383431
noSource: args.no_source,
384432
noTests: args.no_tests,
385433
includeTests: args.include_tests,
386434
});
387435
break;
436+
case 'explain':
437+
result = explainData(args.target, dbPath, { noTests: args.no_tests });
438+
break;
388439
case 'diff_impact':
389440
result = diffImpactData(dbPath, {
390441
staged: args.staged,

0 commit comments

Comments
 (0)