Skip to content

feat: revision grouping — EditResult with group_id, accept_group/reject_group (ISSUES.md #37) - #54

Merged
pablospe merged 4 commits into
mainfrom
task/ecf16061-Revision-grouping---group_id
Jul 15, 2026
Merged

pablospe merged 4 commits into
mainfrom
task/ecf16061-Revision-grouping---group_id

Conversation

@pablospe

@pablospe pablospe commented Jul 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements revision grouping (ISSUES.md item #37 — not a GitHub issue): every logical edit operation registers the revisions it creates as one revision group, so a multi-revision edit — especially rewrite_paragraph, where each revision is a diff hunk — can be accepted or rejected as a unit instead of hunk-by-hunk, which garbles the paragraph when only partially applied.

API

  • EditResult (new, exported): return type of replace / delete / insert_after / insert_before / rewrite_paragraph and the element type of batch_edit / batch_rewrite results. A str subclass whose value is the new paragraph ref, so it is drop-in compatible everywhere a ref string is expected; carries group_id and revision_ids.
  • Document.accept_group(group_id) / Document.reject_group(group_id): resolve every revision of one edit as a unit (reverse-id, loop-until-no-progress — same pattern as accept_all). Members already resolved individually are skipped.
  • Revision.group_id: stamped on session-created revisions by list_revisions(); None for foreign/pre-session revisions.
  • Groups are in-memory, per-open-Document: save() keeps them; close()/reopen drops them (unknown ids raise RevisionError). Nothing is written into the .docx.

Implementation

  • RevisionManager._grouped() context manager captures the <w:ins>/<w:del> elements processed by attribute injection (via a new single-slot DocxXMLEditor.collect_tracked_changes() collector), then filters to elements that are ours, still DOM-attached, and not already registered — foreign split halves and adopted tails stay out.
  • Change-id allocation gains a monotonic floor (_max_change_id) so ids of removed revisions are never reissued into id-keyed bookkeeping.
  • Both batch paths snapshot/restore the group registry alongside the existing DOM snapshot for atomic rollback.
  • Own-insertion edge paths keep groups truthful: a replace consuming an entire own pending insertion returns the id of the re-wrapped <w:ins> (its group is reachable from the result), and a split-off tail of an own insertion is adopted into its origin insertion's group (_adopt_split_tail), so rejecting the original edit removes both halves.

Testing

  • 26 new tests in tests/test_revision_groups.py: EditResult contract, group stamping, accept/reject as unit, per-id interplay, rewrite grouping (headline case), per-op batch groups, batch rollback registry restore, session scoping across save/reopen, foreign-insertion split-half exclusion, monotonic id floor, own-insertion split adoption.
  • Full suite: 1056 passed, 2 skipped; ruff check / ruff format --check clean; ty check exit 0 (6 pre-existing warnings in untouched test files).
  • Docs updated: docs/api.md (EditResult, accept_group/reject_group sections) and skills/docx/SKILL.md.

Summary by CodeRabbit

  • New Features

    • Tracked-edit methods now return EditResult (a paragraph-ref string subclass) that includes revision group details.
    • Added accept_group(group_id) and reject_group(group_id) to apply or undo all revisions from one logical edit atomically.
    • Batch edits and paragraph rewrites now return an EditResult per operation/rewrite.
    • EditResult is re-exported from the top-level docx_editor package.
  • Documentation

    • Updated API docs and examples to cover EditResult, group_id/revision_ids, and group lifecycle/behavior.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 30 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cdda10e8-d624-4a4f-9f49-1325769e9a69

📥 Commits

Reviewing files that changed from the base of the PR and between 01c7d6e and 65dd1b4.

📒 Files selected for processing (3)
  • docs/api.md
  • docx_editor/document.py
  • docx_editor/track_changes.py
📝 Walkthrough

Walkthrough

Tracked-edit operations now return EditResult values containing paragraph references and revision-group metadata. Revision groups are captured across edits, rewrites, batches, and splits, with new APIs to accept or reject groups. Documentation and comprehensive grouping tests were added.

Changes

Revision Group Management

Layer / File(s) Summary
Tracked-change collection and monotonic IDs
docx_editor/xml_editor.py
Tracked-change elements are collected during grouped operations, and change IDs remain monotonic across mutations and rejections.
Revision grouping and resolution
docx_editor/track_changes.py
Grouped edit contexts assign shared IDs and revision members across edit, rewrite, insertion, deletion, split, and batch paths; group registries are restored during rollback and groups can be accepted or rejected.
EditResult and Document API integration
docx_editor/track_changes.py, docx_editor/document.py, docx_editor/__init__.py
Edit methods and rewrites return EditResult objects, batch operations expose grouped results, group-level Document methods are added, and EditResult is re-exported publicly.
Public documentation and behavioral coverage
docs/api.md, skills/docx/SKILL.md, tests/test_revision_groups.py
Documentation describes grouped results and session lifetime, while tests cover grouping, resolution, rollback, foreign revisions, insertion splits, and monotonic IDs.

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Document
  participant RevisionManager
  participant DocxXMLEditor
  Caller->>Document: replace, delete, insert, or rewrite
  Document->>RevisionManager: execute grouped operation
  RevisionManager->>DocxXMLEditor: create and collect tracked changes
  DocxXMLEditor-->>RevisionManager: revision elements and IDs
  RevisionManager-->>Document: group ID and revision IDs
  Document-->>Caller: EditResult
  Caller->>Document: accept_group or reject_group
  Document->>RevisionManager: resolve group members
  RevisionManager-->>Caller: resolved revision count
Loading

Possibly related PRs

  • pablospe/docx-editor#10: Earlier batch-edit implementation changes the same batching surface extended here with revision-group results.
  • pablospe/docx-editor#11: Introduces the rewrite APIs whose return values and grouping behavior are changed here.

Poem

I’m a bunny with grouped edits in sight,
Each revision hops in a unit just right.
Accept one group, or send it away,
Paragraph refs still guide the way.
Thump, thump, a tidy review treat!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the PR’s main additions: revision grouping, EditResult metadata, and group accept/reject APIs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/ecf16061-Revision-grouping---group_id

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.

@codecov

codecov Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.68508% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.3%. Comparing base (a5a54a5) to head (65dd1b4).

Files with missing lines Patch % Lines
docx_editor/track_changes.py 95.3% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main     #54     +/-   ##
=======================================
- Coverage   95.3%   95.3%   -0.1%     
=======================================
  Files         11      11             
  Lines       3451    3576    +125     
  Branches     701     719     +18     
=======================================
+ Hits        3291    3410    +119     
- Misses        76      79      +3     
- Partials      84      87      +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@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 `@docx_editor/track_changes.py`:
- Around line 539-546: Replace the heterogeneous tuple used by
_registry_snapshot and _restore_registry with a dedicated dataclass, following
the existing _GroupCapture pattern. Define fields for the group counter, groups
mapping, and revision-groups mapping, return that dataclass from
_registry_snapshot, and update _restore_registry to access its named fields
while preserving the existing rollback behavior.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 352c750d-cc7b-49fb-b68c-c43116dd9f23

📥 Commits

Reviewing files that changed from the base of the PR and between a5a54a5 and b9af2da.

📒 Files selected for processing (7)
  • docs/api.md
  • docx_editor/__init__.py
  • docx_editor/document.py
  • docx_editor/track_changes.py
  • docx_editor/xml_editor.py
  • skills/docx/SKILL.md
  • tests/test_revision_groups.py

Comment thread docx_editor/track_changes.py Outdated
Comment on lines +539 to +546
def _registry_snapshot(self) -> tuple[int, dict[int, tuple[int, ...]], dict[int, int]]:
"""Snapshot the group registry for rollback alongside a DOM snapshot."""
return (self._group_counter, dict(self._groups), dict(self._revision_groups))

def _restore_registry(self, snapshot: tuple[int, dict[int, tuple[int, ...]], dict[int, int]]) -> None:
"""Restore the group registry captured by ``_registry_snapshot``."""
self._group_counter, self._groups, self._revision_groups = snapshot

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Return a dataclass instead of a bare 3-tuple.

_registry_snapshot/_restore_registry pass around tuple[int, dict[int, tuple[int, ...]], dict[int, int]] — three heterogeneous values via positional tuple. _GroupCapture right above already shows the preferred pattern in this same file.

♻️ Proposed fix using a dataclass
+@dataclass
+class _RegistrySnapshot:
+    """Point-in-time copy of the group registry, for rollback."""
+
+    group_counter: int
+    groups: dict[int, tuple[int, ...]]
+    revision_groups: dict[int, int]
+
-    def _registry_snapshot(self) -> tuple[int, dict[int, tuple[int, ...]], dict[int, int]]:
+    def _registry_snapshot(self) -> "_RegistrySnapshot":
         """Snapshot the group registry for rollback alongside a DOM snapshot."""
-        return (self._group_counter, dict(self._groups), dict(self._revision_groups))
+        return _RegistrySnapshot(self._group_counter, dict(self._groups), dict(self._revision_groups))

-    def _restore_registry(self, snapshot: tuple[int, dict[int, tuple[int, ...]], dict[int, int]]) -> None:
+    def _restore_registry(self, snapshot: "_RegistrySnapshot") -> None:
         """Restore the group registry captured by ``_registry_snapshot``."""
-        self._group_counter, self._groups, self._revision_groups = snapshot
+        self._group_counter = snapshot.group_counter
+        self._groups = snapshot.groups
+        self._revision_groups = snapshot.revision_groups

As per coding guidelines: "Use named objects (dataclasses) instead of tuples for functions returning 3 or more values."

🤖 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 `@docx_editor/track_changes.py` around lines 539 - 546, Replace the
heterogeneous tuple used by _registry_snapshot and _restore_registry with a
dedicated dataclass, following the existing _GroupCapture pattern. Define fields
for the group counter, groups mapping, and revision-groups mapping, return that
dataclass from _registry_snapshot, and update _restore_registry to access its
named fields while preserving the existing rollback behavior.

Source: Coding guidelines

pablospe added 2 commits July 15, 2026 02:34
Multi-reviewer findings (iteration 1):
- Split-off tail of an own insertion with no origin group (pre-session)
  was claimed by the splitting operation's _grouped capture — via the
  rewrite path the poisoned group even reached EditResult.group_id, so
  the documented reject_group undo idiom would remove pre-existing text.
  _adopt_split_tail now always registers tails (origin group or None) and
  the registry value type is widened to int | None.
- Drop dead _GroupCapture.revision_ids (never read).
- Single int(rev_id) parse in _parse_revision.
- SKILL.md: fix stale 'plain string' return-value line; api.md: batch_edit
  group_id=None caveat.

Skipped (false positive): claimed _grouped nesting in batch_edit —
_apply_single_edit dispatches to low-level helpers, not wrapped methods.
Multi-reviewer findings (iteration 2):
- HIGH: splitting a foreign insertion re-serializes its trailing children
  through attribute injection; a pre-session own revision nested in there
  (e.g. our w:del inside their w:ins) was collected and claimed into the
  splitting operation's group, so reject_group resurrected already-deleted
  text. The collector now only reports elements whose w:id it freshly
  assigned — pre-existing revisions passing back through injection keep
  their ids and are never collected. Regression test covers the repro.
- Docs: rewrite group_id=None also covers changes merged into own pending
  insertions (EditResult/rewrite_paragraph docstrings, api.md); drop
  ALL-CAPS emphasis in SKILL.md.
- Coverage: pragma on unreachable return after always-returning loop.

Skipped (false positive): claimed missing 'return results' in batch_edit —
present on the success path, verified by source and passing batch tests.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
docx_editor/track_changes.py (1)

1448-1457: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

New instance of a repeated "find the fresh w:ins/w:del id in new_nodes" pattern.

This fix correctly surfaces the wrapper's real id instead of -1, but the same node-scan-and-return-id snippet is now repeated ~6-7 times across this file (e.g. lines 1474-1477, 1549-1551, 1619-1621, 1811-1813, 2093-2095, 2236-2238). Worth extracting a small shared helper (e.g. _first_new_element_id(nodes, tag)), especially since this diff adds another copy of it.

🤖 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 `@docx_editor/track_changes.py` around lines 1448 - 1457, Extract the repeated
fresh revision-ID scan into a shared helper such as _first_new_element_id(nodes,
tag), returning the first matching element’s w:id. Replace the duplicated
w:ins/w:del node loops throughout the affected methods, including the insertion
flow around new_nodes, while preserving the existing fallback behavior when no
matching element is found.
docx_editor/document.py (2)

742-770: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

delete()'s "Returns" doc omits the documented group_id=None case.

_grouped()'s own docstring (track_changes.py) and the TestOwnInsertionSplits.test_middle_delete_adopts_split_tail_into_origin_group test confirm delete() can return group_id=None when the delete only trims text inside the caller's own pending insertion (the split tail is adopted into the origin insertion's group, not a new one for the delete). rewrite_paragraph() and batch_edit() docstrings both explicitly call out their group_id=None case, but delete()'s "Returns" section here doesn't mention it at all — a caller relying on result.group_id for accept_group()/reject_group() could be surprised.

📝 Proposed doc fix
         Returns:
             EditResult — the new paragraph reference with updated hash (e.g.,
             "P2#c3d4"), carrying ``group_id``/``revision_ids`` of the
-            revisions this edit created.
+            revisions this edit created (``group_id`` is None when the
+            deletion only trimmed text inside your own pending insertion,
+            with no new revision of its own — e.g. the split tail is
+            adopted into the origin insertion's group instead).
🤖 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 `@docx_editor/document.py` around lines 742 - 770, Update the delete()
docstring’s Returns section to state that EditResult.group_id may be None when
the deletion only trims text within the caller’s own pending insertion and
adopts the split tail into the origin group; retain the existing description for
edits that create revisions.

1137-1195: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Guard None before calling accept_group/reject_group docx_editor/document.py:1137-1195
EditResult.group_id is int | None, but these helpers only accept int, so the documented doc.accept_group(result.group_id) pattern will fail on no-op edits with RevisionError("Unknown revision group: None"). Either accept int | None here or show a None guard at the call site.

🤖 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 `@docx_editor/document.py` around lines 1137 - 1195, Update
Document.accept_group and reject_group to handle EditResult.group_id values of
None without raising an unknown-group error, while preserving existing behavior
for valid integer group IDs. Adjust the type annotations and return handling
consistently with the chosen no-op behavior, and update the docstrings’ examples
or parameter documentation if needed.
docs/api.md (1)

300-316: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

delete() doc gap — shares root cause with the docstring gap in docx_editor/document.py.

See the consolidated comment for this shared fix.

🤖 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 `@docs/api.md` around lines 300 - 316, Update the `delete()` API documentation
and its corresponding `Document.delete` docstring together so they describe the
same behavior and parameters, including how authorship and pending insertions
are handled. Keep the documented signature, occurrence-selection rules, return
type, and `AmbiguousTextError` behavior consistent across both locations.
🤖 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 `@tests/test_revision_groups.py`:
- Around line 471-497: Split the combined content assertion in
test_presession_rewrite_does_not_claim_split_tail into separate assert
statements, one validating that “BETA” is present and another that “beta” is
absent; likewise split the assertion for “alpha” and “gamma” into individual
checks so each failure identifies the specific missing or unexpected text.

---

Outside diff comments:
In `@docs/api.md`:
- Around line 300-316: Update the `delete()` API documentation and its
corresponding `Document.delete` docstring together so they describe the same
behavior and parameters, including how authorship and pending insertions are
handled. Keep the documented signature, occurrence-selection rules, return type,
and `AmbiguousTextError` behavior consistent across both locations.

In `@docx_editor/document.py`:
- Around line 742-770: Update the delete() docstring’s Returns section to state
that EditResult.group_id may be None when the deletion only trims text within
the caller’s own pending insertion and adopts the split tail into the origin
group; retain the existing description for edits that create revisions.
- Around line 1137-1195: Update Document.accept_group and reject_group to handle
EditResult.group_id values of None without raising an unknown-group error, while
preserving existing behavior for valid integer group IDs. Adjust the type
annotations and return handling consistently with the chosen no-op behavior, and
update the docstrings’ examples or parameter documentation if needed.

In `@docx_editor/track_changes.py`:
- Around line 1448-1457: Extract the repeated fresh revision-ID scan into a
shared helper such as _first_new_element_id(nodes, tag), returning the first
matching element’s w:id. Replace the duplicated w:ins/w:del node loops
throughout the affected methods, including the insertion flow around new_nodes,
while preserving the existing fallback behavior when no matching element is
found.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bd439b61-e163-4e03-b95f-43c31a5def12

📥 Commits

Reviewing files that changed from the base of the PR and between b9af2da and 01c7d6e.

📒 Files selected for processing (6)
  • docs/api.md
  • docx_editor/document.py
  • docx_editor/track_changes.py
  • docx_editor/xml_editor.py
  • skills/docx/SKILL.md
  • tests/test_revision_groups.py

Comment on lines +471 to +497
def test_presession_rewrite_does_not_claim_split_tail(self, temp_docx):
# Same defect through the public rewrite path: without tail
# registration, the rewrite's EditResult.group_id would hold ONLY
# the pre-session insertion's tail, and reject_group (the documented
# undo idiom) would rip out pre-existing text instead of undoing
# the rewrite.
with Document.open(temp_docx) as doc:
ref = find_ref(doc, "quick brown fox")
doc.insert_after("fox", " alpha beta gamma", paragraph=ref)
doc.save()

with Document.open(temp_docx) as doc:
ref = find_ref(doc, "quick brown fox")
current = _paragraph_text(doc, 1)
result = doc.rewrite_paragraph(ref, current.replace("beta", "BETA"))

# The only physical change happens inside our own pre-session
# insertion — no trackable revisions, so no group at all.
assert result.group_id is None
assert doc._revision_manager._groups == {}
assert all(rev.group_id is None for rev in doc.list_revisions())
# Placement-agnostic content check: replacement position inside
# an own insertion is the pre-existing ISSUES.md #31 ordering
# bug, out of scope here.
text = _paragraph_text(doc, 1)
assert "BETA" in text and "beta" not in text
assert "alpha" in text and "gamma" in text

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split combined checks per Ruff PT018.

Lines 496-497 pack two conditions each into one assert; splitting them gives a clearer failure message pinpointing which half failed.

🧪 Proposed split
             text = _paragraph_text(doc, 1)
-            assert "BETA" in text and "beta" not in text
-            assert "alpha" in text and "gamma" in text
+            assert "BETA" in text
+            assert "beta" not in text
+            assert "alpha" in text
+            assert "gamma" in text
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_presession_rewrite_does_not_claim_split_tail(self, temp_docx):
# Same defect through the public rewrite path: without tail
# registration, the rewrite's EditResult.group_id would hold ONLY
# the pre-session insertion's tail, and reject_group (the documented
# undo idiom) would rip out pre-existing text instead of undoing
# the rewrite.
with Document.open(temp_docx) as doc:
ref = find_ref(doc, "quick brown fox")
doc.insert_after("fox", " alpha beta gamma", paragraph=ref)
doc.save()
with Document.open(temp_docx) as doc:
ref = find_ref(doc, "quick brown fox")
current = _paragraph_text(doc, 1)
result = doc.rewrite_paragraph(ref, current.replace("beta", "BETA"))
# The only physical change happens inside our own pre-session
# insertion — no trackable revisions, so no group at all.
assert result.group_id is None
assert doc._revision_manager._groups == {}
assert all(rev.group_id is None for rev in doc.list_revisions())
# Placement-agnostic content check: replacement position inside
# an own insertion is the pre-existing ISSUES.md #31 ordering
# bug, out of scope here.
text = _paragraph_text(doc, 1)
assert "BETA" in text and "beta" not in text
assert "alpha" in text and "gamma" in text
def test_presession_rewrite_does_not_claim_split_tail(self, temp_docx):
# Same defect through the public rewrite path: without tail
# registration, the rewrite's EditResult.group_id would hold ONLY
# the pre-session insertion's tail, and reject_group (the documented
# undo idiom) would rip out pre-existing text instead of undoing
# the rewrite.
with Document.open(temp_docx) as doc:
ref = find_ref(doc, "quick brown fox")
doc.insert_after("fox", " alpha beta gamma", paragraph=ref)
doc.save()
with Document.open(temp_docx) as doc:
ref = find_ref(doc, "quick brown fox")
current = _paragraph_text(doc, 1)
result = doc.rewrite_paragraph(ref, current.replace("beta", "BETA"))
# The only physical change happens inside our own pre-session
# insertion — no trackable revisions, so no group at all.
assert result.group_id is None
assert doc._revision_manager._groups == {}
assert all(rev.group_id is None for rev in doc.list_revisions())
# Placement-agnostic content check: replacement position inside
# an own insertion is the pre-existing ISSUES.md `#31` ordering
# bug, out of scope here.
text = _paragraph_text(doc, 1)
assert "BETA" in text
assert "beta" not in text
assert "alpha" in text
assert "gamma" in text
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 496-496: Assertion should be broken down into multiple parts

Break down assertion into multiple parts

(PT018)


[warning] 497-497: Assertion should be broken down into multiple parts

Break down assertion into multiple parts

(PT018)

🤖 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/test_revision_groups.py` around lines 471 - 497, Split the combined
content assertion in test_presession_rewrite_does_not_claim_split_tail into
separate assert statements, one validating that “BETA” is present and another
that “beta” is absent; likewise split the assertion for “alpha” and “gamma” into
individual checks so each failure identifies the specific missing or unexpected
text.

Source: Linters/SAST tools

…turn

Iteration-3 review: batch_rewrite docs (api.md and docstring) now carry the
same group_id=None caveat as rewrite_paragraph; the 'no cover' pragma added
in iteration 2 sat on a line the splice-in-place branch falls through to —
removed so coverage stays honest. No code-behavior changes.
@pablospe
pablospe merged commit dd6b81d into main Jul 15, 2026
10 checks passed
pablospe added a commit that referenced this pull request Jul 22, 2026
…ES.md #54) (#68)

Third revision-tracking tier: one whole call (single edit, or an entire batch_edit/batch_rewrite) = one changeset ⊇ ≥1 group ⊇ revisions. A changeset is the (author, date) equivalence class over groups (global, not contiguous). Recorded live via a reentrant _changeset() boundary; reconstructed as inferred changesets at parse time; registry snapshot/restore extended so a failed batch leaves no ghost changeset. Adds Document.accept_changeset/reject_changeset, changeset_id on EditResult, changeset_id/changeset_source on Revision, changeset_id on RevisionError.
@pablospe
pablospe deleted the task/ecf16061-Revision-grouping---group_id branch August 30, 2026 19:28
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