Skip to content

feat(quick): direct mutations through the catalog — bookmarks/links, annotations, forms/attachments/metadata (#231 PR 2) - #271

Merged
mberrys merged 11 commits into
devfrom
gh-231-quick-mutations
Oct 12, 2026
Merged

mberrys merged 11 commits into
devfrom
gh-231-quick-mutations

Conversation

@mberrys

@mberrys mberrys commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Epic #231 PR 2 — direct mutations through the catalog (bookmarks/links, annotations, forms/attachments/metadata)

Second of three PRs for epic #231, stacked on dev (PR 1, #267, is merged). The decision the epic required before annotation code is recorded first: ADR-014 (docs/adr/adr-014-direct-revision-authoring-edits.md) settles direct revision versus planned operation for authoring edits — direct Core mutation through CommandCatalog per edit, fenced, one operation-history event and rollback point per accepted edit; the governed Fix flow stays the only route for publication-affecting operations (PR 3).

Thirty-nine commands gain handlers and land as implemented, all through one facade gateway (one DirectEditRequest/DirectEditOutcome, dispatching by edit kind to the family writer):

  • Bookmarks and hyperlinks (L03-13b — Edit bookmarks and author hyperlinks in Quick #233). Create/reorder/auto-generate bookmarks, canonical import/export, URI links and all nine actionCreateHyperlinkToThisPDF* destination variants — each variant serialized through Core's writer and read back through Core's reader; a stale revision refuses with a typed reason before any write; a document without modify permission refuses the edit. Outline rewrites refuse unsupported structures (bookmark/unsupported-outline-item) instead of dropping them.
  • Annotations (L03-13c — Author markup, shape, and note annotations in Quick #234). All four text markups, six shapes, inline text and the seven sticky-note variants, each created through Core's annotation model with its appearance stream; per-subtype read-back tests; permission gate maps modify-annotations; every accepted edit appends one history event and rollback point (pdfauthoringhistory), and a provenance write that fails is reported on the completed invocation rather than pretending the edit did not land.
  • Forms, attachments, metadata (L03-13k — Fill forms, edit attachments and metadata, and send by email in Quick #242). Four new command ids (actionSetFormFieldValue, actionAddAttachment, actionRemoveAttachment, actionEditDocumentMetadata; expected_action_count 109 → 113) plus actionSend_by_E_Mail. Form values round-trip (text, checkbox, choice; signature fields excluded; an XFA-only form is reported unsupported); attachments add/remove through the /Names tree with extraction through the atomic writer that refuses executable auto-open; metadata edits merge Info and XMP without voiding foreign properties (pdfaid:part survives) and refuse an unparseable packet rather than replacing it; send-by-email hands the current published revision to the mail client and refuses an unsaved document.

The handler-coverage ratchet tightens from 48 to 13 KEEP/ABSORB commands still awaiting a handler; PR 3 takes it to zero and closes the epic.

Validation: check-change.py --base origin/dev passed at the head SHA — 130/130 checks across build_policy, core, documentation, interaction and quick (risk: high — real document mutations), with every target built, the 93-suite ctest set run in the worktree build, clang-format over every changed file, and the catalog/ratchet/architecture/governance/mirror-parity checks green. Limits: no native-GPU smoke (headless offscreen + software backend only), no release-profile build locally, and the pwsh/WSL-dependent CI lanes are absent on this host.

Gaps, stated rather than dressed up:

Quality review: every family enters through the catalog and the facade, so a stale revision, denied permission, XFA-only form or executable attachment is a typed refusal that changes nothing, never a half-write; the XMP writer merges rather than regenerates so an authoring edit cannot silently void a PDF/A or PDF/X claim; the outline writer refuses structures it cannot faithfully rewrite. The one deliberate compromise is the QML affordance gap above — the mutation contract is complete, the interaction surface is not.

Advances #231 — the gate and the first two surface groups are in place; the epic closes when the ratchet reaches zero in PR 3.
Advances #233 — all acceptance bullets evidenced (catalog-routed mutations producing new revisions, import/export round-trip, per-variant read-back, stale-revision refusal); the import/export dialogs remain follow-up.
Advances #234 — all acceptance bullets evidenced (ADR-014 recorded; per-subtype read-back with appearance streams; history model of #124; permission refusal); the gesture bridge remains follow-up.
Advances #242 — all acceptance bullets evidenced (catalog-routed mutations; atomic extraction with executable refusal; published-revision mail hand-off; XFA refusal); the form overlay, attachment panel and metadata editor remain follow-up.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Records the #234 precondition: annotation, bookmark, link, form,
attachment and metadata edits are direct Core mutations through
CommandCatalog producing a new revision (fenced, one history event
per edit), not planned Fix operations; adds the 013 row missing
from the ADR index.
#231 PR 2)

Cherry-picks the three slice commits and repairs the conflict regions with
complete blocks from each slice: one facade gateway per family (unified in
follow-up), the 39 implemented ids, expected_action_count 109 -> 113, the
handler ratchet re-recorded to 13, and the PR fragment plus evidence manifest.
…uest (#231 PR 2, ADR-014)

Collapses the three family gateways into the single DirectEditRequest /
DirectEditOutcome / DocumentFacade::applyDirectEdit shape ADR-014 records,
dispatching by edit kind to the annotation, outline and document helpers.
Refusal codes and fence order per family are unchanged; all call sites and
the facade tests are adapted.
… proof lanes (#231 PR 2)

Adds UnitTestsAuthoringEdit to the core module (paths + tests) so the
catalog target has a proof lane, rewrites the evidence manifest against the
touched subsystems' required lanes (core, interaction, quick, build_policy,
governed-execution incl. the canonical-json-golden fixture and packaging
workflows; the independent-validation stub is listed unresolved), and
clang-formats pdfauthoringedit.h.
The defaulted operator== makes GCC 13 instantiate the definition while
QList's own equality is still forming (has_operator_equal probe), a hard
error CI caught; the explicit declaration is portable and MSVC-identical.
…lpers

firstLinkOnPage/linkOnPage returned a raw pointer into a temporary or local
PDFAnnotationPtr, so the annotation was freed before use; glibc's freed-chunk
metadata clobbers the vtable and the next dynamic_cast segfaults (MSVC
survived by heap luck). The helpers now return the owning pointer and call
sites downcast explicitly.
@mberrys
mberrys merged commit 15fed58 into dev Oct 12, 2026
16 checks passed
@mberrys
mberrys deleted the gh-231-quick-mutations branch October 12, 2026 19:01
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.

1 participant