Skip to content

Threaded Comments & Review — issue #25 (comments + replies render in PowerPoint)#55

Merged
MHoroszowski merged 7 commits into
masterfrom
feature/threaded-comments
May 17, 2026
Merged

Threaded Comments & Review — issue #25 (comments + replies render in PowerPoint)#55
MHoroszowski merged 7 commits into
masterfrom
feature/threaded-comments

Conversation

@MHoroszowski
Copy link
Copy Markdown
Owner

Closes #25.

Modern PowerPoint threaded comments (<p188:cm>, 2018/8/main) — full
add/read/reply/resolve API plus the OOXML wiring PowerPoint actually
requires. Maintainer visual signoff received (PowerPoint Mac: parent
threads and replies render in the Comments pane).

What was broken

Five PowerPoint-only defects, all invisible to the test trinity (only a
real PowerPoint open caught them):

  1. <p188:txBody> missing required <a:bodyPr> (a7c6cb9).
  2. Comment-part content type was …ms-powerpoint.threadedComments+xml → must be …ms-powerpoint.comments+xml.
  3. slide→part reltype was …/2018/10/relationships/threadedComment…/comments.
  4. presentation→authors reltype was …/threadedCommentAuthors…/authors.
  5. <p188:cm> missing its required <pc:sldMkLst> slide-binding moniker (ns …/2013/main/command).
  6. <p188:replyLst> emitted after <p188:txBody>[MS-PPTX] CT_Comment is a strict sequence anchor → pos? → replyLst? → txBody; out-of-order replyLst made PowerPoint render the parent and silently drop every reply.

#2–5 root-caused by diffing a comment PowerPoint itself authored; #6 from
the authoritative Microsoft Open-Specs CT_Comment XSD. Constant values
changed in opc/constants.py (symbol names kept so the PartFactory
registry and producer move in lockstep); pc namespace added; child
ordering fixed in oxml/comments.py.

Verification

  • python3 -m pytest tests/ -q3761 passed
  • python3 -m ruff check src testsAll checks passed!
  • python3 -m behave features/ --no-color1062 scenarios, 0 failed; 3215 steps, 0 failed
  • New regression class DescribeThreadedCommentPowerPointContract (8 assertions) locks all six axes incl. the [MS-PPTX] sequence; red→green proven by stashing the serializer.
  • Opened in real PowerPoint via Interceptor: parent threads render (author, text, resolved status) and the reply thread renders (AX node + "View 1 more reply"; pre-fix reply count was zero). Cato cross-vendor audit: pass.

UAT script (uat/, gitignored) runs clean; maintainer visually
confirmed in PowerPoint.

Implements all 8 sub-features additively, built from the OOXML spec
(no upstream prior art):

- SF1 CommentAuthors (legacy integer-id) + modern GUID-keyed authors
- SF2 legacy <p:cm> + modern <p188:cm> oxml/part classes (2018/8/main)
- SF3 Slide.comments add/remove/iter (lazy modern part create+relate)
- SF4 Comment.replies threaded chains
- SF5 author/text/created_at(tz-aware)/anchor_position accessors
- SF6 Comment.resolve()/resolved (modern; legacy raises, documented)
- SF7 Shape.comments per-shape anchored filter
- SF8 legacy<->modern coexistence on round-trip (both enumerable)

DEFECT caught by Interceptor visual verification (trinity was green):
modern <p188:cm> referenced GUID authorIds while only a legacy
integer-id commentAuthors.xml existed -> dangling refs -> PowerPoint
repair dialog. Fixed: modern /ppt/authors.xml
(application/vnd.ms-powerpoint.authors+xml, GUID-keyed <p188:author>,
presentation->authors threadedCommentAuthors rel). providerId made
RequiredAttribute so it always serializes (PowerPoint always writes it).

Tests: +97 pytest (tests/test_issue25_comments.py, 43 issue-25),
+14 behave. Trinity: pytest 3751, ruff clean, behave 1062, 0 failed.
uat_issue25.py 17/17 (script-QA). Post-fix PowerPoint Review-pane
visual deferred to maintainer UAT per repo §6a (env wall on re-open;
residual risk disclosed — structural reasoning produced one false-green
on this part). No push/PR pre-signoff.

Refs #25  Closes scanny#487 scanny#538 scanny#756
Cato cross-vendor audit flagged `anchorShapeId` as an out-of-schema
attribute on <p188:cm> (2018/8/main defines only id/authorId/created/
status) — a plausible second PowerPoint repair-dialog trigger that the
deferred post-fix visual would have hidden.

Anchor now stored in <p188:extLst>/<p188:ext uri="{fork URI}"> — the
OOXML-sanctioned extension mechanism conformant consumers MUST ignore
on unknown URI. SF7 per-shape filter + round-trip preserved
(anchored comment round-trips True). <p188:cm> now carries only
schema attributes.

Trinity green: pytest 3751, ruff clean, behave 1062, 0 failed.
Refs #25
…UAT)

Maintainer opened uat_issue25_comments.pptx in PowerPoint: file opened
clean (no repair) but ZERO comments displayed. Root cause: the add
path builds the body via get_or_add_txBody() (a bare <p188:txBody/>)
then sets text via _set_txBody_text, which only PRESERVED a pre-existing
<a:bodyPr> and never created one. <p188:txBody> is a DrawingML
a:CT_TextBody whose schema REQUIRES <a:bodyPr> as first child;
PowerPoint silently drops comments with a bodyPr-less txBody — no
repair dialog, the comment is just absent. The entire green trinity +
17/17 UAT could not see this; only maintainer visual review did.

Fix: _set_txBody_text now GUARANTEES a leading <a:bodyPr/> (inserts it
when absent) for both comments and replies. Added
DescribeThreadedCommentBodyPrRegression (2 tests) asserting every
txBody carries <a:bodyPr> before <a:p>, so the silent-drop class is
now test-caught. uat_issue25.py OUT path moved under ./uat/ per
CLAUDE.md §6a update.

Trinity: pytest 3753, ruff clean, behave 1062, 0 failed.
Refs #25
… threads now render (issue #25)

Threaded comments were invisible in PowerPoint's Comments pane despite a
green trinity and clean script UAT. Root-caused by diffing our OOXML
against a threaded comment PowerPoint for Mac itself authored+saved
(ground truth, captured this session) — three string axes and one
element were inferred wrong in Waves 1-2:

  - comment-part content type: was application/vnd.ms-powerpoint.threadedComments+xml
    -> application/vnd.ms-powerpoint.comments+xml
  - slide -> comment-part reltype: was .../2018/10/relationships/threadedComment
    -> .../2018/10/relationships/comments
  - presentation -> authors reltype: was .../2018/10/relationships/threadedCommentAuthors
    -> .../2018/10/relationships/authors
  - every <p188:cm> now emits its required first child
    <pc:sldMkLst><pc:docMk/><pc:sldMk cId="0" sldId="{p:sldId}"/></pc:sldMkLst>
    (ns .../2013/main/command) — the slide binding PowerPoint needs;
    omitting it (or the fork-private extLst anchor) leaves the comment
    unbound.

PowerPoint resolves the comment part by the slide relationship type; an
unrecognized reltype meant the part was never loaded -> empty pane.

Files: opc/constants.py (3 values; symbols unchanged so the PartFactory
registry + producer move in lockstep — Cato-audited), oxml/ns.py (+pc),
oxml/comments.py (sldMkLst ZeroOrOne + set_slide_marker), comments.py
(Comments.add binds the slide). 6 regression assertions in
DescribeThreadedCommentPowerPointContract lock all four axes
(red->green proven by stashing src) plus set_slide_marker idempotency.

Verification: pytest 3759 passed; ruff check + format clean; behave
1062 scenarios / 3215 steps 0 failed. Regenerated UAT deck opened in
real PowerPoint via Interceptor — Comments pane renders 3 threads,
correct author/text, resolved status persisted (uat/FIXED_comments_rendering.png
vs uat/REPRO_no_comments_pane.png). Cato cross-vendor audit: pass.

KNOWN-INCOMPLETE: PowerPoint still drops every <p188:reply> (replies do
not render; only parent comments do). This is a logically independent
reply-level contract defect, deliberately NOT inference-fixed — it needs
its own PowerPoint-authored <p188:reply> ground-truth diff. Tracked as a
scoped follow-up in the session ISA.

UAT script runs clean — pending maintainer visual signoff in
PowerPoint/Keynote. No push/PR per repo §6a. Refs #25.
…— replies now render (issue #25)

Follow-up to ca4da3f. After the four parent-binding fixes, PowerPoint
rendered parent comment threads but SILENTLY DROPPED every reply
(`interceptor macos find "Merging"` → 0; only empty Reply boxes). The
advisor (Rule 2) correctly flagged reply-rendering as a logically
independent concern none of the four parent fixes touched.

Root cause is authoritative, not inference: the [MS-PPTX] CT_Comment
schema (learn.microsoft.com/openspecs MS-PPTX, 2018/8/main) defines

  <xsd:complexType name="CT_Comment"><xsd:sequence>
    <xsd:group ref="EG_CommentAnchor"/>                <!-- pc:sldMkLst -->
    <xsd:element name="pos" minOccurs="0"/>
    <xsd:element name="replyLst" type="CT_CommentReplyList" minOccurs="0"/>
    <xsd:group ref="EG_CommentProperties"/>            <!-- txBody, extLst -->
  </xsd:sequence></xsd:complexType>

replyLst MUST precede txBody. Our CT_ThreadedComment ZeroOrOne
successors emitted txBody first, replyLst after — out of sequence, so
PowerPoint validated the parent and discarded the misplaced replyLst.
The reply element itself (CT_CommentReply = EG_CommentProperties only)
was already correct; this was purely a child-ordering defect.

Fix: reorder the successor tuples in src/pptx/oxml/comments.py so the
emitted order is pc:sldMkLst, [pos], p188:replyLst, p188:txBody,
[p188:extLst] — matching the schema sequence exactly. No API change.

Two new regression assertions lock it
(it_places_replyLst_before_txBody_per_ms_pptx_sequence,
it_keeps_reply_txBody_with_bodyPr_after_reorder); red->green proven by
stashing oxml/comments.py (the order test fails pre-fix).

Verification: pytest 3761 passed; ruff check All checks passed; ruff
format clean; behave 1062 scenarios / 3215 steps 0 failed. Regenerated
UAT deck: replyLst-before-txBody True, reply texts present. Opened in
real PowerPoint via Interceptor — AX tree now exposes the reply node
"Comment from Alex Reviewer. Merging now." plus a "View 1 more reply"
control (PowerPoint parsed the full reply list); pre-fix that count was
zero. uat/FIXED_replies_rendering.png.

Issue #25 now fully resolves: parent comments AND replies render in
PowerPoint's Comments pane. UAT script runs clean — pending maintainer
visual signoff in PowerPoint/Keynote. No push/PR per repo §6a. Refs #25.
@MHoroszowski MHoroszowski merged commit 2b70119 into master May 17, 2026
16 checks passed
@MHoroszowski MHoroszowski deleted the feature/threaded-comments branch May 17, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Epic] Threaded Comments & Review

1 participant