@beyondwork/docx-react-component 1.0.1 → 1.0.2

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 (172) hide show
  1. package/README.md +44 -104
  2. package/package.json +76 -46
  3. package/src/README.md +85 -0
  4. package/src/api/README.md +22 -0
  5. package/src/api/public-types.ts +525 -0
  6. package/src/compare/diff-engine.ts +530 -0
  7. package/src/compare/export-redlines.ts +162 -0
  8. package/src/compare/snapshot.ts +37 -0
  9. package/src/component-inventory.md +99 -0
  10. package/src/core/README.md +10 -0
  11. package/src/core/commands/README.md +3 -0
  12. package/src/core/commands/formatting-commands.ts +161 -0
  13. package/src/core/commands/image-commands.ts +144 -0
  14. package/src/core/commands/index.ts +1013 -0
  15. package/src/core/commands/list-commands.ts +370 -0
  16. package/src/core/commands/review-commands.ts +108 -0
  17. package/src/core/commands/text-commands.ts +119 -0
  18. package/src/core/schema/README.md +3 -0
  19. package/src/core/schema/text-schema.ts +512 -0
  20. package/src/core/selection/README.md +3 -0
  21. package/src/core/selection/mapping.ts +238 -0
  22. package/src/core/selection/review-anchors.ts +94 -0
  23. package/src/core/state/README.md +3 -0
  24. package/src/core/state/editor-state.ts +580 -0
  25. package/src/core/state/text-transaction.ts +276 -0
  26. package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
  27. package/src/formats/xlsx/io/parse-sheet.ts +289 -0
  28. package/src/formats/xlsx/io/parse-styles.ts +57 -0
  29. package/src/formats/xlsx/io/parse-workbook.ts +75 -0
  30. package/src/formats/xlsx/io/xlsx-session.ts +306 -0
  31. package/src/formats/xlsx/model/cell.ts +189 -0
  32. package/src/formats/xlsx/model/sheet.ts +244 -0
  33. package/src/formats/xlsx/model/styles.ts +118 -0
  34. package/src/formats/xlsx/model/workbook.ts +449 -0
  35. package/src/index.ts +45 -0
  36. package/src/io/README.md +10 -0
  37. package/src/io/docx-session.ts +1763 -0
  38. package/src/io/export/README.md +3 -0
  39. package/src/io/export/export-session.ts +165 -0
  40. package/src/io/export/minimal-docx.ts +115 -0
  41. package/src/io/export/reattach-preserved-parts.ts +54 -0
  42. package/src/io/export/serialize-comments.ts +876 -0
  43. package/src/io/export/serialize-footnotes.ts +217 -0
  44. package/src/io/export/serialize-headers-footers.ts +200 -0
  45. package/src/io/export/serialize-main-document.ts +982 -0
  46. package/src/io/export/serialize-numbering.ts +97 -0
  47. package/src/io/export/serialize-revisions.ts +389 -0
  48. package/src/io/export/serialize-runtime-revisions.ts +265 -0
  49. package/src/io/export/serialize-tables.ts +147 -0
  50. package/src/io/export/split-review-boundaries.ts +194 -0
  51. package/src/io/normalize/README.md +3 -0
  52. package/src/io/normalize/normalize-text.ts +437 -0
  53. package/src/io/ooxml/README.md +3 -0
  54. package/src/io/ooxml/parse-comments.ts +779 -0
  55. package/src/io/ooxml/parse-complex-content.ts +287 -0
  56. package/src/io/ooxml/parse-fields.ts +438 -0
  57. package/src/io/ooxml/parse-footnotes.ts +403 -0
  58. package/src/io/ooxml/parse-headers-footers.ts +483 -0
  59. package/src/io/ooxml/parse-inline-media.ts +431 -0
  60. package/src/io/ooxml/parse-main-document.ts +1846 -0
  61. package/src/io/ooxml/parse-numbering.ts +425 -0
  62. package/src/io/ooxml/parse-revisions.ts +658 -0
  63. package/src/io/ooxml/parse-shapes.ts +271 -0
  64. package/src/io/ooxml/parse-tables.ts +568 -0
  65. package/src/io/ooxml/parse-theme.ts +314 -0
  66. package/src/io/ooxml/part-manifest.ts +136 -0
  67. package/src/io/ooxml/revision-boundaries.ts +351 -0
  68. package/src/io/opc/README.md +3 -0
  69. package/src/io/opc/corrupt-package.ts +166 -0
  70. package/src/io/opc/docx-package.ts +74 -0
  71. package/src/io/opc/package-reader.ts +320 -0
  72. package/src/io/opc/package-writer.ts +273 -0
  73. package/src/legal/bookmarks.ts +196 -0
  74. package/src/legal/cross-references.ts +356 -0
  75. package/src/legal/defined-terms.ts +203 -0
  76. package/src/model/README.md +3 -0
  77. package/src/model/canonical-document.ts +1911 -0
  78. package/src/model/cds-1.0.0.ts +196 -0
  79. package/src/model/snapshot.ts +393 -0
  80. package/src/preservation/README.md +3 -0
  81. package/src/preservation/markup-compatibility.ts +48 -0
  82. package/src/preservation/opaque-fragment-store.ts +89 -0
  83. package/src/preservation/opaque-region.ts +233 -0
  84. package/src/preservation/package-preservation.ts +120 -0
  85. package/src/preservation/preserved-part-manifest.ts +56 -0
  86. package/src/preservation/relationship-retention.ts +57 -0
  87. package/src/preservation/store.ts +185 -0
  88. package/src/review/README.md +16 -0
  89. package/src/review/store/README.md +3 -0
  90. package/src/review/store/comment-anchors.ts +70 -0
  91. package/src/review/store/comment-remapping.ts +154 -0
  92. package/src/review/store/comment-store.ts +331 -0
  93. package/src/review/store/comment-thread.ts +109 -0
  94. package/src/review/store/revision-actions.ts +394 -0
  95. package/src/review/store/revision-store.ts +303 -0
  96. package/src/review/store/revision-types.ts +168 -0
  97. package/src/review/store/runtime-comment-store.ts +43 -0
  98. package/src/runtime/README.md +3 -0
  99. package/src/runtime/ai-action-policy.ts +764 -0
  100. package/src/runtime/document-runtime.ts +967 -0
  101. package/src/runtime/read-only-diagnostics-runtime.ts +232 -0
  102. package/src/runtime/review-runtime.ts +44 -0
  103. package/src/runtime/revision-runtime.ts +107 -0
  104. package/src/runtime/session-capabilities.ts +138 -0
  105. package/src/runtime/surface-projection.ts +570 -0
  106. package/src/runtime/table-commands.ts +87 -0
  107. package/src/runtime/table-schema.ts +140 -0
  108. package/src/runtime/virtualized-rendering.ts +258 -0
  109. package/src/ui/README.md +30 -0
  110. package/src/ui/WordReviewEditor.tsx +1504 -0
  111. package/src/ui/comments/README.md +3 -0
  112. package/src/ui/compatibility/README.md +3 -0
  113. package/src/ui/editor-surface/README.md +3 -0
  114. package/src/ui/headless/comment-decoration-model.ts +124 -0
  115. package/src/ui/headless/revision-decoration-model.ts +128 -0
  116. package/src/ui/headless/selection-helpers.ts +34 -0
  117. package/src/ui/headless/use-editor-keyboard.ts +98 -0
  118. package/src/ui/review/README.md +3 -0
  119. package/src/ui/shared/revision-filters.ts +31 -0
  120. package/src/ui/status/README.md +3 -0
  121. package/src/ui/theme/README.md +3 -0
  122. package/src/ui/toolbar/README.md +3 -0
  123. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +48 -0
  124. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +44 -0
  125. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
  126. package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
  127. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +139 -0
  128. package/src/ui-tailwind/editor-surface/pm-decorations.ts +98 -0
  129. package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
  130. package/src/ui-tailwind/editor-surface/pm-schema.ts +452 -0
  131. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +327 -0
  132. package/src/ui-tailwind/editor-surface/search-plugin.ts +157 -0
  133. package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
  134. package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
  135. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +118 -0
  136. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +52 -0
  137. package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
  138. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +215 -0
  139. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
  140. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +122 -0
  141. package/src/ui-tailwind/index.ts +61 -0
  142. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +276 -0
  143. package/src/ui-tailwind/review/tw-health-panel.tsx +120 -0
  144. package/src/ui-tailwind/review/tw-review-rail.tsx +120 -0
  145. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
  146. package/src/ui-tailwind/status/tw-status-bar.tsx +58 -0
  147. package/src/ui-tailwind/theme/editor-theme.css +190 -0
  148. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +48 -0
  149. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +231 -0
  150. package/src/ui-tailwind/tw-review-workspace.tsx +140 -0
  151. package/src/validation/README.md +3 -0
  152. package/src/validation/compatibility-engine.ts +317 -0
  153. package/src/validation/compatibility-report.ts +160 -0
  154. package/src/validation/diagnostics.ts +203 -0
  155. package/src/validation/import-diagnostics.ts +128 -0
  156. package/src/validation/low-priority-word-surfaces.ts +373 -0
  157. package/dist/chunk-32W6IVQE.js +0 -7725
  158. package/dist/chunk-32W6IVQE.js.map +0 -1
  159. package/dist/index.cjs +0 -23722
  160. package/dist/index.cjs.map +0 -1
  161. package/dist/index.d.cts +0 -7
  162. package/dist/index.d.ts +0 -7
  163. package/dist/index.js +0 -16011
  164. package/dist/index.js.map +0 -1
  165. package/dist/public-types-DqCURAz8.d.cts +0 -1152
  166. package/dist/public-types-DqCURAz8.d.ts +0 -1152
  167. package/dist/tailwind.cjs +0 -8295
  168. package/dist/tailwind.cjs.map +0 -1
  169. package/dist/tailwind.d.cts +0 -323
  170. package/dist/tailwind.d.ts +0 -323
  171. package/dist/tailwind.js +0 -553
  172. package/dist/tailwind.js.map +0 -1
@@ -0,0 +1,1911 @@
1
+ import {
2
+ CDS_SCHEMA_VERSION,
3
+ type ISO8601DateTime,
4
+ type ModelValidationIssue,
5
+ type UUID,
6
+ asPlainObject,
7
+ assertValid,
8
+ expectExactString,
9
+ expectIso8601UtcTimestamp,
10
+ expectString,
11
+ expectUuid,
12
+ stableStringify,
13
+ } from "./cds-1.0.0.ts";
14
+
15
+ const CANONICAL_DOCUMENT_TOP_LEVEL_KEYS = [
16
+ "schemaVersion",
17
+ "docId",
18
+ "createdAt",
19
+ "updatedAt",
20
+ "metadata",
21
+ "styles",
22
+ "numbering",
23
+ "media",
24
+ "content",
25
+ "review",
26
+ "preservation",
27
+ "diagnostics",
28
+ ] as const;
29
+
30
+ const CANONICAL_DOCUMENT_OPTIONAL_KEYS = ["subParts"] as const;
31
+
32
+ const ID_PATTERNS = {
33
+ styleId: /^[A-Za-z_][A-Za-z0-9._-]{0,127}$/,
34
+ abstractNumberingId: /^abstract-num:[A-Za-z0-9._-]{1,120}$/,
35
+ numberingInstanceId: /^num:[A-Za-z0-9._-]{1,120}$/,
36
+ mediaId: /^media:[A-Za-z0-9._/-]{1,120}$/,
37
+ commentId:
38
+ /^(?:comment:[A-Za-z0-9._-]{1,120}|comment-[A-Za-z0-9._-]{1,120}|[0-9]{1,18})$/,
39
+ revisionId:
40
+ /^(?:revision:[A-Za-z0-9._-]{1,120}|change-[A-Za-z0-9._-]{1,120})$/,
41
+ fragmentId: /^fragment:[A-Za-z0-9._-]{1,120}$/,
42
+ warningId: /^warning:[A-Za-z0-9._:-]{1,120}$/,
43
+ diagnosticId: /^diagnostic:[A-Za-z0-9._-]{1,120}$/,
44
+ packagePartName: /^\/[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/,
45
+ relationshipId: /^rId[A-Za-z0-9._-]{1,120}$/,
46
+ } as const;
47
+
48
+ type StableIdDomain = keyof typeof ID_PATTERNS;
49
+
50
+ export interface CanonicalDocument {
51
+ schemaVersion: typeof CDS_SCHEMA_VERSION;
52
+ docId: UUID;
53
+ createdAt: ISO8601DateTime;
54
+ updatedAt: ISO8601DateTime;
55
+ metadata: DocumentMetadata;
56
+ styles: StylesCatalog;
57
+ numbering: NumberingCatalog;
58
+ media: MediaCatalog;
59
+ content: DocumentNode;
60
+ review: ReviewStore;
61
+ preservation: PreservationStore;
62
+ diagnostics: DiagnosticStore;
63
+ subParts?: SubPartsCatalog;
64
+ }
65
+
66
+ export interface DocumentMetadata {
67
+ title?: string;
68
+ subject?: string;
69
+ description?: string;
70
+ language?: string;
71
+ keywords?: string[];
72
+ category?: string;
73
+ customProperties: Record<string, string>;
74
+ }
75
+
76
+ export interface StylesCatalog {
77
+ paragraphs: Record<string, ParagraphStyleDefinition>;
78
+ characters: Record<string, CharacterStyleDefinition>;
79
+ tables: Record<string, TableStyleDefinition>;
80
+ latentStyles?: Record<string, LatentStyleDefinition>;
81
+ }
82
+
83
+ export interface ParagraphStyleDefinition {
84
+ styleId: string;
85
+ basedOn?: string;
86
+ nextStyle?: string;
87
+ displayName: string;
88
+ kind: "paragraph";
89
+ isDefault: boolean;
90
+ }
91
+
92
+ export interface CharacterStyleDefinition {
93
+ styleId: string;
94
+ basedOn?: string;
95
+ displayName: string;
96
+ kind: "character";
97
+ isDefault: boolean;
98
+ }
99
+
100
+ export interface TableStyleDefinition {
101
+ styleId: string;
102
+ basedOn?: string;
103
+ displayName: string;
104
+ kind: "table";
105
+ isDefault: boolean;
106
+ }
107
+
108
+ export interface LatentStyleDefinition {
109
+ name: string;
110
+ locked?: boolean;
111
+ semiHidden?: boolean;
112
+ unhideWhenUsed?: boolean;
113
+ qFormat?: boolean;
114
+ uiPriority?: number;
115
+ }
116
+
117
+ export interface NumberingCatalog {
118
+ abstractDefinitions: Record<string, AbstractNumberingDefinition>;
119
+ instances: Record<string, NumberingInstance>;
120
+ }
121
+
122
+ export interface AbstractNumberingDefinition {
123
+ abstractNumberingId: string;
124
+ levels: NumberingLevelDefinition[];
125
+ }
126
+
127
+ export interface NumberingLevelDefinition {
128
+ level: number;
129
+ format: string;
130
+ text: string;
131
+ startAt?: number;
132
+ paragraphStyleId?: string;
133
+ }
134
+
135
+ export interface NumberingInstance {
136
+ numberingInstanceId: string;
137
+ abstractNumberingId: string;
138
+ overrides: NumberingLevelOverride[];
139
+ }
140
+
141
+ export interface NumberingLevelOverride {
142
+ level: number;
143
+ startAt?: number;
144
+ }
145
+
146
+ export interface MediaCatalog {
147
+ items: Record<string, MediaItem>;
148
+ }
149
+
150
+ export interface MediaItem {
151
+ mediaId: string;
152
+ contentType: string;
153
+ filename: string;
154
+ relationshipId?: string;
155
+ packagePartName: string;
156
+ altText?: string;
157
+ }
158
+
159
+ // ---- Sub-part canonical types ----
160
+
161
+ export type HeaderFooterVariant = "default" | "first" | "even";
162
+
163
+ export interface HeaderDocument {
164
+ variant: HeaderFooterVariant;
165
+ partPath: string;
166
+ relationshipId: string;
167
+ blocks: BlockNode[];
168
+ }
169
+
170
+ export interface FooterDocument {
171
+ variant: HeaderFooterVariant;
172
+ partPath: string;
173
+ relationshipId: string;
174
+ blocks: BlockNode[];
175
+ }
176
+
177
+ export interface FootnoteDefinition {
178
+ noteId: string;
179
+ kind: "footnote" | "endnote";
180
+ blocks: BlockNode[];
181
+ }
182
+
183
+ export interface FootnoteCollection {
184
+ footnotes: Record<string, FootnoteDefinition>;
185
+ endnotes: Record<string, FootnoteDefinition>;
186
+ }
187
+
188
+ export interface ThemeColorScheme {
189
+ name: string;
190
+ colors: Record<string, string>;
191
+ }
192
+
193
+ export interface ThemeFontScheme {
194
+ name: string;
195
+ majorFont?: string;
196
+ minorFont?: string;
197
+ }
198
+
199
+ export interface ThemeDefinition {
200
+ name?: string;
201
+ colorScheme?: ThemeColorScheme;
202
+ fontScheme?: ThemeFontScheme;
203
+ }
204
+
205
+ export interface SubPartsCatalog {
206
+ headers: HeaderDocument[];
207
+ footers: FooterDocument[];
208
+ footnoteCollection?: FootnoteCollection;
209
+ theme?: ThemeDefinition;
210
+ }
211
+
212
+ // ---- Inline footnote reference node ----
213
+
214
+ export interface FootnoteRefNode {
215
+ type: "footnote_ref";
216
+ noteId: string;
217
+ noteKind: "footnote" | "endnote";
218
+ }
219
+
220
+ export type DocumentNode =
221
+ | DocumentRootNode
222
+ | ParagraphNode
223
+ | TableNode
224
+ | TableRowNode
225
+ | TableCellNode
226
+ | SdtNode
227
+ | CustomXmlNode
228
+ | AltChunkNode
229
+ | TextNode
230
+ | HardBreakNode
231
+ | TabNode
232
+ | ColumnBreakNode
233
+ | SymbolNode
234
+ | HyperlinkNode
235
+ | ImageNode
236
+ | FieldNode
237
+ | BookmarkStartNode
238
+ | BookmarkEndNode
239
+ | SectionBreakNode
240
+ | OpaqueInlineNode
241
+ | OpaqueBlockNode
242
+ | FootnoteRefNode
243
+ | ChartPreviewNode
244
+ | SmartArtPreviewNode
245
+ | ShapeNode
246
+ | WordArtNode
247
+ | VmlShapeNode;
248
+
249
+ export interface DocumentRootNode {
250
+ type: "doc";
251
+ children: BlockNode[];
252
+ }
253
+
254
+ export type BlockNode =
255
+ | ParagraphNode
256
+ | TableNode
257
+ | SdtNode
258
+ | CustomXmlNode
259
+ | AltChunkNode
260
+ | SectionBreakNode
261
+ | OpaqueBlockNode;
262
+
263
+ export interface ParagraphSpacing {
264
+ before?: number;
265
+ after?: number;
266
+ line?: number;
267
+ lineRule?: "auto" | "exact" | "atLeast";
268
+ }
269
+
270
+ export interface ParagraphIndentation {
271
+ left?: number;
272
+ right?: number;
273
+ firstLine?: number;
274
+ hanging?: number;
275
+ }
276
+
277
+ export interface TabStop {
278
+ position: number;
279
+ align: "left" | "center" | "right" | "decimal" | "bar" | "clear";
280
+ leader?: "none" | "dot" | "hyphen" | "underscore" | "heavy" | "middleDot";
281
+ }
282
+
283
+ export interface ParagraphBorders {
284
+ top?: BorderSpec;
285
+ left?: BorderSpec;
286
+ bottom?: BorderSpec;
287
+ right?: BorderSpec;
288
+ bar?: BorderSpec;
289
+ between?: BorderSpec;
290
+ }
291
+
292
+ export interface ParagraphShading {
293
+ fill?: string;
294
+ color?: string;
295
+ val?: string;
296
+ }
297
+
298
+ export interface ParagraphNode {
299
+ type: "paragraph";
300
+ styleId?: string;
301
+ numbering?: {
302
+ numberingInstanceId: string;
303
+ level: number;
304
+ };
305
+ alignment?: "left" | "center" | "right" | "both" | "distribute";
306
+ spacing?: ParagraphSpacing;
307
+ indentation?: ParagraphIndentation;
308
+ tabStops?: TabStop[];
309
+ keepNext?: boolean;
310
+ keepLines?: boolean;
311
+ outlineLevel?: number;
312
+ pageBreakBefore?: boolean;
313
+ widowControl?: boolean;
314
+ borders?: ParagraphBorders;
315
+ shading?: ParagraphShading;
316
+ bidi?: boolean;
317
+ suppressLineNumbers?: boolean;
318
+ cnfStyle?: string;
319
+ children: InlineNode[];
320
+ }
321
+
322
+ export interface BorderSpec {
323
+ value?: string;
324
+ size?: number;
325
+ space?: number;
326
+ color?: string;
327
+ }
328
+
329
+ export interface TableBorders {
330
+ top?: BorderSpec;
331
+ left?: BorderSpec;
332
+ bottom?: BorderSpec;
333
+ right?: BorderSpec;
334
+ insideH?: BorderSpec;
335
+ insideV?: BorderSpec;
336
+ }
337
+
338
+ export interface TableCellBorders {
339
+ top?: BorderSpec;
340
+ left?: BorderSpec;
341
+ bottom?: BorderSpec;
342
+ right?: BorderSpec;
343
+ insideH?: BorderSpec;
344
+ insideV?: BorderSpec;
345
+ }
346
+
347
+ export interface TableWidth {
348
+ value: number;
349
+ type: "dxa" | "auto" | "pct" | "nil";
350
+ }
351
+
352
+ export interface CellShading {
353
+ fill?: string;
354
+ color?: string;
355
+ val?: string;
356
+ }
357
+
358
+ export interface TableCellMargins {
359
+ top?: number;
360
+ left?: number;
361
+ bottom?: number;
362
+ right?: number;
363
+ }
364
+
365
+ export interface TableLook {
366
+ val?: string;
367
+ firstRow?: boolean;
368
+ lastRow?: boolean;
369
+ firstColumn?: boolean;
370
+ lastColumn?: boolean;
371
+ noHBand?: boolean;
372
+ noVBand?: boolean;
373
+ }
374
+
375
+ export interface TableNode {
376
+ type: "table";
377
+ styleId?: string;
378
+ propertiesXml?: string;
379
+ gridColumns: number[];
380
+ rows: TableRowNode[];
381
+ width?: TableWidth;
382
+ alignment?: "left" | "center" | "right";
383
+ borders?: TableBorders;
384
+ cellMargins?: TableCellMargins;
385
+ tblLook?: TableLook;
386
+ }
387
+
388
+ export interface TableRowNode {
389
+ type: "table_row";
390
+ propertiesXml?: string;
391
+ cells: TableCellNode[];
392
+ height?: number;
393
+ heightRule?: "auto" | "atLeast" | "exact";
394
+ isHeader?: boolean;
395
+ }
396
+
397
+ export interface TableCellNode {
398
+ type: "table_cell";
399
+ propertiesXml?: string;
400
+ gridSpan?: number;
401
+ verticalMerge?: "restart" | "continue";
402
+ children: BlockNode[];
403
+ width?: TableWidth;
404
+ borders?: TableCellBorders;
405
+ shading?: CellShading;
406
+ verticalAlign?: "top" | "center" | "bottom";
407
+ }
408
+
409
+ export interface SdtNode {
410
+ type: "sdt";
411
+ properties: {
412
+ sdtType?: string;
413
+ alias?: string;
414
+ tag?: string;
415
+ lock?: string;
416
+ propertiesXml?: string;
417
+ };
418
+ children: BlockNode[];
419
+ }
420
+
421
+ export interface CustomXmlNode {
422
+ type: "custom_xml";
423
+ uri?: string;
424
+ element?: string;
425
+ children: BlockNode[];
426
+ }
427
+
428
+ export interface AltChunkNode {
429
+ type: "alt_chunk";
430
+ relationshipId: string;
431
+ }
432
+
433
+ export interface FieldNode {
434
+ type: "field";
435
+ fieldType: "simple" | "complex";
436
+ instruction: string;
437
+ children: InlineNode[];
438
+ }
439
+
440
+ export interface BookmarkStartNode {
441
+ type: "bookmark_start";
442
+ bookmarkId: string;
443
+ name: string;
444
+ }
445
+
446
+ export interface BookmarkEndNode {
447
+ type: "bookmark_end";
448
+ bookmarkId: string;
449
+ }
450
+
451
+ export interface SectionBreakNode {
452
+ type: "section_break";
453
+ propertiesXml?: string;
454
+ }
455
+
456
+ export type InlineNode =
457
+ | TextNode
458
+ | HardBreakNode
459
+ | TabNode
460
+ | HyperlinkNode
461
+ | ImageNode
462
+ | FieldNode
463
+ | BookmarkStartNode
464
+ | BookmarkEndNode
465
+ | OpaqueInlineNode
466
+ | FootnoteRefNode
467
+ | ChartPreviewNode
468
+ | SmartArtPreviewNode
469
+ | ShapeNode
470
+ | WordArtNode
471
+ | VmlShapeNode;
472
+
473
+ export interface TextNode {
474
+ type: "text";
475
+ text: string;
476
+ marks?: TextMark[];
477
+ }
478
+
479
+ export type TextMark =
480
+ | { type: "bold" }
481
+ | { type: "italic" }
482
+ | { type: "underline" }
483
+ | { type: "strikethrough" }
484
+ | { type: "doubleStrikethrough" }
485
+ | { type: "vanish" }
486
+ | { type: "lang"; val: string }
487
+ | { type: "backgroundColor"; color: string }
488
+ | { type: "charSpacing"; val: number }
489
+ | { type: "kerning"; val: number }
490
+ | { type: "emboss" }
491
+ | { type: "imprint" }
492
+ | { type: "shadow" }
493
+ | { type: "position"; val: number }
494
+ | { type: "textFill"; xml: string };
495
+
496
+ export interface HardBreakNode {
497
+ type: "hard_break";
498
+ }
499
+
500
+ export interface ColumnBreakNode {
501
+ type: "column_break";
502
+ }
503
+
504
+ export interface TabNode {
505
+ type: "tab";
506
+ }
507
+
508
+ export interface SymbolNode {
509
+ type: "symbol";
510
+ char: string;
511
+ font?: string;
512
+ marks?: TextMark[];
513
+ }
514
+
515
+ export interface HyperlinkNode {
516
+ type: "hyperlink";
517
+ href: string;
518
+ children: Array<TextNode | HardBreakNode | ColumnBreakNode | TabNode | SymbolNode>;
519
+ }
520
+
521
+ export interface ImageNode {
522
+ type: "image";
523
+ mediaId: string;
524
+ altText?: string;
525
+ placementXml?: string;
526
+ display?: "inline" | "floating";
527
+ floating?: FloatingImageProperties;
528
+ }
529
+
530
+ export interface FloatingImageProperties {
531
+ horizontalPosition?: FloatingAxisPosition;
532
+ verticalPosition?: FloatingAxisPosition;
533
+ wrap?: "none" | "square" | "tight" | "through" | "topAndBottom";
534
+ behindDoc?: boolean;
535
+ layoutInCell?: boolean;
536
+ allowOverlap?: boolean;
537
+ }
538
+
539
+ export interface FloatingAxisPosition {
540
+ relativeFrom?: string;
541
+ align?: string;
542
+ offset?: number;
543
+ }
544
+
545
+ export interface OpaqueInlineNode {
546
+ type: "opaque_inline";
547
+ fragmentId: string;
548
+ warningId: string;
549
+ }
550
+
551
+ // ---- Complex rendering inline nodes (read-only previews) ----
552
+
553
+ /**
554
+ * Read-only preview of a chart (c:chart). The original drawing XML is stored in
555
+ * rawXml for lossless round-trip export. If a fallback image was present in
556
+ * mc:AlternateContent it is referenced by previewMediaId.
557
+ */
558
+ export interface ChartPreviewNode {
559
+ type: "chart_preview";
560
+ previewMediaId?: string;
561
+ rawXml: string;
562
+ }
563
+
564
+ /**
565
+ * Read-only preview of a SmartArt diagram (dgm:*). The original drawing XML is
566
+ * stored in rawXml for lossless round-trip export.
567
+ */
568
+ export interface SmartArtPreviewNode {
569
+ type: "smartart_preview";
570
+ previewMediaId?: string;
571
+ rawXml: string;
572
+ }
573
+
574
+ /**
575
+ * Read-only rendering of a wps:wsp WordprocessingShape. Text content is
576
+ * extracted for display. The original drawing XML is preserved in rawXml.
577
+ */
578
+ export interface ShapeNode {
579
+ type: "shape";
580
+ text?: string;
581
+ geometry?: string;
582
+ rawXml: string;
583
+ }
584
+
585
+ /**
586
+ * Read-only rendering of WordArt — a wps:wsp shape with a text-geometry preset.
587
+ * Text is extracted for display. The original drawing XML is preserved in rawXml.
588
+ */
589
+ export interface WordArtNode {
590
+ type: "wordart";
591
+ text: string;
592
+ geometry?: string;
593
+ rawXml: string;
594
+ }
595
+
596
+ /**
597
+ * Read-only rendering of a VML shape (v:shape, v:rect, v:textbox) from a w:pict
598
+ * element. Text is extracted for display. The original w:pict XML is preserved
599
+ * in rawXml for lossless round-trip export.
600
+ */
601
+ export interface VmlShapeNode {
602
+ type: "vml_shape";
603
+ text?: string;
604
+ shapeType?: string;
605
+ rawXml: string;
606
+ }
607
+
608
+ export interface OpaqueBlockNode {
609
+ type: "opaque_block";
610
+ fragmentId: string;
611
+ warningId: string;
612
+ }
613
+
614
+ export interface DocRange {
615
+ from: number;
616
+ to: number;
617
+ }
618
+
619
+ export interface BoundaryAssoc {
620
+ start: -1 | 1;
621
+ end: -1 | 1;
622
+ }
623
+
624
+ export type CanonicalAnchor =
625
+ | {
626
+ kind: "range";
627
+ range: DocRange;
628
+ assoc: BoundaryAssoc;
629
+ }
630
+ | {
631
+ kind: "node";
632
+ at: number;
633
+ assoc: -1 | 1;
634
+ }
635
+ | {
636
+ kind: "detached";
637
+ lastKnownRange: DocRange;
638
+ reason: "deleted" | "invalidatedByStructureChange" | "importAmbiguity";
639
+ };
640
+
641
+ export interface ReviewStore {
642
+ comments: Record<string, CommentThread>;
643
+ revisions: Record<string, RevisionRecord>;
644
+ }
645
+
646
+ export interface CommentThread {
647
+ commentId: string;
648
+ status: "open" | "resolved" | "detached";
649
+ anchor: CanonicalAnchor;
650
+ createdAt: ISO8601DateTime;
651
+ createdBy?: string;
652
+ authorId?: string;
653
+ body?: string;
654
+ entries?: CommentEntry[];
655
+ resolution?: CommentResolution;
656
+ resolvedAt?: ISO8601DateTime;
657
+ warningIds: string[];
658
+ isResolved?: boolean;
659
+ metadata?: CommentThreadMetadata;
660
+ }
661
+
662
+ export interface CommentEntry {
663
+ entryId: string;
664
+ authorId: string;
665
+ createdAt: ISO8601DateTime;
666
+ body: string;
667
+ metadata?: CommentEntryMetadata;
668
+ }
669
+
670
+ export interface CommentEntryMetadata {
671
+ ooxmlCommentId?: string;
672
+ paraId?: string;
673
+ parentParaId?: string;
674
+ durableId?: string;
675
+ initials?: string;
676
+ }
677
+
678
+ export interface CommentResolution {
679
+ resolvedAt: ISO8601DateTime;
680
+ resolvedBy: string;
681
+ }
682
+
683
+ export interface CommentThreadMetadata {
684
+ source?: "runtime" | "import";
685
+ rootOoxmlCommentId?: string;
686
+ rootParaId?: string;
687
+ }
688
+
689
+ export interface RevisionPropertyChangeData {
690
+ xmlTag: "pPrChange" | "sectPrChange" | "tblPrChange" | "rPrChange";
691
+ beforeXml: string;
692
+ }
693
+
694
+ export interface RevisionMoveData {
695
+ moveId: string;
696
+ direction: "from" | "to";
697
+ }
698
+
699
+ export interface RevisionRecord {
700
+ changeId: string;
701
+ kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
702
+ anchor: CanonicalAnchor;
703
+ authorId?: string;
704
+ createdAt: ISO8601DateTime;
705
+ warningIds?: string[];
706
+ metadata?: RevisionMetadataRecord;
707
+ status: "open" | "accepted" | "rejected" | "detached";
708
+ }
709
+
710
+ export interface RevisionMetadataRecord {
711
+ source?: "runtime" | "import";
712
+ preserveOnlyReason?: string;
713
+ importedRevisionForm?:
714
+ | "run-insertion"
715
+ | "run-deletion"
716
+ | "paragraph-insertion"
717
+ | "paragraph-deletion";
718
+ originalRevisionType?: string;
719
+ ooxmlRevisionId?: string;
720
+ propertyChangeData?: RevisionPropertyChangeData;
721
+ moveData?: RevisionMoveData;
722
+ }
723
+
724
+ export interface PreservationStore {
725
+ opaqueFragments: Record<string, OpaqueFragmentRecord>;
726
+ packageParts: Record<string, PreservedPackagePart>;
727
+ }
728
+
729
+ export interface OpaqueFragmentRecord {
730
+ fragmentId: string;
731
+ payloadKind: "xml-subtree" | "package-part";
732
+ payloadReference: string;
733
+ featureClass: "preserve-only";
734
+ lastKnownRange: DocRange;
735
+ warningId: string;
736
+ packagePartName?: string;
737
+ relationshipId?: string;
738
+ }
739
+
740
+ export interface PreservedPackagePart {
741
+ packagePartName: string;
742
+ contentType: string;
743
+ relationshipIds: string[];
744
+ }
745
+
746
+ export interface DiagnosticStore {
747
+ warnings: DiagnosticWarningEntry[];
748
+ errors: DiagnosticErrorEntry[];
749
+ }
750
+
751
+ export interface DiagnosticWarningEntry {
752
+ diagnosticId: string;
753
+ warningId: string;
754
+ source:
755
+ | "import"
756
+ | "runtime"
757
+ | "review"
758
+ | "preservation"
759
+ | "validation"
760
+ | "export";
761
+ message: string;
762
+ }
763
+
764
+ export interface DiagnosticErrorEntry {
765
+ diagnosticId: string;
766
+ code:
767
+ | "load_failed"
768
+ | "export_failed"
769
+ | "package_corrupt"
770
+ | "validation_failed"
771
+ | "datastore_failed"
772
+ | "internal_invariant";
773
+ message: string;
774
+ isFatal: boolean;
775
+ source: "import" | "runtime" | "validation" | "datastore" | "export";
776
+ }
777
+
778
+ export function createCanonicalDocument(
779
+ input: Omit<CanonicalDocument, "schemaVersion">,
780
+ ): CanonicalDocument {
781
+ const document: CanonicalDocument = {
782
+ schemaVersion: CDS_SCHEMA_VERSION,
783
+ ...input,
784
+ };
785
+
786
+ assertCanonicalDocument(document);
787
+ return document;
788
+ }
789
+
790
+ export function serializeCanonicalDocument(document: CanonicalDocument): string {
791
+ assertCanonicalDocument(document);
792
+ return stableStringify(document);
793
+ }
794
+
795
+ export function createCanonicalDocumentSignature(document: unknown): string {
796
+ return stableStringify(document);
797
+ }
798
+
799
+ export function parseCanonicalDocument(json: string): CanonicalDocument {
800
+ const parsed = JSON.parse(json) as unknown;
801
+ assertCanonicalDocument(parsed);
802
+ return parsed;
803
+ }
804
+
805
+ export function projectCanonicalDocument(
806
+ document: CanonicalDocument,
807
+ ): CanonicalDocument {
808
+ assertCanonicalDocument(document);
809
+ return JSON.parse(stableStringify(document)) as CanonicalDocument;
810
+ }
811
+
812
+ export function assertCanonicalDocument(
813
+ value: unknown,
814
+ ): asserts value is CanonicalDocument {
815
+ const issues = validateCanonicalDocument(value);
816
+ assertValid(issues, "Invalid canonical document.");
817
+ }
818
+
819
+ export function validateCanonicalDocument(
820
+ value: unknown,
821
+ ): ModelValidationIssue[] {
822
+ const issues: ModelValidationIssue[] = [];
823
+ const record = asPlainObject(value, "$", issues);
824
+ if (!record) {
825
+ return issues;
826
+ }
827
+
828
+ validateExactObjectKeys(record, CANONICAL_DOCUMENT_TOP_LEVEL_KEYS, "$", issues, CANONICAL_DOCUMENT_OPTIONAL_KEYS);
829
+ expectExactString(record.schemaVersion, CDS_SCHEMA_VERSION, "$.schemaVersion", issues);
830
+ expectUuid(record.docId, "$.docId", issues);
831
+ expectIso8601UtcTimestamp(record.createdAt, "$.createdAt", issues);
832
+ expectIso8601UtcTimestamp(record.updatedAt, "$.updatedAt", issues);
833
+
834
+ validateMetadata(record.metadata, "$.metadata", issues);
835
+ validateStylesCatalog(record.styles, "$.styles", issues);
836
+ validateNumberingCatalog(record.numbering, "$.numbering", issues);
837
+ validateMediaCatalog(record.media, "$.media", issues);
838
+ validateDocumentNode(record.content, "$.content", issues);
839
+ validateReviewStore(record.review, "$.review", issues);
840
+ validatePreservationStore(record.preservation, "$.preservation", issues);
841
+ validateDiagnosticStore(record.diagnostics, "$.diagnostics", issues);
842
+ if (record.subParts !== undefined) {
843
+ validateSubPartsCatalog(record.subParts, "$.subParts", issues);
844
+ }
845
+
846
+ return issues;
847
+ }
848
+
849
+ function validateMetadata(
850
+ value: unknown,
851
+ path: string,
852
+ issues: ModelValidationIssue[],
853
+ ): void {
854
+ const record = asPlainObject(value, path, issues);
855
+ if (!record) {
856
+ return;
857
+ }
858
+
859
+ const customProperties = asPlainObject(record.customProperties, `${path}.customProperties`, issues);
860
+ if (!customProperties) {
861
+ return;
862
+ }
863
+
864
+ for (const [propertyKey, propertyValue] of Object.entries(customProperties)) {
865
+ if (typeof propertyValue !== "string") {
866
+ issues.push({
867
+ path: `${path}.customProperties.${propertyKey}`,
868
+ message: "customProperties values must be strings.",
869
+ });
870
+ }
871
+ }
872
+ }
873
+
874
+ function validateStylesCatalog(
875
+ value: unknown,
876
+ path: string,
877
+ issues: ModelValidationIssue[],
878
+ ): void {
879
+ const record = asPlainObject(value, path, issues);
880
+ if (!record) {
881
+ return;
882
+ }
883
+
884
+ validateStyleMap(record.paragraphs, `${path}.paragraphs`, issues);
885
+ validateStyleMap(record.characters, `${path}.characters`, issues);
886
+ validateStyleMap(record.tables, `${path}.tables`, issues);
887
+ if (record.latentStyles !== undefined) {
888
+ validateLatentStyleMap(record.latentStyles, `${path}.latentStyles`, issues);
889
+ }
890
+ }
891
+
892
+ function validateStyleMap(
893
+ value: unknown,
894
+ path: string,
895
+ issues: ModelValidationIssue[],
896
+ ): void {
897
+ const record = asPlainObject(value, path, issues);
898
+ if (!record) {
899
+ return;
900
+ }
901
+
902
+ for (const [styleId, definition] of Object.entries(record)) {
903
+ expectDomainString(styleId, "styleId", `${path}.${styleId}`, issues);
904
+ const definitionRecord = asPlainObject(definition, `${path}.${styleId}`, issues);
905
+ if (!definitionRecord) {
906
+ continue;
907
+ }
908
+ if (definitionRecord.styleId !== styleId) {
909
+ issues.push({
910
+ path: `${path}.${styleId}.styleId`,
911
+ message: "styleId must match the map key.",
912
+ });
913
+ }
914
+ }
915
+ }
916
+
917
+ function validateLatentStyleMap(
918
+ value: unknown,
919
+ path: string,
920
+ issues: ModelValidationIssue[],
921
+ ): void {
922
+ const record = asPlainObject(value, path, issues);
923
+ if (!record) {
924
+ return;
925
+ }
926
+
927
+ for (const [styleName, definition] of Object.entries(record)) {
928
+ const definitionRecord = asPlainObject(definition, `${path}.${styleName}`, issues);
929
+ if (!definitionRecord) {
930
+ continue;
931
+ }
932
+ if (definitionRecord.name !== styleName) {
933
+ issues.push({
934
+ path: `${path}.${styleName}.name`,
935
+ message: "name must match the map key.",
936
+ });
937
+ }
938
+ if (definitionRecord.uiPriority !== undefined && typeof definitionRecord.uiPriority !== "number") {
939
+ issues.push({
940
+ path: `${path}.${styleName}.uiPriority`,
941
+ message: "uiPriority must be a number.",
942
+ });
943
+ }
944
+ }
945
+ }
946
+
947
+ function validateNumberingCatalog(
948
+ value: unknown,
949
+ path: string,
950
+ issues: ModelValidationIssue[],
951
+ ): void {
952
+ const record = asPlainObject(value, path, issues);
953
+ if (!record) {
954
+ return;
955
+ }
956
+
957
+ const abstractDefinitions = asPlainObject(
958
+ record.abstractDefinitions,
959
+ `${path}.abstractDefinitions`,
960
+ issues,
961
+ );
962
+ if (abstractDefinitions) {
963
+ for (const [abstractId, definition] of Object.entries(abstractDefinitions)) {
964
+ expectDomainString(
965
+ abstractId,
966
+ "abstractNumberingId",
967
+ `${path}.abstractDefinitions.${abstractId}`,
968
+ issues,
969
+ );
970
+ const definitionRecord = asPlainObject(
971
+ definition,
972
+ `${path}.abstractDefinitions.${abstractId}`,
973
+ issues,
974
+ );
975
+ if (definitionRecord && definitionRecord.abstractNumberingId !== abstractId) {
976
+ issues.push({
977
+ path: `${path}.abstractDefinitions.${abstractId}.abstractNumberingId`,
978
+ message: "abstractNumberingId must match the map key.",
979
+ });
980
+ }
981
+ }
982
+ }
983
+
984
+ const instances = asPlainObject(record.instances, `${path}.instances`, issues);
985
+ if (instances) {
986
+ for (const [instanceId, instance] of Object.entries(instances)) {
987
+ expectDomainString(
988
+ instanceId,
989
+ "numberingInstanceId",
990
+ `${path}.instances.${instanceId}`,
991
+ issues,
992
+ );
993
+ const instanceRecord = asPlainObject(
994
+ instance,
995
+ `${path}.instances.${instanceId}`,
996
+ issues,
997
+ );
998
+ if (!instanceRecord) {
999
+ continue;
1000
+ }
1001
+ if (instanceRecord.numberingInstanceId !== instanceId) {
1002
+ issues.push({
1003
+ path: `${path}.instances.${instanceId}.numberingInstanceId`,
1004
+ message: "numberingInstanceId must match the map key.",
1005
+ });
1006
+ }
1007
+ expectDomainString(
1008
+ instanceRecord.abstractNumberingId,
1009
+ "abstractNumberingId",
1010
+ `${path}.instances.${instanceId}.abstractNumberingId`,
1011
+ issues,
1012
+ );
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ function validateMediaCatalog(
1018
+ value: unknown,
1019
+ path: string,
1020
+ issues: ModelValidationIssue[],
1021
+ ): void {
1022
+ const record = asPlainObject(value, path, issues);
1023
+ if (!record) {
1024
+ return;
1025
+ }
1026
+
1027
+ const items = asPlainObject(record.items, `${path}.items`, issues);
1028
+ if (!items) {
1029
+ return;
1030
+ }
1031
+
1032
+ for (const [mediaId, item] of Object.entries(items)) {
1033
+ expectDomainString(mediaId, "mediaId", `${path}.items.${mediaId}`, issues);
1034
+ const itemRecord = asPlainObject(item, `${path}.items.${mediaId}`, issues);
1035
+ if (!itemRecord) {
1036
+ continue;
1037
+ }
1038
+ if (itemRecord.mediaId !== mediaId) {
1039
+ issues.push({
1040
+ path: `${path}.items.${mediaId}.mediaId`,
1041
+ message: "mediaId must match the map key.",
1042
+ });
1043
+ }
1044
+ expectDomainString(
1045
+ itemRecord.packagePartName,
1046
+ "packagePartName",
1047
+ `${path}.items.${mediaId}.packagePartName`,
1048
+ issues,
1049
+ );
1050
+ if (itemRecord.relationshipId !== undefined) {
1051
+ expectDomainString(
1052
+ itemRecord.relationshipId,
1053
+ "relationshipId",
1054
+ `${path}.items.${mediaId}.relationshipId`,
1055
+ issues,
1056
+ );
1057
+ }
1058
+ }
1059
+ }
1060
+
1061
+ function validateDocumentNode(
1062
+ value: unknown,
1063
+ path: string,
1064
+ issues: ModelValidationIssue[],
1065
+ ): void {
1066
+ const record = asPlainObject(value, path, issues);
1067
+ if (!record) {
1068
+ return;
1069
+ }
1070
+
1071
+ const type = expectString(record.type, `${path}.type`, issues);
1072
+ if (!type) {
1073
+ return;
1074
+ }
1075
+
1076
+ switch (type) {
1077
+ case "doc":
1078
+ case "paragraph":
1079
+ case "sdt":
1080
+ case "custom_xml":
1081
+ case "hyperlink":
1082
+ if (!Array.isArray(record.children)) {
1083
+ issues.push({ path: `${path}.children`, message: "children must be an array." });
1084
+ } else {
1085
+ record.children.forEach((child, index) =>
1086
+ validateDocumentNode(child, `${path}.children[${index}]`, issues),
1087
+ );
1088
+ }
1089
+ if (type === "paragraph" && record.numbering !== undefined) {
1090
+ const numbering = asPlainObject(record.numbering, `${path}.numbering`, issues);
1091
+ if (numbering) {
1092
+ expectDomainString(
1093
+ numbering.numberingInstanceId,
1094
+ "numberingInstanceId",
1095
+ `${path}.numbering.numberingInstanceId`,
1096
+ issues,
1097
+ );
1098
+ }
1099
+ }
1100
+ return;
1101
+ case "alt_chunk":
1102
+ expectDomainString(record.relationshipId, "relationshipId", `${path}.relationshipId`, issues);
1103
+ return;
1104
+ case "image":
1105
+ expectDomainString(record.mediaId, "mediaId", `${path}.mediaId`, issues);
1106
+ if (record.placementXml !== undefined) {
1107
+ expectString(record.placementXml, `${path}.placementXml`, issues);
1108
+ }
1109
+ if (record.display !== undefined) {
1110
+ const display = expectString(record.display, `${path}.display`, issues);
1111
+ if (display && display !== "inline" && display !== "floating") {
1112
+ issues.push({
1113
+ path: `${path}.display`,
1114
+ message: "display must be 'inline' or 'floating'.",
1115
+ });
1116
+ }
1117
+ }
1118
+ if (record.floating !== undefined) {
1119
+ validateFloatingImageProperties(record.floating, `${path}.floating`, issues);
1120
+ }
1121
+ return;
1122
+ case "opaque_inline":
1123
+ case "opaque_block":
1124
+ expectDomainString(record.fragmentId, "fragmentId", `${path}.fragmentId`, issues);
1125
+ expectDomainString(record.warningId, "warningId", `${path}.warningId`, issues);
1126
+ return;
1127
+ case "table":
1128
+ if (!Array.isArray(record.gridColumns)) {
1129
+ issues.push({ path: `${path}.gridColumns`, message: "gridColumns must be an array." });
1130
+ }
1131
+ if (!Array.isArray(record.rows)) {
1132
+ issues.push({ path: `${path}.rows`, message: "rows must be an array." });
1133
+ } else {
1134
+ record.rows.forEach((row, rowIndex) =>
1135
+ validateDocumentNode(row, `${path}.rows[${rowIndex}]`, issues),
1136
+ );
1137
+ }
1138
+ return;
1139
+ case "table_row":
1140
+ if (!Array.isArray(record.cells)) {
1141
+ issues.push({ path: `${path}.cells`, message: "cells must be an array." });
1142
+ } else {
1143
+ record.cells.forEach((cell, cellIndex) =>
1144
+ validateDocumentNode(cell, `${path}.cells[${cellIndex}]`, issues),
1145
+ );
1146
+ }
1147
+ return;
1148
+ case "table_cell":
1149
+ if (!Array.isArray(record.children)) {
1150
+ issues.push({ path: `${path}.children`, message: "children must be an array." });
1151
+ } else {
1152
+ record.children.forEach((child, childIndex) =>
1153
+ validateDocumentNode(child, `${path}.children[${childIndex}]`, issues),
1154
+ );
1155
+ }
1156
+ return;
1157
+ case "field":
1158
+ if (!Array.isArray(record.children)) {
1159
+ issues.push({ path: `${path}.children`, message: "children must be an array." });
1160
+ } else {
1161
+ record.children.forEach((child, index) =>
1162
+ validateDocumentNode(child, `${path}.children[${index}]`, issues),
1163
+ );
1164
+ }
1165
+ return;
1166
+ case "bookmark_start":
1167
+ expectString(record.bookmarkId, `${path}.bookmarkId`, issues);
1168
+ expectString(record.name, `${path}.name`, issues);
1169
+ return;
1170
+ case "bookmark_end":
1171
+ expectString(record.bookmarkId, `${path}.bookmarkId`, issues);
1172
+ return;
1173
+ case "section_break":
1174
+ return;
1175
+ case "text":
1176
+ case "hard_break":
1177
+ case "column_break":
1178
+ case "tab":
1179
+ return;
1180
+ case "symbol":
1181
+ expectString(record.char, `${path}.char`, issues);
1182
+ if (record.font !== undefined) {
1183
+ expectString(record.font, `${path}.font`, issues);
1184
+ }
1185
+ return;
1186
+ case "footnote_ref":
1187
+ expectString(record.noteId, `${path}.noteId`, issues);
1188
+ expectString(record.noteKind, `${path}.noteKind`, issues);
1189
+ return;
1190
+ case "chart_preview":
1191
+ case "smartart_preview":
1192
+ case "shape":
1193
+ case "wordart":
1194
+ case "vml_shape":
1195
+ expectString(record.rawXml, `${path}.rawXml`, issues);
1196
+ return;
1197
+ default:
1198
+ issues.push({
1199
+ path: `${path}.type`,
1200
+ message: `Unsupported node type ${JSON.stringify(type)}.`,
1201
+ });
1202
+ }
1203
+ }
1204
+
1205
+ function validateFloatingImageProperties(
1206
+ value: unknown,
1207
+ path: string,
1208
+ issues: ModelValidationIssue[],
1209
+ ): void {
1210
+ const record = asPlainObject(value, path, issues);
1211
+ if (!record) {
1212
+ return;
1213
+ }
1214
+
1215
+ if (record.horizontalPosition !== undefined) {
1216
+ validateFloatingAxisPosition(record.horizontalPosition, `${path}.horizontalPosition`, issues);
1217
+ }
1218
+ if (record.verticalPosition !== undefined) {
1219
+ validateFloatingAxisPosition(record.verticalPosition, `${path}.verticalPosition`, issues);
1220
+ }
1221
+ if (record.wrap !== undefined) {
1222
+ const wrap = expectString(record.wrap, `${path}.wrap`, issues);
1223
+ if (
1224
+ wrap &&
1225
+ wrap !== "none" &&
1226
+ wrap !== "square" &&
1227
+ wrap !== "tight" &&
1228
+ wrap !== "through" &&
1229
+ wrap !== "topAndBottom"
1230
+ ) {
1231
+ issues.push({
1232
+ path: `${path}.wrap`,
1233
+ message: "wrap must be one of none, square, tight, through, or topAndBottom.",
1234
+ });
1235
+ }
1236
+ }
1237
+ }
1238
+
1239
+ function validateFloatingAxisPosition(
1240
+ value: unknown,
1241
+ path: string,
1242
+ issues: ModelValidationIssue[],
1243
+ ): void {
1244
+ const record = asPlainObject(value, path, issues);
1245
+ if (!record) {
1246
+ return;
1247
+ }
1248
+
1249
+ if (record.relativeFrom !== undefined) {
1250
+ expectString(record.relativeFrom, `${path}.relativeFrom`, issues);
1251
+ }
1252
+ if (record.align !== undefined) {
1253
+ expectString(record.align, `${path}.align`, issues);
1254
+ }
1255
+ if (record.offset !== undefined && typeof record.offset !== "number") {
1256
+ issues.push({
1257
+ path: `${path}.offset`,
1258
+ message: "offset must be a number.",
1259
+ });
1260
+ }
1261
+ }
1262
+
1263
+ function validateReviewStore(
1264
+ value: unknown,
1265
+ path: string,
1266
+ issues: ModelValidationIssue[],
1267
+ ): void {
1268
+ const record = asPlainObject(value, path, issues);
1269
+ if (!record) {
1270
+ return;
1271
+ }
1272
+
1273
+ const comments = asPlainObject(record.comments, `${path}.comments`, issues);
1274
+ if (comments) {
1275
+ for (const [commentId, thread] of Object.entries(comments)) {
1276
+ expectDomainString(commentId, "commentId", `${path}.comments.${commentId}`, issues);
1277
+ const threadRecord = asPlainObject(thread, `${path}.comments.${commentId}`, issues);
1278
+ if (!threadRecord) {
1279
+ continue;
1280
+ }
1281
+ if (threadRecord.commentId !== commentId) {
1282
+ issues.push({
1283
+ path: `${path}.comments.${commentId}.commentId`,
1284
+ message: "commentId must match the map key.",
1285
+ });
1286
+ }
1287
+ validateAnchor(threadRecord.anchor, `${path}.comments.${commentId}.anchor`, issues);
1288
+ expectIso8601UtcTimestamp(
1289
+ threadRecord.createdAt,
1290
+ `${path}.comments.${commentId}.createdAt`,
1291
+ issues,
1292
+ );
1293
+ validateCommentStatus(threadRecord.status, `${path}.comments.${commentId}.status`, issues);
1294
+ if (threadRecord.createdBy !== undefined) {
1295
+ expectString(
1296
+ threadRecord.createdBy,
1297
+ `${path}.comments.${commentId}.createdBy`,
1298
+ issues,
1299
+ );
1300
+ }
1301
+ if (threadRecord.authorId !== undefined) {
1302
+ expectString(
1303
+ threadRecord.authorId,
1304
+ `${path}.comments.${commentId}.authorId`,
1305
+ issues,
1306
+ );
1307
+ }
1308
+ if (threadRecord.body !== undefined) {
1309
+ expectString(threadRecord.body, `${path}.comments.${commentId}.body`, issues);
1310
+ }
1311
+ if (!Array.isArray(threadRecord.warningIds)) {
1312
+ issues.push({
1313
+ path: `${path}.comments.${commentId}.warningIds`,
1314
+ message: "warningIds must be an array.",
1315
+ });
1316
+ } else {
1317
+ threadRecord.warningIds.forEach((warningId, index) =>
1318
+ expectDomainString(
1319
+ warningId,
1320
+ "warningId",
1321
+ `${path}.comments.${commentId}.warningIds[${index}]`,
1322
+ issues,
1323
+ ),
1324
+ );
1325
+ }
1326
+ if (threadRecord.entries !== undefined) {
1327
+ validateCommentEntries(
1328
+ threadRecord.entries,
1329
+ `${path}.comments.${commentId}.entries`,
1330
+ issues,
1331
+ );
1332
+ }
1333
+ if (threadRecord.resolution !== undefined) {
1334
+ validateCommentResolution(
1335
+ threadRecord.resolution,
1336
+ `${path}.comments.${commentId}.resolution`,
1337
+ issues,
1338
+ );
1339
+ }
1340
+ if (threadRecord.resolvedAt !== undefined) {
1341
+ expectIso8601UtcTimestamp(
1342
+ threadRecord.resolvedAt,
1343
+ `${path}.comments.${commentId}.resolvedAt`,
1344
+ issues,
1345
+ );
1346
+ }
1347
+ if (
1348
+ threadRecord.isResolved !== undefined &&
1349
+ typeof threadRecord.isResolved !== "boolean"
1350
+ ) {
1351
+ issues.push({
1352
+ path: `${path}.comments.${commentId}.isResolved`,
1353
+ message: "isResolved must be a boolean.",
1354
+ });
1355
+ }
1356
+ if (threadRecord.metadata !== undefined) {
1357
+ validateCommentThreadMetadata(
1358
+ threadRecord.metadata,
1359
+ `${path}.comments.${commentId}.metadata`,
1360
+ issues,
1361
+ );
1362
+ }
1363
+ }
1364
+ }
1365
+
1366
+ const revisions = asPlainObject(record.revisions, `${path}.revisions`, issues);
1367
+ if (revisions) {
1368
+ for (const [revisionId, revision] of Object.entries(revisions)) {
1369
+ expectDomainString(revisionId, "revisionId", `${path}.revisions.${revisionId}`, issues);
1370
+ const revisionRecord = asPlainObject(
1371
+ revision,
1372
+ `${path}.revisions.${revisionId}`,
1373
+ issues,
1374
+ );
1375
+ if (!revisionRecord) {
1376
+ continue;
1377
+ }
1378
+ if (revisionRecord.changeId !== revisionId) {
1379
+ issues.push({
1380
+ path: `${path}.revisions.${revisionId}.changeId`,
1381
+ message: "changeId must match the map key.",
1382
+ });
1383
+ }
1384
+ validateAnchor(revisionRecord.anchor, `${path}.revisions.${revisionId}.anchor`, issues);
1385
+ validateRevisionKind(revisionRecord.kind, `${path}.revisions.${revisionId}.kind`, issues);
1386
+ validateRevisionStatus(
1387
+ revisionRecord.status,
1388
+ `${path}.revisions.${revisionId}.status`,
1389
+ issues,
1390
+ );
1391
+ expectIso8601UtcTimestamp(
1392
+ revisionRecord.createdAt,
1393
+ `${path}.revisions.${revisionId}.createdAt`,
1394
+ issues,
1395
+ );
1396
+ if (revisionRecord.authorId !== undefined) {
1397
+ expectString(
1398
+ revisionRecord.authorId,
1399
+ `${path}.revisions.${revisionId}.authorId`,
1400
+ issues,
1401
+ );
1402
+ }
1403
+ if (revisionRecord.warningIds !== undefined) {
1404
+ validateWarningIds(
1405
+ revisionRecord.warningIds,
1406
+ `${path}.revisions.${revisionId}.warningIds`,
1407
+ issues,
1408
+ );
1409
+ }
1410
+ if (revisionRecord.metadata !== undefined) {
1411
+ validateRevisionMetadata(
1412
+ revisionRecord.metadata,
1413
+ `${path}.revisions.${revisionId}.metadata`,
1414
+ issues,
1415
+ );
1416
+ }
1417
+ }
1418
+ }
1419
+ }
1420
+
1421
+ function validateCommentStatus(
1422
+ value: unknown,
1423
+ path: string,
1424
+ issues: ModelValidationIssue[],
1425
+ ): void {
1426
+ if (value === "open" || value === "resolved" || value === "detached") {
1427
+ return;
1428
+ }
1429
+ issues.push({
1430
+ path,
1431
+ message: "status must be one of open, resolved, or detached.",
1432
+ });
1433
+ }
1434
+
1435
+ function validateCommentEntries(
1436
+ value: unknown,
1437
+ path: string,
1438
+ issues: ModelValidationIssue[],
1439
+ ): void {
1440
+ if (!Array.isArray(value)) {
1441
+ issues.push({
1442
+ path,
1443
+ message: "entries must be an array.",
1444
+ });
1445
+ return;
1446
+ }
1447
+
1448
+ value.forEach((entry, index) => {
1449
+ const record = asPlainObject(entry, `${path}[${index}]`, issues);
1450
+ if (!record) {
1451
+ return;
1452
+ }
1453
+ expectString(record.entryId, `${path}[${index}].entryId`, issues);
1454
+ expectString(record.authorId, `${path}[${index}].authorId`, issues);
1455
+ expectString(record.body, `${path}[${index}].body`, issues);
1456
+ expectIso8601UtcTimestamp(record.createdAt, `${path}[${index}].createdAt`, issues);
1457
+ if (record.metadata !== undefined) {
1458
+ validateCommentEntryMetadata(record.metadata, `${path}[${index}].metadata`, issues);
1459
+ }
1460
+ });
1461
+ }
1462
+
1463
+ function validateCommentEntryMetadata(
1464
+ value: unknown,
1465
+ path: string,
1466
+ issues: ModelValidationIssue[],
1467
+ ): void {
1468
+ const record = asPlainObject(value, path, issues);
1469
+ if (!record) {
1470
+ return;
1471
+ }
1472
+
1473
+ for (const field of [
1474
+ "ooxmlCommentId",
1475
+ "paraId",
1476
+ "parentParaId",
1477
+ "durableId",
1478
+ "initials",
1479
+ ] as const) {
1480
+ if (record[field] !== undefined) {
1481
+ expectString(record[field], `${path}.${field}`, issues);
1482
+ }
1483
+ }
1484
+ }
1485
+
1486
+ function validateCommentResolution(
1487
+ value: unknown,
1488
+ path: string,
1489
+ issues: ModelValidationIssue[],
1490
+ ): void {
1491
+ const record = asPlainObject(value, path, issues);
1492
+ if (!record) {
1493
+ return;
1494
+ }
1495
+
1496
+ expectIso8601UtcTimestamp(record.resolvedAt, `${path}.resolvedAt`, issues);
1497
+ expectString(record.resolvedBy, `${path}.resolvedBy`, issues);
1498
+ }
1499
+
1500
+ function validateCommentThreadMetadata(
1501
+ value: unknown,
1502
+ path: string,
1503
+ issues: ModelValidationIssue[],
1504
+ ): void {
1505
+ const record = asPlainObject(value, path, issues);
1506
+ if (!record) {
1507
+ return;
1508
+ }
1509
+
1510
+ if (
1511
+ record.source !== undefined &&
1512
+ record.source !== "runtime" &&
1513
+ record.source !== "import"
1514
+ ) {
1515
+ issues.push({
1516
+ path: `${path}.source`,
1517
+ message: "source must be either runtime or import.",
1518
+ });
1519
+ }
1520
+ if (record.rootOoxmlCommentId !== undefined) {
1521
+ expectString(record.rootOoxmlCommentId, `${path}.rootOoxmlCommentId`, issues);
1522
+ }
1523
+ if (record.rootParaId !== undefined) {
1524
+ expectString(record.rootParaId, `${path}.rootParaId`, issues);
1525
+ }
1526
+ }
1527
+
1528
+ function validateRevisionKind(
1529
+ value: unknown,
1530
+ path: string,
1531
+ issues: ModelValidationIssue[],
1532
+ ): void {
1533
+ if (
1534
+ value === "insertion" ||
1535
+ value === "deletion" ||
1536
+ value === "formatting" ||
1537
+ value === "move" ||
1538
+ value === "property-change"
1539
+ ) {
1540
+ return;
1541
+ }
1542
+ issues.push({
1543
+ path,
1544
+ message: "kind must be insertion, deletion, formatting, move, or property-change.",
1545
+ });
1546
+ }
1547
+
1548
+ function validateRevisionStatus(
1549
+ value: unknown,
1550
+ path: string,
1551
+ issues: ModelValidationIssue[],
1552
+ ): void {
1553
+ if (
1554
+ value === "open" ||
1555
+ value === "accepted" ||
1556
+ value === "rejected" ||
1557
+ value === "detached"
1558
+ ) {
1559
+ return;
1560
+ }
1561
+ issues.push({
1562
+ path,
1563
+ message: "status must be one of open, accepted, rejected, or detached.",
1564
+ });
1565
+ }
1566
+
1567
+ function validateWarningIds(
1568
+ value: unknown,
1569
+ path: string,
1570
+ issues: ModelValidationIssue[],
1571
+ ): void {
1572
+ if (!Array.isArray(value)) {
1573
+ issues.push({
1574
+ path,
1575
+ message: "warningIds must be an array.",
1576
+ });
1577
+ return;
1578
+ }
1579
+
1580
+ value.forEach((warningId, index) =>
1581
+ expectDomainString(warningId, "warningId", `${path}[${index}]`, issues),
1582
+ );
1583
+ }
1584
+
1585
+ function validateRevisionMetadata(
1586
+ value: unknown,
1587
+ path: string,
1588
+ issues: ModelValidationIssue[],
1589
+ ): void {
1590
+ const record = asPlainObject(value, path, issues);
1591
+ if (!record) {
1592
+ return;
1593
+ }
1594
+
1595
+ if (
1596
+ record.source !== undefined &&
1597
+ record.source !== "runtime" &&
1598
+ record.source !== "import"
1599
+ ) {
1600
+ issues.push({
1601
+ path: `${path}.source`,
1602
+ message: "source must be either runtime or import.",
1603
+ });
1604
+ }
1605
+ for (const field of [
1606
+ "preserveOnlyReason",
1607
+ "importedRevisionForm",
1608
+ "originalRevisionType",
1609
+ "ooxmlRevisionId",
1610
+ ] as const) {
1611
+ if (record[field] !== undefined) {
1612
+ expectString(record[field], `${path}.${field}`, issues);
1613
+ }
1614
+ }
1615
+
1616
+ if (record.propertyChangeData !== undefined) {
1617
+ const pcd = asPlainObject(record.propertyChangeData, `${path}.propertyChangeData`, issues);
1618
+ if (pcd) {
1619
+ expectString(pcd.xmlTag, `${path}.propertyChangeData.xmlTag`, issues);
1620
+ expectString(pcd.beforeXml, `${path}.propertyChangeData.beforeXml`, issues);
1621
+ }
1622
+ }
1623
+
1624
+ if (record.moveData !== undefined) {
1625
+ const md = asPlainObject(record.moveData, `${path}.moveData`, issues);
1626
+ if (md) {
1627
+ expectString(md.moveId, `${path}.moveData.moveId`, issues);
1628
+ expectString(md.direction, `${path}.moveData.direction`, issues);
1629
+ }
1630
+ }
1631
+ }
1632
+
1633
+ function validatePreservationStore(
1634
+ value: unknown,
1635
+ path: string,
1636
+ issues: ModelValidationIssue[],
1637
+ ): void {
1638
+ const record = asPlainObject(value, path, issues);
1639
+ if (!record) {
1640
+ return;
1641
+ }
1642
+
1643
+ const opaqueFragments = asPlainObject(
1644
+ record.opaqueFragments,
1645
+ `${path}.opaqueFragments`,
1646
+ issues,
1647
+ );
1648
+ if (opaqueFragments) {
1649
+ for (const [fragmentId, fragment] of Object.entries(opaqueFragments)) {
1650
+ expectDomainString(fragmentId, "fragmentId", `${path}.opaqueFragments.${fragmentId}`, issues);
1651
+ const fragmentRecord = asPlainObject(
1652
+ fragment,
1653
+ `${path}.opaqueFragments.${fragmentId}`,
1654
+ issues,
1655
+ );
1656
+ if (!fragmentRecord) {
1657
+ continue;
1658
+ }
1659
+ if (fragmentRecord.fragmentId !== fragmentId) {
1660
+ issues.push({
1661
+ path: `${path}.opaqueFragments.${fragmentId}.fragmentId`,
1662
+ message: "fragmentId must match the map key.",
1663
+ });
1664
+ }
1665
+ validateRange(
1666
+ fragmentRecord.lastKnownRange,
1667
+ `${path}.opaqueFragments.${fragmentId}.lastKnownRange`,
1668
+ issues,
1669
+ );
1670
+ expectDomainString(
1671
+ fragmentRecord.warningId,
1672
+ "warningId",
1673
+ `${path}.opaqueFragments.${fragmentId}.warningId`,
1674
+ issues,
1675
+ );
1676
+ if (fragmentRecord.packagePartName !== undefined) {
1677
+ expectDomainString(
1678
+ fragmentRecord.packagePartName,
1679
+ "packagePartName",
1680
+ `${path}.opaqueFragments.${fragmentId}.packagePartName`,
1681
+ issues,
1682
+ );
1683
+ }
1684
+ if (fragmentRecord.relationshipId !== undefined) {
1685
+ expectDomainString(
1686
+ fragmentRecord.relationshipId,
1687
+ "relationshipId",
1688
+ `${path}.opaqueFragments.${fragmentId}.relationshipId`,
1689
+ issues,
1690
+ );
1691
+ }
1692
+ }
1693
+ }
1694
+
1695
+ const packageParts = asPlainObject(record.packageParts, `${path}.packageParts`, issues);
1696
+ if (packageParts) {
1697
+ for (const [packagePartName, packagePart] of Object.entries(packageParts)) {
1698
+ expectDomainString(
1699
+ packagePartName,
1700
+ "packagePartName",
1701
+ `${path}.packageParts.${packagePartName}`,
1702
+ issues,
1703
+ );
1704
+ const packagePartRecord = asPlainObject(
1705
+ packagePart,
1706
+ `${path}.packageParts.${packagePartName}`,
1707
+ issues,
1708
+ );
1709
+ if (!packagePartRecord) {
1710
+ continue;
1711
+ }
1712
+ if (packagePartRecord.packagePartName !== packagePartName) {
1713
+ issues.push({
1714
+ path: `${path}.packageParts.${packagePartName}.packagePartName`,
1715
+ message: "packagePartName must match the map key.",
1716
+ });
1717
+ }
1718
+ if (Array.isArray(packagePartRecord.relationshipIds)) {
1719
+ packagePartRecord.relationshipIds.forEach((relationshipId, index) =>
1720
+ expectDomainString(
1721
+ relationshipId,
1722
+ "relationshipId",
1723
+ `${path}.packageParts.${packagePartName}.relationshipIds[${index}]`,
1724
+ issues,
1725
+ ),
1726
+ );
1727
+ }
1728
+ }
1729
+ }
1730
+ }
1731
+
1732
+ function validateDiagnosticStore(
1733
+ value: unknown,
1734
+ path: string,
1735
+ issues: ModelValidationIssue[],
1736
+ ): void {
1737
+ const record = asPlainObject(value, path, issues);
1738
+ if (!record) {
1739
+ return;
1740
+ }
1741
+
1742
+ if (Array.isArray(record.warnings)) {
1743
+ record.warnings.forEach((warning, index) => {
1744
+ const warningRecord = asPlainObject(warning, `${path}.warnings[${index}]`, issues);
1745
+ if (!warningRecord) {
1746
+ return;
1747
+ }
1748
+ expectDomainString(
1749
+ warningRecord.diagnosticId,
1750
+ "diagnosticId",
1751
+ `${path}.warnings[${index}].diagnosticId`,
1752
+ issues,
1753
+ );
1754
+ expectDomainString(
1755
+ warningRecord.warningId,
1756
+ "warningId",
1757
+ `${path}.warnings[${index}].warningId`,
1758
+ issues,
1759
+ );
1760
+ });
1761
+ }
1762
+
1763
+ if (Array.isArray(record.errors)) {
1764
+ record.errors.forEach((error, index) => {
1765
+ const errorRecord = asPlainObject(error, `${path}.errors[${index}]`, issues);
1766
+ if (!errorRecord) {
1767
+ return;
1768
+ }
1769
+ expectDomainString(
1770
+ errorRecord.diagnosticId,
1771
+ "diagnosticId",
1772
+ `${path}.errors[${index}].diagnosticId`,
1773
+ issues,
1774
+ );
1775
+ });
1776
+ }
1777
+ }
1778
+
1779
+ function validateAnchor(
1780
+ value: unknown,
1781
+ path: string,
1782
+ issues: ModelValidationIssue[],
1783
+ ): void {
1784
+ const record = asPlainObject(value, path, issues);
1785
+ if (!record) {
1786
+ return;
1787
+ }
1788
+
1789
+ const kind = expectString(record.kind, `${path}.kind`, issues);
1790
+ if (!kind) {
1791
+ return;
1792
+ }
1793
+
1794
+ if (kind === "range") {
1795
+ validateRange(record.range, `${path}.range`, issues);
1796
+ } else if (kind === "detached") {
1797
+ validateRange(record.lastKnownRange, `${path}.lastKnownRange`, issues);
1798
+ }
1799
+ }
1800
+
1801
+ function validateRange(
1802
+ value: unknown,
1803
+ path: string,
1804
+ issues: ModelValidationIssue[],
1805
+ ): void {
1806
+ const record = asPlainObject(value, path, issues);
1807
+ if (!record) {
1808
+ return;
1809
+ }
1810
+
1811
+ if (typeof record.from !== "number" || typeof record.to !== "number") {
1812
+ issues.push({
1813
+ path,
1814
+ message: "Range must contain numeric from and to values.",
1815
+ });
1816
+ }
1817
+ }
1818
+
1819
+ function validateExactObjectKeys(
1820
+ record: Record<string, unknown>,
1821
+ expectedKeys: readonly string[],
1822
+ path: string,
1823
+ issues: ModelValidationIssue[],
1824
+ allowedOptionalKeys?: readonly string[],
1825
+ ): void {
1826
+ const actualKeys = new Set(Object.keys(record));
1827
+ const expected = new Set(expectedKeys);
1828
+ const optional = new Set(allowedOptionalKeys ?? []);
1829
+
1830
+ for (const expectedKey of expectedKeys) {
1831
+ if (!actualKeys.has(expectedKey)) {
1832
+ issues.push({
1833
+ path,
1834
+ message: `Missing required key ${JSON.stringify(expectedKey)}.`,
1835
+ });
1836
+ }
1837
+ }
1838
+
1839
+ for (const actualKey of actualKeys) {
1840
+ if (!expected.has(actualKey) && !optional.has(actualKey)) {
1841
+ issues.push({
1842
+ path: `${path}.${actualKey}`,
1843
+ message:
1844
+ "Unexpected canonical document key. Render/UI state is not part of the canonical envelope.",
1845
+ });
1846
+ }
1847
+ }
1848
+ }
1849
+
1850
+ function validateSubPartsCatalog(
1851
+ value: unknown,
1852
+ path: string,
1853
+ issues: ModelValidationIssue[],
1854
+ ): void {
1855
+ const record = asPlainObject(value, path, issues);
1856
+ if (!record) {
1857
+ return;
1858
+ }
1859
+
1860
+ if (record.headers !== undefined) {
1861
+ if (!Array.isArray(record.headers)) {
1862
+ issues.push({ path: `${path}.headers`, message: "headers must be an array." });
1863
+ } else {
1864
+ record.headers.forEach((header, index) => {
1865
+ const headerRecord = asPlainObject(header, `${path}.headers[${index}]`, issues);
1866
+ if (headerRecord) {
1867
+ expectString(headerRecord.variant, `${path}.headers[${index}].variant`, issues);
1868
+ expectString(headerRecord.partPath, `${path}.headers[${index}].partPath`, issues);
1869
+ expectString(headerRecord.relationshipId, `${path}.headers[${index}].relationshipId`, issues);
1870
+ }
1871
+ });
1872
+ }
1873
+ }
1874
+
1875
+ if (record.footers !== undefined) {
1876
+ if (!Array.isArray(record.footers)) {
1877
+ issues.push({ path: `${path}.footers`, message: "footers must be an array." });
1878
+ } else {
1879
+ record.footers.forEach((footer, index) => {
1880
+ const footerRecord = asPlainObject(footer, `${path}.footers[${index}]`, issues);
1881
+ if (footerRecord) {
1882
+ expectString(footerRecord.variant, `${path}.footers[${index}].variant`, issues);
1883
+ expectString(footerRecord.partPath, `${path}.footers[${index}].partPath`, issues);
1884
+ expectString(footerRecord.relationshipId, `${path}.footers[${index}].relationshipId`, issues);
1885
+ }
1886
+ });
1887
+ }
1888
+ }
1889
+ }
1890
+
1891
+ function expectDomainString(
1892
+ value: unknown,
1893
+ domain: StableIdDomain,
1894
+ path: string,
1895
+ issues: ModelValidationIssue[],
1896
+ ): string | null {
1897
+ const stableId = expectString(value, path, issues);
1898
+ if (!stableId) {
1899
+ return null;
1900
+ }
1901
+
1902
+ if (!ID_PATTERNS[domain].test(stableId)) {
1903
+ issues.push({
1904
+ path,
1905
+ message: `Expected a valid ${domain}.`,
1906
+ });
1907
+ return null;
1908
+ }
1909
+
1910
+ return stableId;
1911
+ }