docx-integrity 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. docx_integrity-0.1.1/.gitignore +10 -0
  2. docx_integrity-0.1.1/LICENSE +21 -0
  3. docx_integrity-0.1.1/PKG-INFO +597 -0
  4. docx_integrity-0.1.1/README.md +544 -0
  5. docx_integrity-0.1.1/corpus/base.docx +0 -0
  6. docx_integrity-0.1.1/corpus/deck.pptx +0 -0
  7. docx_integrity-0.1.1/pyproject.toml +53 -0
  8. docx_integrity-0.1.1/research/add_settings.py +122 -0
  9. docx_integrity-0.1.1/research/build_corpus.py +301 -0
  10. docx_integrity-0.1.1/research/build_pptx_corpus.py +184 -0
  11. docx_integrity-0.1.1/research/calibrate_pptx.py +192 -0
  12. docx_integrity-0.1.1/research/compare_detectors.py +128 -0
  13. docx_integrity-0.1.1/research/mutate.py +171 -0
  14. docx_integrity-0.1.1/research/run_experiment.py +152 -0
  15. docx_integrity-0.1.1/runs/README.md +55 -0
  16. docx_integrity-0.1.1/src/docx_integrity/__init__.py +40 -0
  17. docx_integrity-0.1.1/src/docx_integrity/cli.py +179 -0
  18. docx_integrity-0.1.1/src/docx_integrity/fidelity.py +101 -0
  19. docx_integrity-0.1.1/src/docx_integrity/finding.py +89 -0
  20. docx_integrity-0.1.1/src/docx_integrity/fonts.py +573 -0
  21. docx_integrity-0.1.1/src/docx_integrity/inspector.py +423 -0
  22. docx_integrity-0.1.1/src/docx_integrity/pptx_checks.py +253 -0
  23. docx_integrity-0.1.1/src/docx_integrity/pptx_layout.py +665 -0
  24. docx_integrity-0.1.1/tests/conftest.py +88 -0
  25. docx_integrity-0.1.1/tests/test_agent_runs.py +72 -0
  26. docx_integrity-0.1.1/tests/test_cli.py +151 -0
  27. docx_integrity-0.1.1/tests/test_false_positives.py +114 -0
  28. docx_integrity-0.1.1/tests/test_fidelity.py +81 -0
  29. docx_integrity-0.1.1/tests/test_inspector.py +175 -0
  30. docx_integrity-0.1.1/tests/test_pptx.py +351 -0
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ out/
4
+ /tmp/
5
+ runs/*/work/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .pytest_cache/
10
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmitrii Kovalev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,597 @@
1
+ Metadata-Version: 2.4
2
+ Name: docx-integrity
3
+ Version: 0.1.1
4
+ Summary: Integrity checks for .docx and .pptx: broken references and lost comments in documents, text that does not fit its box in decks. Catches what schema validation and rendering both miss.
5
+ Project-URL: Homepage, https://github.com/Dmitry-Kov/docx-integrity
6
+ Project-URL: Source, https://github.com/Dmitry-Kov/docx-integrity
7
+ Project-URL: Issues, https://github.com/Dmitry-Kov/docx-integrity/issues
8
+ Author: Dmitrii Kovalev
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Dmitrii Kovalev
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: ai-agents,document-integrity,docx,linter,office-open-xml,ooxml,overflow-detection,pptx,text-metrics,tracked-changes,validation
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.9
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Topic :: Office/Business :: Office Suites
42
+ Classifier: Topic :: Software Development :: Quality Assurance
43
+ Classifier: Topic :: Text Processing :: Markup :: XML
44
+ Requires-Python: >=3.9
45
+ Requires-Dist: fonttools>=4.40
46
+ Requires-Dist: lxml>=4.9
47
+ Provides-Extra: dev
48
+ Requires-Dist: pdfplumber>=0.11; extra == 'dev'
49
+ Requires-Dist: pytest>=7.4; extra == 'dev'
50
+ Requires-Dist: python-docx>=1.1; extra == 'dev'
51
+ Requires-Dist: python-pptx>=0.6.23; extra == 'dev'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # docx-integrity
55
+
56
+ [![CI](https://github.com/Dmitry-Kov/docx-integrity/actions/workflows/ci.yml/badge.svg)](https://github.com/Dmitry-Kov/docx-integrity/actions/workflows/ci.yml)
57
+ [![PyPI](https://img.shields.io/pypi/v/docx-integrity)](https://pypi.org/project/docx-integrity/)
58
+ [![Python](https://img.shields.io/pypi/pyversions/docx-integrity)](https://pypi.org/project/docx-integrity/)
59
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
60
+
61
+ `pip install docx-integrity`
62
+
63
+ **Word, LibreOffice and every OOXML schema validator will happily accept a
64
+ `.docx` in which a reviewer's comment has been silently detached from the text
65
+ it was written about.** This repo contains a reproducible harness that produces
66
+ such a file, and a deterministic checker that catches it.
67
+
68
+ The failure is not exotic. It is what you get when an agent edits a contract
69
+ with `python-docx`, which is the first thing most agents reach for.
70
+
71
+ There is a second checker for `.pptx`, which answers a question no OOXML library
72
+ answers: **does the text actually fit the box it was put in?** See
73
+ [Decks](#decks-does-the-text-fit) below.
74
+
75
+ ---
76
+
77
+ ## The finding
78
+
79
+ Two agents were given the same document — a services agreement carrying tracked
80
+ changes from counsel and two reviewer comments — and the same task: update three
81
+ figures in the milestone table.
82
+
83
+ One agent was given no budget constraint. It read the raw XML, noticed the fee
84
+ sat inside counsel's unaccepted insertion, and wrapped its own edits as tracked
85
+ changes under a separate author. The other was told it was a routine edit and to
86
+ be quick. It used `python-docx`.
87
+
88
+ Opened side by side in Word:
89
+
90
+ ![Word for Mac, both files open side by side. Left: the fast agent's output, with
91
+ two comments in the pane. Right: the careful agent's output, with five comments
92
+ including the reviewer's, and the table edits shown as tracked changes.](docs/word-comparison.png)
93
+
94
+ *Left: fast agent. Right: careful agent. Look at the comment panes — the fast
95
+ agent's file is missing M. Reviewer entirely — and at the colour of the table
96
+ figures: blue and underlined on the right, plain black on the left.*
97
+
98
+ | | fast agent | careful agent |
99
+ | -------------------------------- | --------------------------------- | --------------------------------- |
100
+ | Reviewer comment in margin | **absent** | present, anchored to the figure |
101
+ | Table edits | **untracked** (0 `w:ins`, 0 `w:del`) | tracked (9 `w:ins`, 2 `w:del`) |
102
+ | Word warning on open | **none** | none |
103
+ | Word count | 118 | 118 |
104
+ | Comments shown in pane | 2 | 5 |
105
+
106
+ Two defects, and Word reports neither.
107
+
108
+ **First.** The reviewer comment *"Confirm this figure against the source table
109
+ before circulation"* is gone from the margin. Its text is still sitting in
110
+ `word/comments.xml` — it is simply anchored to nothing. The warning to check that
111
+ number vanished at the moment the number changed.
112
+
113
+ **Second, and worse.** All three edits went in *untracked*, in a document
114
+ explicitly under review. Three changes to commercial terms bypass the redline
115
+ entirely.
116
+
117
+ Same word count. Same layout. Same page count. No naive check distinguishes them.
118
+
119
+ ### Why `python-docx` does this
120
+
121
+ Comment anchors live *between* runs, not inside them:
122
+
123
+ ```xml
124
+ <w:p>
125
+ <w:commentRangeStart w:id="1"/>
126
+ <w:r><w:t>EUR 12,000</w:t></w:r>
127
+ <w:commentRangeEnd w:id="1"/>
128
+ <w:r><w:commentReference w:id="1"/></w:r>
129
+ </w:p>
130
+ ```
131
+
132
+ Assigning `paragraph.text` drops every run and creates one new run. The anchors
133
+ go with them:
134
+
135
+ ```xml
136
+ <w:p><w:r><w:t>EUR 14,000</w:t></w:r></w:p>
137
+ ```
138
+
139
+ The result is schema-valid, renders correctly, and opens without complaint. The
140
+ same mechanism eats footnote references, character-style runs and tracked-change
141
+ markup.
142
+
143
+ ---
144
+
145
+ ## Use it
146
+
147
+ ```bash
148
+ pip install docx-integrity
149
+ ```
150
+
151
+ Two dependencies (`lxml`, `fonttools`), Python 3.9+. No model calls, no
152
+ rendering, no network.
153
+
154
+ **Did this file survive editing?**
155
+
156
+ ```bash
157
+ docx-integrity check report.docx
158
+ ```
159
+
160
+ **What did the edit lose?** The question no other tool asks — and the one that
161
+ catches a document stripped of everything, which is otherwise perfectly
162
+ self-consistent:
163
+
164
+ ```bash
165
+ docx-integrity check edited.docx --against original.docx
166
+ ```
167
+
168
+ ```
169
+ edited.docx: 2 error(s), 0 warning(s), 0 info
170
+ [ERROR] CMT005 comment id=1 is orphaned - present in comments.xml but
171
+ anchored to nothing - the reviewer's note is invisible in Word
172
+ [ERROR] FID001 comment anchors: 2 -> 1 (1 lost)
173
+ ```
174
+
175
+ Exit codes are the contract with CI: `0` clean, `1` findings at or above
176
+ `--fail-on` (default `error`), `2` usage error. Add `--json` for machine-readable
177
+ output, `--quiet` to print only what fails the threshold.
178
+
179
+ From Python:
180
+
181
+ ```python
182
+ from docx_integrity import check, compare
183
+
184
+ for f in check("edited.docx"):
185
+ print(f.code, f.severity.value, f.message, f.where)
186
+
187
+ for f in compare("original.docx", "edited.docx"):
188
+ print(f.code, f.message)
189
+ ```
190
+
191
+ ### In CI
192
+
193
+ ```yaml
194
+ - uses: Dmitry-Kov/docx-integrity@v0.1.1
195
+ with:
196
+ files: "out/**/*.docx"
197
+ against: templates/master.docx # optional, enables the fidelity check
198
+ fail-on: error
199
+ ```
200
+
201
+ The action writes a summary to the job page and can emit a JSON report as a
202
+ build artifact. Inputs: `files`, `against`, `fail-on`, `version`, `source`,
203
+ `python-version`, `json-report`. Outputs: `exit-code`, `errors`, `warnings`.
204
+
205
+ Use it on the step *after* anything that edits documents programmatically — a
206
+ generation script, an agent, a template merge. That is where these defects come
207
+ from, and it is the only place they are still cheap to find.
208
+
209
+ ### Decks: does the text fit?
210
+
211
+ ```bash
212
+ docx-integrity check deck.pptx
213
+ ```
214
+
215
+ ```
216
+ deck.pptx: 6 error(s), 4 warning(s), 1 info
217
+ [ERROR] PPT001 text needs 144pt in a 40pt box - 104pt too tall (260% over), 3 line(s)
218
+ -> slide1/OVER_huge_type_tiny_box
219
+ [ERROR] PPT003 word wrap is off and the longest line is 304pt in a 182pt box
220
+ - 122pt runs outside the shape
221
+ -> slide2/OVER_nowrap_single_line
222
+ [WARN ] PPT004 shape extends 142pt past the right edge - content will be cut off
223
+ -> slide3/OFFCANVAS_right
224
+ [WARN ] PPT006 overlaps 'OVERLAP_upper_right' over 23% of the smaller shape
225
+ -> slide3/OVERLAP_lower_left
226
+ ```
227
+
228
+ This is the part [python-pptx has declined for a
229
+ decade](https://github.com/scanny/python-pptx/issues/973) across five issues:
230
+ autofit and overflow need text measurement, and text measurement needs the
231
+ *effective* font size, which is almost never written on the run itself. It has
232
+ to be resolved through the run, the paragraph, the shape's list style, the
233
+ layout placeholder, the master placeholder, the master's text styles, the
234
+ presentation defaults and finally the theme's font scheme.
235
+
236
+ Widths come from the font's own `hmtx`/`cmap` tables via `fontTools`. Nothing is
237
+ rendered or rasterised.
238
+
239
+ **How accurate is it?** `research/calibrate_pptx.py` renders every shape of the
240
+ reference deck one at a time with LibreOffice, extracts the position of every
241
+ glyph, and compares:
242
+
243
+ | | agreement |
244
+ |---|---|
245
+ | line pitch (uniform-size paragraphs, n=12) | median **0.05%**, worst **0.06%** |
246
+ | line count (24 shapes) | **23/24** exact, 1 off by one |
247
+
248
+ Two findings came out of that calibration and neither could have been guessed:
249
+
250
+ **Line spacing in DrawingML is a flat 1.2 x font size, not the font's metrics.**
251
+ Rendering the same string in Calibri, Arial, Times New Roman, Courier New,
252
+ Cambria and Verdana at 12pt and 20pt gives a pitch of exactly 1.2000 x size in
253
+ every case, while those faces' own `ascender + descender + lineGap` ratios range
254
+ from 0.80 to 1.22. Deriving line height from font metrics - correct for Word body
255
+ text - was producing a consistent +1.7% error until this was measured.
256
+
257
+ **The precision limit is about 1%.** The single line-count disagreement is a
258
+ shape whose first line filled its box to within **0.6%**; the renderer broke a
259
+ word earlier. Advance widths cannot resolve a margin that thin, because GPOS
260
+ kerning and shaping are not applied. So anything within a few percent of the
261
+ boundary is reported as borderline (`PPT002`) rather than as overflow, and the
262
+ threshold in `pptx_checks.BORDERLINE` is that measurement rather than a guess.
263
+
264
+ **How good is a metric-compatible substitute, exactly?** This is the one claim
265
+ that cannot be checked on a single machine - Calibri and Carlito are almost
266
+ never both installed - so it was measured across two: Carlito on Linux against
267
+ real Calibri from Microsoft 365 on macOS, both read by this module at 18pt.
268
+
269
+ | sample | Carlito | Calibri | delta |
270
+ |---|---|---|---|
271
+ | digits `0123456789 EUR 44,500.00` | 202.376953 | 202.376953 | **0.000%** |
272
+ | bold caps A–Z | 265.772461 | 265.069336 | −0.265% |
273
+ | caps A–Z | 259.171875 | 258.451172 | −0.278% |
274
+ | clause text | 444.682617 | 443.188477 | −0.336% |
275
+ | pangram | 326.276367 | 324.685547 | −0.488% |
276
+ | lowercase a–z | 213.372070 | 212.132812 | −0.581% |
277
+
278
+ Digits match to the last unit — tabular figures are designed to. Letters do not:
279
+ Carlito runs **0.26–0.58% wider**. So "metric-compatible" means close enough to
280
+ act on, not identical, and the README used to overclaim it.
281
+
282
+ Two consequences worth stating. The substitution error is the same order as the
283
+ GPOS-kerning gap, so the 5% `BORDERLINE` threshold covers both with room. And it
284
+ has a direction: measuring Calibri text with Carlito *overstates* width, so it
285
+ leans toward reporting an overflow that is not there rather than missing one —
286
+ the safe direction for a checker.
287
+
288
+ **A limit that cannot be engineered away.** Which font a deck renders with still
289
+ depends on what is installed where it is opened, so the checker says what it
290
+ measured with:
291
+
292
+ ```
293
+ [INFO ] PPT007 Segoe UI is not installed; measured with DejaVu Sans, which is
294
+ similar but NOT metric-compatible - treat the number as an estimate
295
+ ```
296
+
297
+ ### Severity, and why it is set where it is
298
+
299
+ The rule: **losing something that makes content or an audit trail invisible is
300
+ an error**, because nothing downstream will report it. Losing something that
301
+ only changes how the document looks is a warning.
302
+
303
+ So an orphaned reviewer comment is an error and fails CI by default, while a
304
+ mismatch between table cells and `tblGrid` is a warning — a human will see the
305
+ table re-flow, but nobody will see the missing comment.
306
+
307
+ ---
308
+
309
+ ## Reproduce the experiment
310
+
311
+ ```bash
312
+ pip install -e ".[dev]"
313
+ cd research
314
+ python build_corpus.py # build the reference document (byte-reproducible)
315
+ python run_experiment.py # mutators + inspector + 20-cycle accumulation
316
+ python compare_detectors.py # the headline table below
317
+ ```
318
+
319
+ `libreoffice` on `PATH` is needed for the rendering check. Total runtime is a
320
+ couple of minutes, most of it LibreOffice.
321
+
322
+ `research/add_settings.py` injects `word/settings.xml` into an existing package
323
+ without touching anything else — see `runs/README.md` for why that exists.
324
+
325
+ The reference document is assembled part-by-part rather than with `python-docx`,
326
+ because `python-docx` cannot create most of what needs testing: footnotes,
327
+ comments, tracked changes, content controls. It carries named paragraph and
328
+ character styles, multi-level numbering, two footnotes, two comments, three
329
+ tracked revisions from a named author, a content control, a table with an
330
+ explicit `tblGrid` and a header row, an inline image, an external hyperlink, and
331
+ header/footer parts.
332
+
333
+ ---
334
+
335
+ ## What each verification approach catches
336
+
337
+ `ok` means "no problem found" — i.e. the defect was **missed**.
338
+
339
+ ```
340
+ defect introduced by the agent well- schema render inspector fidelity
341
+ formed (LO) vs source
342
+ python-docx: open and save, no edit ok ok ok ok ok
343
+ python-docx: paragraph.text = ... ok ok ok 2 found 5 losses
344
+ LLM edits a value in raw XML ok ok ok ok ok
345
+ LLM clones a block for "one more clause ok ok ok 1 found 3 losses
346
+ LLM reformatted the XML ok ok ok 5 found ok
347
+ LLM deleted a para holding a footnote a ok ok ok 1 found 3 losses
348
+ LLM renamed a style, left refs dangling ok ok ok 4 found ok
349
+ round-trip through markdown ok ok ok ok 12 losses
350
+
351
+ Real defects introduced: 6
352
+ missed by well-formed check: 6/6
353
+ missed by schema validation: 6/6
354
+ missed by PDF rendering: 6/6
355
+ caught by this prototype: 6/6
356
+ ```
357
+
358
+ Rows one and three are controls — genuinely clean edits, correctly reported clean.
359
+
360
+ The render column is the interesting one. Rendering is the current state of the
361
+ art: [Anthropic's official `pptx` skill](https://github.com/anthropics/skills/blob/main/skills/pptx/SKILL.md)
362
+ converts through LibreOffice to PDF, rasterises the pages, and hands the images
363
+ to a subagent to inspect for overlap and overflow. On this defect class its
364
+ detection rate is zero — not because it looks badly, but because none of these
365
+ defects are visible in a picture.
366
+
367
+ ### Two questions, not one
368
+
369
+ A document stripped of every style, footnote and revision is *perfectly
370
+ self-consistent*. The markdown round-trip row proves it: the inspector finds
371
+ nothing wrong, and the file has lost 100% of its styles, numbering, footnotes,
372
+ comments, revisions, content controls, tables and images.
373
+
374
+ So there are two questions, and both are needed:
375
+
376
+ - **Self-consistency** — do the internal references resolve? (`inspect_docx.py`)
377
+ - **Fidelity** — what was lost relative to the source? (`fidelity.py`)
378
+
379
+ Nothing I could find does the second.
380
+
381
+ ---
382
+
383
+ ## Real agent runs, including the result that went against me
384
+
385
+ The mutators above are hand-written. That makes them a demonstration, not a
386
+ benchmark — so eight real agent runs were done instead. Each agent got its own
387
+ copy of the document, a task phrased the way a user would phrase it, and no hint
388
+ about how to edit. Tooling choice was the variable being measured.
389
+
390
+ | run | class | tool calls | tokens | defects |
391
+ | ------------------------------- | ------- | ---------: | -----: | -------------------- |
392
+ | fee + new clause | careful | 23 | 79k | none |
393
+ | same, + "don't disturb anything" | careful | 29 | 86k | none |
394
+ | table edits | careful | 29 | 73k | none |
395
+ | same, + "don't disturb anything" | careful | 22 | 67k | none |
396
+ | rewrite two paragraphs | careful | 19 | 68k | none |
397
+ | same, + "don't disturb anything" | careful | 18 | 74k | none |
398
+ | **fee, fast** | **fast**| **5** |**36k** | **comment orphaned** |
399
+ | **table, fast** | **fast**| **2** |**34k** | **comment orphaned** |
400
+
401
+ **Six careful runs produced zero structural defects.** All six independently
402
+ declined `python-docx` — several said outright that it cannot round-trip tracked
403
+ changes — went to raw XML with targeted replacements, and wrapped their edits as
404
+ tracked changes with separate authorship. Two spotted that the fee sat inside
405
+ counsel's pending insertion and built correct `w:ins > w:del` nesting for it.
406
+
407
+ So the claim "agents corrupt documents" is **wrong as stated**, and I am not
408
+ making it. The honest claim is narrower:
409
+
410
+ > The variance between agents is total, and it is invisible. Same task, same
411
+ > document, same day: one pipeline produces a correct multi-author redline, the
412
+ > other silently detaches the reviewer's warning. Nothing downstream can tell
413
+ > which one you got.
414
+
415
+ That is a benchmarking and CI problem more than a linting problem. The useful
416
+ question is not "is this file broken" but "which of my document pipelines is
417
+ safe".
418
+
419
+ The obvious objection — *just use a better agent* — has a cost answer. The
420
+ careful runs spent 67–86k tokens and 18–29 tool calls on a two-line edit. Nobody
421
+ pays that for routine work at volume, so routine work will keep taking the cheap
422
+ path. And the fast agent did not report a problem, because it did not know it had
423
+ caused one.
424
+
425
+ ### The agent runs also found three bugs in the checker
426
+
427
+ Worth stating plainly, because it is the main argument for running real agents
428
+ rather than writing mutators:
429
+
430
+ | code | was | now |
431
+ | -------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
432
+ | `REV003` | any `w:delText` inside `w:ins` flagged as a defect | `w:ins > w:del` nesting is legal and means "inserted by one author, deleted by another". Tests the *nearest* revision ancestor now |
433
+ | `PKG005` | zip directory entries (`word/`, `docProps/`) flagged as uncovered parts | skipped — not OPC parts, and Word tolerates them |
434
+ | `FID002` | any increase in construct count flagged as duplication | an agent may legitimately add a clause. Real duplication is caught by colliding ids (`REV001`) |
435
+
436
+ For a linter, precision matters more than recall: one that cries wolf on a valid
437
+ file gets switched off. Regression after the fix is clean — all six hand-written
438
+ defects still caught, zero false positives across the eight agent runs.
439
+
440
+ ---
441
+
442
+ ## What the inspector checks
443
+
444
+ | code | check |
445
+ | ----------- | ------------------------------------------------------------------------------------------------------------ |
446
+ | `PKG001-006`| OPC package integrity, content-type coverage, presence of `Default Extension="rels"` (OPC-legal without it, but Word calls the package corrupt) |
447
+ | `XML001` | well-formedness of every XML part |
448
+ | `REL001-003`| every `r:id` / `r:embed` resolves in `.rels`; targets exist as parts; unreferenced relationships |
449
+ | `STY001-002`| `pStyle` / `rStyle` / `tblStyle` resolve; `basedOn` / `next` / `link` resolve |
450
+ | `NUM001-004`| `numId` → `w:num` → `abstractNumId` → `w:abstractNum`; `ilvl` defined |
451
+ | `FTN001-002`| footnote references resolve; orphaned footnotes |
452
+ | `CMT001-005`| `commentRangeStart` ↔ `commentRangeEnd` ↔ `commentReference` ↔ `comments.xml` |
453
+ | `REV001-003`| revision-id uniqueness; `w:del` carries `w:delText` not `w:t`, respecting legal nesting |
454
+ | `TBL001-002`| `tblGrid` present; cells per row vs grid columns, accounting for `gridSpan` |
455
+ | `SDT001-002`| content-control integrity |
456
+ | `TXT001` | edge whitespace in runs without `xml:space="preserve"` |
457
+ | `FID001-003`| losses and additions relative to the source; drop in text volume |
458
+
459
+ And for `.pptx`:
460
+
461
+ | code | check |
462
+ | ----------- | ------------------------------------------------------------------------------------------- |
463
+ | `PPT000` | text could not be measured at all - reported as an error, never as "clean" |
464
+ | `PPT001` | text taller than its box, beyond the measurement tolerance |
465
+ | `PPT002` | text within tolerance of overflowing - borderline, may go either way |
466
+ | `PPT003` | word wrap off and the longest line runs outside the shape |
467
+ | `PPT004` | shape extends past the slide edge, or sits entirely outside it |
468
+ | `PPT005` | shrink-to-fit requested but no `fontScale` stored - the result depends on the renderer |
469
+ | `PPT006` | two text-bearing shapes overlap |
470
+ | `PPT007` | the declared font is unavailable, so measurements for those shapes are estimates |
471
+
472
+ Every finding carries a code, a severity and an XPath to the offending node.
473
+ The codes are stable, so they are safe to grep for and safe to suppress.
474
+
475
+ No model calls, no rendering, no network — a few hundred lines of `lxml`.
476
+
477
+ The suite has 90 tests, and the three most useful ones are regressions for false
478
+ positives that **real agent runs** exposed and hand-written fixtures never would
479
+ have (`tests/test_false_positives.py`). The committed agent outputs in `runs/`
480
+ are themselves a fixture: six correct edits that must stay clean, two broken
481
+ ones that must be caught.
482
+
483
+ ---
484
+
485
+ ## Accumulation
486
+
487
+ Twenty successive edit cycles, following the round-trip design of
488
+ [DELEGATE-52](https://arxiv.org/abs/2604.15597) but measuring at the file level
489
+ rather than semantically:
490
+
491
+ - Footnotes and comment anchors drop to **50%** on the *first* cycle and stay
492
+ there. The loss is irreversible; later edits do not restore it.
493
+ - Tracked changes grow to **233%** through duplicated revision ids.
494
+ - An error introduced at cycle 3 survives to cycle 20.
495
+ - LibreOffice converts all twenty versions without a single complaint.
496
+
497
+ ---
498
+
499
+ ## Repository layout
500
+
501
+ ```
502
+ src/docx_integrity/
503
+ inspector.py .docx self-consistency
504
+ fidelity.py .docx losses relative to a source
505
+ fonts.py font resolution and text measurement
506
+ pptx_layout.py property inheritance and line layout for decks
507
+ pptx_checks.py .pptx overflow, overlap, off-canvas
508
+ cli.py the command line
509
+ tests/ 90 tests, including the false-positive regressions
510
+ research/ the experiments: corpus builders, mutators, calibration
511
+ corpus/base.docx the reference document, byte-reproducible
512
+ corpus/deck.pptx the reference deck, ground truth in the shape names
513
+ runs/ eight real agent outputs, used as fixtures
514
+ action.yml the GitHub Action
515
+ ```
516
+
517
+ ---
518
+
519
+ ## Limitations
520
+
521
+ Read these before citing any number here.
522
+
523
+ - **One synthetic document.** A real benchmark needs dozens of real documents of
524
+ varied types. This is a single hand-built reference file.
525
+ - **Schema validation is approximated.** The full ECMA-376 XSDs are not bundled;
526
+ `compare_detectors.py` checks namespaces and root elements. For these
527
+ mutations the verdict matches what a real XSD gives — every one is
528
+ schema-legal, because they break referential integrity rather than grammar —
529
+ but swap in a real validator before quoting the schema column.
530
+ - **Word for Mac only.** The screenshot is Word for Mac. Word for Windows is not
531
+ tested, and neither is ONLYOFFICE as a third renderer. Cross-renderer
532
+ divergence is likely and is not characterised here.
533
+ - **`OfficeCLI` not compared.** Its `validate` command is schema-only by its own
534
+ documentation, and `view issues` covers text overflow, contrast, alt text and
535
+ inconsistent fonts — no overlap with the defects here. That should be confirmed
536
+ by running it, which I have not done.
537
+ - **Eight agent runs is a small sample**, on one document, with one task family,
538
+ on one day. The careful/fast split is suggestive, not established.
539
+ - **The pptx calibration is against LibreOffice, not PowerPoint.** Agreement to
540
+ 0.05% on line pitch shows the model is sound; it does not prove PowerPoint
541
+ agrees. Running the same corpus through PowerPoint is the missing half.
542
+ - **Only the Carlito/Calibri pairing has been measured** (see the table above).
543
+ The other entries in `METRIC_SUBSTITUTES` — Caladea/Cambria, Liberation
544
+ Sans/Arial, Liberation Serif/Times New Roman, Liberation Mono/Courier New,
545
+ Gelasio/Georgia — are taken on their designers' word and should get the same
546
+ treatment.
547
+ - **No GPOS kerning or shaping.** Only the legacy `kern` table is read. This is
548
+ the ~1% precision limit described above, and it makes measurements for
549
+ complex scripts and heavily-ligatured display faces untrustworthy.
550
+ - **Font discovery is best-effort.** `fc-match` is used where fontconfig exists;
551
+ otherwise the standard font directories are scanned. A machine with neither
552
+ gets a `PPT000` error saying overflow was not checked - which is the point,
553
+ but it does mean the text checks are only as good as the fonts installed.
554
+ - **The mutators are illustrative.** They model documented patterns, but they are
555
+ written by hand and should be read as regression fixtures, not evidence about
556
+ how agents behave. The agent runs are the evidence.
557
+
558
+ ---
559
+
560
+ ## Prior work
561
+
562
+ - [DELEGATE-52](https://arxiv.org/abs/2604.15597) — Laban, Schnabel, Neville
563
+ (Microsoft Research, 2026). Frontier models corrupt ~25% of document content
564
+ over long delegated workflows; agentic tool use does not help. Measures
565
+ *semantic* degradation. This repo is the file-level complement — the structural
566
+ half nobody has published.
567
+ - [python-pptx #973](https://github.com/scanny/python-pptx/issues/973) and four
568
+ sibling issues — text autofit is unimplementable without text metrics. Root
569
+ cause of most "the AI deck looks broken" reports.
570
+ - [Python-Redlines](https://github.com/JSv4/Python-Redlines) — its README
571
+ documents an unfixed revision-id collision that makes Word report "unreadable
572
+ content". Mutator `C2_copyclause` reproduces that defect class.
573
+ - [Office-o-tron](https://github.com/DEVSDMF/office-o-tron) and other OOXML
574
+ validators — answer "is this schema-valid", not "will Word render this
575
+ correctly".
576
+ - [adeu](https://github.com/dealfluence/adeu), safe-docx, docx-redline-js — the
577
+ small ecosystem of tools attempting faithful docx editing. None ships a
578
+ conformance suite, so nobody can say which of them is safe.
579
+
580
+ ---
581
+
582
+ ## Where this is going
583
+
584
+ The gap I set out to test is real: no existing tool answers "is this
585
+ agent-produced document safe to send". But the agent runs reframed it. The value
586
+ is less in catching broken files after the fact and more in **ranking pipelines
587
+ before you trust them** — which argues for a public benchmark across the docx
588
+ editing tools and agent harnesses, run on a real corpus, with a reproducible
589
+ method.
590
+
591
+ That is what I would like to build next, and it needs a corpus. If you have
592
+ document workflows where an agent edits files that someone else then reviews, I
593
+ would like to hear what breaks for you.
594
+
595
+ ## License
596
+
597
+ MIT.