feat(docx-mcp): ODF comments — add_comment + get_comments for .odt (Phase 2b-1)#341
Conversation
…hase 2b-1) Extend the provider-aware ODF (.odt) lane with comments, backed by inline office:annotation markup in content.xml (no separate comments part). odf-core: - Add the DC namespace; extract Segment/buildSegments into a shared text_segments module that skips office:annotation / office:annotation-end subtrees so a comment body never leaks into the host paragraph's visible text nor registers as a phantom block (collectBlocks skips them too). - New comments module: whole-paragraph anchoring via structural insertion (independent of text segmentation) and ranged anchoring via a single text-node split (cross-node ranges return MATCH_SPANS_MULTIPLE_NODES); office:name ids allocated by scanning all existing annotation names. - OdfDocument.addComment / getComments delegating to the comments module. docx-mcp: - tools/odf/add_comment + get_comments mirroring the DOCX param/response shapes; replies (parent_comment_id) on .odt return UNSUPPORTED_FOR_ODF. - add the tools to ODF_SUPPORTED_TOOLS, update the guard + resolver hints, register handlers + isOdfRequest dispatch branches, and refresh the tool catalog + generated docs. - Re-point the add-odf-grep-insert OPLR-08 guard tests (and spec wording) at compare_documents, now that add_comment is supported. New OpenSpec change add-odf-comments (mcp-server OPCM-01..05, odf-core OANN-01..05). Verified against a real NVCA .odt: comments persist through save/reopen and LibreOffice reads them as native comments.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
LLM-Based Quality GateOverall: ✅ PASS (5 pass · 0 warn · 9 skipped · 14 total)
Full checklist questions
Estimated cost (this run): $0.0173 — 55,045 input + 294 output tokens (≈4 chars/token) on |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…llocation Peer review (Codex + agy) on the final diff surfaced three issues in the new office:annotation path: - addAnnotation accepted malformed ranges (reversed, one-sided, out-of-bounds), which duplicated visible text on a reversed range. Validate the range and fail closed with INVALID_RANGE before mutating the DOM. (Not reachable via the MCP handler, whose ranges come from findUniqueSubstringMatch, but the odf-core API is public.) - Multi-line comment bodies were lossy: makeAnnotation wrote a literal \n into one text node (LibreOffice renders it as a space) and annotationBodyText read via textContent (drops text:line-break / text:s). Write blank-line-split text:p with text:line-break (parity with insertParagraph) and read bodies via buildSegments so multi-line comments round-trip. - allocateName ignored the synthetic ids readAnnotations assigns to custom-named (non-__Annot__N) annotations, so a returned commentId could coincide with another annotation's id. Reserve maxParsed + (count of non-matching annotations) + 1. Adds odf-core tests for each: INVALID_RANGE rejection (no text mutation), multi-line round-trip, and id allocation past custom-named annotations.
LLM-Based Quality Gate
Overall: ✅ PASS (5 pass · 0 warn · 9 skipped · 14 total)
Full checklist questions
Estimated cost (this run): $0.0183 — 58,208 input + 314 output tokens (≈4 chars/token) on |
✅ Post-merge smoke passedMerged: Steps
Real-world fixtures
Cleanup
Log: |
Summary
Extends the provider-aware ODF (
.odt) lane with comments — the lighter half of Phase 2b — backed by inlineoffice:annotationmarkup incontent.xml(no separate comments part, rels, orcommentsExtended.xml). Mirrors the DOCXadd_comment/get_commentsparam + response shapes so agents get the same contract across providers.compare_documents(the tracked-changes atomizer) stays deferred to a follow-up PR.What changed
odf-core
DCnamespace; extractSegment/buildSegmentsinto a sharedshared/odf/text_segments.tsthat skipsoffice:annotation/office:annotation-endsubtrees so a comment body never leaks into the host paragraph's visible text nor registers as a phantom block (collectBlocksskips them too).comments.ts: whole-paragraph anchoring via structural insertion (annotation as first inline child, end after the last — independent of text segmentation) and ranged (anchor_text) anchoring via a single text-node split; cross-node ranges returnMATCH_SPANS_MULTIPLE_NODES.office:nameids are allocated by scanning all existing annotation names.OdfDocument.addComment/getCommentsdelegating tocomments.ts; exportOdfComment.docx-mcp
tools/odf/add_comment.ts+get_comments.tsmirroring the DOCX shapes. Replies (parent_comment_id) on a.odtreturnUNSUPPORTED_FOR_ODF(ODF has no first-class reply graph — deferred).ODF_SUPPORTED_TOOLS, update the guard + resolver hint strings, register handlers +isOdfRequestdispatch branches, refreshtool_catalog.ts+ the generated tool docs.add-odf-grep-insertOPLR-08 guard tests and spec wording atcompare_documents, now thatadd_commentis supported.OpenSpec — new
add-odf-commentschange (mcp-serverOPCM-01..05, odf-coreOANN-01..05).Design review
Reviewed before building (Codex, read-only dynamic). Two BLOCKERs were caught, independently reproduced, and fixed here: (B1) the annotation body leaking into the paragraph stream, and (B2) whole-paragraph anchoring needing a structural path separate from the single-text-node contract. SHOULDs adopted:
authorstays required;office:nameallocation scans all existing names; spec wording amended (not just tests).Verification
lint:workspaces(0 errors),check:cycles,check:release-isolation, allure label/filename/quality (0 errors),openspec validate --strict,check:spec-coverage --strict,check:tool-docs..odt(source.docx→.odtviasoffice): whole-paragraph + ranged comments viadispatchToolCall,get_commentsreturns them with author/date/anchor, persisted through save→reopen, and LibreOffice round-trips the annotation to a native comment (author "Jane Doe", body preserved). Guards intact: replies andcompare_documentson.odtstill returnUNSUPPORTED_FOR_ODF.Out of scope
compare_documents(Phase 2b-2), comment replies/threads,delete_commentfor ODF,.ods/.odp, durable injectedxml:idanchors.