Headless docx redlining — Patrick edits files, Word is the review surface - #169
Merged
Merged
Conversation
Evaluates @ansonlai/docx-redline-js (MIT) as the tracked-changes engine for the post-editor direction: edit .docx on disk, review in Word, no in-app editor. spike.ts exercises redline + comment + accept/reject round-trips on a minimal generated docx; spike2.ts does the same on a real USPTO office action (bring your own .docx, gitignored) and measures collateral damage. Findings: runs under Bun with @xmldom/xmldom injected; edits confine to the target paragraphs; accept/reject round-trips are exact. One engine bug found: reconstruction mode stamps spurious w:rPrChange (ghost 'Format' revisions) on unchanged runs — strippable in a post-process, properly fixable upstream.
Interactive mini-Patrick for the no-editor protocol: watches the canonical draft, detects Word (~$) / LibreOffice (.~lock) markers, applies redlines immediately when unlocked, parks them while open and applies on close, and scans each save for new @patrick comments as an in-document command channel. --sim mode fakes the lock marker and verifies the park/apply machine.
Adds the living-doc plan (measured integration surface, target architecture, two-PR slicing, risks) and a public USPTO office action as the real-world docx fixture for the new engine's tests.
The adapter (lib/docx/redline.ts) is the only import point for @ansonlai/docx-redline-js and files off its measured sharp edges: it resolves the target paragraph itself (exact + unambiguous, so the engine's fuzzy matcher can never redline the wrong paragraph), supersedes Patrick's own pending revisions before a re-edit (redlines are original -> latest, never stacked), verifies the landed text before returning, and strips the engine's ghost format revisions. The dance (lib/docx/dance.ts) shares the draft with Word/LibreOffice by protocol: reads never block, writes apply immediately when the draft is closed and park while a lock marker is present, draining on a 1s tick (comments before redlines so anchors resolve). Status exposes lock state, parked count, @patrick comment mentions, and failed parked edits.
read_draft / edit_paragraph / add_draft_comment / read_draft_comments — server-executed against the active draft on disk through the dance (names single-sourced in @patrick/shared). The seven no-execute editor tools and their client round-trip are gone; pinned-docx context extraction now uses the adapter's own text walk instead of DocxReviewer. New routes: draft-status (+ clear-failures), docx-text (the in-app preview), and open (OS opener for Word). Capabilities prose teaches the dance: save = talk to Patrick, close = let Patrick write.
A .docx tab is now a live text preview + the dance status bar (lock state, parked-edit count, @patrick comment mentions, failed parked edits, Open in Word) — deliberately not a Word imitation; review happens in Word's own review pane. Deletes the editor mount (docx-viewer), the active-editor ref registry, and the zoom pill; drops the @eigenpal deps, the editor CSS @source, and the editor branch of onToolCall (draft tools resolve server-side). Spikes are lint/knip-exempt like other non-app code.
Field testing found parallel tool calls losing writes: every mutation is read-file -> transform -> write-file, and the model fires calls concurrently, so 12 parallel comments collapsed to 3 (each read the same base bytes; last write won). All mutations now serialize through a per-draft promise chain — same discipline as mutateChart — with a 12-parallel-comments regression test. The preview now shows pending redlines as real ins/del marks (run-level extraction in the adapter, rendered as <ins>/<del>), and the panel refreshes the moment a mutating draft tool result streams back instead of waiting for the status poll.
Every Patrick edit is now a surgical tracked change (reject-all restores the text exactly, byte-identical elsewhere), so the copy-on-unlock indirection that protected originals from the old editor's whole-file round-trip is no longer earning its clunkiness. requestUnlock now flips the original .docx itself to editable — one explicit consent per document — after snapshotting its pristine bytes to .patrick/backups (never overwritten by a re-unlock). editable ≡ .docx && (createdInPatrick || unlocked), single-sourced in @patrick/shared isEditableDoc. Originals still can't be renamed or deleted, and paraId backfill goes with the old anchor-by-id tools.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Adapter (redline.ts): - strict post-apply verification at the edited position only (block i..i+delta when the engine splits paragraphs) — newText appearing elsewhere in the document can no longer vouch for a mangled apply - refuse paragraphs carrying the ATTORNEY's pending tracked changes (editing through them risks absorbing their authorship) - merge result.numberingXml so list-shaped rewrites can't reference numbering ids that don't exist (Word repair prompt) - text boxes: skip w:txbxContent + mc:Fallback in walks and hide nested w:p from reads/edits (indices keep their engine-aligned slots) — box text was extracted twice and permanently unambiguous to edit - tabs/breaks extract as separators; comment ids seed from the document so a restart can't mint a duplicate id Dance: - new ops drain older parked ops first (a stale parked edit could supersede a newer edit to the same paragraph) - parked ops persist to .patrick/parked/ and survive an app restart - engine throws inside a drain are recorded as failures instead of silently discarding the rest of the queue; draft writes are atomic (tmp + rename); mentions memoised by mtime; 7-char Word lock tier Context/product: - a pinned docx that gets unlocked keeps serving the PRISTINE backup as the pinned source (immutable, cacheable) instead of the live draft - the manifest names other editable drafts so a second unlocked doc doesn't vanish from Patrick's world - createDraft/unlock HITL handlers set the active draft synchronously (state + transport ref) so the auto-continued turn can't ship activeDraft: null; the sticky-draft effect keeps unknown-yet drafts - the dead PUT document-bytes route is gone (it bypassed the dance)
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Removes the in-app docx editor from Patrick's critical path. Patrick now edits
.docxfiles on disk as native Word tracked changes (headless reconciliation via MIT-licensed@ansonlai/docx-redline-js), and Word/LibreOffice is the viewing + review surface. The three vendored@eigenpal/docx-editor-*packages are no longer imported by any app — their deletion is the follow-up PR, keeping this one revertible.How
apps/api/src/lib/docx/redline.ts) — the only import point for the redline library, guarding its measured sharp edges: exact unambiguous paragraph resolution (its fuzzy matcher can silently redline the wrong paragraph), supersede-on-re-edit (stacked redlines double-apply on accept), post-verify before write (a failed edit never mutates), ghostw:rPrChangestripping.apps/api/src/lib/docx/dance.ts) — sharing a file with Word by protocol: reads never block; writes apply when the draft is closed and park while a Word/LO lock marker exists, draining the moment it's closed. All mutations serialize through a per-draft queue (parallel tool calls once collapsed 12 comments to 3). Save-triggered@Patrickcomment pickup makes Word comments an instruction channel.read_draft/edit_paragraph/add_draft_comment/read_draft_comments) replace the seven no-execute editor tools and the whole client round-trip.requestUnlocknow flips the original itself to editable (pristine bytes snapshotted to.patrick/backups) instead of minting a "(Patrick) copy"; safe because every edit is a rejectable redline and untouched XML stays byte-identical.Testing