A .docx that lands in your inbox during a negotiation is not always what it looks like on screen. The default view in Word shows you a clean document, or a tidy set of the other side's redlines, and you read it as the whole story. It is often not the whole story. The same file can carry revisions the default view does not paint: edits recorded but not shown, comments collapsed out of sight, formatting changes tracked as revisions, text marked hidden, whole passages moved. None of that is exotic. It is ordinary Word behavior, and it is sitting in the file, in a published format, waiting for anyone who opens the box to look.
The risk is specific and it is not hypothetical. The other side edited a number, or a definition, or a governing-law clause, and their view setting or yours meant you never saw the mark. You accepted the document as read. Now you are bound to a change you did not negotiate. This post is about the small, deterministic tool we run on every .docx that enters our Compare flow, what it actually inspects, and why looking at the raw document standard is the right way to catch this and the wrong job for a model.
Why the default view is not the document
A .docx is not a page.
It is a ZIP archive of XML parts, and that XML is OOXML, the open and published standard behind the Word format.
When someone turns on Track Changes and edits, Word does not merely color the text.
It records each edit as a first-class element inside the XML: an insertion is a w:ins, a deletion is a w:del, and each one carries an author, a date, and a revision id.
That is the same markup we write when we produce a real redline, and it is the same markup we read to catch a hidden one.
Here is the gap that blindsides people.
Whether you see those recorded revisions depends on a view setting, not on whether they exist.
The revisions live in the file regardless of what the screen shows.
A document can be displayed as "No Markup" or "Final" and read as clean while every one of those w:ins and w:del elements is still there.
Comments live in a separate part of the archive, word/comments.xml, and a collapsed comment pane does not remove them.
Text can carry a w:vanish property that hides it from the default rendering while leaving it fully present in the XML.
The view is a lens.
The XML is the truth, and the two are allowed to disagree.
What the detector actually inspects
The whole scan, from an uploaded file to the banner, is one deterministic pass with a couple of deliberate escape hatches.
The two dotted branches are the fail-open behavior: a single corrupt part is skipped rather than aborting the scan, and a detector that raises outright never blocks the upload, it just captures an alert and lets the file through.
When a .docx enters Compare, before we diff it against anything, we open the archive and walk its content parts looking for revision markup.
We do not sample.
We read every part that can legally carry tracked changes: the main document, a secondary document part if present, footnotes, endnotes, the glossary document, and every header and footer.
Comments matter to a lawyer too, so we read word/comments.xml separately and count the entries.
What we count is the full set of revision types OOXML defines, not just the obvious two. The detector recognizes, grouped into the buckets it reports:
- Insertions and deletions (
w:ins,w:del). - Moves, where a block was relocated rather than retyped (
w:moveFrom,w:moveTo, and their range-start markers). - Formatting revisions, where the tracked change is to formatting rather than words (
w:pPrChange,w:rPrChange,w:sectPrChange, the tablew:tblPrChange/w:tblPrExChange/w:trPrChange/w:tcPrChange/w:tblGridChange, and legacyw:numberingChange). - Table-structure revisions, where cells were inserted, deleted, or merged (
w:cellIns,w:cellDel,w:cellMerge). - Structural revisions to the document's custom-XML markup (
w:customXmlInsRangeStart,w:customXmlDelRangeStart). - Comments (each
w:commentinword/comments.xml). - Hidden text (a
w:vanishrun property that is switched on).
The reason the list is that long is that the dangerous case is rarely the loud one.
A hundred visible insertions get noticed.
The single tracked change that reformats a clause, or the one merged table cell that shifts an obligation, or the lone hidden run, is exactly the edit that hides inside a document a lawyer skimmed as clean.
A detector that only understood w:ins and w:del would wave those through.
Simplified, the core of the walk is a single pass over every element in a part:
# simplified: fold one part's revision evidence into a running scan
for element in root.iter():
namespace, local = qname(element.tag)
if namespace not in WORDPROCESSING_NAMESPACES:
continue
if local in REVISION_NAMES: # ins, del, moves, *PrChange, cell*, customXml*
scan.counts[local] += 1
scan.note_author_and_date(element)
elif local == "vanish" and toggle_is_on(element):
scan.hidden_text_runs += 1
In plain English: for each element in the part, if it is one of the revision types we recognize we tally it and record who made it and when, and if it is a hidden-text toggle that is actually switched on we count that too.
Two details in that loop are load-bearing.
We check the namespace because Word can emit revisions in either the transitional 2006 namespace or the strict ISO namespace, and a detector that hard-codes one would miss the other.
And a w:vanish is only hidden text if the toggle is on, so we read its value rather than assuming presence means enabled, because an explicit "off" is a real and legal thing to write, where an OFF toggle is any of the falsey tokens false, 0, off, no, or n.
Counting has to be careful not to double-report a single operation. A move shows up in the XML as both a range-start marker and a run wrapper, so we count the range-start markers when they are present and fall back to the run wrappers only when they are not, and for a custom-XML revision we count only the range-start marker and skip its paired range-end, so one relocation or one structural edit is one tally, not two. The distinct author names are capped at a bounded number so a pathological file with thousands of tiny revisions cannot blow up the response, while the count itself stays exact.
Alongside the counts we harvest the authors and the dates. Every insertion, deletion, move, and comment names an author and a timestamp, so the detector collects the distinct author names and the earliest and latest revision dates in the file. That turns a bare "this document has changes" into "this document has changes by these people, spanning these dates," which is what tells a lawyer whether the marks are their own leftover edits or the counterparty's.
What it reports, and the banner
When the walk finds nothing, the detector returns nothing, and the upload response stays clean. That silence is deliberate: no revisions, no comments, no hidden text means no warning, and the lawyer is not trained to ignore a banner that cries wolf on every file.
When it does find something, it returns a structured summary the interface turns into a yellow warning banner on the upload. The summary is assembled from the counts, and it reads like a sentence a person would say:
# simplified example of the banner text the detector produces
This document contains 7 unaccepted tracked changes
(4 insertions, 2 deletions, 1 formatting change) by 2 authors.
It also has 3 unresolved comments.
The comparison will be based on the revisions-shown view, so
accept or reject these changes first for an accurate redline.
In plain English: the banner tells the lawyer exactly how many of each kind of hidden revision the file carries, how many people made them, whether there are unresolved comments or hidden text, and why it matters right now. That last line is not decoration. Compare diffs the revisions-shown view of a document, so a source that already carries its own pending edits produces a confusing double redline against the other file. The banner tells the lawyer to resolve the document's own changes first, which fixes both the immediate diff and the larger problem of never having seen those changes at all. The response also names the affected parts, so a revision buried in a footer or a footnote is reported as living there rather than silently folded into the body count.
Why deterministic XML inspection is the right tool
This is a job for reading the standard, not for asking a model, and the reason is the whole point.
A language model reasons over the text it is shown.
The revisions we are hunting are, by definition, the ones the text does not show.
Point a model at the visible content of a "clean" document and it has nothing to reason about, because the w:del that removed a clause left no visible trace and the w:vanish run is not on the page.
The detector does not read the rendered document.
It reads the archive, and in the archive the deleted clause is a w:del element sitting in plain sight.
The deeper reason is the two failure modes a model brings to a question like this, both of which a lawyer cannot afford.
A model can hallucinate a revision that is not there, and a false "the other side changed your indemnity" is its own kind of damage.
A model can also miss one, and a missed revision is the exact harm the tool exists to prevent.
Counting XML elements does neither.
If there are seven w:ins elements, the count is seven, every time, on the same bytes.
It cannot invent an eighth and it cannot overlook one.
For a check whose only value is that you can trust its answer, determinism is not a nicety, it is the feature.
The implementation earns that trust with the unglamorous parts too. The XML parser is hardened against the standard document-parsing attacks, with external entities, DTD loading, and network access all disabled, because a hostile .docx is a real thing to receive in a negotiation. It is tolerant of minor corruption, so one malformed part degrades to skipping that part rather than aborting the whole scan. And the detector is best-effort by contract: if it fails, it fails open and the upload proceeds, because a document-inspection tool that blocks your document is worse than the problem it was solving. A failure is never swallowed silently, it is captured as an alert, but it never stands between the lawyer and their file.
Test it yourself in about two minutes
You do not need our code to see this, and you should not take our word for it. You need a real document and one upload.
Take a .docx that has been through an actual negotiation, one that went back and forth between two sides. Open it in Word first the way you normally would, in whatever view it opens in, and note that it looks settled: clean, or with a redline you believe you have already read. Now run that same file through a tool that inspects the document rather than the picture of it, ours or any other, and watch what it reports.
A tool that is really reading the standard will tell you what the default view did not: the insertions and deletions still recorded but not shown, the formatting change tracked as a revision, the comment collapsed out of the pane, the hidden run, the block that was moved, and who made each and when. If it surfaces revisions your Word view did not, you have just learned that "looks clean" and "is clean" were never the same claim, and you have learned it on a file you were about to rely on. If a tool shows you nothing but the visible view already showed you nothing, ask it the only question that matters: did you read the XML, or did you read the page. One of those answers can miss the change that binds you. The other cannot.
