@beyondwork/docx-react-component 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -135,3 +135,685 @@ Shared platform and planned xlsx docs:
135
135
  This repo is not trying to become a generic office clone.
136
136
 
137
137
  It is building fidelity-first office-document runtimes with explicit preservation and calm, reviewable UI.
138
+
139
+ ## Using the package
140
+
141
+ ### WordReviewEditor
142
+
143
+ `WordReviewEditor` is a React component for loading, editing, and exporting `.docx` files with full comment and tracked-change (redline) support. It is exported from `@beyondwork/docx-react-component`.
144
+
145
+ ### Installation
146
+
147
+ ```tsx
148
+ import {
149
+ WordReviewEditor,
150
+ type WordReviewEditorRef,
151
+ type WordReviewEditorProps,
152
+ type WordReviewEditorEvent,
153
+ } from "@beyondwork/docx-react-component";
154
+ ```
155
+
156
+ ### Basic mount
157
+
158
+ ```tsx
159
+ import { useRef } from "react";
160
+ import { WordReviewEditor, type WordReviewEditorRef } from "@beyondwork/docx-react-component";
161
+
162
+ export function MyEditor({ docxBytes }: { docxBytes: Uint8Array }) {
163
+ const editorRef = useRef<WordReviewEditorRef>(null);
164
+
165
+ return (
166
+ <WordReviewEditor
167
+ ref={editorRef}
168
+ documentId="doc-001"
169
+ currentUser={{ userId: "u1", displayName: "Alice" }}
170
+ initialDocx={docxBytes}
171
+ onEvent={(event) => console.log(event)}
172
+ />
173
+ );
174
+ }
175
+ ```
176
+
177
+ ---
178
+
179
+ ### Props reference
180
+
181
+ | Prop | Type | Description |
182
+ |---|---|---|
183
+ | `documentId` | `string` | **Required.** Stable identifier for this document. |
184
+ | `currentUser` | `EditorUser` | **Required.** The user performing edits and adding comments. |
185
+ | `initialDocx` | `Uint8Array \| ArrayBuffer` | Raw `.docx` bytes to load on first mount. |
186
+ | `initialSessionState` | `EditorSessionState` | Previously saved session state to restore. |
187
+ | `initialSnapshot` | `PersistedEditorSnapshot` | Previously saved snapshot to restore. |
188
+ | `externalDocSource` | `ExternalDocumentSource` | Alternative source with explicit `kind` (`"docx"`, `"session"`, `"snapshot"`). |
189
+ | `readOnly` | `boolean` | When `true`, all editing commands are disabled. |
190
+ | `reviewMode` | `"editing" \| "review"` | Shell layout hint — affects toolbar/panel arrangement but not editing authority. |
191
+ | `markupDisplay` | `"clean" \| "simple" \| "all"` | Controls tracked-change visibility. |
192
+ | `showReviewPanel` | `boolean` | Shows or hides the right-side comment and tracked-change panel. |
193
+ | `autosave` | `AutosaveConfig` | Enables automatic saving. |
194
+ | `hostAdapter` | `EditorHostAdapter` | Callbacks for `load`, `saveSession`, `saveExport`. |
195
+ | `datastore` | `EditorDatastoreAdapter` | Alternative persistence adapter with `load`, `saveSnapshot`. |
196
+ | `onEvent` | `(event: WordReviewEditorEvent) => void` | Unified event handler (see [Events](#events)). |
197
+ | `onWarning` | `(warning: EditorWarning) => void` | Fired for non-fatal warnings. |
198
+ | `onError` | `(error: EditorError) => void` | Fired for fatal errors. |
199
+
200
+ #### EditorUser
201
+
202
+ ```ts
203
+ interface EditorUser {
204
+ userId: string;
205
+ displayName: string;
206
+ email?: string;
207
+ avatarUrl?: string;
208
+ }
209
+ ```
210
+
211
+ #### AutosaveConfig
212
+
213
+ ```ts
214
+ interface AutosaveConfig {
215
+ enabled?: boolean;
216
+ debounceMs?: number; // default: 2000
217
+ }
218
+ ```
219
+
220
+ ---
221
+
222
+ ### Show / hide UI regions
223
+
224
+ #### Review panel
225
+
226
+ The right-side panel lists comment threads and tracked changes.
227
+
228
+ ```tsx
229
+ <WordReviewEditor showReviewPanel={false} ... /> // hide panel
230
+ <WordReviewEditor showReviewPanel={true} ... /> // show panel (default)
231
+ ```
232
+
233
+ #### Tracked-change display mode
234
+
235
+ `markupDisplay` controls how tracked changes appear in the document body.
236
+
237
+ | Value | Behaviour |
238
+ |---|---|
239
+ | `"clean"` | Show the accepted version — insertions visible, deletions hidden. |
240
+ | `"simple"` | Show a simplified view of changes without inline markup. |
241
+ | `"all"` | Show all insertion and deletion marks inline (Word's "Show Markup" mode). |
242
+
243
+ ```tsx
244
+ <WordReviewEditor markupDisplay="clean" ... />
245
+ ```
246
+
247
+ You can also change the display mode at runtime:
248
+
249
+ ```ts
250
+ // no ref method for markupDisplay — pass as a prop; React re-renders propagate the change.
251
+ ```
252
+
253
+ #### Document mode
254
+
255
+ `DocumentMode` controls editing authority, not just appearance.
256
+
257
+ | Mode | Effect |
258
+ |---|---|
259
+ | `"editing"` | Edits are applied directly (no tracking). |
260
+ | `"suggesting"` | Every edit is automatically wrapped in a tracked change. |
261
+ | `"viewing"` | Document is read-only regardless of the `readOnly` prop. |
262
+
263
+ Set via ref:
264
+
265
+ ```ts
266
+ editorRef.current.setDocumentMode("suggesting");
267
+ ```
268
+
269
+ Or pass `reviewMode="review"` as a prop to start in a review-friendly shell layout (the component internally maps this to `"suggesting"` document mode).
270
+
271
+ #### Read-only mode
272
+
273
+ ```tsx
274
+ <WordReviewEditor readOnly={true} ... />
275
+ ```
276
+
277
+ All editing, commenting, and tracked-change commands are blocked. The toolbar is still rendered but all buttons are disabled.
278
+
279
+ #### Workspace layout
280
+
281
+ ```ts
282
+ editorRef.current.setWorkspaceMode("canvas"); // continuous scroll
283
+ editorRef.current.setWorkspaceMode("page"); // paginated view
284
+ ```
285
+
286
+ ---
287
+
288
+ ### Imperative ref
289
+
290
+ Obtain the ref via `useRef<WordReviewEditorRef>()`:
291
+
292
+ ```tsx
293
+ const editorRef = useRef<WordReviewEditorRef>(null);
294
+ <WordReviewEditor ref={editorRef} ... />
295
+
296
+ // then:
297
+ editorRef.current?.addComment({ body: "Needs revision" });
298
+ ```
299
+
300
+ ---
301
+
302
+ ### Comment operations
303
+
304
+ #### Add a comment
305
+
306
+ ```ts
307
+ addComment(params: AddCommentParams): string
308
+ // returns the new commentId
309
+ ```
310
+
311
+ ```ts
312
+ interface AddCommentParams {
313
+ anchor?: EditorAnchorProjection; // defaults to the current selection
314
+ body?: string;
315
+ authorId?: string; // defaults to currentUser.userId
316
+ }
317
+ ```
318
+
319
+ **Important**: if you want the comment to land on a specific text selection, capture the anchor *before* opening any draft UI (e.g. a modal or popover), because opening a modal typically collapses the editor selection.
320
+
321
+ ```ts
322
+ // 1. Capture anchor while text is still selected
323
+ const snapshot = editorRef.current.getRenderSnapshot();
324
+ const anchor = snapshot.selection.activeRange;
325
+
326
+ // 2. Open your draft UI, let user type a message...
327
+ // 3. On submit:
328
+ const commentId = editorRef.current.addComment({ anchor, body: draftText });
329
+ ```
330
+
331
+ #### Resolve a comment
332
+
333
+ ```ts
334
+ editorRef.current.resolveComment(commentId);
335
+ ```
336
+
337
+ Marks the thread as resolved. The comment remains in the document and can be exported; it is moved to the resolved list in the sidebar.
338
+
339
+ #### Reopen a resolved comment
340
+
341
+ ```ts
342
+ editorRef.current.reopenComment(commentId);
343
+ ```
344
+
345
+ #### Delete a comment permanently
346
+
347
+ ```ts
348
+ editorRef.current.deleteComment(commentId);
349
+ ```
350
+
351
+ Removes the comment entirely. Use this to clean up failed or unwanted drafts.
352
+
353
+ #### Add a reply to an existing thread
354
+
355
+ ```ts
356
+ editorRef.current.addCommentReply(commentId, "Reply text here");
357
+ ```
358
+
359
+ #### Edit a comment body
360
+
361
+ ```ts
362
+ editorRef.current.editCommentBody(commentId, "Updated text");
363
+ ```
364
+
365
+ #### Scroll to a comment
366
+
367
+ ```ts
368
+ editorRef.current.scrollToComment(commentId);
369
+ ```
370
+
371
+ #### Open (focus) a comment in the sidebar
372
+
373
+ ```ts
374
+ editorRef.current.openComment(commentId);
375
+ ```
376
+
377
+ #### Get all comments
378
+
379
+ ```ts
380
+ const sidebar: CommentSidebarSnapshot = editorRef.current.getComments();
381
+ ```
382
+
383
+ ```ts
384
+ interface CommentSidebarSnapshot {
385
+ activeCommentId?: string;
386
+ openCommentIds: string[];
387
+ resolvedCommentIds: string[];
388
+ detachedCommentIds: string[];
389
+ totalCount: number;
390
+ threads: CommentSidebarThreadSnapshot[];
391
+ }
392
+
393
+ interface CommentSidebarThreadSnapshot {
394
+ commentId: string;
395
+ status: "open" | "resolved" | "detached";
396
+ anchor: EditorAnchorProjection;
397
+ excerpt: string; // the anchored text snippet
398
+ entries: CommentSidebarThreadEntrySnapshot[];
399
+ entryCount: number;
400
+ createdAt: string; // ISO 8601
401
+ createdBy: string; // userId
402
+ resolvedAt?: string;
403
+ resolvedBy?: string;
404
+ }
405
+ ```
406
+
407
+ #### Detached comments
408
+
409
+ A comment becomes **detached** when the text it was anchored to is deleted. Detached comments:
410
+
411
+ - Still appear in `sidebar.detachedCommentIds` and have `status: "detached"`.
412
+ - Have `anchor.kind === "detached"` with a `lastKnownRange` and a `reason` (`"deleted"`, `"invalidatedByStructureChange"`, or `"importAmbiguity"`).
413
+ - Do **not** block DOCX export.
414
+ - Can be resolved, reopened, or deleted via the same methods above.
415
+
416
+ ---
417
+
418
+ ### Tracked-change operations
419
+
420
+ #### Get all tracked changes
421
+
422
+ ```ts
423
+ const changes: TrackedChangesSnapshot = editorRef.current.getTrackedChanges();
424
+ ```
425
+
426
+ ```ts
427
+ interface TrackedChangesSnapshot {
428
+ pendingChangeIds: string[];
429
+ acceptedChangeIds: string[];
430
+ rejectedChangeIds: string[];
431
+ detachedChangeIds: string[];
432
+ actionableChangeIds: string[];
433
+ preserveOnlyChangeIds: string[];
434
+ totalCount: number;
435
+ revisions: TrackedChangeEntrySnapshot[];
436
+ }
437
+
438
+ interface TrackedChangeEntrySnapshot {
439
+ revisionId: string;
440
+ kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
441
+ status: "active" | "accepted" | "rejected" | "detached";
442
+ actionability: "actionable" | "preserve-only";
443
+ canAccept: boolean;
444
+ canReject: boolean;
445
+ anchor: EditorAnchorProjection;
446
+ anchorLabel: string;
447
+ excerpt?: string;
448
+ detail?: string;
449
+ authorId: string;
450
+ createdAt: string;
451
+ }
452
+ ```
453
+
454
+ `preserve-only` revisions (`formatting`, `move`) can be displayed but cannot be individually accepted or rejected through the API.
455
+
456
+ #### Accept a single change
457
+
458
+ ```ts
459
+ editorRef.current.acceptChange(revisionId);
460
+ ```
461
+
462
+ #### Reject a single change
463
+
464
+ ```ts
465
+ editorRef.current.rejectChange(revisionId);
466
+ ```
467
+
468
+ #### Accept all pending changes
469
+
470
+ ```ts
471
+ editorRef.current.acceptAllChanges();
472
+ ```
473
+
474
+ #### Reject all pending changes
475
+
476
+ ```ts
477
+ editorRef.current.rejectAllChanges();
478
+ ```
479
+
480
+ #### Scroll to a tracked change
481
+
482
+ ```ts
483
+ editorRef.current.scrollToRevision(revisionId);
484
+ ```
485
+
486
+ ---
487
+
488
+ ### Export
489
+
490
+ ```ts
491
+ const result = await editorRef.current.exportDocx({ fileName: "output.docx" });
492
+ // result.bytes is the Uint8Array of the .docx file
493
+ ```
494
+
495
+ Export will throw if any non-detached comment no longer maps to a serializable range in the document. To diagnose, check `getComments().threads` for entries where `status !== "detached"` but whose `anchor.kind` is unexpected.
496
+
497
+ ---
498
+
499
+ ### Events
500
+
501
+ All events are dispatched through the single `onEvent` prop. The `type` discriminator narrows the payload.
502
+
503
+ ```tsx
504
+ <WordReviewEditor
505
+ onEvent={(event) => {
506
+ if (event.type === "comment_added") {
507
+ console.log("New comment:", event.commentId);
508
+ }
509
+ }}
510
+ />
511
+ ```
512
+
513
+ #### `ready`
514
+
515
+ Fired once after the document finishes loading and the editor is interactive.
516
+
517
+ ```ts
518
+ {
519
+ type: "ready";
520
+ documentId: string;
521
+ sessionId: string;
522
+ source: "docx" | "session" | "snapshot";
523
+ stats: DocumentStats; // storyLength, commentCount, revisionCount
524
+ compatibility: CompatibilityReport;
525
+ comments: CommentSidebarSnapshot;
526
+ trackedChanges: TrackedChangesSnapshot;
527
+ }
528
+ ```
529
+
530
+ #### `comment_added`
531
+
532
+ Fired when a new comment thread is created (via `addComment` or the toolbar).
533
+
534
+ ```ts
535
+ {
536
+ type: "comment_added";
537
+ documentId: string;
538
+ commentId: string;
539
+ anchor: EditorAnchorProjection;
540
+ }
541
+ ```
542
+
543
+ #### `comment_resolved`
544
+
545
+ Fired when a comment is resolved (via `resolveComment` or the sidebar).
546
+
547
+ ```ts
548
+ {
549
+ type: "comment_resolved";
550
+ documentId: string;
551
+ commentId: string;
552
+ }
553
+ ```
554
+
555
+ There is no separate `comment_removed` event — deletions are silent. Query `getComments()` after a `dirty_changed` event if you need to detect deletions.
556
+
557
+ #### `change_accepted`
558
+
559
+ Fired when a tracked change is accepted.
560
+
561
+ ```ts
562
+ {
563
+ type: "change_accepted";
564
+ documentId: string;
565
+ changeId: string;
566
+ }
567
+ ```
568
+
569
+ #### `change_rejected`
570
+
571
+ Fired when a tracked change is rejected.
572
+
573
+ ```ts
574
+ {
575
+ type: "change_rejected";
576
+ documentId: string;
577
+ changeId: string;
578
+ }
579
+ ```
580
+
581
+ #### `selection_changed`
582
+
583
+ Fired whenever the editor cursor or selection changes.
584
+
585
+ ```ts
586
+ {
587
+ type: "selection_changed";
588
+ documentId: string;
589
+ selection: SelectionSnapshot;
590
+ }
591
+
592
+ interface SelectionSnapshot {
593
+ anchor: number;
594
+ head: number;
595
+ isCollapsed: boolean;
596
+ activeRange: EditorAnchorProjection;
597
+ storyTarget?: EditorStoryTarget;
598
+ }
599
+ ```
600
+
601
+ Use `selection.activeRange` as the `anchor` argument to `addComment` — but capture it *before* opening any modal UI.
602
+
603
+ #### `dirty_changed`
604
+
605
+ Fired when the document transitions between clean and dirty (unsaved) states.
606
+
607
+ ```ts
608
+ {
609
+ type: "dirty_changed";
610
+ documentId: string;
611
+ isDirty: boolean;
612
+ }
613
+ ```
614
+
615
+ #### `story_changed`
616
+
617
+ Fired when the user navigates between document stories (e.g. main body → header/footer).
618
+
619
+ ```ts
620
+ {
621
+ type: "story_changed";
622
+ documentId: string;
623
+ activeStory: EditorStoryTarget;
624
+ }
625
+ ```
626
+
627
+ #### `export_completed`
628
+
629
+ Fired after a successful `exportDocx` call, after the host `saveExport` callback (if any) has resolved.
630
+
631
+ ```ts
632
+ {
633
+ type: "export_completed";
634
+ documentId: string;
635
+ result: ExportResult; // result.bytes, result.fileName, result.mimeType
636
+ }
637
+ ```
638
+
639
+ #### `session_saved`
640
+
641
+ Fired after the host `saveSession` callback resolves.
642
+
643
+ ```ts
644
+ {
645
+ type: "session_saved";
646
+ documentId: string;
647
+ sessionState: EditorSessionState;
648
+ savedAt: string;
649
+ isAutosave: boolean;
650
+ }
651
+ ```
652
+
653
+ #### `snapshot_saved`
654
+
655
+ Fired after the datastore `saveSnapshot` callback resolves.
656
+
657
+ ```ts
658
+ {
659
+ type: "snapshot_saved";
660
+ documentId: string;
661
+ snapshot: PersistedEditorSnapshot;
662
+ isAutosave: boolean;
663
+ }
664
+ ```
665
+
666
+ #### `autosave_state`
667
+
668
+ Fired when the autosave lifecycle transitions.
669
+
670
+ ```ts
671
+ {
672
+ type: "autosave_state";
673
+ documentId: string;
674
+ state: "idle" | "pending" | "saving" | "saved" | "error";
675
+ }
676
+ ```
677
+
678
+ #### `warning_added` / `warning_cleared`
679
+
680
+ Non-fatal import or rendering warnings.
681
+
682
+ ```ts
683
+ { type: "warning_added"; documentId: string; warning: EditorWarning; }
684
+ { type: "warning_cleared"; documentId: string; warningId: string; code: EditorWarningCode; }
685
+ ```
686
+
687
+ #### `error`
688
+
689
+ Fatal editor error. The editor may be in an unrecoverable state after this event.
690
+
691
+ ```ts
692
+ {
693
+ type: "error";
694
+ documentId: string;
695
+ error: EditorError; // error.code, error.message, error.isFatal
696
+ }
697
+ ```
698
+
699
+ #### Workflow events
700
+
701
+ These events relate to the optional workflow overlay feature.
702
+
703
+ ```ts
704
+ { type: "workflow_overlay_changed"; documentId: string; snapshot: WorkflowScopeSnapshot; }
705
+ { type: "workflow_active_work_item_changed"; documentId: string; activeWorkItemId: string | null; }
706
+ { type: "command_blocked"; documentId: string; command: string; reasons: WorkflowBlockedCommandReason[]; }
707
+ ```
708
+
709
+ ---
710
+
711
+ ### Key types
712
+
713
+ #### EditorAnchorProjection
714
+
715
+ Describes a position or range in the document. Returned by `selection.activeRange` and stored on comments/revisions.
716
+
717
+ ```ts
718
+ type EditorAnchorProjection =
719
+ | { kind: "range"; from: number; to: number; assoc: { start: -1|1; end: -1|1 } }
720
+ | { kind: "node"; at: number; assoc: -1|1 }
721
+ | { kind: "detached"; lastKnownRange: { from: number; to: number };
722
+ reason: "deleted" | "invalidatedByStructureChange" | "importAmbiguity" };
723
+ ```
724
+
725
+ A `"range"` anchor is required for `addComment` if the document contains tables or the selection spans multiple characters. The `from`/`to` positions are in the editor's internal runtime coordinate space — always capture them from `getRenderSnapshot().selection.activeRange`, never construct them manually.
726
+
727
+ #### DocumentMode
728
+
729
+ ```ts
730
+ type DocumentMode = "editing" | "suggesting" | "viewing";
731
+ ```
732
+
733
+ #### WorkspaceMode
734
+
735
+ ```ts
736
+ type WorkspaceMode = "canvas" | "page";
737
+ ```
738
+
739
+ ---
740
+
741
+ ### Common patterns
742
+
743
+ #### Full add-comment flow with custom UI
744
+
745
+ ```tsx
746
+ function CommentButton({ editorRef }: { editorRef: React.RefObject<WordReviewEditorRef> }) {
747
+ const [draft, setDraft] = useState<{ anchor: EditorAnchorProjection; text: string } | null>(null);
748
+
749
+ function openDraft() {
750
+ // Capture anchor BEFORE the modal steals focus from the editor
751
+ const snapshot = editorRef.current?.getRenderSnapshot();
752
+ if (!snapshot) return;
753
+ const anchor = snapshot.selection.activeRange;
754
+ if (anchor.kind !== "range" || anchor.from === anchor.to) return;
755
+ setDraft({ anchor, text: "" });
756
+ }
757
+
758
+ function submit() {
759
+ if (!draft) return;
760
+ editorRef.current?.addComment({ anchor: draft.anchor, body: draft.text });
761
+ setDraft(null);
762
+ }
763
+
764
+ return (
765
+ <>
766
+ <button onClick={openDraft}>Comment</button>
767
+ {draft && (
768
+ <dialog open>
769
+ <textarea value={draft.text} onChange={(e) => setDraft({ ...draft, text: e.target.value })} />
770
+ <button onClick={submit}>Add</button>
771
+ <button onClick={() => setDraft(null)}>Cancel</button>
772
+ </dialog>
773
+ )}
774
+ </>
775
+ );
776
+ }
777
+ ```
778
+
779
+ #### Listen for comment and review-change events
780
+
781
+ ```tsx
782
+ <WordReviewEditor
783
+ onEvent={(event) => {
784
+ switch (event.type) {
785
+ case "comment_added":
786
+ console.log("comment added:", event.commentId, event.anchor);
787
+ break;
788
+ case "comment_resolved":
789
+ console.log("comment resolved:", event.commentId);
790
+ break;
791
+ case "change_accepted":
792
+ console.log("change accepted:", event.changeId);
793
+ break;
794
+ case "change_rejected":
795
+ console.log("change rejected:", event.changeId);
796
+ break;
797
+ }
798
+ }}
799
+ />
800
+ ```
801
+
802
+ #### Resolve all open comments programmatically
803
+
804
+ ```ts
805
+ const { threads } = editorRef.current.getComments();
806
+ for (const thread of threads) {
807
+ if (thread.status === "open") {
808
+ editorRef.current.resolveComment(thread.commentId);
809
+ }
810
+ }
811
+ ```
812
+
813
+ #### Accept or reject all actionable changes
814
+
815
+ ```ts
816
+ editorRef.current.acceptAllChanges();
817
+ // or
818
+ editorRef.current.rejectAllChanges();
819
+ ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.20",
4
+ "version": "1.0.22",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
6
  "type": "module",
7
7
  "sideEffects": [
@@ -73,6 +73,14 @@
73
73
  "types": "./src/core/commands/table-structure-commands.ts",
74
74
  "import": "./src/core/commands/table-structure-commands.ts"
75
75
  },
76
+ "./core/commands/style-commands": {
77
+ "types": "./src/core/commands/style-commands.ts",
78
+ "import": "./src/core/commands/style-commands.ts"
79
+ },
80
+ "./core/commands/section-layout-commands": {
81
+ "types": "./src/core/commands/section-layout-commands.ts",
82
+ "import": "./src/core/commands/section-layout-commands.ts"
83
+ },
76
84
  "./core/state/editor-state": {
77
85
  "types": "./src/core/state/editor-state.ts",
78
86
  "import": "./src/core/state/editor-state.ts"
@@ -726,7 +726,7 @@ function exportDocxEditorSession(
726
726
  state.initialCanonicalSignature;
727
727
  const canReuse = canReuseSourceBytesForCurrentDocument(state, currentDocument);
728
728
  const commentCount = Object.keys(currentDocument.review?.comments ?? {}).length;
729
- console.error(`[DEBUG-EXPORT] docId=${sessionState.documentId} signatureMatch=${signatureMatch} canReuse=${canReuse} preservedDefs=${state.preservedCommentDefinitions.length} blockingDiags=${state.blockingCommentDiagnostics.length} comments=${commentCount}`);
729
+
730
730
  if (signatureMatch && canReuse) {
731
731
  return {
732
732
  bytes: new Uint8Array(state.sourceBytes),