Skip to content

feat: changeset tier — accept/reject_changeset over whole calls (ISSUES.md #54) - #68

Merged
pablospe merged 2 commits into
mainfrom
task/b60cdba5-Changeset-tier--changeset_id
Jul 22, 2026
Merged

pablospe merged 2 commits into
mainfrom
task/b60cdba5-Changeset-tier--changeset_id

Conversation

@pablospe

@pablospe pablospe commented Jul 22, 2026 •

Copy link
Copy Markdown
Owner

Implements the changeset tier (ISSUES.md #54, v0.7.0) — the third
revision-tracking tier, one level above revision groups:

one call (a single edit, or a whole batch_edit / batch_rewrite)
  = one changeset  ⊇  one-or-more groups  ⊇  revisions

A changeset is the (author, date) equivalence class over groups — a
global class, not a contiguous run — so a batch_edit whose ops land in
different paragraphs is one changeset even though its groups are non-contiguous.

What's new

  • Document.accept_changeset(id) / reject_changeset(id) — resolve every
    group a whole call created, as one unit. Rump-tolerant.
  • EditResult.changeset_id — carried by every result a call returns
    (shared across a batch); None iff group_id is None.
  • Revision.changeset_id / changeset_source — stamped by
    list_revisions(); "recorded" for this session's calls, "inferred" for
    revisions reconstructed at parse time.
  • RevisionError.changeset_id — structured field for unknown-changeset errors.

Mechanics

  • Recorded live via a reentrant _changeset() boundary that mirrors
    frozen_timestamp: only the outermost call bundles, so a nested
    rewrite_paragraph inside batch_rewrite merges into one changeset while a
    standalone call gets its own.
  • Reconstructed as inferred changesets at parse time, partitioning groups by
    identical w:author + w:date.
  • Registry snapshot/restore extended to the changeset maps, so a failed
    batch_edit/batch_rewrite never leaves a ghost changeset.
  • All EditResult construction routes through the single _edit_result
    factory; the _resolve_group internals were extracted into a shared
    _resolve_ids reused by group and changeset resolution.

Tests & docs

  • 14 new TestChangesetTier tests (single edit, non-contiguous batch,
    batch_rewrite reentrancy, standalone separation, unknown-id error, rollback
    registry restore, save/reopen round-trip, same-paragraph over-merge contract,
    rump changesets, partial resolution, author/date partitioning, ungrouped,
    foreign) + updated test_foreign_revisions fixtures.
  • docs/api.md updated for the new methods and fields.

Verification

  • Changeset + foreign tests: 86 passed · batch_edit + session: 136 passed (no regression)
  • ruff check / ruff format --check: clean · ty check: exit 0

Summary by CodeRabbit

  • New Features

    • Added changeset tracking for edits and revisions.
    • Added the ability to accept or reject all revisions from a single edit call.
    • Added changeset details to edit results and revision information.
    • Added changeset identifiers to revision errors for clearer error reporting.
  • Documentation

    • Updated API documentation and examples to describe changeset behavior and metadata.

…ES.md #54)

Third revision-tracking tier, one level above groups:

    one call (a single edit, or a whole batch_edit/batch_rewrite)
      = one changeset  ⊇  ≥1 group  ⊇  revisions

A changeset is the (author, date) equivalence class over groups — a
global class, not a contiguous run, so a batch_edit spanning several
paragraphs is one changeset over non-contiguous groups. Recorded live
via a reentrant _changeset() boundary (nested rewrite_paragraph inside
batch_rewrite merges into one changeset); reconstructed as inferred
changesets at parse time. Registry snapshot/restore extended so a failed
batch never leaves a ghost changeset.

Adds Document.accept_changeset/reject_changeset, changeset_id on
EditResult, changeset_id/changeset_source on Revision, and changeset_id
on RevisionError. 14 new tests + updated foreign-revision fixtures; docs
updated.
@coderabbitai

coderabbitai Bot commented Jul 22, 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: 33 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: 5306220d-150b-4edb-8344-555361f65c1b

📥 Commits

Reviewing files that changed from the base of the PR and between 995199f and 4894040.

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

Walkthrough

Adds a changeset tier above revision groups, tracking revisions created by whole edit calls. It introduces changeset metadata, accept/reject APIs, inferred reconstruction, rollback-safe registries, updated error contracts, documentation, and comprehensive tests.

Changes

Changeset tier

Layer / File(s) Summary
Public changeset contracts
docx_editor/track_changes.py, docx_editor/exceptions.py, docx_editor/document.py, docs/api.md
Adds changeset_id metadata to EditResult and Revision, extends RevisionError, and exposes document-level accept/reject changeset methods with corresponding documentation.
Changeset registry and reconstruction
docx_editor/track_changes.py
Adds changeset registries, rollback snapshots, reentrancy handling, and inferred changeset reconstruction using author/date equivalence.
Edit wiring and changeset resolution
docx_editor/track_changes.py
Wraps edit entry points in changeset contexts, populates parsed changeset metadata, and resolves all revisions across a changeset.
Changeset behavior validation
tests/test_revision_groups.py, tests/test_foreign_revisions.py
Tests recorded and inferred membership, whole-changeset resolution, rollback, reopening, rump changesets, and foreign revision metadata.

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

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Document
  participant RevisionManager
  participant TrackedDocument
  Document->>RevisionManager: perform batch_edit inside _changeset()
  RevisionManager->>TrackedDocument: create revisions across groups
  RevisionManager-->>Document: return EditResult with changeset_id
  Document->>RevisionManager: reject_changeset(changeset_id)
  RevisionManager->>TrackedDocument: resolve revisions in all member groups
  TrackedDocument-->>Document: return rejected revision count
Loading

Poem

A bunny now groups each editing spree,
With one changeset hopping carefree.
Across every revision, the numbers align,
Accept or reject them all in one line.
“No ghost groups!” the rabbit sings bright—
Rollbacks restore the burrow just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 main change: adding changeset-tier accept/reject behavior for whole calls.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/b60cdba5-Changeset-tier--changeset_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.

@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 `@docs/api.md`:
- Around line 899-900: Update the changeset documentation links at
docs/api.md:899-900 and docs/api.md:985-985 to point directly to the
accept_changeset changeset documentation section, replacing fragile combined
accept_changeset/reject_changeset fragments while preserving the existing link
text and surrounding descriptions.
🪄 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: ef38a382-f724-4931-9004-4ebe9d210150

📥 Commits

Reviewing files that changed from the base of the PR and between 59ea4fd and 995199f.

📒 Files selected for processing (6)
  • docs/api.md
  • docx_editor/document.py
  • docx_editor/exceptions.py
  • docx_editor/track_changes.py
  • tests/test_foreign_revisions.py
  • tests/test_revision_groups.py

Comment thread docs/api.md Outdated
@codecov

codecov Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.9%. Comparing base (59ea4fd) to head (4894040).

Additional details and impacted files
@@          Coverage Diff          @@
##            main     #68   +/-   ##
=====================================
  Coverage   95.8%   95.9%           
=====================================
  Files         11      11           
  Lines       3984    4057   +73     
  Branches     798     803    +5     
=====================================
+ Hits        3818    3891   +73     
  Misses        77      77           
  Partials      89      89           

☔ 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.

…type nits

Multi-reviewer pass (3 Claude subagents + codex + red-team meta-review):
correctness CLEAN, zero blockers. Applied the confirmed LOW nits:

- Remove write-only _ChangesetCapture; _changeset() now yields None. The
  bundled id is always read via changeset_id_of() (through _edit_result /
  list_revisions), never off the context manager, so the capture was dead
  surface (YAGNI).
- Tighten _group_changesets value type to dict[int, int] (None is never stored,
  unlike the sibling _revision_groups).
- changeset_groups() docstring now documents member ordering, mirroring
  group_revisions().
- docs/api.md: name changeset_id in the per-method EditResult 'Returns' blurbs;
  split accept_changeset/reject_changeset into two headers matching every other
  accept/reject pair; fix the EditResult/Revision table anchor links.

Skipped as false positives / no value: a cs '(inferred)' repr marker (a group
and its changeset always share provenance, so it would only duplicate the group
marker), a ChangesetSource alias (identical to GroupSource — DRY), an
EditResult.__new__ param reorder (str subclass, not a dataclass — cosmetic), and
the always-true _group_changesets guard (cheap belt-and-braces).
@pablospe
pablospe merged commit 75fce50 into main Jul 22, 2026
16 of 17 checks passed
pablospe added a commit that referenced this pull request Aug 28, 2026
The rationale for CHANGE_RECORD_TAGS claimed only w:tcPrChange's recorded
subtree can hold revision marks. Two more can: a paragraph-mark w:rPrChange
records CT_ParaRPrOriginal, which opens with EG_ParaRPrTrackChanges, and
w:pPrChange records CT_PPrBase, whose w:numPr admits w:ins/w:numberingChange.

Only the pending-revision path skips those subtrees; list_revisions and
accept_all still walk every w:ins/w:del, so a historical w:del recorded in a
change record is listed and resolved like a live one. That is pre-existing,
and is now pinned by an xfail(strict=True) test so ISSUES.md #68 inherits it
rather than rediscovering it.

Also: guard CHANGE_RECORD_TAGS against drifting out of the property-change
family; group w:trPrChange with the property changes in the warning docstring;
stop labelling every w:rPr context as a paragraph mark, since a change
record's recorded rPr shares the tag; report files whose census never ran
instead of counting them as revision-free; build the location context only
when list_unhandled_revisions has rows; name the author scope in the warning.
pablospe added a commit that referenced this pull request Aug 28, 2026
…#76)

* feat: report revision types accept_all cannot resolve (ISSUES.md #64)

accept_all/reject_all walk w:ins/w:del only, so a redline whose revisions
are format changes or moves returned a bare 0 and read as "there was
nothing to accept". They now return a ResolveResult (an int subclass
carrying .unhandled/.unhandled_types), warn with UnhandledRevisionWarning
when adjudication is incomplete, and list_unhandled_revisions() names the
marks that are still pending.

Adds count_revision_elements()/RevisionCensus and a corpus-harness
--census mode recording which revision types real producers emit.

* fix: don't count marks recorded inside a change record as pending

A w:tcPrChange records the cell's previous w:tcPr, which is CT_TcPrInner and
so may legally hold a w:cellIns/w:cellDel/w:cellMerge. Those describe state
that is already gone, but the unhandled scan descended into the record and
reported them as extra pending revisions. iter_revision_elements now takes
skip_change_records, which _unhandled_elements uses; count_revision_elements
stays a raw inventory and documents the difference.

Also: extract _parse_w_date instead of a third copy of the w:date parse;
correct the "excluded from every filtered call" author rule, which the
author="Unknown" test contradicted; move the SKILL.md honesty check next to
the call it inspects, since it read a result four calls stale; drop the
incorrect ECMA sub-numbers; label the census error count by part rather than
file and stop truncating it silently.

* fix: correct the change-record schema note and pin the handled-path gap

The rationale for CHANGE_RECORD_TAGS claimed only w:tcPrChange's recorded
subtree can hold revision marks. Two more can: a paragraph-mark w:rPrChange
records CT_ParaRPrOriginal, which opens with EG_ParaRPrTrackChanges, and
w:pPrChange records CT_PPrBase, whose w:numPr admits w:ins/w:numberingChange.

Only the pending-revision path skips those subtrees; list_revisions and
accept_all still walk every w:ins/w:del, so a historical w:del recorded in a
change record is listed and resolved like a live one. That is pre-existing,
and is now pinned by an xfail(strict=True) test so ISSUES.md #68 inherits it
rather than rediscovering it.

Also: guard CHANGE_RECORD_TAGS against drifting out of the property-change
family; group w:trPrChange with the property changes in the warning docstring;
stop labelling every w:rPr context as a paragraph mark, since a change
record's recorded rPr shares the tag; report files whose census never ran
instead of counting them as revision-free; build the location context only
when list_unhandled_revisions has rows; name the author scope in the warning.
pablospe added a commit that referenced this pull request Aug 30, 2026
…ES.md #68)

w:moveFrom, w:moveTo and w:pPrChange become first-class, id-addressable
revisions: list_revisions() lists them as move_from / move_to /
property_change rows, accept_revision / reject_revision resolve them, and
accept_all / reject_all, the author filter and the inferred changeset cover
them through the existing _resolve_all loop.

Per-element semantics mirror Word's revision pane. A move's four range marks
are scaffolding: never listed, never counted as unhandled, swept by
_sweep_move_range_marks once no pending move content lies between them.
Rejecting a w:pPrChange restores the recorded properties
(_restore_paragraph_properties), including a self-closing record and an
undefined style id. The accepted text-map view now excludes w:t inside
w:moveFrom, which Word writes for moved-away text.

Listing and the id index use one iter_revision_elements walk over
HANDLED_REVISION_TAGS instead of a getElementsByTagName per tag.
pablospe added a commit that referenced this pull request Aug 30, 2026
…SSUES.md #68)

After accept_all the saved file may hold only UNHANDLED_REVISION_TAGS; any
ins/del/move/pPrChange element left behind fails save2 with
AssertResolvedTypesRemain. The reopen record carries the unhandled census,
the census marker keys on UNHANDLED_REVISION_TAGS, and the README census is
re-run with 0 unresolved elements.
pablospe added a commit that referenced this pull request Aug 30, 2026
…#68)

Revision.type's five values, per-type accept/reject semantics, the
two-rows-per-move rule and lone-half behaviour, and ResolveResult /
UnhandledRevision examples switched to types that are still unhandled.
pablospe added a commit that referenced this pull request Aug 30, 2026
….md #68)

The high-water mark folded only w:ins/w:del ids, while moves and
paragraph-property changes are now addressed by w:id in the same space. On
a document holding only a move, a new edit was allocated id 0 — the foreign
w:moveFrom marker's id — so both reported one group and reject_group on our
own edit removed the foreign move content instead. DocxXMLEditor now folds
every ALL_REVISION_TAGS id in one walk, at open and on fragment parse.

Also strips move paragraph-mark markers from a split paragraph's copied
w:pPr/w:rPr (a copied marker duplicated its id) and hoists comment markers
above w:moveFrom/w:moveTo wrappers as it does above w:ins/w:del, via one
_RUN_TRACK_CHANGE_TAGS constant.
pablospe added a commit that referenced this pull request Aug 30, 2026
… call (ISSUES.md #68)

A w:moveFrom/w:moveTo/w:pPrChange with a missing or non-numeric w:id is
unreachable by id, so list_revisions() omits it — and since its tag left
UNHANDLED_REVISION_TAGS it vanished from list_unhandled_revisions() too,
letting accept_all() report a clean document with the mark (and a
moveFrom's hidden text) still in it. _unhandled_elements now also yields
handled-type marks with no adjudicable id, via one shared _adjudicable_id.

_resolve_all/_resolve_ids defer the move range-mark sweep to a single walk
at the end of the call instead of one per move half (quadratic in the
number of moves). _restore_deletion leaves a w:del nested inside the host
it restores untouched (its delText and rsidDel stay), for both w:del and
w:moveFrom hosts. The sweep treats id-less range marks and a Start whose
id a later Start reuses as unpaired. group_spans skips every paragraph-mark
marker, not only w:ins.

Docstrings for UnhandledRevisionWarning, get_visible_text,
get_original_text, list_unhandled_revisions and _insertion_text_nodes
describe the new contract; the harness save2 post-condition gets a unit
test.
pablospe added a commit that referenced this pull request Aug 30, 2026
…nested ids (ISSUES.md #68)

Move content can leave the document inside another resolved element — a
w:del wrapping a w:moveFrom, a w:ins wrapping a w:moveTo, a rejoined
paragraph whose mark carried a move marker — and the range marks around
it were then orphaned in neither listing. accept_revision/reject_revision
now sweep after every resolution (one deferred walk per bulk call).

_nearest_revision_ancestor_id and _descendant_revision_ids go through
_adjudicable_id, so a non-numeric w:id on a host or nested handled mark
no longer raises from list_revisions(); the mark is reported as unhandled.

The ISSUES.md #56/#62 walk-count pins gained count_revision_walks, a
counter for full-document iter_revision_elements traversals, so they pin
a real constant again now that the resolution path no longer uses
getElementsByTagName. An own edit made inside a foreign w:moveTo is
carried away when that move is rejected — documented as a known gap and
pinned with a strict xfail; docs no longer say list_unhandled_revisions()
lists exactly UNHANDLED_REVISION_TAGS.
pablospe added a commit that referenced this pull request Aug 30, 2026
… (ISSUES.md #68)

DocxXMLEditor.holds_move_range_marks is set by the id-seeding walk when it
sees a MOVE_RANGE_TAGS element and cleared by the sweep once none remain,
so the sweep — a full-document walk — costs nothing on the move-free
document every accept_revision/accept_group used to pay it on.

Also: _restore_deletion keeps w:rsidDel on runs inside a nested w:moveFrom;
the corpus save2 post-condition is measured against accept_all's own
unhandled report rather than a static tag set, so a handled-type mark the
library reported as unreachable does not fail the file while an unreported
leftover (range marks included) still does; the README no longer claims
that accepting the drag-and-drop table move is exact (the source table
survives as empty cells); quickstart, SKILL.md and the accept_all docstring
describe the own-edit-inside-a-foreign-move gap, the moved-table residue and
a pPrChange carried away by a rejected split; _MARKUP_KIND_BY_TAG follows
its siblings' naming.
pablospe added a commit that referenced this pull request Aug 30, 2026
…S.md #68)

A rolled-back batch replaces the DOM wholesale; holds_move_range_marks now
follows it (one walk on the rollback path only). Pins the corpus save2
rule that range marks may stay while their move is reported as unhandled.
pablospe added a commit that referenced this pull request Aug 30, 2026
* feat: resolve moves and paragraph-property changes as revisions (ISSUES.md #68)

w:moveFrom, w:moveTo and w:pPrChange become first-class, id-addressable
revisions: list_revisions() lists them as move_from / move_to /
property_change rows, accept_revision / reject_revision resolve them, and
accept_all / reject_all, the author filter and the inferred changeset cover
them through the existing _resolve_all loop.

Per-element semantics mirror Word's revision pane. A move's four range marks
are scaffolding: never listed, never counted as unhandled, swept by
_sweep_move_range_marks once no pending move content lies between them.
Rejecting a w:pPrChange restores the recorded properties
(_restore_paragraph_properties), including a self-closing record and an
undefined style id. The accepted text-map view now excludes w:t inside
w:moveFrom, which Word writes for moved-away text.

Listing and the id index use one iter_revision_elements walk over
HANDLED_REVISION_TAGS instead of a getElementsByTagName per tag.

* test: fail the corpus save2 stage when resolvable revisions remain (ISSUES.md #68)

After accept_all the saved file may hold only UNHANDLED_REVISION_TAGS; any
ins/del/move/pPrChange element left behind fails save2 with
AssertResolvedTypesRemain. The reopen record carries the unhandled census,
the census marker keys on UNHANDLED_REVISION_TAGS, and the README census is
re-run with 0 unresolved elements.

* docs: document moves and pPrChange as listed revision types (ISSUES.md #68)

Revision.type's five values, per-type accept/reject semantics, the
two-rows-per-move rule and lone-half behaviour, and ResolveResult /
UnhandledRevision examples switched to types that are still unhandled.

* fix: seed the change-id counter from every revision mark's id (ISSUES.md #68)

The high-water mark folded only w:ins/w:del ids, while moves and
paragraph-property changes are now addressed by w:id in the same space. On
a document holding only a move, a new edit was allocated id 0 — the foreign
w:moveFrom marker's id — so both reported one group and reject_group on our
own edit removed the foreign move content instead. DocxXMLEditor now folds
every ALL_REVISION_TAGS id in one walk, at open and on fragment parse.

Also strips move paragraph-mark markers from a split paragraph's copied
w:pPr/w:rPr (a copied marker duplicated its id) and hoists comment markers
above w:moveFrom/w:moveTo wrappers as it does above w:ins/w:del, via one
_RUN_TRACK_CHANGE_TAGS constant.

* fix: report id-less handled marks and sweep range marks once per bulk call (ISSUES.md #68)

A w:moveFrom/w:moveTo/w:pPrChange with a missing or non-numeric w:id is
unreachable by id, so list_revisions() omits it — and since its tag left
UNHANDLED_REVISION_TAGS it vanished from list_unhandled_revisions() too,
letting accept_all() report a clean document with the mark (and a
moveFrom's hidden text) still in it. _unhandled_elements now also yields
handled-type marks with no adjudicable id, via one shared _adjudicable_id.

_resolve_all/_resolve_ids defer the move range-mark sweep to a single walk
at the end of the call instead of one per move half (quadratic in the
number of moves). _restore_deletion leaves a w:del nested inside the host
it restores untouched (its delText and rsidDel stay), for both w:del and
w:moveFrom hosts. The sweep treats id-less range marks and a Start whose
id a later Start reuses as unpaired. group_spans skips every paragraph-mark
marker, not only w:ins.

Docstrings for UnhandledRevisionWarning, get_visible_text,
get_original_text, list_unhandled_revisions and _insertion_text_nodes
describe the new contract; the harness save2 post-condition gets a unit
test.

* fix: sweep range marks after any resolution and tolerate non-numeric nested ids (ISSUES.md #68)

Move content can leave the document inside another resolved element — a
w:del wrapping a w:moveFrom, a w:ins wrapping a w:moveTo, a rejoined
paragraph whose mark carried a move marker — and the range marks around
it were then orphaned in neither listing. accept_revision/reject_revision
now sweep after every resolution (one deferred walk per bulk call).

_nearest_revision_ancestor_id and _descendant_revision_ids go through
_adjudicable_id, so a non-numeric w:id on a host or nested handled mark
no longer raises from list_revisions(); the mark is reported as unhandled.

The ISSUES.md #56/#62 walk-count pins gained count_revision_walks, a
counter for full-document iter_revision_elements traversals, so they pin
a real constant again now that the resolution path no longer uses
getElementsByTagName. An own edit made inside a foreign w:moveTo is
carried away when that move is rejected — documented as a known gap and
pinned with a strict xfail; docs no longer say list_unhandled_revisions()
lists exactly UNHANDLED_REVISION_TAGS.

* perf: skip the range-mark sweep on documents without move range marks (ISSUES.md #68)

DocxXMLEditor.holds_move_range_marks is set by the id-seeding walk when it
sees a MOVE_RANGE_TAGS element and cleared by the sweep once none remain,
so the sweep — a full-document walk — costs nothing on the move-free
document every accept_revision/accept_group used to pay it on.

Also: _restore_deletion keeps w:rsidDel on runs inside a nested w:moveFrom;
the corpus save2 post-condition is measured against accept_all's own
unhandled report rather than a static tag set, so a handled-type mark the
library reported as unreachable does not fail the file while an unreported
leftover (range marks included) still does; the README no longer claims
that accepting the drag-and-drop table move is exact (the source table
survives as empty cells); quickstart, SKILL.md and the accept_all docstring
describe the own-edit-inside-a-foreign-move gap, the moved-table residue and
a pPrChange carried away by a rejected split; _MARKUP_KIND_BY_TAG follows
its siblings' naming.

* fix: recompute the range-mark flag when a snapshot is restored (ISSUES.md #68)

A rolled-back batch replaces the DOM wholesale; holds_move_range_marks now
follows it (one walk on the rollback path only). Pins the corpus save2
rule that range marks may stay while their move is reported as unhandled.
@pablospe
pablospe deleted the task/b60cdba5-Changeset-tier--changeset_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