skills/docx/scripts/merge_runs.py removes namespace declarations on a source run when merging it into the preceding run. If a retained child uses that namespace prefix, the script reports success but writes XML with an unbound prefix. The direct .docx mode overwrites the input archive with the same invalid document part.
Reproduced at dbd4588f9e1033efb41dad4bef2f7947c8993d44 on Python 3.14.7, defusedxml 0.7.1 and lxml 6.1.3, with dependencies resolved offline.
Reproduction
Run from the repository root with defusedxml installed:
import subprocess
import sys
import tempfile
from pathlib import Path
from xml.etree import ElementTree as ET
w = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "word").mkdir()
doc = root / "word/document.xml"
doc.write_text(f'''<w:document xmlns:w="{w}">
<w:body><w:p>
<w:r><w:t>Hello</w:t></w:r>
<w:r xmlns:q="{w}"><q:tab/><w:t>world</w:t></w:r>
</w:p></w:body>
</w:document>''', encoding="utf-8")
ET.parse(doc) # Input is well-formed.
subprocess.run([
sys.executable, "skills/docx/scripts/merge_runs.py", str(root)
], check=True)
print(doc.read_text(encoding="utf-8"))
ET.parse(doc) # ParseError: unbound prefixThe command exits 0 and prints Merged 1 runs. The output contains <q:tab/>, but its xmlns:q declaration is gone.
Expected behavior and validation
Merging these runs should preserve the tab and namespace binding, leaving a parseable document. Namespace declarations are valid on the run itself; moving the declaration to w:document is a workaround, and that control still produces valid XML after merging.
I also validated the input against the repository's wml.xsd with lxml: it passes. Both unpacked-directory and direct .docx modes reproduce the unbound-prefix failure; both corresponding controls with xmlns:q on the document root pass schema validation after merging. The archive fixture included [Content_Types].xml and _rels/.rels pointing to word/document.xml.
The loss occurs when _merge_run_content moves child elements into the target run and _merge_runs_in removes the source run without retaining its namespace declarations. Preserving the effective bindings on moved children, including conflicting prefix bindings, or declining such merges would avoid writing invalid XML.
This is a bug report rather than a patch because the DOCX skill's license restricts derivative works. The existing comment-part namespace reports (#1821/#1824) concern a different script and do not cover this run-merging failure.
Activity
4I’d like to help investigate and resolve this issue.
skills/docx/scripts/merge_runs.pyremoves namespace declarations on a source run when merging it into the preceding run. If a retained child uses that namespace prefix, the script reports success but writes XML with an unbound prefix. The direct.docxmode overwrites the input archive with the same invalid document part.Reproduced at
dbd4588f9e1033efb41dad4bef2f7947c8993d44on Python 3.14.7, defusedxml 0.7.1 and lxml 6.1.3, with dependencies resolved offline.Reproduction
Run from the repository root with defusedxml installed:
import subprocess import sys import tempfile from pathlib import Path from xml.etree import ElementTree as ET w = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) (root / "word").mkdir() doc = root / "word/document.xml" doc.write_text(f'''<w:document xmlns:w="{w}"> <w:body><w:p> <w:r><w:t>Hello</w:t></w:r> <w:r xmlns:q="{w}"><q:tab/><w:t>world</w:t></w:r> </w:p></w:body> </w:document>''', encoding="utf-8") ET.parse(doc) # Input is well-formed. subprocess.run([ sys.executable, "skills/docx/scripts/merge_runs.py", str(root) ], check=True) print(doc.read_text(encoding="utf-8")) ET.parse(doc) # ParseError: unbound prefixThe command exits 0 and prints
Merged 1 runs. The output contains<q:tab/>, but itsxmlns:qdeclaration is gone.Expected behavior and validation
Merging these runs should preserve the tab and namespace binding, leaving a parseable document. Namespace declarations are valid on the run itself; moving the declaration to
w:documentis a workaround, and that control still produces valid XML after merging.I also validated the input against the repository's
wml.xsdwith lxml: it passes. Both unpacked-directory and direct.docxmodes reproduce the unbound-prefix failure; both corresponding controls withxmlns:qon the document root pass schema validation after merging. The archive fixture included[Content_Types].xmland_rels/.relspointing toword/document.xml.The loss occurs when
_merge_run_contentmoves child elements into the target run and_merge_runs_inremoves the source run without retaining its namespace declarations. Preserving the effective bindings on moved children, including conflicting prefix bindings, or declining such merges would avoid writing invalid XML.This is a bug report rather than a patch because the DOCX skill's license restricts derivative works. The existing comment-part namespace reports (#1821/#1824) concern a different script and do not cover this run-merging failure.
I’d like to help investigate and resolve this issue.
This namespace binding problem can be reproduced. The critical problem is that
merge_runs.pysucceeds in generating a DOCX file despite an unbound namespace prefix being used in its XML content. Before providing a patch, can a maintainer specify the acceptable contribution process considering the license limitation for the DOCX skill? I am able to look into regression tests for unpacked directory and.docxfile modes.