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
20 changes: 18 additions & 2 deletions src/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -787,10 +787,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
45 changes: 45 additions & 0 deletions tests/integration/build.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,48 @@ describe('three-tier incremental builds', () => {
expect(output).toContain('No changes detected');
});
});

describe('nested function caller attribution', () => {
let nestDir, nestDbPath;

beforeAll(async () => {
nestDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-nested-'));
// File with an outer function containing a nested helper that is called
fs.writeFileSync(
path.join(nestDir, 'nested.js'),
[
'function outer() {',
' function inner() {',
' return 42;',
' }',
' return inner();',
'}',
'',
].join('\n'),
);
await buildGraph(nestDir, { skipRegistry: true });
nestDbPath = path.join(nestDir, '.codegraph', 'graph.db');
});

afterAll(() => {
if (nestDir) fs.rmSync(nestDir, { recursive: true, force: true });
});

test('enclosing function is the caller of a nested function, not a self-call', () => {
const db = new Database(nestDbPath, { readonly: true });
const edges = db
.prepare(`
SELECT s.name as caller, t.name as callee FROM edges e
JOIN nodes s ON e.source_id = s.id
JOIN nodes t ON e.target_id = t.id
WHERE e.kind = 'calls'
`)
.all();
db.close();
const pairs = edges.map((e) => `${e.caller}->${e.callee}`);
// outer() calls inner() — should produce outer->inner edge
expect(pairs).toContain('outer->inner');
// Should NOT have inner->inner self-call (the old bug)
expect(pairs).not.toContain('inner->inner');
});
});