Skip to content

fix(graph): prune legacy target-resolved File identities - #1158

Open
prondubuisi wants to merge 2 commits into
vitali87:mainfrom
prondubuisi:fix/prune-legacy-target-resolved-file-identity
Open

fix(graph): prune legacy target-resolved File identities#1158
prondubuisi wants to merge 2 commits into
vitali87:mainfrom
prondubuisi:fix/prune-legacy-target-resolved-file-identity

Conversation

@prondubuisi

@prondubuisi prondubuisi commented Aug 10, 2026

Copy link
Copy Markdown

Summary

  • Incremental --update-graph now sweeps pre-GHSA-85gg File nodes that were keyed on a leaf-dereferenced external symlink target. Those nodes sit outside the repo, so the prune containment gate skipped them and they leaked forever.
  • A still-present external symlink is recognized when cached_resolve_posix(repo_path / path) equals the stored absolute_path and that path disagrees with cached_file_identity_posix(...).
  • A deleted external symlink is swept only when this project owns the node (CONTAINS_PACKAGE|CONTAINS_FOLDER|CONTAINS_FILE* from Project). Sibling-project files that share a relative path stay untouched.
  • In-repo symlink records that share the real file's key are left alone (the self-heal case from fix: key File nodes on the link path so a deleted symlink is prunable (#1154) #1155). Symlinked ancestors are compared via parent-resolve identity so they do not false-positive.

Type of Change

  • Bug fix
  • New feature
  • Performance improvement
  • Refactoring (no functional changes)
  • Documentation
  • CI/CD or tooling
  • Dependencies

Related Issues

Fixes #1156

Test Plan

  • Unit tests pass (uv run pytest codebase_rag/tests/test_graph_updater_pruning.py — 21 passed, including the 16 existing prune tests)
  • New tests added (TestPruneLegacyTargetResolvedFileIdentity)
  • Integration tests pass (make test-integration, requires Docker — not run locally; unit tests cover the prune decision table)
  • Manual testing (describe below)

RED verification (tests against unpatched _prune_orphan_nodes):

  • test_prune_removes_live_legacy_external_symlink_file failed (0 deletes)
  • test_prune_removes_deleted_legacy_external_symlink_when_project_owned failed (0 deletes)
  • keep-safe tests still passed on the unpatched code

GREEN after the fix: 21/21 in test_graph_updater_pruning.py. Also 48 passed across prune + incremental updater tests.

Keep-safe coverage:

  • unowned outside File with a missing relative path (sibling project) is not deleted
  • in-repo symlink sharing the target's key is not deleted
  • file under a symlinked ancestor is not deleted (identity uses parent-resolve, same as ingest)

Pre-commit on the changed files: ruff, ruff format, ty, README generation, bandit.

Checklist

AI assistance

AI assistance was used to locate the prune/identity code, implement the focused fix, and write the RED-verified regression tests. The final diff was reviewed against CONTRIBUTING.md (constants, no Any, conventional commit, tests required) and run through the pre-commit hooks listed above.

Why this is safe

File nodes are unique on absolute_path. Deleting an outside key cannot collapse an in-repo file. Deleting an in-repo identity mismatch would be unsafe (it is the real file's MERGE key), so that case is explicitly not pruned. Ownership is consulted only for missing-on-disk outside keys, so test_prune_skips_other_projects still holds.

Summary by CodeRabbit

  • Bug Fixes
    • Improved cleanup of stale file references, including symlinked and externally located files.
    • Preserved valid in-repository symlinks and files under symlinked directories.
    • Avoided removing external files unless they are confirmed to belong to the project.
    • Added more accurate project file path tracking for graph-based operations.
    • Improved handling of unresolved file locations during project cleanup to reduce unintended deletions.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c3dfc30b-2022-4397-8982-70b4d811e46d

📥 Commits

Reviewing files that changed from the base of the PR and between 6160064 and 52e9301.

📒 Files selected for processing (2)
  • codebase_rag/graph_updater.py
  • codebase_rag/tests/test_graph_updater_pruning.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • codebase_rag/tests/test_graph_updater_pruning.py
  • codebase_rag/graph_updater.py

📝 Walkthrough

Walkthrough

The graph updater now reconciles legacy target-resolved external File nodes. It uses filesystem identity checks and a project-owned path query while preserving valid in-repository symlink records.

Changes

Legacy external File identity pruning

Layer / File(s) Summary
Project-owned path query
codebase_rag/constants/graph.py
Adds the paths schema key and a Cypher query for distinct project-owned File absolute paths.
Symlink-aware orphan pruning
codebase_rag/graph_updater.py
Resolves file identities, detects legacy external targets, defers missing paths, and removes records after ownership verification.
Pruning behavior validation
codebase_rag/tests/test_graph_updater_pruning.py
Tests live and missing external targets, ownership parameters, unowned files, and in-repository symlink cases.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GraphUpdater
  participant FileSystem
  participant GraphDatabase
  GraphUpdater->>FileSystem: resolve recorded relative path identity
  FileSystem-->>GraphUpdater: return target or missing path
  GraphUpdater->>GraphDatabase: query project-owned absolute paths
  GraphDatabase-->>GraphUpdater: return matching paths
  GraphUpdater->>GraphDatabase: delete verified stale File nodes
Loading

Possibly related PRs

Suggested reviewers: vitali87

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: pruning legacy target-resolved File identities.
Description check ✅ Passed The description follows the template and documents the fix, issue, tests, validation results, and checklist status.
Linked Issues check ✅ Passed The changes address issue #1156 by reconciling legacy external symlink identities while preserving ownership and symlink safety cases.
Out of Scope Changes check ✅ Passed The constants, pruning logic, ownership query, and regression tests directly support the linked issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@codebase_rag/graph_updater.py`:
- Around line 2119-2139: Update the outside-repository File handling in the
graph-updater flow to batch live legacy-target matches for project-ownership
verification before adding them to deletion candidates, rather than immediately
appending them from _is_live_legacy_target_resolved_file. Ensure only targets
confirmed as owned by the current project are deleted, and add a regression test
covering a live local symlink whose ownership query returns no matching path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ba2ecf79-915e-4be6-ba5a-335a604b5128

📥 Commits

Reviewing files that changed from the base of the PR and between b32c256 and 6160064.

📒 Files selected for processing (3)
  • codebase_rag/constants/graph.py
  • codebase_rag/graph_updater.py
  • codebase_rag/tests/test_graph_updater_pruning.py

Comment thread codebase_rag/graph_updater.py Outdated
@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This change extends incremental graph pruning for legacy File identities associated with external symlinks. A two-project reproduction confirmed that pruning a live external symlink in one project can globally remove a File identity still owned by another project, deleting that project’s containment edge and attached graph data. The deletion path needs to preserve shared File nodes until their final Project owner is removed.

Confidence Score: 4/5

Not safe to merge until pruning no longer globally deletes File identities that remain owned by another project.

The failure was reproduced through the real pruning method with a live external symlink and two project owners, showing that the shared node and both ownership edges are removed.

Files Needing Attention: codebase_rag/graph_updater.py needs an owner check or ownership-edge-only removal before invoking the global File deletion query; the related deletion query is in codebase_rag/constants/graph.py.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex reproduced a focused pruning of the shared external File across two projects and captured evidence for validation.
  • T-Rex produced a proof for a posted P1 finding; the review comment describes the finding in detail.
  • T-Rex validated contract behavior by inspecting the graph updater flow and after-capture state, confirming a single delete and empty ownership after pruning.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Live legacy external-symlink pruning globally deletes a File shared by another Project

    • Bug
      • At codebase_rag/graph_updater.py:2134-2137, a live external symlink whose legacy File key is its resolved external target is considered an orphan. The later deletion at 2162-2164 uses CYPHER_DELETE_FILE, which matches the File exclusively by absolute_path and DETACH DELETEs it. When two Projects own that same File identity through external symlinks, pruning project A deletes the shared File node and thereby removes project B's ownership edge and attached graph data.
    • Cause
      • The live legacy-target branch bypasses _owned_file_absolute_paths() and has no check for another Project owner. CYPHER_DELETE_FILE in codebase_rag/constants/graph.py:371 is intentionally global: MATCH (f:File {absolute_path: $path}) DETACH DELETE f.
    • Fix
      • Before globally deleting a live legacy target-resolved File, query for all Project owners and retain the node when another owner exists; alternatively, remove only project A's ownership relationship and delete the File only when it has no remaining Project ownership path. Add a two-project shared-external-symlink regression test.

    T-Rex Ran code and verified through T-Rex

Prompt To Fix All With AI
### Issue 1
codebase_rag/graph_updater.py:2134-2137
**Live symlink pruning deletes shared File identities**

A live external symlink whose legacy `File.absolute_path` is its resolved target is queued for deletion here without checking whether another Project owns the same File. The subsequent globally keyed `CYPHER_DELETE_FILE` performs a `DETACH DELETE`, so pruning project A removes project B’s containment edge and all graph data attached to the shared node. Check for remaining Project owners before deleting the File, or remove only this project’s ownership relationship and delete the File only after its final owner is gone.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(graph): prune legacy target-resolved..." | Re-trigger Greptile

Comment thread codebase_rag/graph_updater.py Outdated
Comment on lines +2134 to +2137
if self._is_live_legacy_target_resolved_file(
path, abs_path, repo_abs
):
orphans.append((path, abs_path))

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.

P1 Live symlink pruning deletes shared File identities

A live external symlink whose legacy File.absolute_path is its resolved target is queued for deletion here without checking whether another Project owns the same File. The subsequent globally keyed CYPHER_DELETE_FILE performs a DETACH DELETE, so pruning project A removes project B’s containment edge and all graph data attached to the shared node. Check for remaining Project owners before deleting the File, or remove only this project’s ownership relationship and delete the File only after its final owner is gone.

Artifacts

Focused two-project shared external File pruning reproduction source

  • This executable reproduction creates project A and B as owners of one external File identity, runs the real pruning method for project A, and asserts the global delete removes the shared node and both owners.

Before pruning, the shared File has project A and project B owners

  • The successful before run records that the File exists and is owned by both project-a and project-b before pruning.

After project A pruning, global File deletion removes both owners

  • The successful after run exercises GraphUpdater pruning and records the global Cypher delete, no ownership query, and removal of the File plus both owner edges.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: codebase_rag/graph_updater.py
Line: 2134-2137

Comment:
**Live symlink pruning deletes shared File identities**

A live external symlink whose legacy `File.absolute_path` is its resolved target is queued for deletion here without checking whether another Project owns the same File. The subsequent globally keyed `CYPHER_DELETE_FILE` performs a `DETACH DELETE`, so pruning project A removes project B’s containment edge and all graph data attached to the shared node. Check for remaining Project owners before deleting the File, or remove only this project’s ownership relationship and delete the File only after its final owner is gone.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@vitali87

Copy link
Copy Markdown
Owner

Thanks for picking up #1156 so quickly — the batched ownership verification (_owned_file_absolute_paths via CONTAINS_*) is the right direction and addresses CodeRabbit's Major (a node only a sibling reaches is now excluded).

Maintainer note on the residual Greptile P1 (shared File identity), so you can decide how far to take it:

NodeLabel.FILE has a global unique constraint on absolute_path (NODE_UNIQUE_CONSTRAINTS, one constraint per label — not per-project). So if two projects in one shared graph both symlink to the same external target, they MERGE onto a single File node that both containment chains reach. _owned_file_absolute_paths returns that node for either project (both "own" it), so this project's prune still deletes the node the sibling depends on — the ownership check confirms an owner, not the sole owner.

Two ways to close it, your call:

  1. Sole-ownership guard — only sweep when this project is the only Project reaching that absolute_path (e.g. count distinct Project ancestors and require exactly one). Fully correct, a bit more query work.
  2. Surgical delete — instead of CYPHER_DELETE_FILE by bare absolute_path, detach only this project's CONTAINS_FILE edge and delete the node only if it becomes an orphan. Also correct, more invasive.
  3. Document as accepted limitation — the trigger (two projects, same external target, both pre-GHSA-85gg, incremental update) is astronomically narrow, so a comment scoping it may be defensible.

Given how narrow this is, I lean 1 or 3. Whichever you choose, please add a regression test with two projects sharing one external-target File and assert the sibling's node survives. Nice work — this is close.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Incremental --update-graph does not reconcile legacy target-resolved File identities

2 participants