Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/use-cases/titan-paradigm.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## The Problem

In a [viral LinkedIn post](https://www.linkedin.com/posts/johannesr314_claude-vibecoding-activity-7432157088828678144-CiI_), **Johannes R.**, Senior Software Engineer at Google, described the #1 challenge of "vibe coding": keeping a fast-moving codebase from rotting.
In a [LinkedIn post](https://www.linkedin.com/posts/johannesr314_claude-vibecoding-activity-7432157088828678144-CiI_), **Johannes R.**, Senior Software Engineer at Google, described the #1 challenge of "vibe coding": keeping a fast-moving codebase from rotting.

His answer isn't a better prompt. It's a different architecture.

Expand Down
38 changes: 36 additions & 2 deletions src/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -496,17 +496,27 @@ export async function buildGraph(rootDir, opts = {}) {
const deleteMetricsForFile = db.prepare(
'DELETE FROM node_metrics WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
);
let deleteComplexityForFile;
try {
deleteComplexityForFile = db.prepare(
'DELETE FROM function_complexity WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
);
} catch {
deleteComplexityForFile = null;
}
for (const relPath of removed) {
deleteEmbeddingsForFile?.run(relPath);
deleteEdgesForFile.run({ f: relPath });
deleteMetricsForFile.run(relPath);
deleteComplexityForFile?.run(relPath);
deleteNodesForFile.run(relPath);
}
for (const item of parseChanges) {
const relPath = item.relPath || normalizePath(path.relative(rootDir, item.file));
deleteEmbeddingsForFile?.run(relPath);
deleteEdgesForFile.run({ f: relPath });
deleteMetricsForFile.run(relPath);
deleteComplexityForFile?.run(relPath);
deleteNodesForFile.run(relPath);
}

Expand Down Expand Up @@ -787,10 +797,26 @@ export async function buildGraph(rootDir, opts = {}) {
for (const call of symbols.calls) {
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
let caller = null;
let callerSpan = Infinity;
for (const def of symbols.definitions) {
if (def.line <= call.line) {
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
if (row) caller = row;
const end = def.endLine || Infinity;
if (call.line <= end) {
// Call is inside this definition's range — pick narrowest
const span = end - def.line;
if (span < callerSpan) {
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
if (row) {
caller = row;
callerSpan = span;
}
}
} else if (!caller) {
// Fallback: def starts before call but call is past end
// Only use if we haven't found an enclosing scope yet
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
if (row) caller = row;
}
}
}
if (!caller) caller = fileNodeRow;
Expand Down Expand Up @@ -980,6 +1006,14 @@ export async function buildGraph(rootDir, opts = {}) {
debug(`Role classification failed: ${err.message}`);
}

// Compute per-function complexity metrics (cognitive, cyclomatic, nesting)
try {
const { buildComplexityMetrics } = await import('./complexity.js');
await buildComplexityMetrics(db, allSymbols, rootDir, engineOpts);
} catch (err) {
debug(`Complexity analysis failed: ${err.message}`);
}

const nodeCount = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
info(`Graph built: ${nodeCount} nodes, ${edgeCount} edges`);
info(`Stored in ${dbPath}`);
Expand Down
49 changes: 49 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,55 @@ program
});
});

program
.command('complexity [target]')
.description('Show per-function complexity metrics (cognitive, cyclomatic, nesting depth)')
.option('-d, --db <path>', 'Path to graph.db')
.option('-n, --limit <number>', 'Max results', '20')
.option('--sort <metric>', 'Sort by: cognitive | cyclomatic | nesting', 'cognitive')
.option('--above-threshold', 'Only functions exceeding warn thresholds')
.option('-f, --file <path>', 'Scope to file (partial match)')
.option('-k, --kind <kind>', 'Filter by symbol kind')
.option('-T, --no-tests', 'Exclude test/spec files from results')
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
.option('-j, --json', 'Output as JSON')
.action(async (target, opts) => {
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
process.exit(1);
}
const { complexity } = await import('./complexity.js');
complexity(opts.db, {
target,
limit: parseInt(opts.limit, 10),
sort: opts.sort,
aboveThreshold: opts.aboveThreshold,
file: opts.file,
kind: opts.kind,
noTests: resolveNoTests(opts),
json: opts.json,
});
});

program
.command('branch-compare <base> <target>')
.description('Compare code structure between two branches/refs')
.option('--depth <n>', 'Max transitive caller depth', '3')
.option('-T, --no-tests', 'Exclude test/spec files')
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
.option('-j, --json', 'Output as JSON')
.option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
.action(async (base, target, opts) => {
const { branchCompare } = await import('./branch-compare.js');
await branchCompare(base, target, {
engine: program.opts().engine,
depth: parseInt(opts.depth, 10),
noTests: resolveNoTests(opts),
json: opts.json,
format: opts.format,
});
});

program
.command('watch [dir]')
.description('Watch project for file changes and incrementally update the graph')
Expand Down
Loading