Load each of 10 diverse committed fixtures with python-docx and immediately re-save with reproducible=True. Assert the twin retains core structural invariants of the original: paragraph count, presence of the fixture's defining run text, image count, and sectPr presence.
Library verdicts: python-docx: — docxjs: —
| Feature id | docx/round-trip-identity--hyperlink |
|---|---|
| Format | docx |
| Category | round-trip |
| Family | docx/round-trip-identity |
| Axis values | source=hyperlink |
| Spec | ecma-376-5-part-1 § 11.1 |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
paragraph-count-preserved-hyperlinkword/document.xml | match = ^1(\.0)?$The twin must carry exactly the same number of <w:p> elements as the original. | count(//w:body//w:p) | — | — |
sectPr-preserved-hyperlinkword/document.xml | existThe body's terminating <w:sectPr> must survive round-trip — Word refuses to open documents without one. | //w:body/w:sectPr | — | — |
document-rels-present-hyperlinkword/_rels/document.xml.rels | existThe document's relationship to styles.xml must survive — its absence is a classic round-trip loss mode. | //r:Relationships/r:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles'] | — | — |
content-types-document-main-hyperlink[Content_Types].xml | exist[Content_Types].xml must still declare the document main part. | //ct:Override[@ContentType='application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml'] | — | — |
No render assertions declared.
scripts/gen_round_trip_identity.py
#!/usr/bin/env python3
"""Generate round-trip-identity twin fixtures.
For each source fixture, load it via python-docx and immediately save
to ``fixtures/docx/round-trip-identity--<source-id>.docx`` with
``reproducible=True``. The committed twin is what the manifest's
assertions run against; the whole point is to catch python-docx losing
or mutating something on a pure load-save cycle.
"""
from __future__ import annotations
import argparse
import shutil
import sys
from pathlib import Path
from docx import Document
_REPO_ROOT = Path(__file__).resolve().parent.parent
_FIX = _REPO_ROOT / "fixtures" / "docx"
def round_trip(source: Path, out: Path) -> None:
doc = Document(str(source))
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(out), reproducible=True)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", required=True, help="Source fixture logical name (e.g. 'bold-text').")
parser.add_argument("--out", required=True, help="Output fixture path (repo-relative).")
ns = parser.parse_args(argv)
source_path = _FIX / f"{ns.source}.docx"
out_path = _REPO_ROOT / ns.out
if not source_path.exists():
print(f"Source fixture missing: {source_path}", file=sys.stderr)
return 1
round_trip(source_path, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/round-trip-identity--hyperlink",
"kind": "literal",
"title": "Round-trip identity (python-docx)",
"format": "docx",
"category": "round-trip",
"summary": "Load each of 10 diverse committed fixtures with python-docx and immediately re-save with reproducible=True. Assert the twin retains core structural invariants of the original: paragraph count, presence of the fixture's defining run text, image count, and sectPr presence.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "11.1",
"element": "",
"notes": "This family does not target a single OOXML element. It targets the python-docx authoring library's load-save round-trip contract: a file it loads and immediately re-saves must preserve the fixture's externally observable structure. Bugs in this space (e.g. dropped child elements, mutated element ordering, renumbered relationships) do not show up in direct-authoring fixtures because the library also authors the assertion target. Round-trip inverts that."
},
"fixtures": {
"machine": "docx/round-trip-identity--hyperlink"
},
"round_trip": {
"library": "python-docx",
"source_fixture": "docx/round-trip-identity",
"notes": "Mode B (generator-embedded). The generator commits the twin fixture under `round-trip-identity--<source.id>.docx`; this manifest's `source_fixture` value is parameterised per case via the `source` axis."
},
"generator": {
"python": "scripts/gen_round_trip_identity.py",
"arg_template": "--source {source.id} --out fixtures/docx/round-trip-identity--{source.id}.docx"
},
"_expansion": {
"parent_id": "docx/round-trip-identity",
"bindings": {
"source": "hyperlink"
}
},
"assertions": [
{
"id": "paragraph-count-preserved-hyperlink",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "count(//w:body//w:p)",
"must": "match",
"value": "^1(\\.0)?$",
"description": "The twin must carry exactly the same number of <w:p> elements as the original."
},
{
"id": "sectPr-preserved-hyperlink",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:body/w:sectPr",
"must": "exist",
"description": "The body's terminating <w:sectPr> must survive round-trip — Word refuses to open documents without one."
},
{
"id": "document-rels-present-hyperlink",
"part": "word/_rels/document.xml.rels",
"namespaces": {
"r": "http://schemas.openxmlformats.org/package/2006/relationships"
},
"xpath": "//r:Relationships/r:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles']",
"must": "exist",
"description": "The document's relationship to styles.xml must survive — its absence is a classic round-trip loss mode."
},
{
"id": "content-types-document-main-hyperlink",
"part": "[Content_Types].xml",
"namespaces": {
"ct": "http://schemas.openxmlformats.org/package/2006/content-types"
},
"xpath": "//ct:Override[@ContentType='application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml']",
"must": "exist",
"description": "[Content_Types].xml must still declare the document main part."
}
]
}
No rendered reference is available for this case.