Skip to content

Commit 72497dc

Browse files
fix: scope-aware caller selection for nested functions (#129)
* feat: add codegraph path for A→B symbol pathfinding Add `codegraph path <from> <to>` — BFS shortest-path search on the call graph. Given two symbol names, finds the shortest call chain with hop count, intermediate nodes, edge kinds, and alternate path count. Supports --reverse, --max-depth, --kinds, --from-file/--to-file, -T, -j, -k flags. Exposed as symbol_path MCP tool. Impact: 4 functions changed, 3 affected * docs: add Titan Paradigm use case, update docs with roles/co-change/path - Create docs/use-cases/titan-paradigm.md — maps Johannes R.'s multi-agent codebase cleanup architecture (RECON, GAUNTLET, GLOBAL SYNC, STATE MACHINE) to codegraph commands, roadmap items, and post-LLM-integration recommendations - Update roadmap/BACKLOG.md: mark #4 (node classification), #9 (git change coupling), #1 (dead code), #2 (shortest path), #12 (execution flow) as DONE; add 6 new Titan Paradigm-inspired items (#21-#26): composite audit, batch querying, triage priority queue, change validation predicates, graph snapshots, MCP orchestration tools - Update README.md: add roles + co-change to features table, differentiators, commands section, agent template, common flags, comparison table; update MCP tool count 18 → 19 - Update docs/recommended-practices.md: update MCP tool count and tool list, add roles/co-change/path to CLAUDE.md template and developer workflow, add "Understand architectural roles" and "Surface hidden coupling" sections, add co-change step to setup checklist - Add full examples with real output for roles, co-change, and path to docs/examples/CLI.md and docs/examples/MCP.md - Update GitHub repo description with new capabilities * docs: restore Architecture Refactoring phase, fix references - Restore Phase 3 (Architectural Refactoring) to ROADMAP - Renumber phases 4-8 and all cross-references - Fix MCP tool count per Greptile review * fix: correct MCP tool counts and backlog ID collisions Address Greptile review comments on #121: - Update MCP tool counts from 18/19 to 21 (22 in multi-repo mode) across README, recommended-practices, dogfood skill, titan-paradigm - Add missing execution_flow and list_entry_points to tool enumeration - Renumber new backlog items 21-26 → 27-32 to avoid collision with existing items 21-22 * feat: add token savings benchmark (codegraph vs raw navigation) Adds a benchmark suite that measures how much codegraph reduces token usage when AI agents navigate the Next.js codebase (~4k TS files). - scripts/token-benchmark-issues.js: 5 real Next.js PRs as test cases - scripts/token-benchmark.js: runner using Claude Agent SDK (baseline vs codegraph MCP), with --perf flag for build/query benchmarks - scripts/update-token-report.js: JSON → markdown report generator - docs/benchmarks/: methodology docs and placeholder report Impact: 21 functions changed, 7 affected * feat: extend benchmarks with incremental builds and expanded query coverage benchmark.js now measures no-op rebuilds, 1-file rebuilds, and query latency (fn-deps, fn-impact, path, roles) alongside full builds. update-benchmark-report.js renders new Incremental Rebuilds and Query Latency sections in BUILD-BENCHMARKS.md and adds incremental/query rows to the README performance table. All new fields are additive for backward compatibility. Impact: 5 functions changed, 2 affected * ci: include version in automated benchmark commits and PRs Extract version from benchmark result JSON and include it in branch names, commit messages, PR titles, and PR bodies across all 4 benchmark jobs (build, embedding, query, incremental). * fix: update remaining 19-tool references to 21-tool in README * docs: remove "viral" from titan paradigm LinkedIn reference * fix: use endLine for scope-aware caller selection in nested functions Nested/closure functions (e.g. nodeId inside exportMermaid) were incorrectly classified as [dead] because the caller selection loop picked the last definition where line <= call.line, creating self-call edges that got filtered out. Now uses endLine to find the innermost enclosing scope, so calls within an outer function correctly attribute the outer function as caller rather than the nested function itself. Fixes false-positive [dead] for nodeId in branch-compare.js, export.js, and queries.js. Impact: 1 functions changed, 17 affected --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent b13fa31 commit 72497dc

2 files changed

Lines changed: 63 additions & 2 deletions

File tree

src/builder.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -787,10 +787,26 @@ export async function buildGraph(rootDir, opts = {}) {
787787
for (const call of symbols.calls) {
788788
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
789789
let caller = null;
790+
let callerSpan = Infinity;
790791
for (const def of symbols.definitions) {
791792
if (def.line <= call.line) {
792-
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
793-
if (row) caller = row;
793+
const end = def.endLine || Infinity;
794+
if (call.line <= end) {
795+
// Call is inside this definition's range — pick narrowest
796+
const span = end - def.line;
797+
if (span < callerSpan) {
798+
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
799+
if (row) {
800+
caller = row;
801+
callerSpan = span;
802+
}
803+
}
804+
} else if (!caller) {
805+
// Fallback: def starts before call but call is past end
806+
// Only use if we haven't found an enclosing scope yet
807+
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
808+
if (row) caller = row;
809+
}
794810
}
795811
}
796812
if (!caller) caller = fileNodeRow;

tests/integration/build.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,3 +357,48 @@ describe('three-tier incremental builds', () => {
357357
expect(output).toContain('No changes detected');
358358
});
359359
});
360+
361+
describe('nested function caller attribution', () => {
362+
let nestDir, nestDbPath;
363+
364+
beforeAll(async () => {
365+
nestDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-nested-'));
366+
// File with an outer function containing a nested helper that is called
367+
fs.writeFileSync(
368+
path.join(nestDir, 'nested.js'),
369+
[
370+
'function outer() {',
371+
' function inner() {',
372+
' return 42;',
373+
' }',
374+
' return inner();',
375+
'}',
376+
'',
377+
].join('\n'),
378+
);
379+
await buildGraph(nestDir, { skipRegistry: true });
380+
nestDbPath = path.join(nestDir, '.codegraph', 'graph.db');
381+
});
382+
383+
afterAll(() => {
384+
if (nestDir) fs.rmSync(nestDir, { recursive: true, force: true });
385+
});
386+
387+
test('enclosing function is the caller of a nested function, not a self-call', () => {
388+
const db = new Database(nestDbPath, { readonly: true });
389+
const edges = db
390+
.prepare(`
391+
SELECT s.name as caller, t.name as callee FROM edges e
392+
JOIN nodes s ON e.source_id = s.id
393+
JOIN nodes t ON e.target_id = t.id
394+
WHERE e.kind = 'calls'
395+
`)
396+
.all();
397+
db.close();
398+
const pairs = edges.map((e) => `${e.caller}->${e.callee}`);
399+
// outer() calls inner() — should produce outer->inner edge
400+
expect(pairs).toContain('outer->inner');
401+
// Should NOT have inner->inner self-call (the old bug)
402+
expect(pairs).not.toContain('inner->inner');
403+
});
404+
});

0 commit comments

Comments
 (0)