Skip to content

fix(git): recover deleted symbols from the base revision (supersedes #72)#79

Merged
EladBezalel merged 4 commits into
frontops-dev:mainfrom
itayper:fix/deletion-only-base-revision
Jul 9, 2026
Merged

fix(git): recover deleted symbols from the base revision (supersedes #72)#79
EladBezalel merged 4 commits into
frontops-dev:mainfrom
itayper:fix/deletion-only-base-revision

Conversation

@itayper

@itayper itayper commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Closes #74. Supersedes #72.

Continues @noam-aharon20's work in #72 (branch preserved on top of the original commit). Opened as a fresh PR because we lacked push access to that PR's branch.

Summary

domino affected silently drops the dependents of a deleted symbol. With --unified=0 (always used by get_diff), removing lines produces a @@ -X,Y +Z,0 @@ hunk whose new-side count is 0, so it contributed nothing to changed_lines. Downstream, no symbol was resolved, reference traversal was skipped, and every project depending on the deleted symbol was dropped — only the file's own package stayed marked.

What changed since the first review

The first revision anchored a +Z,0 deletion to its new-side line Z and looked that up in the current file. As raised in review, that heuristic is incorrect:

  • After a deletion, line Z belongs to whatever symbol now occupies it in the new file — not the deleted one. Deleting an entire top-level symbol either mis-attributes the change to the next symbol or resolves nothing, so the deleted symbol's real dependents were still dropped.
  • It also left a dead has_hunks branch (flagged by @EladBezalel and CodeRabbit).

This revision replaces the heuristic with the semantically correct approach — issue direction (2): recover the deleted symbols from the base revision.

How it works now

  1. git.rsLINE_RE now captures both sides of the hunk header. A pure-deletion hunk (W == 0) records its old-side lines X..X+Y in a new ChangedFile.deleted_lines field; all other hunks contribute new-side lines to changed_lines as before. The dead has_hunks branch is removed. New helper get_file_at_revision reads git show <merge-base>:./path.
  2. analyzer.rs — the top-level-symbol resolution is split out of find_node_at_line into find_top_level_symbols(&FileSemanticData, …) so it can run against a one-off base-revision parse (parse_source) that never enters self.files. New find_deleted_symbols(file, base_source, deleted_lines) parses the base snapshot and resolves the enclosing top-level symbol at each removed line (de-duplicated).
  3. core.rs — for each changed file with deleted_lines, fetch the base content at the merge-base and merge the recovered symbols into the reference-traversal set. They're traced through the current import graph — consumers still import { Foo } even after Foo is deleted — so their projects are correctly reported.

This handles both shapes uniformly:

Deletion shape Resolves to Result
Member of a still-present declaration (object property, switch case, interface field) enclosing symbol dependents traced ✅
Entire exported declaration the deleted symbol itself dependents traced ✅ (the case the anchor could not solve)

Validation

Two-package Rush fixture; package-b consumes lookup, User, and OBSOLETE from package-a:

Change Before this PR After
Delete an entry from an exported object [package-a] [package-a, package-b]
Delete a field from an exported interface [package-a] [package-a, package-b]
Delete an entire exported symbol [package-a] [package-a, package-b]
Delete a symbol no project consumes [package-a] [package-a] ✅ (stays precise)

cargo test --lib — 212 passed. cargo clippy --all-targets --all-features and cargo fmt --check clean. New integration tests: test_deletion_of_whole_exported_symbol_affects_dependents, test_deletion_of_member_affects_dependents_via_enclosing_symbol.

Real-world validation

Reproduced against a real large Rush monorepo on the exact commit that slipped through CI. The change removed one field from an exported schema object in a shared client package — a pure deletion hunk:

@@ -193 +192,0 @@ export const entitySchema = z.object({
-  removedField: z.string(),

Diffing that commit against its parent (--base <parent> --head <commit>):

Affected projects Notes
Before (1.4.0) 8 only the packages owning the directly-changed files
After (this PR) 49 + every project that consumes the schema
Delta +41, −0 purely additive — no project the old version reported was dropped

The 41 newly-detected projects are exactly the downstream services and apps that import the schema and would break when the field is removed — the set CI skipped. Debug log confirms the mechanism:

Recovered 1 deleted symbol(s) from base revision of ".../<pkg>.ts": ["entitySchema"]
Processing symbol 'entitySchema' ...
Found 2 local references for 'entitySchema'

Out of scope / known limitations

  • Whole-file deletion — a fully deleted file isn't in the analyzer, so it still falls back to package-level marking.
  • Local-reference-only consumption — deleting a whole symbol that a project reaches only through a local reference from another symbol (whose binding the deletion breaks) isn't traced, because the current-file symbol table no longer links the now-dangling reference. Direct cross-file imports of a deleted symbol (the common case) are traced.

Both are follow-ups; neither regresses today's behavior.

noam-aharon20 and others added 2 commits June 10, 2026 10:21
`git diff --unified=0` emits a `+Z,0` hunk when lines are removed. These
deletion-only hunks contributed nothing to `changed_lines`, so downstream
symbol extraction in `core.rs` found no traceable symbol and skipped the
reference-traversal cascade entirely ("No traceable symbols found ...
skipping reference traversal"). The file's owning package was still marked
affected, but every *dependent* of the deleted symbol was missed.

Impact: any change whose diff is purely a deletion — removing a property
from an exported object, a case from a switch, an enum member, an overload —
under-reports affected projects to just the package the file lives in. In a
real monorepo, deleting one entry from an exported lookup object that 26
projects transitively consume reported `["@lama/selectors"]` (1 project)
instead of the 22 projects that actually reference it.

Fix: a `W == 0` deletion hunk now anchors to its new-side line `Z` — the
line just before the deletion point, which is still inside the enclosing
top-level symbol — so the AST lookup resolves that symbol and traces its
references normally. Clamped to 1 for deletions at the top of a file
(`+0,0`).

Known limitation (left for a follow-up): deleting an *entire* top-level
symbol leaves no enclosing node at the anchor line, so the AST lookup finds
nothing and behavior falls back to today's package-level marking. Catching
that requires parsing the base revision at the old-side range.

Adds a regression test and corrects two existing tests that had encoded the
buggy "deletion contributes nothing" assumption.
Reworks the deletion-only fix in response to review. The previous commit
anchored a `+Z,0` deletion hunk to its new-side line `Z` and fed that into
the current-file AST lookup. That heuristic is incorrect: after a deletion,
line `Z` belongs to whatever symbol now occupies it in the *new* file, so
deleting an entire top-level symbol either mis-attributes the change to the
next symbol or resolves nothing — the deleted symbol's real dependents were
still dropped.

Instead, capture the hunk's *old-side* range `-X,Y` in a new
`ChangedFile.deleted_lines` field and, downstream, re-parse the file at the
base revision (`git show <merge-base>:./path`) to resolve the top-level
symbol that enclosed each removed line. This handles both shapes uniformly:

- removing a member of a still-present declaration (object property, switch
  case, interface field) resolves to the enclosing symbol, and
- removing an entire exported declaration resolves to that symbol itself —
  the case the anchor heuristic could not solve.

The recovered symbols are traced through the *current* import graph, where
consumers still import them, so their projects are correctly reported
affected. Deletions whose lines resolve to no declaration contribute nothing
(no regression).

Also drops the now-unreachable `has_hunks` empty-`changed_lines` branch that
review flagged: every non-deletion hunk yields new-side lines and every
deletion yields `deleted_lines`, so a file with hunks is never empty on both.

analyzer: split the top-level-symbol resolution out of `find_node_at_line`
into `find_top_level_symbols(&FileSemanticData, ...)` so it can run against a
one-off base-revision parse (`parse_source`) that never enters `self.files`;
profiler timing now wraps the call in `find_node_at_line`.

Known limitation (unchanged from before): a whole-file deletion, and a
deleted symbol reachable only via a local reference from another symbol whose
binding the deletion breaks, still fall back to package-level marking.

Tests: rewrite the three git unit tests for `deleted_lines` semantics, add a
leading-symbol-deletion case, add analyzer unit tests for `find_deleted_symbols`,
and add integration tests for whole-symbol and member deletion tracing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@itayper, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6373e035-d11a-4366-b611-1a0976f1f506

📥 Commits

Reviewing files that changed from the base of the PR and between 968f918 and 20e1d9c.

📒 Files selected for processing (4)
  • src/git.rs
  • src/report.rs
  • src/types.rs
  • tests/integration_test.rs
📝 Walkthrough

Walkthrough

This PR fixes affected-project detection for pure-deletion diff hunks. ChangedFile gains a deleted_lines field; parse_diff records old-side line ranges for deletion-only hunks; a new get_file_at_revision fetches base-revision file content; the analyzer gains find_deleted_symbols/find_top_level_symbols to resolve deleted symbols; core.rs uses these to record causes and extend traversal. Existing test fixtures and new integration tests are updated accordingly.

Changes

Deletion-only change tracing

Layer / File(s) Summary
ChangedFile schema update
src/types.rs
Adds deleted_lines: Vec<usize> field and Default derive with updated documentation.
Diff parsing for deletion hunks
src/git.rs
Strengthens hunk-header regex, adds get_file_at_revision, and rewrites parse_diff to populate deleted_lines from old-side ranges for pure-deletion hunks; updates/adds tests.
Symbol resolution for deleted lines
src/semantic/analyzer.rs
Adds parse_source and shared find_top_level_symbols helper; implements find_deleted_symbols to resolve enclosing symbols for deleted lines; adds unit tests.
Core reporting and traversal
src/core.rs
Reads base-revision file content, recovers deleted symbols, records change causes at line: 0, extends the unique-symbol set used for traversal, and updates debug logging.
Integration regression tests
tests/integration_test.rs
Adds a lib/app monorepo scaffold and two tests verifying dependents are marked affected for whole-symbol and member-level deletions.
Test fixture updates
src/lockfile.rs, src/named_inputs.rs
Updates ChangedFile literals in existing tests to include the new deleted_lines field.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Core as core.rs
  participant Git as get_file_at_revision
  participant Analyzer as find_deleted_symbols
  participant Report

  Core->>Git: fetch file content at merge-base revision
  Git-->>Core: base file contents (or None)
  Core->>Analyzer: resolve deleted_lines to enclosing symbols
  Analyzer-->>Core: recovered deleted symbols
  Core->>Report: record direct change cause (line: 0)
  Core->>Core: extend unique_symbols with deleted_symbols
  Core->>Core: run reference traversal across affected projects
Loading

Possibly related PRs

  • frontops-dev/domino#48: Both PRs modify src/core.rs's reference-tracing logic around symbol pre-deduplication/unique_symbols.
  • frontops-dev/domino#63: Both PRs modify src/git.rs's diff hunk header parsing and parse_diff line-range handling.
  • frontops-dev/domino#66: Both PRs modify how src/core.rs maps a changed file to its owning package(s).

Suggested reviewers: EladBezalel

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address #74 by recovering deleted symbols from pure-deletion hunks and tracing dependents for both member and whole-symbol deletions.
Out of Scope Changes check ✅ Passed The additional refactors and test updates are directly supporting the deletion-tracing fix and do not appear unrelated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: recovering deleted symbols from the base revision.
✨ 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

🧹 Nitpick comments (1)
tests/integration_test.rs (1)

3946-3951: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider asserting lib is also affected for consistency with the first test.

The first deletion test asserts both lib and app are affected, but this test only checks app. Since lib's source file was modified (the beta member was removed), lib should also be marked affected per the PR objectives: "The affected file's package should still be marked." Adding this assertion would verify the direct-change package is included alongside the dependent.

♻️ Suggested addition
   assert!(
     affected.contains(&"app".to_string()),
     "app should be affected: `beta` was removed from `widget`, which app imports. Got: {:?}",
     affected
   );
+  assert!(
+    affected.contains(&"lib".to_string()),
+    "lib should be affected (its file changed). Got: {:?}",
+    affected
+  );
🤖 Prompt for 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.

In `@tests/integration_test.rs` around lines 3946 - 3951, The deletion test is
only checking the dependent package and misses the directly changed package, so
update the affected-package assertions in this integration test to include both
`lib` and `app`. Use the existing affected-set checks around this scenario to
add an assertion that `lib` is present alongside the current `app` assertion,
keeping the behavior consistent with the first deletion test and the `affected`
collection semantics.
🤖 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 `@src/git.rs`:
- Around line 11-17: The stale test comment near the symmetric-hunk coverage
still refers to a greedy `.*` in `LINE_RE`, but `LINE_RE` now uses explicit
capture groups in `src/git.rs` and no longer matches that description. Update
the comment in the symmetric-hunk test to describe the current `LINE_RE`
behavior accurately, so future changes to `line regex` and the related hunk
handling logic are not justified by outdated assumptions.

---

Nitpick comments:
In `@tests/integration_test.rs`:
- Around line 3946-3951: The deletion test is only checking the dependent
package and misses the directly changed package, so update the affected-package
assertions in this integration test to include both `lib` and `app`. Use the
existing affected-set checks around this scenario to add an assertion that `lib`
is present alongside the current `app` assertion, keeping the behavior
consistent with the first deletion test and the `affected` collection semantics.
🪄 Autofix (Beta)

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

Run ID: 9f044d57-38e6-4b52-b89d-bf03dc733893

📥 Commits

Reviewing files that changed from the base of the PR and between 8b64617 and 968f918.

📒 Files selected for processing (7)
  • src/core.rs
  • src/git.rs
  • src/lockfile.rs
  • src/named_inputs.rs
  • src/semantic/analyzer.rs
  • src/types.rs
  • tests/integration_test.rs

Comment thread src/git.rs
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

📦 Preview Release Available

A preview release has been published for commit 20e1d9c.

Installation

npm install https://github.com/frontops-dev/domino/releases/download/pr-79-20e1d9c/front-ops-domino-1.4.0.tgz

Running the preview

npx https://github.com/frontops-dev/domino/releases/download/pr-79-20e1d9c/front-ops-domino-1.4.0.tgz affected

Details

- git.rs: fix stale symmetric-hunk test comment that referenced the old
  greedy `.*` in `LINE_RE`; describe the current explicit capture groups.
- integration: assert the directly-changed package (`lib`) is also affected in
  the member-deletion test, mirroring the whole-symbol-deletion test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/types.rs Outdated

/// A file with changed lines
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why Default? doesn't look used anywhere — every construction (src + tests) sets both fields explicitly. drop it?

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.

Good catch — dropped the Default derive in 20e1d9c. It was a leftover from an earlier draft; every construction sets both fields explicitly.

Comment thread src/core.rs
.push(AffectCause::DirectChange {
file: file_path.clone(),
symbol: Some(symbol.clone()),
line: 0,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

line: 0 renders as (line 0) in the html report (report.rs:1820). harmless but reads oddly — maybe show (deleted) instead of a fake line?

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.

Agreed. Fixed in 20e1d9c: a deletion cause (line: 0) now renders (deleted) instead of (line 0), and the symbol-less whole-file/asset line-0 case drops the locator entirely. Verified on a real report — 0 (line 0) strings remain; the deleted field shows as partner.ts (deleted) → Symbol.

@EladBezalel

Copy link
Copy Markdown
Collaborator

re: the two out-of-scope items — both correctly scoped out, neither regresses today's behavior. but they're not equal weight:

whole-file deletion — worth a tracked follow-up. deleting a whole module that other projects import is a common refactor, and the gap is real: once the file is gone from disk, oxc_resolver can't resolve consumers' import { X } from './deleted' back to it, so the cascade can't link them and their projects are missed. it's not just "the symbol isn't recovered" — the resolution edge collapses. partially masked by the fact that deleting a file usually comes with editing its importers (which then get detected directly), but barrel re-exports / type-only imports can still slip through. can we open an issue for it?

local-reference-only consumption — fine to defer. narrow edge, and direct cross-file imports (the common case) are handled. 👍

- types.rs: drop the unused `Default` derive on `ChangedFile` — every
  construction (src + tests) sets both fields explicitly.
- report.rs: deletion causes carry `line: 0` (no surviving new-side line);
  render that as "(deleted)" instead of the odd "(line 0)", and omit the
  locator entirely for the symbol-less whole-file/asset line-0 case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@EladBezalel EladBezalel left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm — correct approach (base-revision recovery), addresses the earlier anchor + dead-branch feedback, and the change is purely additive per the validation. the two nits are non-blocking follow-ups. 👏

@EladBezalel EladBezalel merged commit 5beed1e into frontops-dev:main Jul 9, 2026
24 checks passed
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.

affected misses dependents on deletion-only changes (+Z,0 hunk → empty changed_lines)

3 participants