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>
|