Skip to content

Add first-class links and bookmarks (#451) - #480

Merged
JSv4 merged 2 commits into
mainfrom
agent/issue-451-links-bookmarks
Aug 15, 2026
Merged

Add first-class links and bookmarks (#451)#480
JSv4 merged 2 commits into
mainfrom
agent/issue-451-links-bookmarks

Conversation

@JSv4

@JSv4 JSv4 commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Summary

Validation

  • 111 focused regression tests passed: all link/bookmark, move-block, and MCP dispatcher tests
  • 23/23 dedicated link/bookmark tests passed after final audit
  • OpenXmlValidator assertions cover saved packages and bookmark marker invariants
  • LibreOffice DOCX→ODT→DOCX round-trip/reopen smoke check passed
  • git diff --check and builds passed

Stack

This draft targets agent/issue-448-introspection (draft PR #477) so the review contains only issue #451 changes.

Closes #451
Fixes #469
Fixes #470

@JSv4
JSv4 force-pushed the agent/issue-448-introspection branch from 1271a0c to 86fa85e Compare August 14, 2026 09:49
@JSv4
JSv4 force-pushed the agent/issue-451-links-bookmarks branch from f86de5a to 07ecc65 Compare August 14, 2026 09:49
@JSv4
JSv4 force-pushed the agent/issue-448-introspection branch from 86fa85e to fa4a628 Compare August 14, 2026 10:07
@JSv4
JSv4 force-pushed the agent/issue-451-links-bookmarks branch from 07ecc65 to 8370fee Compare August 14, 2026 10:07
@JSv4
JSv4 force-pushed the agent/issue-448-introspection branch from fa4a628 to 8cd7304 Compare August 14, 2026 11:11
@JSv4
JSv4 force-pushed the agent/issue-451-links-bookmarks branch from 8370fee to 8d46a3f Compare August 14, 2026 11:11
@JSv4
JSv4 force-pushed the agent/issue-448-introspection branch from 8cd7304 to 739ad51 Compare August 14, 2026 11:39
@JSv4
JSv4 force-pushed the agent/issue-451-links-bookmarks branch from 8d46a3f to 14fb635 Compare August 14, 2026 11:39
Review fixes on the links/bookmarks surface.

- Bookmark references now cover BOTH consumer families. Beyond w:hyperlink/@w:anchor,
  REF/PAGEREF/NOTEREF/HYPERLINK \l instructions in w:instrText and w:fldSimple/@w:instr
  are scanned, retargeted by RenameBookmark (splicing only the name token, so switches
  survive; a split instruction coalesces onto its first w:instrText) and counted by the
  BookmarkInUse guards in RemoveBookmark and ValidateBookmarkRemoval. Renaming a
  bookmark a TOC cites no longer leaves "Error! Bookmark not defined." behind.
- Word's _GoBack/_Toc*/_Ref*/_Hlt*/_Hlk* namespace is closed to creation (AddBookmark
  and RenameBookmark's destination). Bookmarks Word already placed there stay readable
  and mutable; the policy is documented in docx_mutation_api.md.
- AddHyperlink relocates the whole contiguous sibling range it covers, so a
  w:bookmarkStart/End or w:commentRangeStart/End sitting between selected runs moves
  INTO the new w:hyperlink at its original position instead of being stranded after it.
  A bookmark whose start lay inside the span used to end up ordered after its own end,
  which made the pair unresolvable and the bookmark permanently unmutatable.
- A cross-part MoveBookmark takes a fresh document-global w:id. w:id is part-scoped and
  Word reuses decimals across parts, so carrying the source id into another part could
  collide and make BOTH bookmarks unresolvable.
- The tracked-move bookmark rejection moved into a shared predicate, so ValidMoveTargets
  mirrors MoveBlock and a drag UI cannot advertise a drop the engine refuses.
- SplitHyperlinkAt no longer clones PtOpenXml.Unid, so the two halves of a split link get
  distinct ids and both stay addressable by Update/RemoveHyperlink.
- The comments part joined OwnedPartRelationships.StoryParts. ProjectionScopes.Comments
  was silently empty and FindOwner returned null for comment paragraphs, which made
  PromoteHyperlinkRelationships throw on ReplaceText over a comment paragraph (DS364).
  MCP gained the matching "comments" link scope.
- docx_agent_server.md documented targetKind where the schema key is kind.

Tests: LB021-LB027, ValidMoveTargets_TrackedMode_RoundTripsWithMoveBlock,
python/tests/test_links_bookmarks.py, npm/tests/docx-session-links.spec.ts.
@JSv4
JSv4 changed the base branch from agent/issue-448-introspection to main August 15, 2026 02:39
@JSv4
JSv4 marked this pull request as ready for review August 15, 2026 02:39
@JSv4
JSv4 merged commit 85bc3b2 into main Aug 15, 2026
2 checks passed
JSv4 added a commit that referenced this pull request Aug 15, 2026
Brings the structural tracked-revision work up to date with the five sibling
PRs from the same stack that landed on main while this branch sat still:
#479 atomic batches, #481 isolated preview, #477 introspection, #480
links/bookmarks and #482 images.

One file conflicted, Docxodus.Tests/McpServerDispatcherTests.cs, and only
because both sides appended tests at the same point. Both sides' tests are
kept in full; none was dropped, weakened or merged together. The sole real
collision was the test number MCP099, claimed independently by main's
MCP099_BatchedTableStep_KeepsTableAnchorMappingInItsReceipt (from d558c00)
and by this branch's tracked-changes batch test. Main's numbering is left
untouched because it is already merged and referenced, so this branch's test
takes the next free number instead:

  MCP099_TrackChangesBatchPreviewIsIsolatedAndAtomicApplyResolvesRevision
  -> MCP146_TrackChangesBatchPreviewIsIsolatedAndAtomicApplyResolvesRevision

MCP102 and MCP103 do not collide with anything on main and keep their
numbers.

The batch changeset comparison converged as intended: this branch's f761e49
and main's 983c5bf made the same serialized-projection change, and the merged
tree holds exactly one such comparison with no record-equality remnant. The
comments part added to OwnedPartRelationships.StoryParts by 96f4c2f survives,
and both sides' CHANGELOG entries accumulated under a single [Unreleased].

Verified on the merged tree: the library and the test project build with 0
errors, the full suite is 3712 passed / 0 failed / 3 skipped, and the Release
builds of tools/mcp-server and tools/python-host are clean. Test-attribute
counts are exactly additive across the merge (Fact 2195, Theory 173,
InlineData 1545 = base plus main's additions plus this branch's), confirming
no test was lost.
JSv4 added a commit that referenced this pull request Aug 15, 2026
Bring the native content-control work up to date with main after PRs #479,
#481, #477, #480, #482 and #485 landed.

CHANGELOG.md was the only conflict: both sides added an entry at the top of
[Unreleased]. Resolved by keeping both, main's #455 structural tracked
revisions first and this branch's #452 native content-control operations
second; the rest of main's list is unchanged.

Two semantic fixes a clean textual merge would have hidden:

- Renumber this branch's MCP content-control tests from MCP146/147/148 to
  MCP147/148/149. Main's #485 independently claimed MCP146 for
  MCP146_TrackChangesBatchPreviewIsIsolatedAndAtomicApplyResolvesRevision in
  the same file, so both sides had defined an MCP146.

- docs/architecture/docx_agent_server.md: correct the tools/list count from 17
  to 19 (the catalog now registers 19 tools, three lifecycle plus sixteen
  read/grouped-intent), add docxodus_track_changes to the docxodus_mutations
  batch-step list so it matches the catalog enum and the dispatcher's accepted
  set, and add images/content-controls to the grouped-intent enumeration.
JSv4 added a commit that referenced this pull request Aug 15, 2026
Brings the native-image branch up to date with main (PRs #479, #481,
#477, #480, #482, #485).

Two conflicts, both additive-on-both-sides EditErrorCode collisions,
resolved by keeping every code from both sides:

- npm/src/types.ts: kept the eight image codes from this branch and
  main's unresolved_structural_revision.
- python/src/docx_scalpel/enums.py: same resolution.

The C# EditErrorCode enum in DocxSession.cs auto-merged and remains the
single owner; all three surfaces now carry the same 58 codes.
JSv4 added a commit that referenced this pull request Aug 15, 2026
Brings the branch up to date with main, which had advanced by 20
commits (PRs #479, #481, #477, #480, #482, #485 and #484).

CHANGELOG.md was the only conflict: both sides appended to the same
[Unreleased] / ### Added region. Resolved by keeping every entry from
both sides, with this branch's idempotent mutation transaction entry
ahead of main's accumulated entries. Main's block is otherwise
unchanged, and the historic "[Unreleased] - .NET 8" heading further
down the file is untouched.

EditErrorCode gained four transaction codes on this branch and image
and revision codes on main; the merged enum retains all of them, and
the npm and Python mirrors carry this branch's four. The enum has no
explicit ordinals and is serialized by name, so the reordering #485
introduced does not affect the wire.

This PR remains MCP-transport-only: DocxSessionOps, DocxSessionJson
and the WASM bridge are byte-identical to main after the merge.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant