@beyondwork/docx-react-component 1.0.18 → 1.0.20

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.
Files changed (105) hide show
  1. package/README.md +8 -2
  2. package/package.json +24 -34
  3. package/src/api/README.md +5 -1
  4. package/src/api/public-types.ts +710 -4
  5. package/src/api/session-state.ts +60 -0
  6. package/src/core/commands/formatting-commands.ts +2 -1
  7. package/src/core/commands/image-commands.ts +147 -0
  8. package/src/core/commands/index.ts +19 -3
  9. package/src/core/commands/list-commands.ts +231 -36
  10. package/src/core/commands/paragraph-layout-commands.ts +339 -0
  11. package/src/core/commands/section-layout-commands.ts +680 -0
  12. package/src/core/commands/style-commands.ts +262 -0
  13. package/src/core/search/search-text.ts +357 -0
  14. package/src/core/selection/mapping.ts +41 -0
  15. package/src/core/state/editor-state.ts +4 -1
  16. package/src/index.ts +51 -0
  17. package/src/io/docx-session.ts +623 -56
  18. package/src/io/export/serialize-comments.ts +104 -34
  19. package/src/io/export/serialize-footnotes.ts +198 -1
  20. package/src/io/export/serialize-headers-footers.ts +203 -10
  21. package/src/io/export/serialize-main-document.ts +285 -8
  22. package/src/io/export/serialize-numbering.ts +28 -7
  23. package/src/io/export/split-review-boundaries.ts +181 -19
  24. package/src/io/normalize/normalize-text.ts +144 -32
  25. package/src/io/ooxml/highlight-colors.ts +39 -0
  26. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  27. package/src/io/ooxml/parse-comments.ts +85 -19
  28. package/src/io/ooxml/parse-fields.ts +396 -0
  29. package/src/io/ooxml/parse-footnotes.ts +452 -22
  30. package/src/io/ooxml/parse-headers-footers.ts +657 -29
  31. package/src/io/ooxml/parse-inline-media.ts +30 -0
  32. package/src/io/ooxml/parse-main-document.ts +807 -20
  33. package/src/io/ooxml/parse-numbering.ts +7 -0
  34. package/src/io/ooxml/parse-revisions.ts +317 -38
  35. package/src/io/ooxml/parse-settings.ts +184 -0
  36. package/src/io/ooxml/parse-shapes.ts +25 -0
  37. package/src/io/ooxml/parse-styles.ts +463 -0
  38. package/src/io/ooxml/parse-theme.ts +32 -0
  39. package/src/legal/bookmarks.ts +44 -0
  40. package/src/legal/cross-references.ts +59 -1
  41. package/src/model/canonical-document.ts +250 -4
  42. package/src/model/cds-1.0.0.ts +13 -0
  43. package/src/model/snapshot.ts +87 -2
  44. package/src/review/store/revision-store.ts +6 -0
  45. package/src/review/store/revision-types.ts +1 -0
  46. package/src/runtime/document-layout.ts +332 -0
  47. package/src/runtime/document-navigation.ts +603 -0
  48. package/src/runtime/document-runtime.ts +1754 -78
  49. package/src/runtime/document-search.ts +145 -0
  50. package/src/runtime/numbering-prefix.ts +47 -26
  51. package/src/runtime/page-layout-estimation.ts +212 -0
  52. package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
  53. package/src/runtime/session-capabilities.ts +35 -3
  54. package/src/runtime/story-context.ts +164 -0
  55. package/src/runtime/story-targeting.ts +162 -0
  56. package/src/runtime/surface-projection.ts +324 -36
  57. package/src/runtime/table-schema.ts +89 -7
  58. package/src/runtime/view-state.ts +477 -0
  59. package/src/runtime/workflow-markup.ts +349 -0
  60. package/src/ui/WordReviewEditor.tsx +2469 -1344
  61. package/src/ui/browser-export.ts +52 -0
  62. package/src/ui/editor-command-bag.ts +120 -0
  63. package/src/ui/editor-runtime-boundary.ts +1422 -0
  64. package/src/ui/editor-shell-view.tsx +134 -0
  65. package/src/ui/editor-surface-controller.tsx +51 -0
  66. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  67. package/src/ui/headless/revision-decoration-model.ts +4 -4
  68. package/src/ui/headless/selection-helpers.ts +20 -0
  69. package/src/ui/headless/selection-toolbar-model.ts +22 -0
  70. package/src/ui/headless/use-editor-keyboard.ts +6 -1
  71. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  72. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  73. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  74. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  75. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  76. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
  77. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
  78. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  79. package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
  80. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
  81. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  82. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
  83. package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
  84. package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
  85. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
  86. package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
  87. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
  88. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
  89. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  90. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
  91. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
  92. package/src/ui-tailwind/index.ts +2 -1
  93. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  94. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
  95. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  96. package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
  97. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  98. package/src/ui-tailwind/theme/editor-theme.css +127 -0
  99. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
  100. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
  101. package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
  102. package/src/validation/compatibility-engine.ts +119 -24
  103. package/src/validation/compatibility-report.ts +1 -0
  104. package/src/validation/diagnostics.ts +1 -0
  105. package/src/validation/docx-comment-proof.ts +707 -0
@@ -8,6 +8,7 @@ import {
8
8
  expectExactString,
9
9
  expectIso8601UtcTimestamp,
10
10
  expectString,
11
+ expectStringAllowEmpty,
11
12
  expectUuid,
12
13
  stableStringify,
13
14
  } from "./cds-1.0.0.ts";
@@ -27,7 +28,7 @@ const CANONICAL_DOCUMENT_TOP_LEVEL_KEYS = [
27
28
  "diagnostics",
28
29
  ] as const;
29
30
 
30
- const CANONICAL_DOCUMENT_OPTIONAL_KEYS = ["subParts"] as const;
31
+ const CANONICAL_DOCUMENT_OPTIONAL_KEYS = ["subParts", "fieldRegistry"] as const;
31
32
 
32
33
  const ID_PATTERNS = {
33
34
  styleId: /^[A-Za-z_][A-Za-z0-9._-]{0,127}$/,
@@ -61,6 +62,8 @@ export interface CanonicalDocument {
61
62
  preservation: PreservationStore;
62
63
  diagnostics: DiagnosticStore;
63
64
  subParts?: SubPartsCatalog;
65
+ /** Package-backed field registry for supported field families. */
66
+ fieldRegistry?: FieldRegistry;
64
67
  }
65
68
 
66
69
  export interface DocumentMetadata {
@@ -79,12 +82,14 @@ export interface StylesCatalog {
79
82
  characters: Record<string, CharacterStyleDefinition>;
80
83
  tables: Record<string, TableStyleDefinition>;
81
84
  latentStyles?: Record<string, LatentStyleDefinition>;
85
+ fromPackage?: boolean;
82
86
  }
83
87
 
84
88
  export interface ParagraphStyleDefinition {
85
89
  styleId: string;
86
90
  basedOn?: string;
87
91
  nextStyle?: string;
92
+ outlineLevel?: number;
88
93
  displayName: string;
89
94
  kind: "paragraph";
90
95
  isDefault: boolean;
@@ -131,6 +136,8 @@ export interface NumberingLevelDefinition {
131
136
  text: string;
132
137
  startAt?: number;
133
138
  paragraphStyleId?: string;
139
+ isLegalNumbering?: boolean;
140
+ suffix?: "tab" | "space" | "nothing";
134
141
  }
135
142
 
136
143
  export interface NumberingInstance {
@@ -155,6 +162,9 @@ export interface MediaItem {
155
162
  relationshipId?: string;
156
163
  packagePartName: string;
157
164
  altText?: string;
165
+ display?: "inline" | "floating";
166
+ widthEmu?: number;
167
+ heightEmu?: number;
158
168
  }
159
169
 
160
170
  // ---- Sub-part canonical types ----
@@ -166,6 +176,7 @@ export interface HeaderDocument {
166
176
  partPath: string;
167
177
  relationshipId: string;
168
178
  blocks: BlockNode[];
179
+ sectionIndex?: number;
169
180
  }
170
181
 
171
182
  export interface FooterDocument {
@@ -173,6 +184,7 @@ export interface FooterDocument {
173
184
  partPath: string;
174
185
  relationshipId: string;
175
186
  blocks: BlockNode[];
187
+ sectionIndex?: number;
176
188
  }
177
189
 
178
190
  export interface FootnoteDefinition {
@@ -203,11 +215,25 @@ export interface ThemeDefinition {
203
215
  fontScheme?: ThemeFontScheme;
204
216
  }
205
217
 
218
+ export interface DocumentSettings {
219
+ evenAndOddHeaders?: boolean;
220
+ zoomLevel?: "pageWidth" | "onePage" | number;
221
+ }
222
+
206
223
  export interface SubPartsCatalog {
207
224
  headers: HeaderDocument[];
208
225
  footers: FooterDocument[];
209
226
  footnoteCollection?: FootnoteCollection;
210
227
  theme?: ThemeDefinition;
228
+ finalSectionProperties?: SectionProperties;
229
+ resolvedTheme?: ResolvedTheme;
230
+ settings?: DocumentSettings;
231
+ }
232
+
233
+ export interface ResolvedTheme {
234
+ colors: Record<string, string>;
235
+ majorFont?: string;
236
+ minorFont?: string;
211
237
  }
212
238
 
213
239
  // ---- Inline footnote reference node ----
@@ -305,6 +331,7 @@ export interface ParagraphNode {
305
331
  };
306
332
  alignment?: "left" | "center" | "right" | "both" | "distribute";
307
333
  spacing?: ParagraphSpacing;
334
+ contextualSpacing?: boolean;
308
335
  indentation?: ParagraphIndentation;
309
336
  tabStops?: TabStop[];
310
337
  keepNext?: boolean;
@@ -407,6 +434,23 @@ export interface TableCellNode {
407
434
  verticalAlign?: "top" | "center" | "bottom";
408
435
  }
409
436
 
437
+ export interface SdtCheckboxState {
438
+ checked: boolean;
439
+ checkedChar?: string;
440
+ uncheckedChar?: string;
441
+ }
442
+
443
+ export interface SdtDatePickerState {
444
+ fullDate?: string;
445
+ dateFormat?: string;
446
+ lid?: string;
447
+ }
448
+
449
+ export interface SdtDropdownListItem {
450
+ displayText?: string;
451
+ value: string;
452
+ }
453
+
410
454
  export interface SdtNode {
411
455
  type: "sdt";
412
456
  properties: {
@@ -415,6 +459,11 @@ export interface SdtNode {
415
459
  tag?: string;
416
460
  lock?: string;
417
461
  propertiesXml?: string;
462
+ checkbox?: SdtCheckboxState;
463
+ datePicker?: SdtDatePickerState;
464
+ dropdownList?: SdtDropdownListItem[];
465
+ comboBox?: SdtDropdownListItem[];
466
+ showingPlcHdr?: boolean;
418
467
  };
419
468
  children: BlockNode[];
420
469
  }
@@ -431,11 +480,119 @@ export interface AltChunkNode {
431
480
  relationshipId: string;
432
481
  }
433
482
 
483
+ /**
484
+ * Supported field families that receive first-class canonical treatment.
485
+ * These families have stable registry IDs, dependency metadata, and
486
+ * runtime-owned refresh behavior.
487
+ */
488
+ export type SupportedFieldFamily = "REF" | "PAGEREF" | "NOTEREF" | "TOC";
489
+
490
+ /**
491
+ * Unsupported field families that remain preserve-only.
492
+ * They survive round-trip but do not participate in runtime refresh.
493
+ */
494
+ export type PreserveOnlyFieldFamily =
495
+ | "PAGE"
496
+ | "NUMPAGES"
497
+ | "DATE"
498
+ | "TIME"
499
+ | "AUTHOR"
500
+ | "FILENAME"
501
+ | "MERGEFIELD"
502
+ | "IF"
503
+ | "SEQ"
504
+ | "INDEX"
505
+ | "TC"
506
+ | "STYLEREF"
507
+ | "FORMULA"
508
+ | "UNKNOWN";
509
+
510
+ export type FieldFamily = SupportedFieldFamily | PreserveOnlyFieldFamily;
511
+
512
+ /** Runtime refresh status for a field instance. */
513
+ export type FieldRefreshStatus =
514
+ | "current"
515
+ | "stale"
516
+ | "unresolvable"
517
+ | "preserve-only";
518
+
434
519
  export interface FieldNode {
435
520
  type: "field";
436
521
  fieldType: "simple" | "complex";
437
522
  instruction: string;
438
523
  children: InlineNode[];
524
+ /** Classified field family. Undefined for legacy snapshots. */
525
+ fieldFamily?: FieldFamily;
526
+ /** Target bookmark name for REF/PAGEREF/NOTEREF fields. */
527
+ fieldTarget?: string;
528
+ /** Runtime refresh status. Undefined for legacy or preserve-only fields. */
529
+ refreshStatus?: FieldRefreshStatus;
530
+ }
531
+
532
+ // ─── Field registry ─────────────────────────────────────────────────────────
533
+
534
+ /**
535
+ * Package-backed field registry that catalogs every field instance in the
536
+ * document, grouped by supported vs preserve-only families.
537
+ *
538
+ * Supported field entries carry dependency metadata (bookmark targets) and
539
+ * participate in deterministic refresh. Preserve-only entries survive
540
+ * round-trip but are not refreshable.
541
+ */
542
+ export interface FieldRegistry {
543
+ /** Supported field instances that participate in refresh. */
544
+ supported: FieldRegistryEntry[];
545
+ /** Preserve-only field instances cataloged for round-trip safety. */
546
+ preserveOnly: FieldRegistryEntry[];
547
+ /** Generated TOC structure extracted from heading-driven TOC fields. */
548
+ tocStructure?: TocStructure;
549
+ }
550
+
551
+ export interface FieldRegistryEntry {
552
+ /** Stable document-order index of this field (0-based). */
553
+ fieldIndex: number;
554
+ /** Classified field family. */
555
+ fieldFamily: FieldFamily;
556
+ /** Whether the field is in the supported refresh slice. */
557
+ supported: boolean;
558
+ /** Field instruction text. */
559
+ instruction: string;
560
+ /** Target bookmark name for REF/PAGEREF/NOTEREF fields. */
561
+ fieldTarget?: string;
562
+ /** Current display text extracted from field content. */
563
+ displayText: string;
564
+ /** Paragraph index in document order where this field appears. */
565
+ paragraphIndex: number;
566
+ /** Runtime refresh status. */
567
+ refreshStatus: FieldRefreshStatus;
568
+ }
569
+
570
+ /**
571
+ * Generated table-of-contents structure extracted from TOC fields and
572
+ * heading-styled paragraphs in the document.
573
+ */
574
+ export interface TocStructure {
575
+ /** The raw TOC field instruction (e.g. "TOC \\o \"1-3\" \\h"). */
576
+ instruction: string;
577
+ /** Heading level range the TOC covers. */
578
+ levelRange: { from: number; to: number };
579
+ /** Ordered TOC entries derived from heading paragraphs. */
580
+ entries: TocEntry[];
581
+ /** Whether the TOC content is current with the heading structure. */
582
+ status: "current" | "stale";
583
+ }
584
+
585
+ export interface TocEntry {
586
+ /** Heading text. */
587
+ text: string;
588
+ /** Heading outline level (1-9). */
589
+ level: number;
590
+ /** Paragraph index of the heading in document order. */
591
+ paragraphIndex: number;
592
+ /** Style ID of the heading paragraph, if available. */
593
+ styleId?: string;
594
+ /** Bookmark name anchoring this heading, if present. */
595
+ bookmarkName?: string;
439
596
  }
440
597
 
441
598
  export interface BookmarkStartNode {
@@ -451,7 +608,86 @@ export interface BookmarkEndNode {
451
608
 
452
609
  export interface SectionBreakNode {
453
610
  type: "section_break";
611
+ sectionPropertiesXml?: string;
612
+ /**
613
+ * @deprecated Legacy field from older snapshots. New exports should use
614
+ * sectionPropertiesXml and only contain raw <w:sectPr> content.
615
+ */
454
616
  propertiesXml?: string;
617
+ sectionProperties?: SectionProperties;
618
+ }
619
+
620
+ export interface SectionProperties {
621
+ pageSize?: PageSize;
622
+ pageMargins?: PageMargins;
623
+ columns?: ColumnProperties;
624
+ pageNumbering?: PageNumbering;
625
+ lineNumbering?: SectionLineNumbering;
626
+ pageBorders?: SectionPageBorders;
627
+ documentGrid?: SectionDocumentGrid;
628
+ headerReferences?: HeaderFooterReference[];
629
+ footerReferences?: HeaderFooterReference[];
630
+ sectionType?: "continuous" | "nextPage" | "evenPage" | "oddPage" | "nextColumn";
631
+ titlePage?: boolean;
632
+ }
633
+
634
+ export interface PageSize {
635
+ width: number;
636
+ height: number;
637
+ orientation?: "portrait" | "landscape";
638
+ }
639
+
640
+ export interface PageMargins {
641
+ top: number;
642
+ right: number;
643
+ bottom: number;
644
+ left: number;
645
+ header?: number;
646
+ footer?: number;
647
+ gutter?: number;
648
+ }
649
+
650
+ export interface ColumnProperties {
651
+ count?: number;
652
+ space?: number;
653
+ equalWidth?: boolean;
654
+ columns?: Array<{ width: number; space?: number }>;
655
+ separator?: boolean;
656
+ }
657
+
658
+ export interface PageNumbering {
659
+ format?: string;
660
+ start?: number;
661
+ chapStyle?: string;
662
+ chapSep?: string;
663
+ }
664
+
665
+ export interface SectionLineNumbering {
666
+ countBy?: number;
667
+ start?: number;
668
+ distance?: number;
669
+ restart?: "newPage" | "newSection" | "continuous";
670
+ }
671
+
672
+ export interface SectionPageBorders {
673
+ top?: BorderSpec;
674
+ left?: BorderSpec;
675
+ bottom?: BorderSpec;
676
+ right?: BorderSpec;
677
+ offsetFrom?: "page" | "text";
678
+ display?: "allPages" | "firstPage" | "notFirstPage";
679
+ zOrder?: "front" | "back";
680
+ }
681
+
682
+ export interface SectionDocumentGrid {
683
+ type?: "default" | "lines" | "linesAndChars" | "snapToChars";
684
+ linePitch?: number;
685
+ charSpace?: number;
686
+ }
687
+
688
+ export interface HeaderFooterReference {
689
+ variant: HeaderFooterVariant;
690
+ relationshipId: string;
455
691
  }
456
692
 
457
693
  export type InlineNode =
@@ -487,6 +723,7 @@ export type TextMark =
487
723
  | { type: "doubleStrikethrough" }
488
724
  | { type: "vanish" }
489
725
  | { type: "lang"; val: string }
726
+ | { type: "highlight"; color: string; val: string }
490
727
  | { type: "backgroundColor"; color: string }
491
728
  | { type: "charSpacing"; val: number }
492
729
  | { type: "kerning"; val: number }
@@ -587,6 +824,7 @@ export interface ShapeNode {
587
824
  type: "shape";
588
825
  text?: string;
589
826
  geometry?: string;
827
+ isTextBox?: boolean;
590
828
  rawXml: string;
591
829
  }
592
830
 
@@ -617,6 +855,7 @@ export interface OpaqueBlockNode {
617
855
  type: "opaque_block";
618
856
  fragmentId: string;
619
857
  warningId: string;
858
+ rawXml?: string;
620
859
  }
621
860
 
622
861
  export interface DocRange {
@@ -781,7 +1020,7 @@ export interface DiagnosticErrorEntry {
781
1020
  | "internal_invariant";
782
1021
  message: string;
783
1022
  isFatal: boolean;
784
- source: "import" | "runtime" | "validation" | "datastore" | "export";
1023
+ source: "import" | "runtime" | "validation" | "datastore" | "host" | "export";
785
1024
  details?: unknown;
786
1025
  }
787
1026
 
@@ -1146,6 +1385,9 @@ function validateDocumentNode(
1146
1385
  case "opaque_block":
1147
1386
  expectDomainString(record.fragmentId, "fragmentId", `${path}.fragmentId`, issues);
1148
1387
  expectDomainString(record.warningId, "warningId", `${path}.warningId`, issues);
1388
+ if (record.rawXml !== undefined) {
1389
+ expectString(record.rawXml, `${path}.rawXml`, issues);
1390
+ }
1149
1391
  return;
1150
1392
  case "table":
1151
1393
  if (!Array.isArray(record.gridColumns)) {
@@ -1343,7 +1585,11 @@ function validateReviewStore(
1343
1585
  );
1344
1586
  }
1345
1587
  if (threadRecord.body !== undefined) {
1346
- expectString(threadRecord.body, `${path}.comments.${commentId}.body`, issues);
1588
+ expectStringAllowEmpty(
1589
+ threadRecord.body,
1590
+ `${path}.comments.${commentId}.body`,
1591
+ issues,
1592
+ );
1347
1593
  }
1348
1594
  if (!Array.isArray(threadRecord.warningIds)) {
1349
1595
  issues.push({
@@ -1489,7 +1735,7 @@ function validateCommentEntries(
1489
1735
  }
1490
1736
  expectString(record.entryId, `${path}[${index}].entryId`, issues);
1491
1737
  expectString(record.authorId, `${path}[${index}].authorId`, issues);
1492
- expectString(record.body, `${path}[${index}].body`, issues);
1738
+ expectStringAllowEmpty(record.body, `${path}[${index}].body`, issues);
1493
1739
  expectIso8601UtcTimestamp(record.createdAt, `${path}[${index}].createdAt`, issues);
1494
1740
  if (record.metadata !== undefined) {
1495
1741
  validateCommentEntryMetadata(record.metadata, `${path}[${index}].metadata`, issues);
@@ -98,6 +98,19 @@ export function expectString(
98
98
  return value;
99
99
  }
100
100
 
101
+ export function expectStringAllowEmpty(
102
+ value: unknown,
103
+ path: string,
104
+ issues: ModelValidationIssue[],
105
+ ): string | null {
106
+ if (typeof value !== "string") {
107
+ issues.push({ path, message: "Expected a string." });
108
+ return null;
109
+ }
110
+
111
+ return value;
112
+ }
113
+
101
114
  export function expectExactString<T extends string>(
102
115
  value: unknown,
103
116
  expected: T,
@@ -62,7 +62,7 @@ export interface EditorError {
62
62
  code: EditorErrorCode;
63
63
  message: string;
64
64
  isFatal: boolean;
65
- source: "import" | "runtime" | "validation" | "datastore" | "export";
65
+ source: "import" | "runtime" | "validation" | "datastore" | "host" | "export";
66
66
  details?: Record<string, unknown>;
67
67
  }
68
68
 
@@ -110,6 +110,7 @@ export interface PersistedEditorSnapshot {
110
110
  canonicalDocument: CanonicalDocument;
111
111
  compatibility: CompatibilityReport;
112
112
  warningLog: EditorWarning[];
113
+ protectionSnapshot?: ProtectionSnapshotRecord;
113
114
  sourcePackage?: PersistedSourcePackage;
114
115
  }
115
116
 
@@ -127,7 +128,26 @@ const SNAPSHOT_REQUIRED_TOP_LEVEL_KEYS = [
127
128
  "warningLog",
128
129
  ] as const;
129
130
 
130
- const SNAPSHOT_OPTIONAL_TOP_LEVEL_KEYS = ["sourcePackage"] as const;
131
+ const SNAPSHOT_OPTIONAL_TOP_LEVEL_KEYS = ["sourcePackage", "protectionSnapshot"] as const;
132
+
133
+ export interface ProtectionRangeRecord {
134
+ rangeId: string;
135
+ start?: number;
136
+ end?: number;
137
+ editorGroup?: string;
138
+ editor?: string;
139
+ enforced: boolean;
140
+ enforcementReason: string;
141
+ }
142
+
143
+ export interface ProtectionSnapshotRecord {
144
+ hasDocumentProtection: boolean;
145
+ editType?: string;
146
+ enforcementActive: boolean;
147
+ ranges: ProtectionRangeRecord[];
148
+ enforcedRangeCount: number;
149
+ preservedRangeCount: number;
150
+ }
131
151
 
132
152
  const PERSISTED_EDITOR_SNAPSHOT_VERSIONS = new Set<PersistedEditorSnapshotVersion>([
133
153
  LEGACY_PERSISTED_EDITOR_SNAPSHOT_VERSION,
@@ -174,6 +194,7 @@ const EDITOR_ERROR_SOURCES = new Set<EditorError["source"]>([
174
194
  "runtime",
175
195
  "validation",
176
196
  "datastore",
197
+ "host",
177
198
  "export",
178
199
  ]);
179
200
 
@@ -259,6 +280,9 @@ export function validatePersistedEditorSnapshot(
259
280
  if (record.sourcePackage !== undefined) {
260
281
  validatePersistedSourcePackage(record.sourcePackage, "$.sourcePackage", issues);
261
282
  }
283
+ if (record.protectionSnapshot !== undefined) {
284
+ validateProtectionSnapshot(record.protectionSnapshot, "$.protectionSnapshot", issues);
285
+ }
262
286
 
263
287
  return issues;
264
288
  }
@@ -536,6 +560,7 @@ export function createPersistedEditorSnapshot(params: {
536
560
  canonicalDocument: CanonicalDocument;
537
561
  compatibility: CompatibilityReport;
538
562
  warningLog?: EditorWarning[];
563
+ protectionSnapshot?: ProtectionSnapshotRecord;
539
564
  sourcePackage?: PersistedSourcePackage;
540
565
  }): PersistedEditorSnapshot {
541
566
  assertCanonicalDocument(params.canonicalDocument);
@@ -553,10 +578,70 @@ export function createPersistedEditorSnapshot(params: {
553
578
  canonicalDocument: params.canonicalDocument,
554
579
  compatibility: params.compatibility,
555
580
  warningLog: params.warningLog ?? params.compatibility.warnings,
581
+ protectionSnapshot: params.protectionSnapshot,
556
582
  sourcePackage: params.sourcePackage,
557
583
  };
558
584
  }
559
585
 
586
+ function validateProtectionSnapshot(
587
+ value: unknown,
588
+ path: string,
589
+ issues: ModelValidationIssue[],
590
+ ): void {
591
+ const record = asPlainObject(value, path, issues);
592
+ if (!record) {
593
+ return;
594
+ }
595
+ if (typeof record.hasDocumentProtection !== "boolean") {
596
+ issues.push({ path: `${path}.hasDocumentProtection`, message: "hasDocumentProtection must be a boolean." });
597
+ }
598
+ if (record.editType !== undefined) {
599
+ expectString(record.editType, `${path}.editType`, issues);
600
+ }
601
+ if (typeof record.enforcementActive !== "boolean") {
602
+ issues.push({ path: `${path}.enforcementActive`, message: "enforcementActive must be a boolean." });
603
+ }
604
+ if (!Array.isArray(record.ranges)) {
605
+ issues.push({ path: `${path}.ranges`, message: "ranges must be an array." });
606
+ } else {
607
+ record.ranges.forEach((range, index) => validateProtectionRange(range, `${path}.ranges[${index}]`, issues));
608
+ }
609
+ if (!Number.isInteger(record.enforcedRangeCount)) {
610
+ issues.push({ path: `${path}.enforcedRangeCount`, message: "enforcedRangeCount must be an integer." });
611
+ }
612
+ if (!Number.isInteger(record.preservedRangeCount)) {
613
+ issues.push({ path: `${path}.preservedRangeCount`, message: "preservedRangeCount must be an integer." });
614
+ }
615
+ }
616
+
617
+ function validateProtectionRange(
618
+ value: unknown,
619
+ path: string,
620
+ issues: ModelValidationIssue[],
621
+ ): void {
622
+ const record = asPlainObject(value, path, issues);
623
+ if (!record) {
624
+ return;
625
+ }
626
+ expectString(record.rangeId, `${path}.rangeId`, issues);
627
+ if (record.start !== undefined && !Number.isInteger(record.start)) {
628
+ issues.push({ path: `${path}.start`, message: "start must be an integer." });
629
+ }
630
+ if (record.end !== undefined && !Number.isInteger(record.end)) {
631
+ issues.push({ path: `${path}.end`, message: "end must be an integer." });
632
+ }
633
+ if (record.editorGroup !== undefined) {
634
+ expectString(record.editorGroup, `${path}.editorGroup`, issues);
635
+ }
636
+ if (record.editor !== undefined) {
637
+ expectString(record.editor, `${path}.editor`, issues);
638
+ }
639
+ if (typeof record.enforced !== "boolean") {
640
+ issues.push({ path: `${path}.enforced`, message: "enforced must be a boolean." });
641
+ }
642
+ expectString(record.enforcementReason, `${path}.enforcementReason`, issues);
643
+ }
644
+
560
645
  export function projectAnchorToSnapshot(
561
646
  anchor: Record<string, unknown>,
562
647
  ): Record<string, unknown> {
@@ -44,6 +44,8 @@ export interface RevisionSidebarEntry {
44
44
  canAccept: boolean;
45
45
  canReject: boolean;
46
46
  preserveOnlyReason?: string;
47
+ linkedMoveRevisionId?: string;
48
+ moveDirection?: "from" | "to";
47
49
  }
48
50
 
49
51
  export interface RevisionSidebarProjection {
@@ -212,6 +214,8 @@ function toSidebarEntry(revision: RevisionRecord): RevisionSidebarEntry {
212
214
  const anchorSummary = summarizeRevisionAnchor(revision.anchor);
213
215
  const actionability = getRevisionActionability(revision);
214
216
 
217
+ const moveData = revision.metadata.moveData;
218
+
215
219
  return {
216
220
  revisionId: revision.revisionId,
217
221
  kind: revision.kind,
@@ -228,6 +232,8 @@ function toSidebarEntry(revision: RevisionRecord): RevisionSidebarEntry {
228
232
  canAccept: isRevisionActionable(revision),
229
233
  canReject: isRevisionActionable(revision),
230
234
  preserveOnlyReason: revision.metadata.preserveOnlyReason,
235
+ linkedMoveRevisionId: moveData?.linkedRevisionId,
236
+ moveDirection: moveData?.direction,
231
237
  };
232
238
  }
233
239
 
@@ -28,6 +28,7 @@ export interface PropertyChangeData {
28
28
  export interface MoveData {
29
29
  moveId: string;
30
30
  direction: "from" | "to";
31
+ linkedRevisionId?: string;
31
32
  }
32
33
 
33
34
  export interface RevisionMetadataEnvelope {