docx/relationship-orphan-detection

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: —

Metadata

Feature iddocx/relationship-orphan-detection
Formatdocx
Categoryreferences
Spececma-376-5-part-2 § 9.3 pr:Relationship

XPath assertions

ID / partPredicateXPathpython-docxdocxjs
doc-hyperlink-rid-refers-to-rel
word/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-doc
word/_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-ids
word/_rels/document.xml.rels
absent
Relationship 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-rel
word/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])

Render assertions

No render assertions declared.

Generator source

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())

Manifest (expanded)

{
  "$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."
    }
  ]
}

Fixture

Download relationship-orphan-detection.docx (18.6 KB)

Reference preview

No rendered reference is available for this case.

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