open-legal-products makes Mike's Word review safer across web and Mac

The team has repaired the link between AI edit cards and Word redlines, while giving lawyers more control over how changes land.

workflowchat-ui

The important shift is reliability: an approval card now acts only when Mike can uniquely identify the tracked change it created. That restores useful Accept and Reject controls in Word for the web, supports Word for Mac, and avoids approving the wrong edit when the document is ambiguous.

  • Review or direct apply: keep proposed changes as redlines for approval, or apply a successful edit immediately.
  • Smarter document context: preserve headings, lists and tables when Mike reads a document, helping it understand structure.
  • Safer repeated edits: replace every instance when asked, distinguish identical corrections by location, and remove whole numbered-list items cleanly.
  • Better navigation: show where an edit landed and let users jump to cited material.

This matters to legal teams using Word as the working surface: faster edits are useful only if review controls remain trustworthy.

So what Firms and in-house teams should care if they want AI drafting assistance without weakening the familiar discipline of tracked-change review.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

12 commits from open-legal-products/mike, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
26e91dba fix(word-addin): resolve tracked changes by revision content so edit cards stay linked on Word for the web Amal 2026-08-16 ↗ GitHub
commit body
WHY THIS MATTERS
On Word for the web, clicking Accept or Reject on a Mike edit card did
nothing: the document's redlines stayed pending and the card errored with
"The revisions in this passage changed after Mike applied the edit." The
card and its tracked change were effectively unlinked, which breaks the
core review loop of the add-in. Replicated live against real Word online
before fixing (two same-paragraph spelling fixes; neither could be
resolved from its card).

WHAT WORD ACTUALLY DOES
Office.js ranges do not scope getTrackedChanges() the way the previous
code assumed. Live probing showed three separate behaviors on Word for
the web:
1. Inside the Word.run batch that queues the replacement, the generated
   revisions may not be visible at all (collections come back empty).
2. Afterwards, the search-match range, the inserted-text range, and even
   the containing paragraph each report only the ADDED revision - the
   DELETED strikethrough run sits just outside every range we retain.
3. A retained (tracked) paragraph proxy can later report zero revisions.
Only context.document.body.getTrackedChanges() reliably lists all
pending revisions. The old resolution logic demanded the full
Added+Deleted pair through ONE range proxy (strict whole-set equality in
trackedChangesMatchEdit), so on Word online it always refused.

HOW THE FIX WORKS
Resolution now identifies the edit by revision CONTENT rather than by
proxy identity, in three layers, most precise first:
1. Exact retained child revisions (unchanged fast path; still what the
   mock host and Word desktop hit).
2. Per-anchor, per-side matching: pickEditRevisionSubset() selects, from
   whatever revision set a range reports, exactly one Added revision
   whose text equals toWordText(replacement) and one Deleted revision
   whose text equals the original. A read-only scan pass locates which
   anchor can supply each side before anything mutates.
3. An atomic document-level fallback (resolveEditThroughBody) that finds
   the edit's unambiguous Added/Deleted pair in the body collection and
   resolves both sides in a single batch.
Every layer aborts untouched on any ambiguity (two pending revisions
with identical text), so Mike can never resolve a revision that is not
provably its own. restoreTrackedEdit() gets the same subset matching
plus the body fallback, so cards re-link after a task-pane reload, and
the persistent bookmark now spans the whole edited paragraph when
per-revision ranges are unavailable. Apply-time also re-reads the edited
paragraph once before giving up on exact review controls.

The e2e Office mock gains body.getTrackedChanges() and range.paragraphs
to mirror the real host, and the persistence specs now assert the new
contract: a sibling revision inside the bookmark no longer severs review
controls, while genuinely ambiguous revision sets stay view-only.

Verified live on Word for the web: Accept finalizes the text in the
document, Reject restores the original, and the pane console shows
"[tracked-edit/resolve] body fallback resolved".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f84ffd22 feat(word-addin): Direct/Review toggle chooses whether edits need approval Amal 2026-08-16 ↗ GitHub
commit body
WHY THIS MATTERS
Until now every model edit landed as a pending tracked change that the
user had to accept card by card. That is the right default for careful
legal review, but when a user just wants the fix ("correct my typos"),
the approval step is friction. This adds the explicitly requested
header toggle: apply edits directly, or route them through approval.

HOW IT WORKS
- A Direct/Review segmented control (the web app's glass TabPillButton
  style) sits at the top of the chat header. The choice persists in
  OfficeRuntime.storage under mike_word_edit_apply_mode; the safe
  default is Review (approval).
- Review mode is the existing behavior: streamed edits apply as tracked
  changes, and their cards stay linked for Accept/Reject/View.
- Direct mode reuses the same apply pipeline (search validation,
  ambiguity guards, TrackAll authoring) and then immediately accepts
  the revision, so the document shows final text with no review step.
  Reusing accept-after-apply rather than writing untracked text means
  every safety check and failure path stays identical in both modes.
- The mode is captured per edit at scheduling time, so flipping the
  toggle mid-stream can never split one edit's lifecycle.
- Cards report the outcome with a new "applied" status ("Applied to the
  document."), and the activity strip says "Applied change to the
  document"; review controls are simply absent in Direct mode.
- If the auto-accept cannot complete, the card degrades honestly to the
  unmanaged state pointing at Word's Review tab instead of pretending
  the edit was finalized.

Covered by e2e/edit-apply-mode.spec.ts: default state, direct apply
with immediate acceptance and no leftover pending revisions, approval
flow unchanged, persistence across task-pane reloads, and mid-stream
toggle isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1ff94349 feat(word-addin): formatting tracked changes and click-to-locate document citations Amal 2026-08-16 ↗ GitHub
commit body
WHY THIS MATTERS
Legal review is not only about wording. Reviewers need Mike to (a) propose
styling changes - bold a heading, italicize a defined term, promote a
paragraph to a real Word heading - through the same accept/reject redline
flow as text edits, and (b) back its answers with citations that jump to
the exact contract language in the open document instead of leaving the
reader to hunt for it.

FORMATTING TRACKED CHANGES - HOW IT WORKS
The edit protocol gains a format-only block:
  <original>exact text from the document</original>
  <format>bold</format>
  <reason>...</reason>
Recognized formats: bold, italic, underline (character formatting) and
heading1/heading2/heading3 (Word paragraph styles; "heading 1" spellings
normalize). A block carries either <replacement> or <format>, never both -
a malformed block with both (or a <format> naming nothing recognized)
fails the complete-block match and settles as an "incomplete" card that
never touches Word. Combined rewrite+restyle was deliberately rejected so
the revision set stays provable.

Applying a format block reuses the entire text-edit pipeline (unique
search match, pre-existing-revision skip, TrackAll authoring) but writes
font properties - or, for heading styles, the containing paragraph's
styleBuiltIn - instead of replacing text, which makes Word generate
"Formatted" revisions. The revision-matching layers gain a "formatted"
side with host-informed matching rules, each verified against real Word
for the web:
- Character formats: revision text equals the original passage; N >= 1
  matches accepted because Word may split one restyle per font property
  or run (every text-matching formatted revision is provably this edit's,
  since apply required a unique match of a revision-free passage).
- Heading styles are paragraph-scoped by Word's own model, so their
  revision reports the whole paragraph's text - or, on Word for the web,
  an EMPTY string. Anchor-scoped matching therefore accepts contains-or-
  empty for style edits (ranges only report revisions they intersect, so
  the empty-text rule stays confined to the edited passage), while the
  document-wide fallback demands a unique match, because empty text
  cannot disambiguate two pending paragraph-format revisions.
Cards preview the styling on the original text (no red/green diff) plus a
small label of the formats applied.

Malformed blocks of ANY kind now settle as "incomplete" when the stream
finishes (previously they could sit on a "Receiving change..." spinner
forever; markIncompleteRedlines only ran on abort/error paths).

DOCUMENT CITATIONS - HOW IT WORKS
The shared chat pipeline already emits [n] markers in prose plus a
citations frame carrying each marker's verbatim quote; the pane simply
ignored that frame. Now: the stream boundary surfaces it, the chat hook
attaches citations to the assistant message (and persists them through
local saves; the cloud path already stored the column), and prose
rendering joins each [n] marker to its quote - rendering a chip link with
a reserved "#mike-cite:<encoded quote>" fragment. Literal <cite>verbatim
quote</cite> tags render the quote itself as the chip, as an inline
alternative (partial tags are hidden mid-stream so raw markup never
flashes). Clicking a chip runs selectDocumentText(): an exact-match body
search (case-insensitive fallback), then Range.select(), which is Word's
scroll-and-highlight. A stale citation - text since edited away - is a
silent no-op logged at debug level; citations are navigation, not state,
so nothing is anchored or persisted for them.

Verified live against real Word on the web: a bold ask produced a genuine
Formatted revision with an actionable card; a Heading 1 ask restyled the
paragraph (styleBuiltIn read back "Heading1" after accept, text
byte-identical, revision resolved); and a notice-period question produced
a [1] chip whose click selected the exact termination clause.

The e2e Office mock gains range.font setters and a paragraph styleBuiltIn
setter that materialize Formatted revisions under TrackAll, plus a
citations frame in the chat-stream fixture; new specs cover the
formatting lifecycle (accept, reject, direct mode, heading styles, the
unrecognized-format guard) and citations (chip rendering, mid-tag
streaming, click-to-select, native [n] markers, stale-citation and
case-insensitive fallback).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
673ab898 refactor(word-addin): move the apply-mode control into the composer as a Review switch Amal 2026-08-16 ↗ GitHub
commit body
WHY THIS MATTERS
Review feedback, twice over: (1) the choice of whether an edit applies
directly or waits for approval belongs where the request is written - in
the chat input, next to Send, exactly like the model picker - because the
setting governs what the NEXT send does; (2) a mode with one safe default
and one opt-out reads better as a single labeled switch than as two
segmented buttons. "Review" ON is the approval flow; switching it off
applies edits directly.

HOW IT WORKS
The composer's action row gains the shared ToggleSwitch (the web app's
switch styling) labeled "Review": checked maps to the approval mode,
unchecked to direct apply. The preference itself is unchanged - the same
mike_word_edit_apply_mode key in OfficeRuntime.storage, default approval -
so existing users keep their stored choice. App threads the mode and its
setter through ChatPanel/ChatView into ChatInput; FloatingHeader loses
the control entirely. ChatView also gains the identity-stable
handleLocateCitation callback that routes citation-chip clicks (from the
citations feature) to selectDocumentText without re-rendering memoized
message rows.

The long-standing "no switches" guard in chat.spec.ts asserted that
document context and change tracking expose no opt-out switches; the
Review toggle is a deliberate exception (it governs how edits resolve,
never whether the document is sent or tracked), so the guard now asserts
exactly one switch exists and that it is the Review control. The
apply-mode specs assert switch semantics (aria-checked) in the composer
and that the header renders nothing. Their prompts also switch to the
current "How can I help?" composer placeholder: they were authored
against the pre-rebase placeholder and, piped through `tail`, their
failures were masked by the pipeline's exit code - the suite now runs
with the exit code captured explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
438d6e1e fix(word-addin): author and resolve tracked edits so cards work on Word for Mac desktop Amal 2026-08-17 ↗ GitHub
commit body
WHY THIS MATTERS
On Word for Mac (16.112), Accept/Reject on a Mike edit card silently
failed: the card errored with the generic "Word couldn't update this
change" while the redline stayed pending, forcing users to double-accept
from Word's Review tab. Replicated live in the real host before fixing.

WHAT THE MAC HOST ACTUALLY DOES (live-probed, OOXML-verified)
1. insertText(Replace) under TrackAll applies the DELETION immediately and
   untracked - the .docx holds only w:ins, no w:del - so the deletion half
   of a replacement never exists as a revision. The "exactly one Added +
   one Deleted" ownership proof can then never succeed, every resolution
   layer refuses honestly, and the card degrades. (Superset of the
   Microsoft-acknowledged, still-open office-js#5188; Windows and web
   track the pair correctly.)
2. Split operations - insertText(after) + delete() on the match - DO
   produce a real tracked deletion (w:del with w:delText in the file), but
   its text reads EMPTY through every JS surface (#5188 proper).
3. The desktop-only Word.Revision API (WordApiDesktop 1.4,
   document.revisions) reliably lists all revisions and accept()/reject()
   work; compareLocationWith() reports each replacement's Delete revision
   as Adjacent to exactly its own Insert.
4. Retained cross-batch range proxies intermittently die with
   GeneralException at Document._GetObjectByReferenceId.

HOW THE FIX WORKS
1. Authoring: replacements queue insertText(..., After) + match.delete()
   instead of insertText(Replace) - the only form every host records as a
   full Added+Deleted pair (also the pattern production Word add-ins use;
   both public Mac bug reports were filed against the Replace form). Pure
   deletions just delete().
2. Resolution: where WordApiDesktop 1.4 is supported, the document-level
   fallback reads Word.Revision objects instead of getTrackedChanges. The
   ownership proof adapts to the empty-text bug in two provable steps:
   exact text match first; an empty-text Delete may stand in for the
   edit's deleted side only when it is the SOLE pending Delete in the
   document, or when adjacency to the edit's uniquely matched Insert
   singles it out (AdjacentBefore/After - split authoring writes the
   insertion immediately beside the deletion). Anything ambiguous still
   aborts untouched.
3. Restore: the same Revision-surface match re-links cards after a task
   pane reload (batched and single-edit paths), holding no revision
   proxies - resolution re-verifies at decision time.
4. Error honesty: when the document-level fallback reaches a semantic
   "changed" verdict, that now outranks a dead-proxy GeneralException from
   the anchor pass, so the card reports why instead of the generic
   failure. Silent catch sites also log their errors.

The e2e mock mirrors the new authoring: insertText still materializes the
revision pair (the mock is a web-host model), range.delete() is tracked
(no-op after an insert; a Deleted-only group for pure deletions), and a
pure deletion no longer fabricates an empty Added revision. Spec write
expectations move from location "Replace" to "After".

Verified live on Word for Mac 16.112: two same-paragraph spelling fixes
resolve from their cards (adjacency disambiguation picking the right
empty-text Delete), accept-all drains the Review tab to zero, Direct-mode
auto-accept and a Formatted heading revision resolve, and the document's
final text was confirmed via body + OOXML reads. The web path is
untouched (body-level getTrackedChanges fallback unchanged; the desktop
path is capability-gated), and the full hermetic suite passes.

Trade-off: split authoring renders a replacement as adjacent Deleted +
Added runs rather than one combined replace mark - visually
near-identical, and the standard shape other legal redlining add-ins
produce.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JN4mcnhSK7xpxzF2b3WFkE
435aa48d refactor(word-addin): composer controls - apply-mode menu, workflows under "+", icon-only model picker Amal 2026-08-17 ↗ GitHub
commit body
WHY THIS MATTERS
Composer feedback, three parts: (1) the Review on/off switch read as a
setting, not a choice - the mode belongs in a menu that names both options
and their consequences; (2) the standalone workflow button crowded the
action row for a feature reached occasionally; (3) the model picker's full
model name dominated the row. The row now holds "+", the apply-mode
control, and two icons - everything fits one line even in a narrow pane.

HOW IT WORKS
- The apply-mode control is a plain icon+label trigger (no background, no
  chevron): Eye "Review" or Pen "Edit". It opens the shared Dropdown
  (same primitive as the model picker) titled "How should edits be
  applied?" with a described option per mode and a check on the active
  one:
    Review - Review and propose tracked changes which are applied after
    approval (default)
    Edit - Directly edit the document in tracked changes
  Only the UI naming changes: the stored preference stays
  mike_word_edit_apply_mode = approval | direct, so existing users keep
  their choice.
- The workflow entry moves into the "+" menu alongside Desktop Files and
  Web files, opening the same WorkflowModal; the dedicated Waypoints
  button is gone. Attaching from the Workflows tab is unchanged.
- The model picker collapses to a Settings2 icon; the selected model's
  name lives in its tooltip and the menu itself.

Spec updates follow the semantics: the "no switches" guard reverts to
asserting zero switches (the mode control is a menu button, not an
opt-out); the apply-mode specs drive the dropdown (data-selected +
trigger label) instead of aria-checked; the workflow-attach spec opens
via "+ → Workflows"; and the narrow-pane spec asserts the single-line
action row in place of the removed button's geometry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JN4mcnhSK7xpxzF2b3WFkE
7fbd8132 feat(word-addin): send the document to Mike as structure-annotated markdown Amal 2026-08-19 ↗ GitHub
commit body
WHY THIS MATTERS
The pane used to send Word's flat body.text as document_context. Flat
text erases everything Word knows about structure: a contract's clause
hierarchy becomes an undifferentiated wall of prose, headings are
indistinguishable from body lines, and a table arrives as run-together
cell text. The model reasons worse about a document it cannot see the
shape of.

THE CONSTRAINT THAT SHAPES THE DESIGN
Two features depend on the model quoting document text VERBATIM, because
the add-in locates quotes with Word's search API: edit blocks
(<original> becomes a tracked-change target) and citations (a clicked
quote is found and selected). Naive markdown breaks both - a model
reading "**Term**" or "# Definitions" quotes characters that exist
nowhere in the document, and every such search misses.

HOW IT WORKS
lib/documentMarkdown.ts renders structure ADDITIVELY, leaving each
passage's own text byte-identical:
- heading paragraphs (Title, Heading1-6 styles) gain leading # marks;
- list items gain their real Word labels ("a.", "1.", bullets → "-")
  plus nesting indentation;
- tables render as pipe tables from Table.values;
- inline bold/italic is deliberately NOT represented - emphasis markers
  sit inside passages and would poison verbatim quoting.
useWordDoc's readDocumentMarkdown() walks body.paragraphs and
body.tables in document order; if any structure API misbehaves on a host
it falls back to flat body.text, because a degraded context beats a
failed send.

DEFENSE IN DEPTH FOR MARKER-QUOTING MODELS
The prompt (wordPrompt.ts) now explains the rendering and forbids the
markers in <original> and citation quotes. If a model copies one anyway,
stripStructuralMarkers() undoes exactly what the renderer adds - at most
ONE marker layer, so "# 1. Introduction" where "1." is real document
text survives - and both the edit-apply loop and the citation locator
retry a missed verbatim search with the stripped text, adopting it as
the edit's identity so revision scanning, anchors, and persistence all
agree with what was actually located.

Backend: document_context stays an opaque string (same parsing, same
200k-char cap); the inline doc's file_type becomes text/markdown.

Verified: add-in typecheck + production build green; backend word-chat
suites green (75 tests); full add-in e2e suite green on chromium and
webkit. E2E coverage that fails on flat text and passes here lands in
the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
01867ebd test(word-addin): e2e proof of the markdown context and marker-quote fallbacks Amal 2026-08-19 ↗ GitHub
commit body
WHY A MOCK EXTENSION
The Office mock's body exposed only text/search - no paragraphs or
tables - so the structured read was untestable (it would throw and fall
back to flat text in every spec). The seed gains documentBlocks: when
present, body.paragraphs/body.tables describe those blocks; when absent
(every pre-existing spec) the collections stay undefined, which is
exactly the shape of a host without the structure APIs, so the flat-text
fallback keeps its coverage for free.

WHAT THE THREE TESTS PIN DOWN (each fails on flat body.text)
1. document_context carries "# " headings, list labels with the real
   Word listString, and pipe tables - flat text contains none of these.
2. An edit whose <original> is "# Definitions" (the renderer's heading
   marker included) still lands as ONE tracked change on "Definitions":
   the verbatim search misses, the stripped-marker retry locates the
   underlying text. On flat-text code this edit dies as
   "Skipped - source text was not found."
3. A citation chip quoting "a. Affiliate means..." (list label included)
   still selects the real passage in Word via the same stripped retry;
   before, both the exact and case-insensitive searches miss and nothing
   is ever selected.

Full suite: 266 passed (chromium + webkit), 6 of them new.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
30758b55 feat(word-addin): edit-protocol groundwork - occurrence parsing, honest skip statuses, location-hint slot Amal 2026-08-19 ↗ GitHub
commit body
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
3c651a6b feat(word-addin): multi-occurrence tracked edits - positional identity, replace-all passes, whole-paragraph deletion Amal 2026-08-19 ↗ GitHub
commit body
WHY THIS MATTERS
Three user-visible failures shared one root cause - the add-in identified
everything (search targets, revisions, deletions) by TEXT alone:
1. "Replace X with Y" on a document with several X's silently did nothing
   (the model's prose claimed success while the card skipped).
2. Two pending edits that shared replacement text - the same correction in
   two places - broke each other: Accept on either card errored with
   "The revisions in this passage changed...". This was a LIVE bug on main.
3. "Remove point 2" from a numbered list emptied the paragraph but left
   the numbered husk behind, so Word kept counting a blank item.

WHAT IS POSITIONAL REVISION IDENTITY
On Word for the web, retained range proxies under-report revisions (an
anchor on the inserted text sees only the Added half), so resolution falls
back to the document-wide collection and identifies Mike's revisions by
text with an exactly-one-per-side safety rule: if two pending revisions
carry the same text, nothing is touched. Correct, but it made identical
corrections mutually un-resolvable. The fix narrows candidates by LOCATION
first: each candidate revision's range is compared against the edit's own
retained anchors with Range.compareLocationWith (WordApi 1.3, read-only -
expandTo/intersectWith pollute the web undo stack, office-js#5715), and
only then must exactly one per side remain. "Exactly one inside my own
passage" is strictly stronger than "exactly one in the document".
Containment accepts the full relation set {equal, inside*, contains*}
because hosts disagree on boundary relations (office-js#2527); the Deleted
half also accepts adjacency, since split authoring writes the insertion
immediately after the deleted original. Each anchor gets its own Word.run:
a stale proxy fails its whole batch, and the next anchor still deserves
its chance.

HOW REPLACE-ALL WORKS
An edit carrying <occurrence>all</occurrence> reuses the single-match
pipeline unchanged, once per occurrence:
- Each pass targets the LAST revision-free match in document order.
  Reverse order sidesteps office-js#2800 (Word-web corrupts later match
  positions once an earlier one changes length); "revision-free" is the
  natural bookkeeping - a pass's own tracked changes make that occurrence
  drop out of the next pass's candidate filter.
- Every pass produces its own handle, verification, and hidden bookmark
  (stableEditId `${key}#${pass}`), so ALL existing exactly-one safety
  guards apply per occurrence without modification. The alternative -
  teaching every guard "exactly N" - was rejected as unsound: N identical
  revisions from OTHER cards can satisfy a count, and a replace-all card
  could then resolve someone else's revisions.
- The controller aggregates the passes under ONE card whose Accept/Reject
  resolves every occurrence together, in Review and Direct modes, and a
  reloaded pane rediscovers the passes from the anchor registry prefix.
- matchWholeWord guards substring over-reach ("Bank" in "Banking"), but
  only for ASCII-word-bounded originals - the option is broken on the web
  for accented text and @/#-prefixed terms (office-js#985, #3360, #5223).
- Occurrences that already carry revisions are FILTERED, not fatal, so a
  replace-all coexists with pending redlines and reports a partial apply
  honestly ("Applied N of M occurrences...").

HOW WHOLE-PARAGRAPH DELETION WORKS
A delete-only edit whose <original> equals its paragraph's ENTIRE text
escalates from deleting the text range to deleting the paragraph's Whole
range - the paragraph mark included, which is the character whose removal
makes Word renumber the surviving list items. The byte-equality gate means
the escalation can never remove text the model did not quote; table cells
stay text-only (removing a cell's lone paragraph is a structural table
change); deletedTextMatchesOriginal() tolerates the trailing paragraph
mark that Word appends to such a Deleted revision's reported text. The
paragraph proxy also feeds every card's location hint, and is taken from
the SELECTED target match - never blindly items[0] - so replace-all passes
compare against the right paragraph.

TEST STRATEGY
The Office.js mock gains what real Word already had: position memory.
Revisions written through a search match are remembered per position
(query + match index), so a re-search "sees" them - without this, a
replace-all's second pass would re-apply the same occurrence forever.
compareLocationWith is mocked as revision-group identity, and
Office.context.requirements now answers WordApi (desktop sets stay
unsupported, keeping the suite on the web-shaped paths). New specs:
replace-all.spec.ts (review/reject/direct/reload), the
identical-replacement regression in chat.spec.ts - verified to FAIL with
narrowing disabled - and list-item-deletion.spec.ts for the renumbering
escalation. 292/292 across chromium + webkit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5yxFV2YCFmR61KLyB33yi
0f54e7fb feat(backend): Word edit-protocol contract - unique originals, occurrence-all, whole-item deletion rules Amal 2026-08-19 ↗ GitHub
commit body
WHY THIS MATTERS
The client refuses any <original> that matches more than one place in the
document (safety: applying one block to every identical clause would
silently broaden the edit). But the prompt never told the model that -
worse, it demanded the SHORTEST possible passage, actively steering models
into ambiguous quotes that the client then skipped while the streamed
prose claimed success. The other half of the bug: "replace all X with Y"
was inexpressible, so it always dead-ended.

WHAT THE CONTRACT NOW SAYS
- <original> must identify exactly ONE place: when the target text also
  appears elsewhere, extend the quote with surrounding words from its own
  paragraph until it is document-unique. The old "shortest passage" rule
  becomes "shortest passage that is still unique" - the two rules used to
  conflict.
- Escape hatch: if no unique passage fits the 200-character budget (the
  budget exists because Word's search API rejects long strings - the
  documented cap is 255, enforced inconsistently per host), ASK the user
  which occurrence they mean instead of guessing.
- Occurrence-targeted requests ("the second one", "the one in the closing
  paragraph") must quote context FROM that occurrence and name the
  location in <reason> - the reason line renders on the card, giving the
  user free visible disambiguation before they accept.
- Replace-all: ONE block with the exact repeated text plus
  <occurrence>all</occurrence>, never context-extended, never any other
  value. The client applies it to every occurrence.
- Citations get the same uniqueness rule: the click-to-locate control
  silently jumped to the FIRST occurrence of a repeated quote, which may
  not be the cited one.
- Whole-item deletions must quote the paragraph's ENTIRE text (the client
  escalates to removing the paragraph mark so Word renumbers the list) and
  never "renumber" by editing list numbers, which are renderer annotations
  rather than document text.

DEPLOY-ORDER NOTE (why this commit is last)
The prompt starts EMITTING <occurrence> only here, after the parser
commits: an older cached add-in that receives an unknown tag never seals
the block, leaving a permanently "Incomplete" card. In production the
add-in must ship before the backend prompt does. The commit order in this
branch mirrors that constraint on purpose.

The documentContext test pins the prompt's marker-annotation guidance so
the renderer contract (leading # marks, list markers, table pipes are
annotations, NOT document characters) cannot drift silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5yxFV2YCFmR61KLyB33yi
e99bfc5b feat(word-addin): conflicted cards gain "Accept & apply" - supersede pending revisions explicitly Amal 2026-08-19 ↗ GitHub
commit body
WHY THIS MATTERS
Mike refuses to apply an edit whose target passage already carries
pending tracked changes, and that guard is correct: layering a tracked
replacement over pending revisions would make the card's Accept/Reject
silently resolve changes the card never showed, and Word hosts merge
overlapping revisions differently. But the guard was a dead end - a
pending one-word spelling fix blocked a whole-paragraph rewrite, and the
card's only advice was to go resolve things in Word by hand. The common
case (the pending changes are your own trivia; the rewrite subsumes
them) deserved a one-click path.

WHY NOT SILENT WARN-AND-APPLY
The card system's core invariant is that the revisions bound to a card
are exactly the revisions that card created - capture happens right
after applying, and only holds because the range was revision-free.
Applying over occupied text breaks Accept (resolves a stranger's change
as a side effect), breaks Reject (reverts someone's fix as collateral),
and rests on host-divergent revision merging. So superseding stays a
human decision, made visible.

HOW IT WORKS
The conflicted card now carries an "Accept & apply" action:
1. acceptPendingRevisionsForEdit (useWordDoc) re-locates the target with
   the same search rules as apply (verbatim, then markdown-marker
   stripped; unique match unless occurrence=all) and accepts the
   revisions occupying it - making the range revision-free.
2. The edit's ORIGINAL apply lifecycle reruns from scratch
   (applyStreamedEdit, via a retry registry that conflicted outcomes
   populate), so replace-all passes, persistence, anchors, and direct
   mode all behave exactly as a first-time apply.
Between the two steps the document can shift; that is safe because the
rerun re-validates everything and simply reports conflicted again if new
revisions appeared. If one occupying revision is an earlier Mike card,
accepting it flows through that card's own revalidation and it reports
resolved rather than dangling.

The mock's seeded pre-existing revisions learn a real accept(): the
acceptance is recorded and the seed retired, mirroring Word where an
accepted change stops being pending. e2e proves the full arc - conflicted
card, zero writes, one click, the occupying revision accepted, the edit
landing as a fresh redline whose Accept resolves only itself.

Full suite: 294 passed (chromium + webkit), 2 new.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-1279.md from inside the repo you want the changes in.

⬇ Download capture-thread-1279.md