Skip to content

Commit 8495f90

Browse files
authored
feat: resolve moves and w:pPrChange as revisions (ISSUES.md #68) (#82)
* 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.
1 parent 00e01e2 commit 8495f90

17 files changed

Lines changed: 2259 additions & 641 deletions

‎benchmarks/corpus/README.md‎

Lines changed: 69 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -95,25 +95,29 @@ trigger it manually with `workflow_dispatch` after changes.
9595
Every run also counts revision-bearing elements by tag across each file's
9696
`word/*.xml` parts (recorded as `rec["census"]` in `results.json`). It is
9797
informational — never a stage, never a failure — and exists to answer which
98-
revision types real-world producers actually emit. Observed 2026-08-29 over the
99-
77-file corpus:
98+
revision types real-world producers actually emit. The `save2` stage adds the
99+
one hard check derived from it: after `accept_all()`, the saved
100+
`word/document.xml` may hold only what `accept_all()` itself reported as
101+
unhandled — a `w:ins`, `w:moveFrom`, `w:pPrChange` or move range mark left
102+
behind unreported fails the file with `AssertResolvedTypesRemain`. Observed
103+
2026-08-30 over the 77-file corpus:
100104

101105
```text
102106
tag elements files producers
103107
w:ins 162 10 LibreOffice (docx re-save), LibreOffice core ooxmlexport fix
104108
w:del 136 11 LibreOffice (docx re-save), Open-Xml-PowerTools test fixture
105109
*w:tcPrChange 36 6 Open-Xml-PowerTools test fixture (Word)
106-
*w:moveFrom 27 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
107-
*w:moveTo 27 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
108-
*w:pPrChange 11 5 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
110+
w:moveFrom 27 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
111+
w:moveTo 27 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
112+
w:pPrChange 11 5 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
109113
*w:cellMerge 6 1 Open-Xml-PowerTools test fixture (Word)
110114
*w:tblGridChange 6 6 Open-Xml-PowerTools test fixture (Word)
111115
*w:cellDel 4 1 Open-Xml-PowerTools test fixture (Word)
112116
*w:cellIns 4 1 Open-Xml-PowerTools test fixture (Word)
113-
*w:moveFromRangeEnd 4 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
114-
*w:moveFromRangeStart 4 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
115-
*w:moveToRangeEnd 4 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
116-
*w:moveToRangeStart 4 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
117+
w:moveFromRangeEnd 4 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
118+
w:moveFromRangeStart 4 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
119+
w:moveToRangeEnd 4 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
120+
w:moveToRangeStart 4 4 LibreOffice core ooxmlexport fixture (Word/LO mixed), Open-X
117121
*w:tblPrChange 4 4 Open-Xml-PowerTools test fixture (Word)
118122
*w:rPrChange 3 2 Open-Xml-PowerTools test fixture (Word)
119123
*w:tblPrExChange 3 2 Open-Xml-PowerTools test fixture (Word)
@@ -130,7 +134,7 @@ tag elements files producers
130134
*w:sectPrChange 1 1 Open-Xml-PowerTools test fixture (Word)
131135
132136
26/77 files carry at least one revision element
133-
* = not resolved by accept_all/reject_all (169 element(s), ISSUES.md #68)
137+
* = not resolved by accept_all/reject_all (88 element(s))
134138
135139
w:ins/w:del by parent element (structural markers vs content revisions):
136140
w:rPr 172 <- paragraph-mark ins/del, or a change record's rPr
@@ -144,32 +148,55 @@ w:ins/w:del by parent element (structural markers vs content revisions):
144148
- poi_ExternalEntityInText.docx [word/document.xml]: EntitiesForbidden: ...
145149
```
146150

147-
Read as evidence for ISSUES.md #68:
151+
Read as evidence:
148152

149-
- **Every unhandled type now has genuine Word output behind it.** The 21
153+
- **Every unhandled type has genuine Word output behind it.** The 21
150154
`oxpt_RP*` files were authored in Word (see Provenance) and between them
151-
emit all 26 tags in `UNHANDLED_REVISION_TAGS`. Before they were added, the
152-
five revision-bearing files were almost all LibreOffice-produced and only
153-
moves and `w:pPrChange` had any real-producer evidence; the rest was
155+
emit all 19 tags in `UNHANDLED_REVISION_TAGS` as well as the moves and
156+
`w:pPrChange` that ISSUES.md #68 took out of it. Before they were added,
157+
the five revision-bearing files were almost all LibreOffice-produced and
158+
only moves and `w:pPrChange` had any real-producer evidence; the rest was
154159
hand-authored XML in `tests/test_unhandled_revisions.py` (still there, for
155160
the edge shapes).
156-
- **Moves are the largest unhandled family** (27 + 27 + range marks) and they
157-
are real, not synthetic: `locore_TC-table-DnD-move.docx` (a Word
158-
drag-and-drop move re-exported by LibreOffice core) and the Word-native
159-
`oxpt_RP015-MoveFrom-MoveTo.docx` / `oxpt_RP018-MoveFrom-MoveTo-CC.docx`
160-
(the latter inside a content control, with `customXmlMove*` marks).
161-
`accept_all()` resolves none of them and leaves the whole redline pending.
162-
- **Property changes are the next family**: `w:pPrChange` in five files
163-
(Word: `oxpt_RP022`, `RP025`, `RP037`, plus the kitchen-sink `RP001`),
164-
`w:rPrChange` on a run (`oxpt_RP037`) and on a paragraph mark
165-
(`oxpt_RP024`), `w:sectPrChange` (`oxpt_RP027`), `w:numberingChange`
166-
(`oxpt_RP026`), and the whole table family — `w:tblPrChange`,
167-
`w:tblPrExChange`, `w:trPrChange`, `w:tcPrChange`, `w:tblGridChange`
168-
(`oxpt_RP028`, `RP033`, `RP001`).
161+
- **Moves resolve, on every producer that emits them** (ISSUES.md #68):
162+
`locore_TC-table-DnD-move.docx` (a Word drag-and-drop move of a whole table,
163+
re-exported by LibreOffice core) carries 20 move marks — `accept_all()`
164+
lists and resolves its 16 `w:moveFrom`/`w:moveTo` elements as one inferred
165+
changeset and sweeps the 4 range marks with them, leaving the table at its
166+
destination exactly once; `reject_all()` puts it back. The Word-native
167+
`oxpt_RP015-MoveFrom-MoveTo.docx` resolves clean (6 elements);
168+
`oxpt_RP018-MoveFrom-MoveTo-CC.docx` resolves its 7 move elements and leaves
169+
the 8 `customXmlMove*` marks around its content control pending. Half the
170+
DnD file's move elements are paragraph-mark markers
171+
(`w:pPr/w:rPr/w:moveFrom`, one per moved cell); like a deleted paragraph
172+
mark these resolve *approximately* — the marker is dropped without merging
173+
or splitting paragraphs — so the moved text ends up at its destination
174+
exactly once, but the source table survives as a 2×2 table of empty cells
175+
(the four leading empty lines in the fixture's expected visible text),
176+
where Word would remove it.
177+
Both shapes are mirrored by hand-authored fixtures in
178+
`tests/test_move_and_ppr_change_resolution.py` and run against the real
179+
files by `tests/test_corpus_fixtures.py` when the corpus is built.
180+
- **Paragraph-property changes resolve too**: `w:pPrChange` in five files
181+
(Word: `oxpt_RP022`, `RP025`, `RP037`, the kitchen-sink `RP001`, and
182+
`locore_UnknownStyleInRedline.docx`, whose two records share `w:id="0"`, one
183+
self-closing, one naming a style `styles.xml` does not define — accept drops
184+
the records, reject restores the recorded properties verbatim). `RP037`
185+
also carries two `w:pPrChange` inside `word/styles.xml` style definitions,
186+
outside the part the library reads (ISSUES.md #30); they show in the
187+
per-part census and are not a `save2` failure, the same way
188+
`oxpt_RP050-Deleted-Footnote.docx`'s deletions in `word/footnotes.xml` are
189+
not.
190+
- **The rest of the property family is still unhandled**: `w:rPrChange` on a
191+
run (`oxpt_RP037`) and on a paragraph mark (`oxpt_RP024`), `w:sectPrChange`
192+
(`oxpt_RP027`), `w:numberingChange` (`oxpt_RP026`), and the whole table
193+
family — `w:tblPrChange`, `w:tblPrExChange`, `w:trPrChange`,
194+
`w:tcPrChange`, `w:tblGridChange` (`oxpt_RP028`, `RP033`, `RP001`). These
195+
stay behind the honesty floor (`ResolveResult.unhandled`, ISSUES.md #64).
169196
- **Table-structure revisions exist in the wild**: `w:cellIns` (`oxpt_RP035`),
170197
`w:cellDel` (`oxpt_RP034`), `w:cellMerge` (`oxpt_RP036`), each alongside the
171198
`w:ins`/`w:del` Word writes for the cells' content.
172-
- **The structural `w:ins`/`w:del` contexts are now the majority row**: 172
199+
- **The structural `w:ins`/`w:del` contexts are the majority row**: 172
173200
paragraph-mark markers (`w:pPr/w:rPr/w:ins|del`; the kitchen-sink file alone
174201
has 160) and 22 table-row markers (`w:trPr`, from `oxpt_RP009`/`RP010` and
175202
`RP001`), against 73 ordinary content revisions. These resolve
@@ -179,10 +206,12 @@ Read as evidence for ISSUES.md #68:
179206
math run (`oxpt_RP013`), and the `w:moveFrom`/`w:moveTo` rows are Word's
180207
own paragraph-mark markers inside the kitchen-sink file's moved paragraphs.
181208

182-
The `*`-marked tags are exactly `UNHANDLED_REVISION_TAGS`
183-
(`docx_editor/track_changes.py`), which is also what `accept_all()` /
209+
The `*`-marked tags are `UNHANDLED_REVISION_TAGS`
210+
(`docx_editor/track_changes.py`), which is what `accept_all()` /
184211
`reject_all()` report as `.unhandled` and what `list_unhandled_revisions()`
185-
lists.
212+
lists — plus, in a document a nonconforming producer wrote, any handled-type
213+
mark with no numeric `w:id` (nothing id-keyed can resolve it; none in this
214+
corpus).
186215

187216
## Failure semantics
188217

@@ -204,23 +233,22 @@ lists.
204233
`w:trackRevisions` flag, or a vanished edit marker still fails. A waiver
205234
that turns out to be unnecessary fails the file with
206235
`StaleSurvivalWaiver`, so the manifest cannot quietly outlive the behavior
207-
it documents. Two files carry one today, both verified by dumping
208-
LibreOffice's import as flat ODT:
236+
it documents. One file carries one today, verified by dumping LibreOffice's
237+
import as flat ODT:
209238
- `poi_FieldCodes.docx` — the first paragraph is an `AUTHOR` field result;
210239
Writer fields carry no redlines, so LibreOffice flattens our del/ins into
211240
the result text (`ANTONIANTONI-EDITED`).
212-
- `locore_TC-table-DnD-move.docx` — the first paragraph sits inside a
213-
foreign `w:moveFrom` (moved-away text); LibreOffice folds our deletion
214-
into the move's own deletion region and only the insertion comes back.
215-
(That the library edits inside `w:moveFrom` content at all — text that is
216-
deleted at its source — is a finding for ISSUES.md #68, not for the gate.)
241+
`locore_TC-table-DnD-move.docx` carried one until ISSUES.md #68: its first
242+
paragraphs sit inside a foreign `w:moveFrom` (moved-away text), which the
243+
library now treats as invisible, so the harness edits the first paragraph
244+
that is actually visible and the redline survives LibreOffice unaided.
217245
- The harness exits nonzero if any file has a real failure (failed stage or
218246
harness error), and if the corpus directory is empty or a `--only` filter
219247
matches nothing (a run that tested nothing must not look green).
220248
Baseline: 76 clean + 1 rejected → exit 0. Of the 76, the `lo_roundtrip`
221-
survival assertion ran on 68 (1 of them with the flag check not
249+
survival assertion ran on 69 (1 of them with the flag check not
222250
applicable, `onlyoffice_sample.docx`), was skipped on 6 with no editable
223-
paragraph, and was waived on 2.
251+
paragraph, and was waived on 1.
224252

225253
## Provenance policy
226254

‎benchmarks/corpus/corpus_harness.py‎

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
save1 save-as to out/<name>_edited.docx + zip/XML validation
2323
reopen reopen saved file, assert the edit survived and the revision
2424
count is coherent, then accept_all
25-
save2 save to out/<name>_final.docx + zip/XML validation
25+
save2 save to out/<name>_final.docx + zip/XML validation, then
26+
assert the saved word/document.xml holds only what
27+
accept_all itself reported as unhandled: an ins/del/move/
28+
pPrChange element or move range mark left behind unreported
29+
is a failure
2630
lo_roundtrip soffice --headless --convert-to docx on the *edited* output,
2731
then assert that what we wrote survived the re-save
2832
pdf soffice --headless --convert-to pdf on the final output
@@ -37,8 +41,8 @@
3741
never the signal relied on), and lo_roundtrip additionally reopens the
3842
re-saved file and checks that our w:trackRevisions flag and our own
3943
insertion/deletion are still there. Where
40-
LibreOffice's own model cannot hold our redline (inside a field result,
41-
inside a foreign move), the manifest records a ``survival_waiver`` with the
44+
LibreOffice's own model cannot hold our redline (inside a field result),
45+
the manifest records a ``survival_waiver`` with the
4246
reason; see apply_manifest_expectations.
4347
4448
An input that fails input_validate and is then refused by Document.open is
@@ -49,8 +53,8 @@
4953
across its ``word/*.xml`` parts, recorded as ``rec["census"]``. It is
5054
informational and can never fail a run, so it is not a stage — it exists to
5155
say which revision types real-world producers actually emit, which is the
52-
evidence base for handling the ones this library does not resolve
53-
(ISSUES.md #68). ``--census`` prints just that table, in seconds.
56+
evidence base for handling the ones this library does not resolve.
57+
``--census`` prints just that table, in seconds.
5458
"""
5559

5660
import argparse
@@ -193,7 +197,7 @@ def census_file(path: Path) -> dict:
193197

194198
def print_census(results: list[dict]) -> None:
195199
"""Print the corpus-wide revision census: which tags real producers emit."""
196-
from docx_editor.track_changes import HANDLED_REVISION_TAGS, UNHANDLED_REVISION_TAGS
200+
from docx_editor.track_changes import UNHANDLED_REVISION_TAGS
197201

198202
totals: dict[str, int] = {}
199203
files_with: dict[str, set[str]] = {}
@@ -230,14 +234,14 @@ def print_census(results: list[dict]) -> None:
230234
print("\nRevision census (all word/*.xml parts)")
231235
print(f"{'tag':<32}{'elements':>10}{'files':>7} producers")
232236
for tag, n in sorted(totals.items(), key=lambda kv: (-kv[1], kv[0])):
233-
mark = " " if tag in HANDLED_REVISION_TAGS else "*"
237+
mark = "*" if tag in UNHANDLED_REVISION_TAGS else " "
234238
prods = ", ".join(sorted(producers[tag]))
235239
print(f"{mark}{tag:<31}{n:>10}{len(files_with[tag]):>7} {prods[:60]}")
236240
if not totals:
237241
print(" (no revision elements found in any corpus file)")
238242
print(f"\n{carrying}/{n_files} files carry at least one revision element")
239243
unhandled_total = sum(n for tag, n in totals.items() if tag in UNHANDLED_REVISION_TAGS)
240-
print(f"* = not resolved by accept_all/reject_all ({unhandled_total} element(s), ISSUES.md #68)")
244+
print(f"* = not resolved by accept_all/reject_all ({unhandled_total} element(s))")
241245
if contexts:
242246
print("\nw:ins/w:del by parent element (structural markers vs content revisions):")
243247
for parent, n in sorted(contexts.items(), key=lambda kv: (-kv[1], kv[0])):
@@ -658,26 +662,59 @@ def fail_rest(from_stage: str):
658662
)
659663
fail_rest("reopen")
660664
return result
661-
stages["reopen"] = {"status": "pass", "accepted": accepted}
665+
stages["reopen"] = {
666+
"status": "pass",
667+
"accepted": accepted,
668+
"unhandled": dict(accepted.unhandled_types),
669+
}
662670
except Exception as e:
663671
stages["reopen"] = err_record(e)
664672
fail_rest("reopen")
665673
return result
666674

667-
# Stage 6: save2 + validate
675+
# Stage 6: save2 + validate + post-condition: after accept_all, the
676+
# saved file may hold only revision types the library never resolves.
668677
try:
678+
from docx_editor.track_changes import MOVE_RANGE_TAGS, UNHANDLED_REVISION_TAGS
679+
669680
doc2.save(out2)
670681
v = validate_docx(out2)
671-
if v["ok"]:
672-
stages["save2"] = {"status": "pass"}
673-
else:
682+
if not v["ok"]:
674683
stages["save2"] = {
675684
"status": "fail",
676685
"error_type": "OutputValidation:" + v["error_type"],
677686
"error": v["error"][:400],
678687
}
679688
fail_rest("save2")
680689
return result
690+
# word/document.xml only: the part accept_all reads (ISSUES.md #30).
691+
# A redline in styles.xml or footnotes.xml is visible in the
692+
# per-part census but is not something accept_all claimed to do.
693+
# Measured against accept_all's own report, not a static tag set:
694+
# a handled-type mark it could not reach (no numeric w:id) is
695+
# reported in unhandled_types and may stay; only what it left
696+
# behind *unreported* is a failure. Range marks are scaffolding
697+
# swept with their move, so they may remain only while a move
698+
# element of their family is still reported as unhandled.
699+
final_census = census_file(out2)
700+
body_tags = final_census.get("parts", {}).get("word/document.xml", {}).get("by_tag", {})
701+
reported = accepted.unhandled_types
702+
leftover = {
703+
tag: n - reported.get(tag, 0)
704+
for tag, n in body_tags.items()
705+
if tag not in UNHANDLED_REVISION_TAGS and tag not in MOVE_RANGE_TAGS and n > reported.get(tag, 0)
706+
}
707+
for tag, n in body_tags.items():
708+
if tag in MOVE_RANGE_TAGS and not reported.get("w:" + tag[len("w:") : tag.index("Range")], 0):
709+
leftover[tag] = n
710+
if leftover:
711+
stages["save2"] = assert_fail(
712+
"AssertResolvedTypesRemain",
713+
f"accept_all() left unreported revision elements in the saved file: {leftover}",
714+
)
715+
fail_rest("save2")
716+
return result
717+
stages["save2"] = {"status": "pass", "census": final_census.get("by_tag", {})}
681718
except Exception as e:
682719
stages["save2"] = err_record(e)
683720
fail_rest("save2")
@@ -717,8 +754,8 @@ def apply_manifest_expectations(rec: dict) -> None:
717754
counts as failed and keeps its own diagnostics.
718755
719756
``survival_waiver``: a documented reason why LibreOffice cannot keep our
720-
redline in *this* file (a redline inside a field result, inside a foreign
721-
move, ...). The survival assertion is then reported as a skip with that
757+
redline in *this* file (a redline inside a field result, ...). The
758+
survival assertion is then reported as a skip with that
722759
reason — never as a pass — and a waiver that turns out to be unnecessary
723760
(everything survived) is itself a failure, so the manifest cannot quietly
724761
outlive the LibreOffice behavior it describes. Only ``WAIVABLE_ASSERTIONS``

‎benchmarks/corpus/manifest.json‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,7 @@
363363
"producer": "LibreOffice core ooxmlexport fixture (Word/LO mixed)",
364364
"source": "https://raw.githubusercontent.com/LibreOffice/core/1c29ec6c2b2ec1f343545915ba66b7e9718b3792/sw/qa/extras/ooxmlexport/data/TC-table-DnD-move.docx",
365365
"size": 12977,
366-
"sha256": "32419eb1c66dc5ca",
367-
"survival_waiver": "the edited paragraph sits inside a foreign w:moveFrom (moved-away text); LibreOffice folds our deletion into the move's own deletion region on import"
366+
"sha256": "32419eb1c66dc5ca"
368367
},
369368
"locore_99_Fields.docx": {
370369
"kind": "download",

0 commit comments

Comments
 (0)