Repository navigation
add --type comment doesn't sync commentsExtended.xml/commentsIds.xml/commentsExtensible.xml unless done/parentId is passed #429
Description
Activity
Confirmed on 1.0.152 (macOS arm64,
ffa8a0af,officecli.dllsha2568879524487282769…) — so this is not limited to 1.0.146/Windows.Measured, counting entries per part in the zip after each step:
create; add /body --type paragraph add /body/p[1] --type comment --prop text=first --prop author=Alice set /comments/comment[1] --prop done=true -> comments.xml=1 commentsExtended.xml=1 commentsIds.xml=absent commentsExtensible.xml=absent add /body/p[1] --type comment --prop text=second --prop author=Bob # no done, no parentId -> comments.xml=2 commentsExtended.xml=1 <-- one entry shortSo the second
addleaves the part that already exists one entry behindcomments.xml, exactly as reported. Each part is individually schema-valid, which is whyvalidateis silent — the inconsistency is cross-part, not per-part.The workaround also reproduces, and localising it points straight at the cause:
add ... --prop text=second --prop author=Bob --prop done=false -> comments.xml=2 commentsExtended.xml=2 (entry: <w15:commentEx w15:paraId="00100004" w15:done="false" />)Cause
Handlers/Word/WordHandler.Add.Misc.csupserts thew15:commentExonly inside two conditional branches — one forparentId, one fordone/resolved. Pass neither and no entry is written at all. The comment sitting directly above those branches states the invariant the code then doesn't implement:// Word writes a commentEx for every comment; ensure the parent's // thread-root entry exists, then link this reply to it.
UpsertCommentEx(WordHandler.CommentsExt.cs:50) is already find-or-create and already defaults new entries toDone=false"to mirror Word'sw15:done="0"output", andEnsureCommentsExRootalready handles creating the part — so the mechanism is all there; theaddpath just doesn't call it unless a prop asks.What I'd change
Make the upsert unconditional for the newly added comment, after the existing branches:
if (string.IsNullOrEmpty(commentBody.ParagraphId?.Value)) AssignParaId(commentBody); UpsertCommentEx(commentBody.ParagraphId!.Value!, null, null);
Because the upsert is find-or-create, the
parentIdanddonebranches keep working unchanged (thedonebranch's value still wins — it is applied after). This makes the entry appear whether the part already exists (the case in your repro, and the one that matters for files Word produced) or not.Not in that change
commentsIds.xmlandcommentsExtensible.xmlhave no writer in the add path at all — the only code that touches them is thedump/raw round-trip inWordBatchEmitter.AuxParts.cs, which passes them through verbatim. Writing them means plumbing a part that has no props and never gets created, including a durable-id forw16cid:commentId, which is a design decision rather than a missing call. I've scoped that as a separate change rather than bundling it, per the repo's Rule 1. Your note that Word's own repair reconcilescommentsIds.xmlbut dropscommentsExtensible.xmlis useful there — it suggests the two shouldn't be treated as symmetric.I'll open a PR for the
commentsExtended.xmlhalf with a before/after on the counts above.Reproduced exactly as described (on 1.0.152 too), and fixed on
mainfor the next release.Rule now: every comment sidecar part that already exists in the document is kept in step —
add --type commentappends the comment's entry to commentsExtended.xml (w15:commentEx, done=false), commentsIds.xml (w16cid:commentIdwith a fresh 8-hex durableId) and commentsExtensible.xml (w16cex:commentExtensiblewith the same durableId and a whole-second dateUtc, as Word writes) — with or withoutdone/parentId. Removing a comment (directly, or via the orphan cleanup when its host paragraph goes) drops its entry from all three. A part that does not exist is never created, so a document that never had them stays minimal.Verified with a Word-shaped fixture (all four parts present): 2/2/2/2 → add → 3/3/3/3 → remove → 2/2/2/2, and desktop Word opens the result. Thanks for the precise repro, and for the note about Word's own repair dropping commentsExtensible — that made the case for keeping the third part in step rather than leaving it to recovery.


What happens
Adding a comment to a docx that already has real threaded comments (and therefore already has
commentsExtended.xml,commentsIds.xml, andcommentsExtensible.xmlalongsidecomments.xml) only writes the new comment intocomments.xml. The other three parts are left untouched unlessdoneorparentIdis passed explicitly onadd— and even then, onlycommentsExtended.xmlgets an entry.commentsIds.xmlandcommentsExtensible.xmlhave no equivalent prop at all and are never written to, regardless of what's passed.This leaves the four comment-related parts cross-referentially inconsistent (each individually schema-valid, so
officecli validatedoesn't catch it), and Word flags the file as needing repair when a human opens it ("Word found unreadable content, do you want to recover this document?").Repro
officecli create test.docx, add a paragraph, add a comment, thenofficecli setdone=trueon it — this createscommentsExtended.xmlwith one entry (confirming the part only appears once something explicitly writes to it).officecli add --type comment --prop text=... --prop author=..., with or without--prop done=false.comments.xmlhas both comments.commentsExtended.xmlonly has the first (or, withdone=falseon the secondadd, both — butcommentsIds.xml/commentsExtensible.xmlnever get an entry for either comment, ever).Expected
add --type commentkeeps all four comment parts consistent, matching what Word itself writes when a human adds a comment.Environment
officecli 1.0.146, Windows.
Additional context
Compared this against Word's own repair-on-open behavior for a file left in this inconsistent state. Word's repair does correctly reconcile
commentsExtended.xmlandcommentsIds.xml(adds the missing comment's entry to both), but dropscommentsExtensible.xmlentirely rather than reconciling it — so even Word's own recovery path doesn't fully restore that fourth part.