@beyondwork/docx-react-component 1.0.106 → 1.0.109

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 (190) hide show
  1. package/package.json +19 -5
  2. package/src/api/geometry-overlay-rects.ts +5 -0
  3. package/src/api/package-version.ts +1 -1
  4. package/src/api/page-anchor-id.ts +5 -0
  5. package/src/api/public-types.ts +16 -9
  6. package/src/api/table-node-specs.ts +6 -0
  7. package/src/api/v3/_create.ts +2 -1
  8. package/src/api/v3/_page-anchor-id.ts +52 -0
  9. package/src/api/v3/_runtime-handle.ts +92 -1
  10. package/src/api/v3/ai/_audit-time.ts +5 -0
  11. package/src/api/v3/ai/_pe2-evidence.ts +38 -0
  12. package/src/api/v3/ai/attach.ts +7 -2
  13. package/src/api/v3/ai/replacement.ts +101 -18
  14. package/src/api/v3/ai/resolve.ts +2 -2
  15. package/src/api/v3/ai/review.ts +177 -3
  16. package/src/api/v3/index.ts +1 -0
  17. package/src/api/v3/runtime/collab.ts +462 -0
  18. package/src/api/v3/runtime/document.ts +503 -20
  19. package/src/api/v3/runtime/geometry.ts +97 -0
  20. package/src/api/v3/runtime/layout.ts +744 -0
  21. package/src/api/v3/runtime/perf-probe.ts +14 -0
  22. package/src/api/v3/runtime/viewport.ts +9 -8
  23. package/src/api/v3/ui/_types.ts +149 -55
  24. package/src/api/v3/ui/chrome-preset-model.ts +5 -5
  25. package/src/api/v3/ui/debug.ts +115 -2
  26. package/src/api/v3/ui/index.ts +13 -0
  27. package/src/api/v3/ui/overlays.ts +0 -8
  28. package/src/api/v3/ui/surface.ts +56 -0
  29. package/src/api/v3/ui/viewport.ts +22 -9
  30. package/src/core/commands/image-commands.ts +1 -0
  31. package/src/core/commands/index.ts +6 -0
  32. package/src/core/schema/text-schema.ts +43 -5
  33. package/src/core/selection/mapping.ts +8 -1
  34. package/src/core/selection/review-anchors.ts +5 -1
  35. package/src/core/state/text-transaction.ts +8 -2
  36. package/src/io/export/serialize-revisions.ts +149 -1
  37. package/src/io/normalize/normalize-text.ts +6 -0
  38. package/src/io/ooxml/parse-bookmark-references.ts +55 -0
  39. package/src/io/ooxml/parse-fields.ts +24 -2
  40. package/src/io/ooxml/parse-headers-footers.ts +38 -5
  41. package/src/io/ooxml/parse-main-document.ts +153 -9
  42. package/src/io/ooxml/parse-numbering.ts +20 -0
  43. package/src/io/ooxml/parse-revisions.ts +19 -8
  44. package/src/io/opc/package-reader.ts +98 -8
  45. package/src/model/anchor.ts +4 -3
  46. package/src/model/canonical-document.ts +220 -2
  47. package/src/model/canonical-hash.ts +221 -0
  48. package/src/model/canonical-layout-inputs.ts +245 -6
  49. package/src/model/layout/index.ts +1 -0
  50. package/src/model/layout/page-graph-types.ts +118 -1
  51. package/src/model/review/revision-types.ts +14 -3
  52. package/src/preservation/store.ts +20 -4
  53. package/src/review/README.md +1 -1
  54. package/src/review/store/revision-actions.ts +14 -2
  55. package/src/runtime/collab/event-types.ts +67 -1
  56. package/src/runtime/collab/runtime-collab-sync.ts +177 -5
  57. package/src/runtime/diagnostics/layout-guard-warning.ts +18 -0
  58. package/src/runtime/document-heading-outline.ts +147 -0
  59. package/src/runtime/document-navigation.ts +8 -243
  60. package/src/runtime/document-runtime.ts +240 -97
  61. package/src/runtime/edit-dispatch/dispatch-text-command.ts +11 -0
  62. package/src/runtime/formatting/layout-inputs.ts +38 -5
  63. package/src/runtime/formatting/numbering/geometry.ts +28 -2
  64. package/src/runtime/geometry/adjacent-geometry-intake.ts +835 -0
  65. package/src/runtime/geometry/caret-geometry.ts +5 -6
  66. package/src/runtime/geometry/geometry-facet.ts +60 -10
  67. package/src/runtime/geometry/geometry-index.ts +591 -20
  68. package/src/runtime/geometry/geometry-types.ts +59 -0
  69. package/src/runtime/geometry/hit-test.ts +11 -1
  70. package/src/runtime/geometry/overlay-rects.ts +5 -3
  71. package/src/runtime/geometry/project-anchors.ts +1 -1
  72. package/src/runtime/geometry/word-layout-v2-line-intake.ts +323 -0
  73. package/src/runtime/layout/index.ts +6 -0
  74. package/src/runtime/layout/layout-engine-instance.ts +6 -1
  75. package/src/runtime/layout/layout-engine-version.ts +181 -16
  76. package/src/runtime/layout/layout-facet-types.ts +6 -0
  77. package/src/runtime/layout/page-graph.ts +21 -4
  78. package/src/runtime/layout/paginated-layout-engine.ts +139 -15
  79. package/src/runtime/layout/project-block-fragments.ts +265 -7
  80. package/src/runtime/layout/public-facet.ts +78 -24
  81. package/src/runtime/layout/table-row-continuation-contract.ts +107 -0
  82. package/src/runtime/layout/table-row-split.ts +92 -35
  83. package/src/runtime/prerender/cache-envelope.ts +2 -2
  84. package/src/runtime/prerender/cache-key.ts +5 -4
  85. package/src/runtime/prerender/customxml-cache.ts +0 -1
  86. package/src/runtime/render/render-kernel.ts +1 -1
  87. package/src/runtime/revision-runtime.ts +112 -10
  88. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  89. package/src/runtime/scopes/action-validation.ts +22 -2
  90. package/src/runtime/scopes/capabilities.ts +316 -0
  91. package/src/runtime/scopes/compile-scope-bundle.ts +14 -0
  92. package/src/runtime/scopes/compiler-service.ts +108 -4
  93. package/src/runtime/scopes/content-control-evidence.ts +79 -0
  94. package/src/runtime/scopes/create-issue.ts +5 -5
  95. package/src/runtime/scopes/evidence.ts +91 -0
  96. package/src/runtime/scopes/formatting/apply.ts +2 -0
  97. package/src/runtime/scopes/geometry-evidence.ts +130 -0
  98. package/src/runtime/scopes/index.ts +54 -0
  99. package/src/runtime/scopes/issue-lifecycle.ts +224 -0
  100. package/src/runtime/scopes/layout-evidence.ts +374 -0
  101. package/src/runtime/scopes/multi-paragraph-refusal.ts +37 -0
  102. package/src/runtime/scopes/preservation-boundary.ts +7 -1
  103. package/src/runtime/scopes/replacement/apply.ts +97 -34
  104. package/src/runtime/scopes/scope-kinds/paragraph.ts +108 -12
  105. package/src/runtime/scopes/semantic-scope-types.ts +242 -3
  106. package/src/runtime/scopes/visualization.ts +28 -0
  107. package/src/runtime/surface-projection.ts +44 -5
  108. package/src/runtime/telemetry/perf-probe.ts +216 -0
  109. package/src/runtime/virtualized-rendering.ts +36 -1
  110. package/src/runtime/workflow/ai-issue-lifecycle.ts +253 -0
  111. package/src/runtime/workflow/coordinator.ts +39 -11
  112. package/src/runtime/workflow/derived-scope-resolver.ts +63 -9
  113. package/src/runtime/workflow/index.ts +3 -0
  114. package/src/runtime/workflow/overlay-lane-types.ts +58 -0
  115. package/src/runtime/workflow/overlay-lanes.ts +168 -10
  116. package/src/runtime/workflow/overlay-store.ts +2 -2
  117. package/src/runtime/workflow/redline-posture-calibration.ts +257 -0
  118. package/src/runtime/workflow/word-field-matrix-calibration.ts +231 -0
  119. package/src/session/_sync-legacy.ts +17 -27
  120. package/src/session/import/loader.ts +6 -4
  121. package/src/session/import/source-package-evidence.ts +186 -2
  122. package/src/session/index.ts +5 -6
  123. package/src/session/session.ts +30 -56
  124. package/src/session/types.ts +8 -13
  125. package/src/shell/session-bootstrap.ts +155 -81
  126. package/src/ui/WordReviewEditor.tsx +520 -12
  127. package/src/ui/editor-shell-view.tsx +14 -4
  128. package/src/ui/editor-surface-controller.tsx +5 -3
  129. package/src/ui/headless/selection-tool-resolver.ts +1 -2
  130. package/src/ui/presence-overlay-lane.ts +0 -1
  131. package/src/ui/ui-controller-factory.ts +7 -0
  132. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +5 -1
  133. package/src/ui-tailwind/chrome/editor-action-registry.ts +105 -5
  134. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +7 -0
  135. package/src/ui-tailwind/chrome/layer-debug-contracts.ts +208 -0
  136. package/src/ui-tailwind/chrome/resolve-target-kind.ts +13 -0
  137. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +11 -3
  138. package/src/ui-tailwind/chrome/tw-command-palette.tsx +36 -6
  139. package/src/ui-tailwind/chrome/tw-context-menu.tsx +6 -1
  140. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +42 -109
  141. package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +26 -6
  142. package/src/ui-tailwind/chrome/tw-navigation-command-bar.tsx +328 -0
  143. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +8 -4
  144. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +129 -1
  145. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +19 -5
  146. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
  147. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +28 -12
  148. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +30 -3
  149. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +116 -10
  150. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +223 -94
  151. package/src/ui-tailwind/chrome-overlay/tw-presence-overlay-lane.tsx +157 -0
  152. package/src/ui-tailwind/chrome-overlay/tw-review-overlay-lane-markers.tsx +259 -0
  153. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +5 -2
  154. package/src/ui-tailwind/chrome-overlay/tw-substrate-overlay-lanes.tsx +314 -0
  155. package/src/ui-tailwind/debug/README.md +4 -1
  156. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +272 -0
  157. package/src/ui-tailwind/debug/layer11-word-field-matrix-evidence.ts +160 -0
  158. package/src/ui-tailwind/editor-surface/perf-probe.ts +14 -215
  159. package/src/ui-tailwind/editor-surface/pm-decorations.ts +42 -0
  160. package/src/ui-tailwind/editor-surface/pm-position-map.ts +38 -2
  161. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -4
  162. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +34 -5
  163. package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +9 -19
  164. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -2
  165. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +145 -0
  166. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +16 -11
  167. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +8 -10
  168. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +3 -0
  169. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +4 -2
  170. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +60 -20
  171. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +16 -11
  172. package/src/ui-tailwind/review/tw-health-panel.tsx +36 -17
  173. package/src/ui-tailwind/review/tw-review-rail.tsx +7 -4
  174. package/src/ui-tailwind/review-workspace/diagnostics-visibility.ts +44 -0
  175. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +11 -0
  176. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +16 -1
  177. package/src/ui-tailwind/review-workspace/types.ts +26 -12
  178. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +40 -11
  179. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +2 -1
  180. package/src/ui-tailwind/review-workspace/use-page-markers.ts +15 -26
  181. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +35 -18
  182. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +41 -32
  183. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +2 -1
  184. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +2 -1
  185. package/src/ui-tailwind/status/tw-status-bar.tsx +6 -5
  186. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +52 -80
  187. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +12 -48
  188. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +9 -4
  189. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +328 -361
  190. package/src/ui-tailwind/tw-review-workspace.tsx +152 -286
@@ -29,21 +29,54 @@ export interface OpcPackage {
29
29
  sourceByteLength: number;
30
30
  }
31
31
 
32
+ export interface OpcPackageReadLimits {
33
+ maxEntries?: number;
34
+ maxEntryUncompressedBytes?: number;
35
+ maxTotalUncompressedBytes?: number;
36
+ }
37
+
38
+ export interface ReadOpcPackageOptions {
39
+ limits?: OpcPackageReadLimits;
40
+ }
41
+
32
42
  const EOCD_SIGNATURE = 0x06054b50;
33
43
  const CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
34
44
  const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
35
45
 
36
- export function readOpcPackage(source: Uint8Array | ArrayBuffer): OpcPackage {
46
+ const DEFAULT_MAX_ZIP_ENTRIES = 4096;
47
+ const DEFAULT_MAX_ENTRY_UNCOMPRESSED_BYTES = 64 * 1024 * 1024;
48
+ const DEFAULT_MAX_TOTAL_UNCOMPRESSED_BYTES = 256 * 1024 * 1024;
49
+
50
+ interface ResolvedOpcPackageReadLimits {
51
+ maxEntries: number;
52
+ maxEntryUncompressedBytes: number;
53
+ maxTotalUncompressedBytes: number;
54
+ }
55
+
56
+ export function readOpcPackage(
57
+ source: Uint8Array | ArrayBuffer,
58
+ options: ReadOpcPackageOptions = {},
59
+ ): OpcPackage {
37
60
  const bytes = source instanceof Uint8Array ? source : new Uint8Array(source);
38
- const centralDirectory = readCentralDirectory(bytes);
61
+ const limits = resolveReadLimits(options.limits);
62
+ const centralDirectory = readCentralDirectory(bytes, limits);
39
63
  const parts = new Map<string, OpcPackagePart>();
64
+ let totalUncompressedBytes = 0;
40
65
 
41
66
  for (const entry of centralDirectory) {
42
67
  if (entry.path.endsWith("/")) {
43
68
  continue;
44
69
  }
45
70
 
46
- const storedBytes = readZipEntry(bytes, entry);
71
+ assertEntryWithinLimits(entry, limits);
72
+ totalUncompressedBytes += entry.uncompressedSize;
73
+ if (totalUncompressedBytes > limits.maxTotalUncompressedBytes) {
74
+ throw new Error(
75
+ `Invalid ZIP archive: cumulative uncompressed size ${totalUncompressedBytes} exceeds limit ${limits.maxTotalUncompressedBytes}.`,
76
+ );
77
+ }
78
+
79
+ const storedBytes = readZipEntry(bytes, entry, limits);
47
80
  const normalizedPath = normalizePartPath(entry.path);
48
81
  const surfaceKind = getSurfaceKind(normalizedPath);
49
82
 
@@ -111,15 +144,38 @@ export function readOpcPackage(source: Uint8Array | ArrayBuffer): OpcPackage {
111
144
  };
112
145
  }
113
146
 
114
- function readCentralDirectory(archive: Uint8Array): ZipCentralDirectoryEntry[] {
147
+ function resolveReadLimits(
148
+ limits: OpcPackageReadLimits | undefined,
149
+ ): ResolvedOpcPackageReadLimits {
150
+ return {
151
+ maxEntries: limits?.maxEntries ?? DEFAULT_MAX_ZIP_ENTRIES,
152
+ maxEntryUncompressedBytes:
153
+ limits?.maxEntryUncompressedBytes ?? DEFAULT_MAX_ENTRY_UNCOMPRESSED_BYTES,
154
+ maxTotalUncompressedBytes:
155
+ limits?.maxTotalUncompressedBytes ?? DEFAULT_MAX_TOTAL_UNCOMPRESSED_BYTES,
156
+ };
157
+ }
158
+
159
+ function readCentralDirectory(
160
+ archive: Uint8Array,
161
+ limits: ResolvedOpcPackageReadLimits,
162
+ ): ZipCentralDirectoryEntry[] {
115
163
  const view = new DataView(archive.buffer, archive.byteOffset, archive.byteLength);
116
164
  const eocdOffset = findEndOfCentralDirectory(archive, view);
117
165
  const entryCount = view.getUint16(eocdOffset + 10, true);
166
+ if (entryCount > limits.maxEntries) {
167
+ throw new Error(
168
+ `Invalid ZIP archive: entry count ${entryCount} exceeds limit ${limits.maxEntries}.`,
169
+ );
170
+ }
118
171
  const centralDirectoryOffset = view.getUint32(eocdOffset + 16, true);
119
172
  const entries: ZipCentralDirectoryEntry[] = [];
120
173
  let offset = centralDirectoryOffset;
121
174
 
122
175
  for (let index = 0; index < entryCount; index += 1) {
176
+ if (offset + 46 > archive.byteLength) {
177
+ throw new Error(`Invalid ZIP central directory entry at offset ${offset}.`);
178
+ }
123
179
  const signature = view.getUint32(offset, true);
124
180
  if (signature !== CENTRAL_DIRECTORY_SIGNATURE) {
125
181
  throw new Error(`Invalid ZIP central directory signature at offset ${offset}.`);
@@ -135,6 +191,10 @@ function readCentralDirectory(archive: Uint8Array): ZipCentralDirectoryEntry[] {
135
191
  const fileCommentLength = view.getUint16(offset + 32, true);
136
192
  const localHeaderOffset = view.getUint32(offset + 42, true);
137
193
  const fileNameOffset = offset + 46;
194
+ const nextOffset = fileNameOffset + fileNameLength + extraFieldLength + fileCommentLength;
195
+ if (nextOffset > archive.byteLength) {
196
+ throw new Error(`Invalid ZIP central directory entry at offset ${offset}.`);
197
+ }
138
198
  const fileName = decodeUtf8(archive.subarray(fileNameOffset, fileNameOffset + fileNameLength));
139
199
 
140
200
  entries.push({
@@ -147,7 +207,7 @@ function readCentralDirectory(archive: Uint8Array): ZipCentralDirectoryEntry[] {
147
207
  generalPurposeBitFlag,
148
208
  });
149
209
 
150
- offset += 46 + fileNameLength + extraFieldLength + fileCommentLength;
210
+ offset = nextOffset;
151
211
  }
152
212
 
153
213
  return entries;
@@ -167,9 +227,27 @@ function findEndOfCentralDirectory(archive: Uint8Array, view: DataView): number
167
227
  throw new Error("Invalid ZIP archive: end of central directory not found.");
168
228
  }
169
229
 
170
- function readZipEntry(archive: Uint8Array, entry: ZipCentralDirectoryEntry): Uint8Array {
230
+ function assertEntryWithinLimits(
231
+ entry: ZipCentralDirectoryEntry,
232
+ limits: ResolvedOpcPackageReadLimits,
233
+ ): void {
234
+ if (entry.uncompressedSize > limits.maxEntryUncompressedBytes) {
235
+ throw new Error(
236
+ `Invalid ZIP archive: entry ${entry.path} uncompressed size ${entry.uncompressedSize} exceeds limit ${limits.maxEntryUncompressedBytes}.`,
237
+ );
238
+ }
239
+ }
240
+
241
+ function readZipEntry(
242
+ archive: Uint8Array,
243
+ entry: ZipCentralDirectoryEntry,
244
+ limits: ResolvedOpcPackageReadLimits,
245
+ ): Uint8Array {
171
246
  const view = new DataView(archive.buffer, archive.byteOffset, archive.byteLength);
172
247
  const headerOffset = entry.localHeaderOffset;
248
+ if (headerOffset + 30 > archive.byteLength) {
249
+ throw new Error(`Invalid ZIP local file header offset ${headerOffset}.`);
250
+ }
173
251
  const signature = view.getUint32(headerOffset, true);
174
252
  if (signature !== LOCAL_FILE_HEADER_SIGNATURE) {
175
253
  throw new Error(`Invalid ZIP local file header signature at offset ${headerOffset}.`);
@@ -178,16 +256,28 @@ function readZipEntry(archive: Uint8Array, entry: ZipCentralDirectoryEntry): Uin
178
256
  const fileNameLength = view.getUint16(headerOffset + 26, true);
179
257
  const extraFieldLength = view.getUint16(headerOffset + 28, true);
180
258
  const dataOffset = headerOffset + 30 + fileNameLength + extraFieldLength;
259
+ if (dataOffset + entry.compressedSize > archive.byteLength) {
260
+ throw new Error(`Invalid ZIP entry ${entry.path}: compressed data exceeds archive bounds.`);
261
+ }
181
262
  const compressed = archive.subarray(dataOffset, dataOffset + entry.compressedSize);
182
263
 
264
+ let inflated: Uint8Array;
183
265
  switch (entry.compression) {
184
266
  case "store":
185
- return new Uint8Array(compressed);
267
+ inflated = new Uint8Array(compressed);
268
+ break;
186
269
  case "deflate":
187
- return inflateSync(compressed);
270
+ inflated = inflateSync(compressed);
271
+ break;
188
272
  default:
189
273
  throw new Error(`Unsupported ZIP compression for ${entry.path}.`);
190
274
  }
275
+ if (inflated.byteLength > limits.maxEntryUncompressedBytes) {
276
+ throw new Error(
277
+ `Invalid ZIP archive: entry ${entry.path} inflated size ${inflated.byteLength} exceeds limit ${limits.maxEntryUncompressedBytes}.`,
278
+ );
279
+ }
280
+ return inflated;
191
281
  }
192
282
 
193
283
  function mapCompressionMethod(method: number): OpcCompressionMethod {
@@ -28,14 +28,15 @@ export interface BoundaryAssoc {
28
28
  /**
29
29
  * Range anchor — flat shape `{ kind: "range", from, to, assoc }` after the
30
30
  * 2026-04-23 flat-wins collapse. Prior to the collapse this carried a nested
31
- * `{ range: DocRange, assoc }` shape. See `canonical-document.ts` CanonicalAnchor
32
- * for the matching canonical shape + `repairCanonicalDocumentEnvelope` for the
33
- * persisted-snapshot legacy-to-flat migration.
31
+ * `{ range: DocRange, assoc }` shape. `range` remains as a readback-compatible
32
+ * mirror for review/package round-trip consumers that still inspect nested
33
+ * active ranges; `from` / `to` remain authoritative.
34
34
  */
35
35
  export interface RangeAnchor {
36
36
  kind: "range";
37
37
  from: Position;
38
38
  to: Position;
39
+ range?: DocRange;
39
40
  assoc: BoundaryAssoc;
40
41
  }
41
42
 
@@ -108,6 +108,43 @@ export interface CanonicalDocumentFragment {
108
108
  readonly blocks: BlockNode[];
109
109
  }
110
110
 
111
+ /**
112
+ * Durable source-package pointer for canonical facts that must reconcile with
113
+ * Word-first telemetry without exposing package XML or document text.
114
+ */
115
+ export interface CanonicalSourceRef {
116
+ readonly sourceId: string;
117
+ readonly partPath?: string;
118
+ readonly storyKind?: string;
119
+ readonly element?: string;
120
+ readonly ordinal?: number;
121
+ readonly startOffset?: number;
122
+ readonly endOffset?: number;
123
+ }
124
+
125
+ /**
126
+ * Redacted field evidence used by layout/runtime telemetry. Raw instruction
127
+ * and result text remain on editable field nodes and registry entries where
128
+ * they already exist; this shape is the hash-addressable comparison surface.
129
+ */
130
+ export interface CanonicalFieldEvidenceRef {
131
+ readonly sourceRef?: CanonicalSourceRef;
132
+ readonly instructionHash?: string;
133
+ readonly resultHash?: string;
134
+ readonly locked?: boolean;
135
+ readonly dirty?: boolean;
136
+ }
137
+
138
+ export interface CanonicalHyperlinkFieldCarrier {
139
+ readonly sourceRef?: CanonicalSourceRef;
140
+ readonly fieldFamily?: FieldFamily;
141
+ readonly refreshStatus?: FieldRefreshStatus;
142
+ readonly instructionHash?: string;
143
+ readonly resultHash?: string;
144
+ readonly locked?: boolean;
145
+ readonly dirty?: boolean;
146
+ }
147
+
111
148
  /**
112
149
  * Draft-only mutable view of `CanonicalDocumentFragment`. Same
113
150
  * semantics as {@link Mutable}. Used at fragment-construction sites;
@@ -370,6 +407,8 @@ export interface NumPicBullet {
370
407
 
371
408
  export interface AbstractNumberingDefinition {
372
409
  readonly abstractNumberingId: string;
410
+ /** Source numbering-part pointer for Word telemetry joins. */
411
+ readonly sourceRef?: CanonicalSourceRef;
373
412
  readonly levels: NumberingLevelDefinition[];
374
413
  readonly nsid?: string;
375
414
  readonly multiLevelType?: "singleLevel" | "multilevel" | "hybridMultilevel";
@@ -421,6 +460,8 @@ export interface NumberingLevelOverrideDefinition {
421
460
 
422
461
  export interface NumberingInstance {
423
462
  readonly numberingInstanceId: string;
463
+ /** Source numbering-part pointer for Word telemetry joins. */
464
+ readonly sourceRef?: CanonicalSourceRef;
424
465
  readonly abstractNumberingId: string;
425
466
  readonly overrides: NumberingLevelOverride[];
426
467
  }
@@ -1049,9 +1090,12 @@ export interface CanonicalFontEntry {
1049
1090
 
1050
1091
  export interface ParagraphNode {
1051
1092
  readonly type: "paragraph";
1093
+ readonly sourceRef?: CanonicalSourceRef;
1052
1094
  readonly styleId?: string;
1053
1095
  numbering?: {
1054
1096
  readonly numberingInstanceId: string;
1097
+ /** Source `<w:numPr>` pointer for Word telemetry joins. */
1098
+ readonly sourceRef?: CanonicalSourceRef;
1055
1099
  readonly level: number;
1056
1100
  };
1057
1101
  readonly alignment?: "left" | "center" | "right" | "both" | "distribute";
@@ -1230,6 +1274,7 @@ export interface TableStyleFormatting {
1230
1274
 
1231
1275
  export interface TableNode {
1232
1276
  readonly type: "table";
1277
+ readonly sourceRef?: CanonicalSourceRef;
1233
1278
  readonly styleId?: string;
1234
1279
  /** @deprecated Use `unknownPropertyChildren`. Kept for snapshot back-compat. */
1235
1280
  readonly propertiesXml?: string;
@@ -1358,6 +1403,7 @@ export type PreserveOnlyFieldFamily =
1358
1403
  | "TIME"
1359
1404
  | "AUTHOR"
1360
1405
  | "FILENAME"
1406
+ | "HYPERLINK"
1361
1407
  | "MERGEFIELD"
1362
1408
  | "IF"
1363
1409
  | "SEQ"
@@ -1381,14 +1427,20 @@ export type FieldRefreshStatus =
1381
1427
  export interface FieldNode {
1382
1428
  readonly type: "field";
1383
1429
  readonly fieldType: "simple" | "complex";
1430
+ /** Stable canonical field identity for source/runtime telemetry joins. */
1431
+ readonly canonicalFieldId?: string;
1384
1432
  readonly instruction: string;
1385
1433
  readonly children: InlineNode[];
1434
+ readonly sourceRef?: CanonicalSourceRef;
1386
1435
  /** Classified field family. Undefined for legacy snapshots. */
1387
1436
  readonly fieldFamily?: FieldFamily;
1388
1437
  /** Target bookmark name for REF/PAGEREF/NOTEREF fields. */
1389
1438
  readonly fieldTarget?: string;
1390
1439
  /** Runtime refresh status. Undefined for legacy or preserve-only fields. */
1391
1440
  readonly refreshStatus?: FieldRefreshStatus;
1441
+ /** Word field state flags when parsed from source package evidence. */
1442
+ readonly locked?: boolean;
1443
+ readonly dirty?: boolean;
1392
1444
  /** Parsed field switches. Present only when the instruction contains recognized switches. */
1393
1445
  switches?: {
1394
1446
  readonly hyperlink?: boolean;
@@ -1460,6 +1512,8 @@ export interface FieldRegistry {
1460
1512
  export interface FieldRegistryEntry {
1461
1513
  /** Stable document-order index of this field (0-based). */
1462
1514
  readonly fieldIndex: number;
1515
+ /** Stable canonical field identity for source/runtime telemetry joins. */
1516
+ readonly canonicalFieldId?: string;
1463
1517
  /** Classified field family. */
1464
1518
  readonly fieldFamily: FieldFamily;
1465
1519
  /** Whether the field is in the supported refresh slice. */
@@ -1474,8 +1528,13 @@ export interface FieldRegistryEntry {
1474
1528
  readonly paragraphIndex: number;
1475
1529
  /** Stable story key for the story that owns this field instance. */
1476
1530
  readonly storyKey?: string;
1531
+ /** Source-package pointer for joining this field to Word oracle telemetry. */
1532
+ readonly sourceRef?: CanonicalSourceRef;
1477
1533
  /** Runtime refresh status. */
1478
1534
  readonly refreshStatus: FieldRefreshStatus;
1535
+ /** Word field state flags when parsed from source package evidence. */
1536
+ readonly locked?: boolean;
1537
+ readonly dirty?: boolean;
1479
1538
  /** Parsed field switches carried from FieldNode.switches. Present only for REF/PAGEREF/NOTEREF/TOC entries with recognized switches. */
1480
1539
  readonly switches?: FieldNode["switches"];
1481
1540
  }
@@ -1813,12 +1872,16 @@ export interface SymbolNode {
1813
1872
  export interface HyperlinkNode {
1814
1873
  readonly type: "hyperlink";
1815
1874
  readonly href: string;
1875
+ readonly sourceRef?: CanonicalSourceRef;
1876
+ /** Treats `<w:hyperlink>` as a Word field carrier without forcing raw text into telemetry. */
1877
+ readonly fieldCarrier?: CanonicalHyperlinkFieldCarrier;
1816
1878
  readonly children: Array<TextNode | HardBreakNode | ColumnBreakNode | PageBreakNode | TabNode | SymbolNode>;
1817
1879
  }
1818
1880
 
1819
1881
  export interface ImageNode {
1820
1882
  readonly type: "image";
1821
1883
  readonly mediaId: string;
1884
+ readonly sourceRef?: CanonicalSourceRef;
1822
1885
  readonly altText?: string;
1823
1886
  readonly placementXml?: string;
1824
1887
  readonly display?: "inline" | "floating";
@@ -2150,6 +2213,7 @@ export interface TextBoxBodyProperties {
2150
2213
 
2151
2214
  export interface DrawingFrameNode {
2152
2215
  readonly type: "drawing_frame";
2216
+ readonly sourceRef?: CanonicalSourceRef;
2153
2217
  readonly anchor: AnchorGeometry;
2154
2218
  content:
2155
2219
  | PictureContent
@@ -2251,7 +2315,13 @@ export interface CommentThreadMetadata {
2251
2315
  }
2252
2316
 
2253
2317
  export interface RevisionPropertyChangeData {
2254
- readonly xmlTag: "pPrChange" | "sectPrChange" | "tblPrChange" | "rPrChange";
2318
+ readonly xmlTag:
2319
+ | "pPrChange"
2320
+ | "sectPrChange"
2321
+ | "tblPrChange"
2322
+ | "trPrChange"
2323
+ | "tcPrChange"
2324
+ | "rPrChange";
2255
2325
  readonly beforeXml: string;
2256
2326
  }
2257
2327
 
@@ -2868,6 +2938,13 @@ function validateNumberingCatalog(
2868
2938
  message: "abstractNumberingId must match the map key.",
2869
2939
  });
2870
2940
  }
2941
+ if (definitionRecord?.sourceRef !== undefined) {
2942
+ validateCanonicalSourceRef(
2943
+ definitionRecord.sourceRef,
2944
+ `${path}.abstractDefinitions.${abstractId}.sourceRef`,
2945
+ issues,
2946
+ );
2947
+ }
2871
2948
  }
2872
2949
  }
2873
2950
 
@@ -2900,6 +2977,13 @@ function validateNumberingCatalog(
2900
2977
  `${path}.instances.${instanceId}.abstractNumberingId`,
2901
2978
  issues,
2902
2979
  );
2980
+ if (instanceRecord.sourceRef !== undefined) {
2981
+ validateCanonicalSourceRef(
2982
+ instanceRecord.sourceRef,
2983
+ `${path}.instances.${instanceId}.sourceRef`,
2984
+ issues,
2985
+ );
2986
+ }
2903
2987
  }
2904
2988
  }
2905
2989
  }
@@ -2979,6 +3063,9 @@ function validateDocumentNode(
2979
3063
  if (type === "paragraph" && record.styleId !== undefined) {
2980
3064
  expectDomainString(record.styleId, "styleId", `${path}.styleId`, issues);
2981
3065
  }
3066
+ if (type === "paragraph" && record.sourceRef !== undefined) {
3067
+ validateCanonicalSourceRef(record.sourceRef, `${path}.sourceRef`, issues);
3068
+ }
2982
3069
  if (type === "paragraph" && record.numbering !== undefined) {
2983
3070
  const numbering = asPlainObject(record.numbering, `${path}.numbering`, issues);
2984
3071
  if (numbering) {
@@ -2988,6 +3075,9 @@ function validateDocumentNode(
2988
3075
  `${path}.numbering.numberingInstanceId`,
2989
3076
  issues,
2990
3077
  );
3078
+ if (numbering.sourceRef !== undefined) {
3079
+ validateCanonicalSourceRef(numbering.sourceRef, `${path}.numbering.sourceRef`, issues);
3080
+ }
2991
3081
  if (typeof numbering.level !== "number") {
2992
3082
  issues.push({
2993
3083
  path: `${path}.numbering.level`,
@@ -2998,6 +3088,12 @@ function validateDocumentNode(
2998
3088
  }
2999
3089
  if (type === "hyperlink") {
3000
3090
  expectString(record.href, `${path}.href`, issues);
3091
+ if (record.sourceRef !== undefined) {
3092
+ validateCanonicalSourceRef(record.sourceRef, `${path}.sourceRef`, issues);
3093
+ }
3094
+ if (record.fieldCarrier !== undefined) {
3095
+ validateHyperlinkFieldCarrier(record.fieldCarrier, `${path}.fieldCarrier`, issues);
3096
+ }
3001
3097
  }
3002
3098
  return;
3003
3099
  case "alt_chunk":
@@ -3005,6 +3101,9 @@ function validateDocumentNode(
3005
3101
  return;
3006
3102
  case "image":
3007
3103
  expectDomainString(record.mediaId, "mediaId", `${path}.mediaId`, issues);
3104
+ if (record.sourceRef !== undefined) {
3105
+ validateCanonicalSourceRef(record.sourceRef, `${path}.sourceRef`, issues);
3106
+ }
3008
3107
  if (record.placementXml !== undefined) {
3009
3108
  expectString(record.placementXml, `${path}.placementXml`, issues);
3010
3109
  }
@@ -3030,6 +3129,9 @@ function validateDocumentNode(
3030
3129
  }
3031
3130
  return;
3032
3131
  case "table":
3132
+ if (record.sourceRef !== undefined) {
3133
+ validateCanonicalSourceRef(record.sourceRef, `${path}.sourceRef`, issues);
3134
+ }
3033
3135
  if (!Array.isArray(record.gridColumns)) {
3034
3136
  issues.push({ path: `${path}.gridColumns`, message: "gridColumns must be an array." });
3035
3137
  }
@@ -3061,7 +3163,15 @@ function validateDocumentNode(
3061
3163
  return;
3062
3164
  case "field":
3063
3165
  expectString(record.fieldType, `${path}.fieldType`, issues);
3166
+ if (record.canonicalFieldId !== undefined) {
3167
+ expectString(record.canonicalFieldId, `${path}.canonicalFieldId`, issues);
3168
+ }
3064
3169
  expectString(record.instruction, `${path}.instruction`, issues);
3170
+ if (record.sourceRef !== undefined) {
3171
+ validateCanonicalSourceRef(record.sourceRef, `${path}.sourceRef`, issues);
3172
+ }
3173
+ validateOptionalBoolean(record.locked, `${path}.locked`, issues);
3174
+ validateOptionalBoolean(record.dirty, `${path}.dirty`, issues);
3065
3175
  if (!Array.isArray(record.children)) {
3066
3176
  issues.push({ path: `${path}.children`, message: "children must be an array." });
3067
3177
  } else {
@@ -3137,6 +3247,9 @@ function validateDocumentNode(
3137
3247
  }
3138
3248
  return;
3139
3249
  case "drawing_frame": {
3250
+ if (record.sourceRef !== undefined) {
3251
+ validateCanonicalSourceRef(record.sourceRef, `${path}.sourceRef`, issues);
3252
+ }
3140
3253
  const anchor = asPlainObject(record.anchor, `${path}.anchor`, issues);
3141
3254
  const content = asPlainObject(record.content, `${path}.content`, issues);
3142
3255
  if (anchor) {
@@ -3230,6 +3343,99 @@ function validateDocumentNode(
3230
3343
  }
3231
3344
  }
3232
3345
 
3346
+ function validateOptionalBoolean(
3347
+ value: unknown,
3348
+ path: string,
3349
+ issues: ModelValidationIssue[],
3350
+ ): void {
3351
+ if (value !== undefined && typeof value !== "boolean") {
3352
+ issues.push({ path, message: "must be a boolean when present." });
3353
+ }
3354
+ }
3355
+
3356
+ function validateCanonicalSourceRef(
3357
+ value: unknown,
3358
+ path: string,
3359
+ issues: ModelValidationIssue[],
3360
+ ): void {
3361
+ const record = asPlainObject(value, path, issues);
3362
+ if (!record) {
3363
+ return;
3364
+ }
3365
+
3366
+ expectString(record.sourceId, `${path}.sourceId`, issues);
3367
+
3368
+ for (const key of ["partPath", "storyKind", "element"] as const) {
3369
+ if (record[key] !== undefined) {
3370
+ expectString(record[key], `${path}.${key}`, issues);
3371
+ }
3372
+ }
3373
+
3374
+ for (const key of ["ordinal", "startOffset", "endOffset"] as const) {
3375
+ if (record[key] !== undefined && (!Number.isInteger(record[key]) || (record[key] as number) < 0)) {
3376
+ issues.push({
3377
+ path: `${path}.${key}`,
3378
+ message: `${key} must be a non-negative integer when present.`,
3379
+ });
3380
+ }
3381
+ }
3382
+ }
3383
+
3384
+ function validateHashRef(
3385
+ value: unknown,
3386
+ path: string,
3387
+ issues: ModelValidationIssue[],
3388
+ ): void {
3389
+ if (value === undefined) return;
3390
+ const hash = expectString(value, path, issues);
3391
+ if (hash && !/^[A-Fa-f0-9]{64}$/.test(hash)) {
3392
+ issues.push({
3393
+ path,
3394
+ message: "hash must be a 64-character hex SHA-256 digest.",
3395
+ });
3396
+ }
3397
+ }
3398
+
3399
+ function validateHyperlinkFieldCarrier(
3400
+ value: unknown,
3401
+ path: string,
3402
+ issues: ModelValidationIssue[],
3403
+ ): void {
3404
+ const record = asPlainObject(value, path, issues);
3405
+ if (!record) {
3406
+ return;
3407
+ }
3408
+
3409
+ if (record.sourceRef !== undefined) {
3410
+ validateCanonicalSourceRef(record.sourceRef, `${path}.sourceRef`, issues);
3411
+ }
3412
+ if (
3413
+ record.fieldFamily !== undefined &&
3414
+ !(
3415
+ SUPPORTED_FIELD_FAMILIES.has(record.fieldFamily as SupportedFieldFamily) ||
3416
+ PRESERVE_ONLY_FIELD_FAMILIES.has(record.fieldFamily as PreserveOnlyFieldFamily)
3417
+ )
3418
+ ) {
3419
+ issues.push({
3420
+ path: `${path}.fieldFamily`,
3421
+ message: "fieldFamily must be a SupportedFieldFamily or PreserveOnlyFieldFamily when present.",
3422
+ });
3423
+ }
3424
+ if (
3425
+ record.refreshStatus !== undefined &&
3426
+ !FIELD_REFRESH_STATUSES.has(record.refreshStatus as FieldRefreshStatus)
3427
+ ) {
3428
+ issues.push({
3429
+ path: `${path}.refreshStatus`,
3430
+ message: "refreshStatus must be one of: current, stale, unresolvable, preserve-only.",
3431
+ });
3432
+ }
3433
+ validateHashRef(record.instructionHash, `${path}.instructionHash`, issues);
3434
+ validateHashRef(record.resultHash, `${path}.resultHash`, issues);
3435
+ validateOptionalBoolean(record.locked, `${path}.locked`, issues);
3436
+ validateOptionalBoolean(record.dirty, `${path}.dirty`, issues);
3437
+ }
3438
+
3233
3439
  function validateFloatingImageProperties(
3234
3440
  value: unknown,
3235
3441
  path: string,
@@ -4290,6 +4496,7 @@ const PRESERVE_ONLY_FIELD_FAMILIES: ReadonlySet<PreserveOnlyFieldFamily> = new S
4290
4496
  "TIME",
4291
4497
  "AUTHOR",
4292
4498
  "FILENAME",
4499
+ "HYPERLINK",
4293
4500
  "MERGEFIELD",
4294
4501
  "IF",
4295
4502
  "SEQ",
@@ -4381,6 +4588,10 @@ function validateFieldRegistryEntry(
4381
4588
  });
4382
4589
  }
4383
4590
 
4591
+ if (record.canonicalFieldId !== undefined) {
4592
+ expectString(record.canonicalFieldId, `${path}.canonicalFieldId`, issues);
4593
+ }
4594
+
4384
4595
  if (
4385
4596
  typeof record.fieldFamily !== "string" ||
4386
4597
  !(
@@ -4419,6 +4630,10 @@ function validateFieldRegistryEntry(
4419
4630
  expectString(record.storyKey, `${path}.storyKey`, issues);
4420
4631
  }
4421
4632
 
4633
+ if (record.sourceRef !== undefined) {
4634
+ validateCanonicalSourceRef(record.sourceRef, `${path}.sourceRef`, issues);
4635
+ }
4636
+
4422
4637
  if (
4423
4638
  typeof record.refreshStatus !== "string" ||
4424
4639
  !FIELD_REFRESH_STATUSES.has(record.refreshStatus as FieldRefreshStatus)
@@ -4426,9 +4641,12 @@ function validateFieldRegistryEntry(
4426
4641
  issues.push({
4427
4642
  path: `${path}.refreshStatus`,
4428
4643
  message:
4429
- "refreshStatus must be one of: current, stale, unresolvable, preserve-only.",
4644
+ "refreshStatus must be one of: current, stale, unresolvable, preserve-only.",
4430
4645
  });
4431
4646
  }
4647
+
4648
+ validateOptionalBoolean(record.locked, `${path}.locked`, issues);
4649
+ validateOptionalBoolean(record.dirty, `${path}.dirty`, issues);
4432
4650
  }
4433
4651
 
4434
4652
  function validateTocStructure(