Skip to content

Fix parent suppression when container's own declaration also changes - #98

Merged
rs545837 merged 1 commit into
Ataraxy-Labs:mainfrom
nminev:feat/precise-parent-suppression
Apr 30, 2026
Merged

Fix parent suppression when container's own declaration also changes#98
rs545837 merged 1 commit into
Ataraxy-Labs:mainfrom
nminev:feat/precise-parent-suppression

Conversation

@nminev

@nminev nminev commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #94 (closed). This covers the suppression precision fix only.

The bug

The current suppress_redundant_parents fires whenever any child entity changes — which is correct in most cases. But if a container's own declaration also changed in the same commit (e.g. a class gains a base class while a method is also renamed), the parent Modified gets suppressed anyway, silently dropping a real change from the output.

Fix

For Modified containers, strip the child entity lines from both sides of the parent content and compare what's left. If the parent's own declaration changed, keep it in the output. Added/Deleted containers are still suppressed unconditionally since their children carry all the detail.

Two files changed:

differ.rs — replaces the blanket child-check in suppress_redundant_parents with the line-number-based own-content comparison for Modified containers. Adds a strip_children_content helper and two integration tests that pin both behaviours (basic suppression still works, precision case now works).

identity.rs — removes the older Modified-only dedup block from match_entities. It was added before suppress_redundant_parents existed in differ.rs and is now dead code.

Tests

test_parent_suppressed_when_only_child_modified       — passes (existing behaviour unchanged)
test_parent_not_suppressed_when_own_declaration_changes — passes (the bug, now fixed)

The existing suppression fires whenever any child entity changes, which
incorrectly hides a parent Modified when both the parent's own declaration
and a child changed in the same commit (e.g. class gains a base class
while a method is also renamed).

Fix: for Modified containers, strip child content by line number from
both sides and compare what remains. If the parent's own declaration
changed, keep it in the output. Added/Deleted containers are still
suppressed unconditionally since their children carry all the detail.

Also removes the older Modified-only dedup from match_entities — it was
superseded by suppress_redundant_parents in differ.rs.

@inspect-review inspect-review 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.

inspect review

Triage: 10 entities analyzed | 0 critical, 0 high, 5 medium, 5 low
Verdict: standard_review

Findings (2)

  1. [low] strip_children_content can exclude the wrong lines due to 1-based vs 0-based indexing mismatch. It computes indices as child.start_line.saturating_sub(parent_start_line) / child.end_line.saturating_sub(parent_start_line) and then compares them directly to lines.iter().enumerate() indices (0-based). If start_line/end_line are 1-based (typical for line numbers), the excluded range will be shifted by 1, making before_own == after_own comparisons incorrect and causing wrong suppression decisions. Evidence: let start_idx = child.start_line.saturating_sub(parent_start_line); ... excluded.insert(i); with lines.iter().enumerate().
  2. [low] strip_children_content can incorrectly exclude the parent's first line when a child is outside/above the parent range because it uses saturating_sub without validating that the child span is within the parent content. If child.start_line < parent_start_line, start_idx becomes 0 and the loop will exclude line index 0 of the parent content even though the child is not actually inside the parent snippet. Evidence: let start_idx = child.start_line.saturating_sub(parent_start_line); ... for i in start_idx..=end_idx.max(start_idx) { if i < lines.len() { excluded.insert(i); } }.

Reviewed by inspect | Entity-level triage found 0 high-risk changes

@rs545837
rs545837 merged commit 6271bcc into Ataraxy-Labs:main Apr 30, 2026
2 checks passed
nminev added a commit to nminev/sem that referenced this pull request Apr 30, 2026
The JSON parser was depth-2: it surfaced top-level keys and one layer of
children, then treated everything deeper as opaque text. A change to
package.json's scripts.build surfaced as "scripts modified" with the
entire scripts object as before/after content.

Builds on the parent_name field merged in Ataraxy-Labs#97 and the precision guard
merged in Ataraxy-Labs#98.

Parser is now fully recursive — every key at every depth is an entity
identified by JSON Pointer (/scripts/build). Arrays remain opaque
(elements have no stable identity), but array-typed keys themselves are
entities. JSON entity IDs are file::pointer (entity_type dropped) so a
key whose value type changes (scalar↔object↔array) keeps the same ID
and matches Phase 1 as Modified instead of Deleted+Added.

parent_name traverses parent_id to build the full ancestor chain
(e.g. jest::config for an entity at /jest/config/timeout). Empty
ancestor names — package-lock.json's "" root-package key — are skipped
so displayed paths stay clean.

Suppression extends Ataraxy-Labs#98's precision check:
- "object" joins CONTAINER_TYPES so JSON parents are eligible
- The Modified-suppression branch additionally requires entity_type to
  match across before/after, so scalar↔object value transitions keep
  the parent change
- An additional pass drops Moved entries when the entity's old_parent_id
  is itself in the change set, catching parent-rename failures where
  children matched by structural hash and the parent did not

Behavior is documented in JSON_SEMANTIC_DIFF_SPEC.md, including the
remaining limitation around parent-rename plus content change in the
same commit.

Adds 38 BDD-style tests in json.rs covering top-level, nested rename/
add/delete, deep nesting, type transitions, array opacity, parent-
rename fallbacks, document edge cases, and the empty-string-key case
from package-lock.json.

All 190 sem-core tests pass.
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.

2 participants