Problem
TextReplacementEngine.applyOneReplacement's multi-run path silently removes Runs whose only structural payload is rawElements. Word's strict OOXML validator subsequently rejects the resulting docx because the comment-marker triplet (commentRangeStart + commentRangeEnd + commentReference) becomes incomplete.
Discovered while rescuing the NTPU master's thesis (kiki830621/collaboration_guo_analysis#20). Word reported "the file is corrupt and cannot be opened" on every output of the rescue pipeline. Diagnosis:
| Marker |
Target docx (input) |
Output docx |
Diff |
<w:commentRangeStart w:id="23"/> |
✅ |
✅ |
— |
<w:commentRangeEnd w:id="23"/> |
✅ |
✅ |
— |
<w:commentReference w:id="23"/> |
✅ |
❌ missing |
dropped during replaceText("適應性", "配適度") |
Root cause
// TextReplacementEngine.swift line 351
private static func isTextRun(_ run: Run) -> Bool {
return run.rawXML == nil && run.drawing == nil // ← misses rawElements
}
A Run shaped like
<w:r>
<w:rPr><w:rStyle w:val="af2"/>…</w:rPr>
<w:commentReference w:id="23"/>
</w:r>
parses with text == "", rawXML == nil, drawing == nil, rawElements == [RawElement(name: "commentReference", xml: ...)].
isTextRun returns true, so the multi-run remove pass at line 402-410 deletes this Run wholesale — even though it carries the comment payload.
Repro (failing test)
func testReplaceMultiRunPreservesCommentReferenceRun() throws {
var commentRefRun = Run(text: "")
commentRefRun.rawElements = [
RawElement(name: "commentReference", xml: "<w:commentReference w:id=\"23\"/>")
]
var runs = [
Run(text: "適應性"),
commentRefRun, // ← empty text + rawElements
Run(text: ",本研究")
]
// multi-run match: "性,本" spans runs[0..2] with the commentRef Run between
let count = try TextReplacementEngine.replace(
runs: &runs, find: "性,本", with: "X"
)
XCTAssertEqual(count, 1)
XCTAssertEqual(runs.count, 3) // ← FAILS pre-fix: runs.count == 2
XCTAssertEqual(runs[1].rawElements?.first?.name, "commentReference")
}
Pre-fix output:
XCTAssertEqual failed: ("2") is not equal to ("3") - Run carrying only rawElements was deleted by multi-run remove pass
Fix
In applyOneReplacement at line 402-410, gate the removal on isTextRun(r) && (r.rawElements?.isEmpty ?? true) — a Run with non-empty rawElements carries structural payload that must survive replacement.
Branch / PR coming up via the v0.27.0 release.
Affected operations
Document.replaceText (and any caller using it: che-word-mcp replace_text / replace_text_batch MCP tools, this rescue script's caption fix + abstract substitution phases, …) hits this whenever:
- The match is multi-run (sRunIdx ≠ eRunIdx)
- A Run sits strictly between sRunIdx and eRunIdx
- That Run has only
rawElements (no rawXML, no drawing, empty text)
Most common trigger: the <w:r><w:commentReference/></w:r> immediately following a <w:commentRangeEnd/> marker in Word-authored documents.
Severity
P0 — silently breaks docx schema. Word strict validator refuses to open. Lenient parsers (libxml2, pandoc, python-docx) read the broken file fine, masking the problem until end-user opens in Word.
Type
bug
Problem
TextReplacementEngine.applyOneReplacement's multi-run path silently removes Runs whose only structural payload israwElements. Word's strict OOXML validator subsequently rejects the resulting docx because the comment-marker triplet (commentRangeStart+commentRangeEnd+commentReference) becomes incomplete.Discovered while rescuing the NTPU master's thesis (kiki830621/collaboration_guo_analysis#20). Word reported "the file is corrupt and cannot be opened" on every output of the rescue pipeline. Diagnosis:
<w:commentRangeStart w:id="23"/><w:commentRangeEnd w:id="23"/><w:commentReference w:id="23"/>replaceText("適應性", "配適度")Root cause
A Run shaped like
parses with
text == "",rawXML == nil,drawing == nil,rawElements == [RawElement(name: "commentReference", xml: ...)].isTextRunreturnstrue, so the multi-run remove pass at line 402-410 deletes this Run wholesale — even though it carries the comment payload.Repro (failing test)
Pre-fix output:
Fix
In
applyOneReplacementat line 402-410, gate the removal onisTextRun(r) && (r.rawElements?.isEmpty ?? true)— a Run with non-emptyrawElementscarries structural payload that must survive replacement.Branch / PR coming up via the v0.27.0 release.
Affected operations
Document.replaceText(and any caller using it:che-word-mcpreplace_text/replace_text_batchMCP tools, this rescue script's caption fix + abstract substitution phases, …) hits this whenever:rawElements(no rawXML, no drawing, empty text)Most common trigger: the
<w:r><w:commentReference/></w:r>immediately following a<w:commentRangeEnd/>marker in Word-authored documents.Severity
P0 — silently breaks docx schema. Word strict validator refuses to open. Lenient parsers (libxml2, pandoc, python-docx) read the broken file fine, masking the problem until end-user opens in Word.
Type
bug