feat(word-addin): edit-protocol groundwork - occurrence parsing, honest skip statuses, location-hint slot

↗ view on GitHub · Amal · 2026-08-19 · 30758b55

WHY THIS MATTERS
When a user asks Mike to replace a word that appears several times in the
document, the add-in used to dead-end: the model had no way to say "all
occurrences", and every skip collapsed into one misleading card message
("source text was not found" - even when the text WAS found, more than
once, or was simply too long to search for). This commit lays the client
groundwork for multi-occurrence editing without changing any behavior yet:
every piece here is inert until the engine commit wires it up.

WHAT IS THE <occurrence> TAG
The edit transport protocol (<original>/<replacement>/<reason> blocks
streamed inside the model's answer) gains one optional tag:

  <original>Acme Corp</original>
  <replacement>Acme Ltd</replacement>
  <occurrence>all</occurrence>
  <reason>Rename the company everywhere.</reason>

"all" is the ONLY value. There is deliberately no numeric form
(<occurrence>3</occurrence>) - three independent reasons, each fatal:
1. Language models miscount occurrences.
2. The index space is unstable mid-stream: a tracked deletion keeps the
   deleted text searchable, and a replacement that CONTAINS the original
   ("Supplier" → "Supplier Inc.") grows the match count as edits apply.
3. Word's search silently drops overlapping matches (office-js#4992), so
   "3rd occurrence in the document" and search result items[2] can
   legitimately disagree.
An index failure would be silent and land a redline on the wrong sentence;
"all" needs no counting at all. Unknown values fail closed to the
historical single-occurrence contract.

HOW IT WORKS
- redline.ts parses the tag in BOTH parsers - the sealed-block regex
  (COMPLETE_TAGGED_EDIT) and the streaming provisional parser - because a
  tag only one of them knows would either never seal a block or leak raw
  markup into the visible card mid-stream. blockIndex assignment is
  byte-identical for tag-free content: it feeds the digest that names each
  edit's hidden Word bookmark, so changing it would orphan every
  historical edit's anchor.
- wordChatTypes.ts adds two card statuses ("unsearchable", "conflicted")
  so the three previously-conflated skip reasons can each say something
  true, plus locationHint/appliedMatches runtime fields.
- EditCard.tsx maps the new statuses to accurate copy, upgrades the
  ambiguous message to name the match count and the way out ("Tell Mike
  which one to change."), and reports multi-place applies ("Applied to
  the document in N places.").
- EditCardUI.tsx (the design-system card shared with the web app) gains an
  optional locationHint line - "In: "paragraph snippet"" - so a user can
  catch an edit that landed in the wrong place BEFORE accepting it.
- wordEditAnchors.ts adds listWordEditAnchorIds(prefix): a replace-all
  edit persists one anchor per applied occurrence under `${key}#${pass}`,
  and a reloaded task pane discovers how many passes existed by
  enumerating the registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5yxFV2YCFmR61KLyB33yi
Repository open-legal-products/mike
Author Amal <mamalanand3@gmail.com>
Authored
Committed
Parents 01867ebd
Stats 5 files changed , +127 , -4
Part of Make Word edit approval reliable across hosts

Capture this commit into my fork

Download a Markdown prompt that tells Claude how to port this exact commit into your working tree. Run it via claude -p < capture-commit-30758b55.md from inside the repo you want the change in.

⬇ Download capture-commit-30758b55.md