Skip to content

fix: scope-aware caller selection for nested functions#129

Merged
carlos-alm merged 13 commits intomainfrom
feat/path-command
Feb 26, 2026
Merged

fix: scope-aware caller selection for nested functions#129
carlos-alm merged 13 commits intomainfrom
feat/path-command

Conversation

@carlos-alm
Copy link
Contributor

Summary

  • Fixes false-positive [dead] classification for nested/closure functions (e.g. nodeId inside exportMermaid)
  • Root cause: caller selection loop picked the last definition where line <= call.line, creating self-call edges that got filtered out → fan_in = 0[dead]
  • Fix: uses endLine to find the innermost enclosing scope, so calls correctly attribute the outer function as caller
  • All 3 affected nodeId instances in branch-compare.js, export.js, and queries.js now correctly show as [core]

Test plan

  • New integration test verifies outer→inner edge (not inner→inner self-call)
  • All 636 tests pass
  • Lint clean
  • node src/cli.js where nodeId -T shows [core] for all 3 instances
  • node src/cli.js fn-impact nodeId -T shows correct caller chains

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
- 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
- Restore Phase 3 (Architectural Refactoring) to ROADMAP
- Renumber phases 4-8 and all cross-references
- Fix MCP tool count per Greptile review
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
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
…verage

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
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).
Resolve conflicts:
- benchmark.yml: keep version-in-branch-name convention
- README.md: take main's dual-engine performance table
- BUILD-BENCHMARKS.md: main's dual-engine data + incremental/query sections
- update-benchmark-report.js: take main's clearer regex comment

Impact: 12 functions changed, 13 affected
Take main's latest benchmark numbers (PR #125) while preserving
the new incremental rebuild and query latency fields from the branch.
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
@claude
Copy link

claude bot commented Feb 26, 2026

Claude finished @carlos-alm's task —— View job


Reviewing this PR for scope-aware caller selection fix

Review Todo List

  • Phase 1: Root Cause Analysis - Understand the problem and approach
  • Phase 2: Configuration Change Detection - Check for suspicious config changes
  • Phase 3: Test Integrity - Verify test changes are proper
  • Phase 4: Code Quality Review - Examine the fix implementation
  • Provide final rating and recommendation

Phase 1: Root Cause Analysis ✅

Problem Being Solved: The PR fixes false-positive [dead] classification for nested/closure functions. Previously, a function like nodeId inside exportMermaid would be incorrectly marked as [dead] because the caller selection logic created self-call edges that got filtered out, leading to fan_in = 0.

Root Cause Identified: The caller selection loop used line <= call.line to pick the last definition that starts before the call. For nested functions, this would pick the inner function itself as the caller of its own calls, creating inner→inner self-call edges that get filtered out.

Fix Approach: The solution uses endLine to find the innermost enclosing scope. It now checks if the call is within the definition's range (def.line <= call.line <= def.endLine) and picks the narrowest enclosing scope, ensuring calls correctly attribute to the outer function as the caller.

This is a legitimate bug fix addressing a real structural analysis issue, not hiding symptoms.

Phase 2: Configuration Change Detection ✅

No High-Risk Changes Detected:

  • No changes to biome/linting rules
  • No test timeouts or coverage threshold modifications
  • No build configuration changes
  • One minor docs change: removal of "viral" from a LinkedIn reference (neutral cleanup)

The change is focused entirely on the core parsing logic with proper test coverage.

Phase 3: Test Integrity ✅

Excellent Test Coverage: The new integration test at lines 361-404 specifically validates the fix:

test('enclosing function is the caller of a nested function, not a self-call', () => {
  // Creates outer() { function inner() { return 42; } return inner(); }
  // Verifies outer->inner edge exists (correct)
  // Verifies inner->inner edge does NOT exist (the bug this fixes)
});

Test Quality:

  • ✅ Creates isolated test fixture with nested function scenario
  • ✅ Tests positive case: outer->inner edge should exist
  • ✅ Tests negative case: inner->inner self-call should NOT exist
  • ✅ Uses proper test isolation with beforeAll/afterAll cleanup
  • ✅ No weakened assertions or test relaxation

Phase 4: Code Quality Review ✅

Implementation Quality (src/builder.js:788-811):

let caller = null;
let callerSpan = Infinity;  // NEW: Track narrowest enclosing scope
for (const def of symbols.definitions) {
  if (def.line <= call.line) {
    const end = def.endLine || Infinity;
    if (call.line <= end) {  // NEW: Call is within definition range
      const span = end - def.line;
      if (span < callerSpan) {  // NEW: Pick narrowest scope
        // ... assign caller
        callerSpan = span;
      }
    } else if (!caller) {  // NEW: Fallback only if no enclosing scope found
      // ... fallback logic
    }
  }
}

Strengths:

  • Precise scope detection: Uses endLine to determine if call is truly within a function's scope
  • Innermost scope selection: callerSpan ensures nested functions don't get overshadowed by outer ones
  • Robust fallback: Maintains existing behavior for edge cases (definitions without endLine)
  • No breaking changes: Preserves existing logic paths, just makes them more precise
  • Clear logic flow: Code is readable and comments explain the intent

Architecture: The fix targets the exact root cause (caller selection) without side effects on other parsing logic.

Edge Cases Handled:

  • Functions without endLine (graceful degradation with || Infinity)
  • Multiple nested levels (innermost wins via callerSpan comparison)
  • Calls outside any function scope (fallback to file node)

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: False-positive [dead] classification for nested functions due to incorrect self-call edge creation
**Why This Approach**: Uses endLine for precise scope detection instead of simple line ordering - directly targets the root cause
**Risk Assessment**: Low risk - focused fix with excellent test coverage and no configuration changes

## Critical Concerns
None identified. This is a well-implemented bug fix.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: APPROVE
- Reasoning: Excellent root cause analysis, precise implementation, comprehensive test coverage. The fix directly addresses the structural analysis bug without side effects. Only minor deduction for not including specific examples of the 3 affected nodeId instances mentioned in the PR description being verified in tests.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 26, 2026

Greptile Summary

This PR fixes a bug where nested/closure functions were incorrectly classified as [dead] due to false self-call edges. The root cause was that the caller selection algorithm picked the last definition where line <= call.line, which for nested functions meant the inner function itself. The fix uses endLine to identify the innermost enclosing scope by selecting the definition with the smallest span that contains the call line.

  • Enhanced caller selection in builder.js to check if call is within definition's range (call.line <= endLine) and pick the narrowest enclosing scope
  • Added fallback logic for definitions where call is outside the range but no enclosing scope found yet
  • New integration test verifies outer→inner edge exists and inner→inner self-call edge does not exist
  • Minor doc update removes "viral" from LinkedIn post reference

Confidence Score: 5/5

  • This PR is safe to merge - fixes a specific bug with targeted logic change and good test coverage
  • The fix is well-designed and addresses the root cause of the bug by using scope span to identify the correct caller. The algorithm correctly handles nested scopes by picking the narrowest enclosing definition. Test coverage directly validates the bug fix (outer→inner edge exists, inner→inner does not). The fallback logic for definitions without endLine or calls outside all scopes is reasonable. No breaking changes or risky refactoring.
  • No files require special attention

Important Files Changed

Filename Overview
src/builder.js Enhanced caller selection to use endLine for scope-aware attribution, fixing false-positive [dead] classification for nested functions
tests/integration/build.test.js Added test case verifying nested function caller attribution (outer→inner, not inner→inner)

Last reviewed commit: 0cb6324

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@carlos-alm carlos-alm merged commit 72497dc into main Feb 26, 2026
17 checks passed
@carlos-alm carlos-alm deleted the feat/path-command branch February 26, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant