Negative invariant: every r:id referenced in word/document.xml must have a matching <Relationship Id='...'/> in word/_rels/document.xml.rels, and conversely every non-internal relationship in the rels file should have at least one referent in the document (or be an expected package-level rel like styles/theme).
Library verdicts: python-docx: — docxjs: —
| Feature id | docx/relationship-orphan-detection |
|---|---|
| Format | docx |
| Category | references |
| Spec | ecma-376-5-part-2 § 9.3 pr:Relationship |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
doc-hyperlink-rid-refers-to-relword/document.xml | match = ^rId[0-9]+$The hyperlink's r:id must be a well-formed rId token. | string((//w:hyperlink/@r:id)[1]) | — | — |
rels-contains-the-rid-referenced-by-docword/_rels/document.xml.rels | match = ^1(\.0+)?$Exactly one hyperlink relationship must exist in the rels file (matching the one r:id referenced by the document). | count(//pr:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink']) | — | — |
no-duplicate-rel-idsword/_rels/document.xml.rels | absentRelationship Ids must be unique within the rels file. This XPath selects any Relationship whose Id collides with a prior one. | //pr:Relationship[@Id = preceding-sibling::pr:Relationship/@Id] | — | — |
no-orphan-hyperlink-relword/document.xml | match = ^1(\.0+)?$The document must contain exactly one w:hyperlink with an r:id, matching the one hyperlink relationship in the rels file. A count of 0 would mean the rels entry is orphaned; >1 would need to match a rels-side count change. | count(//w:hyperlink[@r:id]) | — | — |
No render assertions declared.
scripts/gen_relationship_orphan_detection_docx.py
#!/usr/bin/env python3
"""Generate ``fixtures/docx/relationship-orphan-detection.docx``.
A minimal document with exactly one external hyperlink. Designed to
satisfy the ``docx/relationship-orphan-detection`` manifest, which
asserts the following cross-part invariant:
* word/document.xml references exactly one hyperlink r:id.
* word/_rels/document.xml.rels carries exactly one hyperlink
Relationship entry.
* Relationship Ids in the rels file are unique.
.. versionadded:: 2026.05.0
"""
from __future__ import annotations
from pathlib import Path
from docx import Document
_REPO_ROOT = Path(__file__).resolve().parent.parent
_OUT = _REPO_ROOT / "fixtures" / "docx" / "relationship-orphan-detection.docx"
def main() -> int:
doc = Document()
p = doc.add_paragraph()
p.add_hyperlink(
url="https://example.com/no-orphan",
text="Resolved",
style=None,
)
_OUT.parent.mkdir(parents=True, exist_ok=True)
doc.save(_OUT, reproducible=True)
print(_OUT)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/relationship-orphan-detection",
"title": "Relationship orphan detection (docx)",
"format": "docx",
"category": "references",
"summary": "Negative invariant: every r:id referenced in word/document.xml must have a matching <Relationship Id='...'/> in word/_rels/document.xml.rels, and conversely every non-internal relationship in the rels file should have at least one referent in the document (or be an expected package-level rel like styles/theme).",
"spec": {
"source": "ecma-376-5-part-2",
"clause": "9.3",
"element": "pr:Relationship",
"rnc_reference": "spec/ecma-376-5/part-2/rnc/opc-relationships.rnc",
"xsd_reference": "spec/ecma-376-5/part-2/xsd/opc-relationships.xsd",
"notes": "Part-2 OPC defines that every Relationship Id in a *.rels file must be unique within that file, and every r:id attribute in the owning part must resolve to one of them. This fixture carries exactly one hyperlink with rId=rId9 both in the document and in the rels. The assertions pin the link in both directions: rId9 must exist in the document AND in the rels with the matching Type. An orphan would manifest as one assertion passing and the other failing."
},
"fixtures": {
"machine": "docx/relationship-orphan-detection"
},
"generator": {
"python": "scripts/gen_relationship_orphan_detection_docx.py"
},
"assertions": [
{
"id": "doc-hyperlink-rid-refers-to-rel",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
},
"xpath": "string((//w:hyperlink/@r:id)[1])",
"must": "match",
"value": "^rId[0-9]+$",
"description": "The hyperlink's r:id must be a well-formed rId token."
},
{
"id": "rels-contains-the-rid-referenced-by-doc",
"part": "word/_rels/document.xml.rels",
"namespaces": {
"pr": "http://schemas.openxmlformats.org/package/2006/relationships"
},
"xpath": "count(//pr:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink'])",
"must": "match",
"value": "^1(\\.0+)?$",
"description": "Exactly one hyperlink relationship must exist in the rels file (matching the one r:id referenced by the document)."
},
{
"id": "no-duplicate-rel-ids",
"part": "word/_rels/document.xml.rels",
"namespaces": {
"pr": "http://schemas.openxmlformats.org/package/2006/relationships"
},
"xpath": "//pr:Relationship[@Id = preceding-sibling::pr:Relationship/@Id]",
"must": "absent",
"description": "Relationship Ids must be unique within the rels file. This XPath selects any Relationship whose Id collides with a prior one."
},
{
"id": "no-orphan-hyperlink-rel",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
},
"xpath": "count(//w:hyperlink[@r:id])",
"must": "match",
"value": "^1(\\.0+)?$",
"description": "The document must contain exactly one w:hyperlink with an r:id, matching the one hyperlink relationship in the rels file. A count of 0 would mean the rels entry is orphaned; >1 would need to match a rels-side count change."
}
]
}
No rendered reference is available for this case.