Skip to content

fix(docx): report the losses the export left silent or misnamed, and hold every node field to a decision - #855

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-report-silent-losses
Oct 5, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-report-silent-losses

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Why

DocxExportReport promises a caller that what the Word file does not carry is said, not approximated in silence. The promise is in the DocxSemanticBackend(Consumer) constructor Javadoc. Three kinds of loss reached the file with at most a LOG.warn, so a service reading the report was told the document lost nothing:

  • a row's fill, outline and side borders: writeRow writes the row as a table with no shading or borders, and paintOf reads only sections and containers;
  • anything in a page zone other than a paragraph, page field or spacer, such as a logo (docx.zone.unsupportedNode, log only);
  • DocumentOutputOptions.watermark, protection and viewerPreferences, which have zero references in the module. A file asked to be password-protected came back open, and nothing said so.

A fourth was reported for the wrong reason. A node kind of the caller's own was "dropped geometry with no semantic Word analogue", while what was lost was its text, its pictures and its children.

Nothing stopped this list from growing. A field added to a node is drawn by the PDF backend at once, and the DOCX export never has to decide what to do with it.

What changed

  • reportUnwrittenRowPaint names what the row loses (row paint): fill, outline, borders, and whether they were rounded.

    • In the body it is DROPPED, a row with no columns included, but only where the layout gives the row some height. The page paints a row only then.
    • A page zone's row is DROPPED; it is written as one line.
    • A row composed in a table cell follows what the table's cell drawing actually did. drawCellDrawing draws a cell's paint as a shape only where it frames no text, and the layout does not say which box is the row's. So:
      • DROPPED where the table drew none, which is what happens to a fill round a label and a value;
      • APPROXIMATED, saying which way, where the table drew shapes.
    • Borders count only sides of non-zero width. A corner radius with nothing to round is not named.
  • Page zone content (DROPPED, page zone content) is reported once per node.

    • A zone's content is built once per section and written into each kind of header or footer it is given. So the dedup set is by identity (IdentityHashMap): two unnamed logos are two notes, and one logo written into a first-page header and the ordinary one is one note.
    • In a file with several sections, each note names its section.
  • reportUnwrittenOutputOptions runs once per export, on both export and exportSections.

    • watermark is reported per section that sets one.
    • protection and viewer preferences are reported once for the file. They are document-wide, as the PDF backend's multi-section render takes the first a section sets (PdfFixedLayoutBackend, "the first section that declares each wins").
  • Unknown node kinds are named as unknown, with what is lost (droppedBecause, heldNodes).

    • isBuiltIn is an exact package match (node package, layout package). A prefix match took the test's own node, in com.demcha.compose.document.backend…, for a built-in.
    • A custom node whose shapes are drawn keeps its APPROXIMATED note and says what else is lost.
    • A page field in the body is dropped for that reason, not for "geometry".
    • The once-per-kind log line no longer carries the first node's count.
  • DocxNodeFieldLedgerTest records a fate for:

    • every field of every concrete node class in the node and layout packages: 25 kinds, nested and non-public classes scanned too, read from the core classes directory or jar;
    • every output option.

    The fates are WRITTEN in the body, REPORTED, INERT (29), or GAP with what is lost (55 node fields, plus page zones, where a paragraph's alignment, spacing and direction and a row's columns are not written). A kind or field with no entry fails, and so does a node class that is not a record. Closing a gap moves its entry in the same change.

  • The recipe, the capability matrix (watermark, protection, viewer preferences and page-zone rows), render-docx/README.md and the CHANGELOG say which losses are reported.

Verification

  • ./mvnw -B -ntp install -pl :graph-compose-render-docx → BUILD SUCCESS: 972 tests, 0 failures, 1 skipped (the property-gated fidelity probe).
    • 24 tests are new: 19 in DocxReportedLossesTest and 5 in DocxNodeFieldLedgerTest.
    • The report tests include negatives: a row that paints nothing, a row of no height, a zone of text and page numbers, and a built-in drawing that keeps the geometry wording.
    • Both composed-cell outcomes are tested.
  • Each guard fails when what it guards is taken away:
    • Deleting a ledger entry (RowNode.gap) or a whole kind (PageBreakNode) fails the ledger.
    • Disabling the zone dedup fails both "once" tests; they give the zone a second header kind through a first-page-only zone.
    • The composed-cell DROPPED test fails on the first commit's always-APPROXIMATED branch.
  • The DOCX bytes do not change. The 62 corpus documents exported deterministically before and after are byte-identical: DocxFidelityCorpusTest -Dgraphcompose.docxFidelity=export, SHA-256 per file, 0 of 62 differ.
  • Documentation guards: -pl :graph-compose-core -Dtest='com.demcha.documentation.**' → 166 tests. The qa documentation guards plus DocxPageZoneTest, DocxTransparentWrapperTest and CrossModuleBoundaryArchTest → 45 tests. All green.
  • The full reactor gate was not run; no public API, POM or workflow file changed.

Lane: render-docx backend (report only, no change to what is written) plus tests and docs.

…ode field to a decision

A row's fill, outline and side borders, anything in a page zone but text and
page fields, a watermark, a protection, viewer preferences and a node kind of
the caller's own all reached the Word file with at most a log line, so a caller
reading DocxExportReport was told the document lost nothing.

Each is now a report note: row paint (DROPPED; APPROXIMATED for a row composed
in a table cell, whose paint the cell drawing may draw), page zone content
(once per node, by identity, though a zone is written into each kind of header
it is given), watermark per section, protection and viewer preferences once
for the file, and an unknown node kind named as unknown, with what it holds,
instead of as dropped geometry. A page field in the body says why it is
dropped. Built-in kinds are those of the node and layout packages exactly.

DocxNodeFieldLedgerTest records, for every field of every public node class
and every output option, whether the export writes it, reports it, has nothing
to carry, or leaves a gap naming what is lost, and fails on a kind, field or
non-record node class nobody decided. The DOCX bytes do not change.
…rd a zone's losses

A row composed in a table cell was always reported APPROXIMATED, though the
cell drawing draws a cell's paint only where it frames no text: a filled
label-and-value row lost its fill and the report called it approximated. It is
now DROPPED where the table drew none of its cells' paint, and APPROXIMATED,
saying which way, where it drew shapes. A row of no columns and no height,
which the page does not paint, is no longer reported; the note names a rounded
paint and lists its parts as a sentence.

Page zone notes name their section in a file of several. The once-a-kind log
line no longer carries the first node's count.

The ledger scans every concrete node class in the node and layout packages,
nested and non-public ones included, and records page zones' own gap - a
paragraph's alignment, spacing and direction and a row's columns are not
written there - under the zones option.
@DemchaAV DemchaAV changed the title fix(docx): report the losses the export left silent, and hold every node field to a decision fix(docx): report the losses the export left silent or misnamed, and hold every node field to a decision Oct 5, 2026
@DemchaAV
DemchaAV merged commit 7eadcac into 2.5-dev Oct 5, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-report-silent-losses branch October 5, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant