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.
- docx_integrity-0.1.1/.gitignore +10 -0
- docx_integrity-0.1.1/LICENSE +21 -0
- docx_integrity-0.1.1/PKG-INFO +597 -0
- docx_integrity-0.1.1/README.md +544 -0
- docx_integrity-0.1.1/corpus/base.docx +0 -0
- docx_integrity-0.1.1/corpus/deck.pptx +0 -0
- docx_integrity-0.1.1/pyproject.toml +53 -0
- docx_integrity-0.1.1/research/add_settings.py +122 -0
- docx_integrity-0.1.1/research/build_corpus.py +301 -0
- docx_integrity-0.1.1/research/build_pptx_corpus.py +184 -0
- docx_integrity-0.1.1/research/calibrate_pptx.py +192 -0
- docx_integrity-0.1.1/research/compare_detectors.py +128 -0
- docx_integrity-0.1.1/research/mutate.py +171 -0
- docx_integrity-0.1.1/research/run_experiment.py +152 -0
- docx_integrity-0.1.1/runs/README.md +55 -0
- docx_integrity-0.1.1/src/docx_integrity/__init__.py +40 -0
- docx_integrity-0.1.1/src/docx_integrity/cli.py +179 -0
- docx_integrity-0.1.1/src/docx_integrity/fidelity.py +101 -0
- docx_integrity-0.1.1/src/docx_integrity/finding.py +89 -0
- docx_integrity-0.1.1/src/docx_integrity/fonts.py +573 -0
- docx_integrity-0.1.1/src/docx_integrity/inspector.py +423 -0
- docx_integrity-0.1.1/src/docx_integrity/pptx_checks.py +253 -0
- docx_integrity-0.1.1/src/docx_integrity/pptx_layout.py +665 -0
- docx_integrity-0.1.1/tests/conftest.py +88 -0
- docx_integrity-0.1.1/tests/test_agent_runs.py +72 -0
- docx_integrity-0.1.1/tests/test_cli.py +151 -0
- docx_integrity-0.1.1/tests/test_false_positives.py +114 -0
- docx_integrity-0.1.1/tests/test_fidelity.py +81 -0
- docx_integrity-0.1.1/tests/test_inspector.py +175 -0
- docx_integrity-0.1.1/tests/test_pptx.py +351 -0
|
@@ -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
|
+
[](https://github.com/Dmitry-Kov/docx-integrity/actions/workflows/ci.yml)
|
|
57
|
+
[](https://pypi.org/project/docx-integrity/)
|
|
58
|
+
[](https://pypi.org/project/docx-integrity/)
|
|
59
|
+
[](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
|
+

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