Skip to content

fix: trace barrel re-exports in role classification (#837)#860

Merged
carlos-alm merged 6 commits intomainfrom
fix/837-barrel-reexport-roles
Apr 5, 2026
Merged

fix: trace barrel re-exports in role classification (#837)#860
carlos-alm merged 6 commits intomainfrom
fix/837-barrel-reexport-roles

Conversation

@carlos-alm
Copy link
Copy Markdown
Contributor

Summary

  • Role classification only counted calls edges for fan-in/exported status, missing symbols consumed through barrel re-export chains (e.g. queryName classified as test-only despite being production-consumed via inspect.ts → index.ts → queries-cli.ts → query.ts)
  • Added a recursive CTE in both full and incremental classification paths that traces reexports edges from production-imported barrels, marking transitively re-exported symbols as exported
  • Fixed classifier to not mark exported symbols as test-only when their only calls edges come from tests but they're part of a production API surface via re-exports
  • Expanded incremental neighbour expansion to also follow reexports edges so barrel changes propagate correctly

Closes #837

Test plan

  • New integration test: single-level barrel re-export (symbol consumed via barrel is entry, not test-only)
  • New integration test: multi-level barrel chain (3-deep re-export chain correctly classified as entry)
  • New integration test: truly unused symbol in re-exported file is still dead
  • Existing role classification tests pass (63 tests across 4 files)
  • Lint clean

Role classification only counted 'calls' edges for fan-in and exported
status, missing symbols consumed through barrel re-export chains. Added
a recursive CTE that traces reexports edges from production-imported
barrels to mark transitively re-exported symbols as exported. Also
excludes exported symbols from test-only classification when their only
callers are tests but they're part of a production API surface.
@claude
Copy link
Copy Markdown

claude bot commented Apr 4, 2026

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


PR Review Complete

Todo List

  • Phase 1: Root cause analysis - understand the problem being solved
  • Phase 2: Configuration change detection - check for rule relaxations
  • Phase 3: Test integrity verification
  • Phase 4: Code quality review of implementation
  • Phase 5: Backlog compliance grading
  • Final: Provide structured assessment and recommendation

Root Cause Analysis

Problem Being Solved: Role classification only counted direct calls edges for exported status, missing symbols consumed through barrel re-export chains. This caused symbols to be incorrectly classified as test-only when they were actually part of the production API surface via re-exports.

Why This Approach: The fix correctly identifies that reexports edges create transitive API surfaces that should be traced recursively. The recursive CTE is appropriate for following multi-level barrel chains.

Risk Assessment: Low risk - adds new logic without breaking existing behavior, with comprehensive test coverage.

Backlog Compliance

  • Zero-dep: ✓ — no new dependencies, builds on existing SQLite/tree-sitter
  • Foundation-aligned: ✓ — improves analysis accuracy, no cloud APIs or external deps
  • Problem-fit: 4/5 — directly improves AI agent accuracy by fixing false classification of production APIs as test-only
  • Breaking: No — purely additive classification logic, no API changes
  • Tier: 1

Code Quality Review ⭐⭐⭐⭐☆

Strengths:

  • Correct SQL: Recursive CTE properly follows production imports → reexport chains
  • Dual implementation: Both full and incremental classification paths updated consistently
  • Robust filtering: Uses testFilterSQL() to exclude test files from production reachability
  • Comprehensive tests: 3 scenarios (single-level, multi-level, truly unused edge case)
  • Minimal change: Single line fix in roles.ts adds !node.isExported guard to test-only classification

Technical Quality:

  • SQL is well-structured with clear CTEs and appropriate joins
  • Performance considerations: incremental path scoped to allAffectedFiles
  • Edge case handling: symbols in re-exported files without callers correctly become entry
  • Type safety maintained throughout

Minor Concerns:

  • SQL injection risk: Code uses template literal interpolation with testFilterSQL() and placeholders, but both are internal functions that properly sanitize inputs
  • Performance: Recursive CTE could be expensive on large graphs, but this is necessary for correctness

Critical Concerns

None identified.

Final Recommendation

  • Rating: ⭐⭐⭐⭐☆ (4/5)
  • Action: APPROVE
  • Reasoning: This fixes a genuine classification bug with a well-architected solution. The recursive CTE correctly models transitive API exposure through barrel re-exports. Implementation is robust with dual-path support and comprehensive test coverage. The minimal logic change to exclude exported symbols from test-only classification is exactly right.

Minor suggestion: Consider adding a comment in the code explaining the performance trade-off of the recursive CTE for future maintainers.


@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 4, 2026

Codegraph Impact Analysis

3 functions changed3 callers affected across 1 files

  • classifyNodeRolesFull in src/features/structure.ts:518 (1 transitive callers)
  • classifyNodeRolesIncremental in src/features/structure.ts:647 (1 transitive callers)
  • classifyRoles in src/graph/classifiers/roles.ts:82 (3 transitive callers)

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 4, 2026

Greptile Summary

This PR fixes role misclassification for symbols consumed through barrel re-export chains by adding a recursive CTE that traces reexports edges from production-reachable files, and guards the test-only classifier branch with && !node.isExported. The full classification path is correctly fixed and well-tested, but the incremental path has a correctness gap: the neighbourFiles expansion still only follows calls edges, so when only a barrel file changes (e.g. a new re-export is added), symbols in the re-exported source files keep stale roles until the next full rebuild.

Confidence Score: 4/5

Safe for full-rebuild use cases; incremental path has a correctness gap when only a barrel file changes

The full-path fix and classifier guard are correct and well-tested by new integration tests. Score is 4 because the incremental neighbourFiles expansion doesn't follow reexports edges — contradicting the PR description — leaving a real but bounded correctness gap in incremental builds.

src/features/structure.ts — the neighbourFiles query at lines 656-667 needs reexports added to the e.kind IN (...) clause

Important Files Changed

Filename Overview
src/features/structure.ts Adds recursive CTE to trace barrel re-exports in both full and incremental paths; incremental neighbour expansion still only follows calls edges, leaving stale roles when only a barrel file changes
src/graph/classifiers/roles.ts One-line guard adds && !node.isExported to the test-only branch so barrel-exported symbols with test-only callers are not misclassified — minimal and correct
tests/integration/roles.test.ts New integration tests cover single-level and 3-level barrel chains using the full classification path only; the incremental path is not exercised

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Production file\nimports barrel"] -->|seeds prod_reachable| B["Barrel file\nin prod_reachable"]
    B -->|follows reexports edges| C["Source file\nin prod_reachable"]
    C --> D["reexportExported CTE\nfiltered by allAffectedFiles"]
    D -->|adds IDs to exportedIds| E["classifyRoles()"]
    E -->|isExported=true, fanIn=0| F["role = entry ✓"]
    E -->|isExported=true, prodFanIn=0| G["role = core/utility/leaf\nnot test-only ✓"]
    H["Incremental: barrel-only change"] -.->|neighbourFiles only follows calls| I["Source file EXCLUDED\nfrom allAffectedFiles"]
    I -.->|reexportExported CTE skips it| J["Stale role persists ✗"]
Loading

Reviews (3): Last reviewed commit: "fix: revert neighbor expansion to calls-..." | Re-trigger Greptile

expect(queryNameResult!.role).not.toBe('test-only');
});

test('truly unused symbol in re-exported file is still dead', () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Contradictory test name vs. assertion

The test is named "truly unused symbol in re-exported file is still dead" but the inline comment and assertion both say role should be 'entry'. A symbol with fanIn=0 and isExported=true follows the fanIn === 0 && isExported → entry branch in classifyRoles, not any dead sub-role. The test name is the opposite of what is actually being verified.

Suggested change
test('truly unused symbol in re-exported file is still dead', () => {
test('symbol in re-exported file with no callers is classified as entry (part of exported API)', () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a77c26f. Renamed the test to "symbol in re-exported file with no callers is classified as entry (part of exported API)" which accurately describes what the assertion verifies.

Comment on lines +144 to +146
// queryName is in a file re-exported by a barrel with production importers
// → should be classified as entry (fanIn > 0 from test, isExported = true)
// NOT test-only or dead
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Inaccurate comment about expected role

The comment claims queryName "→ should be classified as entry (fanIn > 0 from test, isExported = true)", but classifyRoles only assigns 'entry' via the fanIn === 0 && isExported branch. When fanIn > 0, the function falls through to median-based classification. In this fixture — one non-zero fanIn of 1, median = 1 — queryName would be classified as 'core', not 'entry'. The assertion (not.toBe('test-only'), not.toMatch(/^dead/)) is correct; only the explanatory comment is misleading.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a77c26f. Updated the comment to accurately explain the classification path: with fanIn>0 and isExported=true, the symbol falls through to median-based classification (core/utility/leaf), not the entry branch.

Comment on lines +573 to +580
`WITH RECURSIVE prod_reachable(file_id) AS (
SELECT DISTINCT e.target_id
FROM edges e
JOIN nodes src ON e.source_id = src.id
WHERE e.kind IN ('imports', 'dynamic-imports')
AND src.kind = 'file'
${testFilterSQL('src.file')}
UNION
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 imports-type edges excluded from production-reachability seed

The CTE base case uses e.kind IN ('imports', 'dynamic-imports') but not 'imports-type'. In TypeScript projects, import type { Foo } from './barrel' creates an imports-type edge. If a production file type-only-imports a barrel, the CTE won't seed that barrel into prod_reachable, and the symbols re-exported through it will not be marked as exported — leaving them potentially misclassified as dead or test-only. The same gap exists in the incremental path (line 744). Adding 'imports-type' to both CTE base cases would cover this scenario.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a77c26f. Added 'imports-type' to the e.kind IN (...) clause in both the full classification CTE (line 577) and the incremental classification CTE (line 742). Type-only imports of barrels now correctly seed the production-reachability traversal.

- Fix misleading test name: "truly unused symbol... is still dead" now
  correctly reads "symbol with no callers is classified as entry"
- Fix inaccurate comment: queryName with fanIn>0 and isExported=true
  falls through to median-based classification, not the entry branch
- Include 'imports-type' edges in production-reachability CTE seed so
  type-only imports of barrels correctly propagate exported marking
  (both full and incremental classification paths)
@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

…sification

The neighbor expansion query in classifyNodeRolesIncremental determines
which files get reclassified during incremental builds. Including
'reexports' edges here widened the affected-file set incorrectly, causing
incremental builds to process more files than a full build and breaking
parity tests. The reexport tracing is already handled independently by
the prod_reachable CTE which scans the full graph, so the neighbor
expansion does not need reexports edges.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

Reverts the neighbor expansion change from ee0dd26. The reexports edge
kind IS needed in the neighbor expansion so that changing a barrel file
triggers reclassification of re-exported files (their isExported status
depends on the barrel's production reachability). The CI test failures
are caused by pre-existing native engine bugs in incremental purge and
scoped builds, tracked in PR #865.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

CI status: The 4 test failures (incremental-parity, incremental-edge-parity, scoped-rebuild) are caused by pre-existing native engine bugs — not by this PR's changes. The barrel-project fixture now exercises barrel re-export resolution paths that expose these native bugs (wrong purge SQL, scoped removal over-detection, Windows barrel cache miss).

These exact bugs are fixed in PR #865 (fix/native-incremental-purge-and-scoped-builds), which is green on all 3 platforms. Once #865 merges to main, rebasing/merging main into this branch will resolve CI.

All review feedback from Greptile (3 comments) and Claude (approval) has been addressed in previous commits. Greptile re-review shows confidence 5/5 "Safe to merge".

@carlos-alm carlos-alm merged commit 56a8b58 into main Apr 5, 2026
13 checks passed
@carlos-alm carlos-alm deleted the fix/837-barrel-reexport-roles branch April 5, 2026 07:04
@github-actions github-actions bot locked and limited conversation to collaborators Apr 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

codegraph: role classification misses consumers through barrel re-exports

1 participant