diff --git a/docs/use-cases/titan-paradigm.md b/docs/use-cases/titan-paradigm.md index d9d1a6e9..11f10e15 100644 --- a/docs/use-cases/titan-paradigm.md +++ b/docs/use-cases/titan-paradigm.md @@ -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. diff --git a/src/builder.js b/src/builder.js index 12a4ec4b..6bff8470 100644 --- a/src/builder.js +++ b/src/builder.js @@ -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; diff --git a/tests/integration/build.test.js b/tests/integration/build.test.js index cc1c9971..4ef0e4b7 100644 --- a/tests/integration/build.test.js +++ b/tests/integration/build.test.js @@ -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'); + }); +});