@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
@@ -12,6 +12,7 @@ import type {
12
12
  TableRowNode,
13
13
  TextMark,
14
14
  Mutable,
15
+ CanonicalSourceRef,
15
16
  } from "../../model/canonical-document.ts";
16
17
  import type { LegacyFormFieldNode } from "../../model/canonical-document.ts";
17
18
  import { resolveHighlightColor } from "./highlight-colors.ts";
@@ -58,6 +59,7 @@ import { parseShapeXml, parseVmlXml } from "./parse-shapes.ts";
58
59
 
59
60
  const TAB_ALIGN_VOCAB = new Set<TabStop["align"]>(["left", "center", "right", "decimal", "num", "bar", "clear"]);
60
61
  const TAB_LEADER_VOCAB = new Set<TabStop["leader"]>(["none", "dot", "hyphen", "underscore", "heavy", "middleDot"]);
62
+ let activeHeaderFooterFieldSequenceOrdinal = 0;
61
63
 
62
64
  // ---- Public types ----
63
65
 
@@ -229,6 +231,7 @@ function parseHdrFtrXml(
229
231
  opts: ParseHeaderFooterOpts = { relationships: [] },
230
232
  ): ParsedHeaderFooterDocument {
231
233
  currentSourceXml = xml;
234
+ activeHeaderFooterFieldSequenceOrdinal = 0;
232
235
  let root: XmlElementNode;
233
236
  try {
234
237
  root = parseXml(xml) as XmlElementNode;
@@ -326,11 +329,12 @@ function parseParagraphElement(
326
329
  "complex",
327
330
  activeComplexField.children,
328
331
  activeComplexField.legacyFormField,
332
+ activeComplexField.sourceRef,
329
333
  ),
330
334
  );
331
335
  activeComplexField = null;
332
336
  }
333
- pushFieldNode(children, child, "simple");
337
+ pushFieldNode(children, child, "simple", opts.sourcePartPath);
334
338
  } else if (name === "sdt") {
335
339
  // coord-11 §22 — structured-document-tag wrapping run-level content
336
340
  // inside a header/footer paragraph. Word commonly uses these to
@@ -355,7 +359,7 @@ function parseParagraphElement(
355
359
  } else if (gname === "bookmarkStart" || gname === "bookmarkEnd") {
356
360
  children.push(parseBookmarkElement(grandchild));
357
361
  } else if (gname === "fldSimple") {
358
- pushFieldNode(children, grandchild, "simple");
362
+ pushFieldNode(children, grandchild, "simple", opts.sourcePartPath);
359
363
  }
360
364
  // Nested sdt / other elements ignored — deeper nesting is rare
361
365
  // enough that opaque round-trip via the block-level sdt parser
@@ -372,6 +376,7 @@ function parseParagraphElement(
372
376
  "complex",
373
377
  activeComplexField.children,
374
378
  activeComplexField.legacyFormField,
379
+ activeComplexField.sourceRef,
375
380
  ),
376
381
  );
377
382
  }
@@ -391,6 +396,7 @@ interface ActiveComplexField {
391
396
  instruction: string;
392
397
  children: Array<Extract<InlineNode, { type: "text" | "hard_break" | "tab" }>>;
393
398
  mode: "instruction" | "result";
399
+ sourceRef?: CanonicalSourceRef;
394
400
  legacyFormField?: LegacyFormFieldNode;
395
401
  }
396
402
 
@@ -419,6 +425,7 @@ function appendRunNodes(
419
425
  instruction: "",
420
426
  children: [],
421
427
  mode: "instruction",
428
+ sourceRef: createFieldSequenceSourceRef(opts.sourcePartPath ?? "/word/document.xml", rElement),
422
429
  ...(legacyFormField ? { legacyFormField } : {}),
423
430
  };
424
431
  } else if (fldType === "separate" && activeComplexField) {
@@ -431,6 +438,7 @@ function appendRunNodes(
431
438
  "complex",
432
439
  activeComplexField.children,
433
440
  activeComplexField.legacyFormField,
441
+ activeComplexField.sourceRef,
434
442
  ),
435
443
  );
436
444
  }
@@ -443,7 +451,7 @@ function appendRunNodes(
443
451
  if (activeComplexField) {
444
452
  activeComplexField.instruction += extractTextContent(child);
445
453
  } else {
446
- pushFieldNode(nodes, child, "complex");
454
+ pushFieldNode(nodes, child, "complex", opts.sourcePartPath);
447
455
  }
448
456
  continue;
449
457
  }
@@ -522,7 +530,7 @@ function parseRunElement(
522
530
  } else if (name === "bookmarkStart" || name === "bookmarkEnd") {
523
531
  nodes.push(parseBookmarkElement(child));
524
532
  } else if (name === "instrText") {
525
- pushFieldNode(nodes, child, "complex");
533
+ pushFieldNode(nodes, child, "complex", opts.sourcePartPath);
526
534
  } else if (name === "drawing") {
527
535
  const drawingXml = currentSourceXml.slice(child.start, child.end);
528
536
  const drawingResult = parseDrawingInlineNode(drawingXml, opts);
@@ -824,6 +832,7 @@ function pushFieldNode(
824
832
  nodes: InlineNode[],
825
833
  element: XmlElementNode,
826
834
  fieldType: "simple" | "complex",
835
+ sourcePartPath = "/word/document.xml",
827
836
  ): void {
828
837
  const instruction = readFieldInstruction(element);
829
838
  if (!instruction) {
@@ -832,7 +841,13 @@ function pushFieldNode(
832
841
 
833
842
  const classification = classifyFieldInstruction(instruction);
834
843
 
835
- nodes.push(createFieldNode(instruction, fieldType));
844
+ nodes.push(createFieldNode(
845
+ instruction,
846
+ fieldType,
847
+ [],
848
+ undefined,
849
+ createFieldSequenceSourceRef(sourcePartPath, element),
850
+ ));
836
851
  }
837
852
 
838
853
  function createFieldNode(
@@ -840,6 +855,7 @@ function createFieldNode(
840
855
  fieldType: "simple" | "complex",
841
856
  children: InlineNode[] = [],
842
857
  legacyFormField?: LegacyFormFieldNode,
858
+ sourceRef?: CanonicalSourceRef,
843
859
  ): Extract<InlineNode, { type: "field" }> {
844
860
  const classification = classifyFieldInstruction(instruction);
845
861
  return {
@@ -850,10 +866,27 @@ function createFieldNode(
850
866
  fieldFamily: classification.family,
851
867
  ...(classification.target ? { fieldTarget: classification.target } : {}),
852
868
  refreshStatus: classification.supported ? "stale" : "preserve-only",
869
+ ...(sourceRef ? { sourceRef } : {}),
853
870
  ...(legacyFormField ? { legacyFormField } : {}),
854
871
  };
855
872
  }
856
873
 
874
+ function createFieldSequenceSourceRef(
875
+ partPath: string,
876
+ node: XmlElementNode,
877
+ ): CanonicalSourceRef {
878
+ activeHeaderFooterFieldSequenceOrdinal += 1;
879
+ return {
880
+ sourceId: `part:${partPath}#fieldSequence:${activeHeaderFooterFieldSequenceOrdinal}`,
881
+ partPath,
882
+ storyKind: /\/footer\d+\.xml$/i.test(partPath) ? "footer" : "header",
883
+ element: "fieldSequence",
884
+ ordinal: activeHeaderFooterFieldSequenceOrdinal,
885
+ startOffset: node.start,
886
+ endOffset: node.end,
887
+ };
888
+ }
889
+
857
890
  function readFieldInstruction(element: XmlElementNode): string | undefined {
858
891
  const instruction =
859
892
  readStringAttr(element, "w:instr") ??
@@ -20,6 +20,7 @@ import type {
20
20
  PageSize,
21
21
  PageMargins,
22
22
  ColumnProperties,
23
+ CanonicalSourceRef,
23
24
  PageNumbering,
24
25
  SectionDocumentGrid,
25
26
  SectionLineNumbering,
@@ -28,6 +29,7 @@ import type {
28
29
  UnknownPropertyChild,
29
30
  Mutable,
30
31
  } from "../../model/canonical-document.ts";
32
+ import { sha256TextHex } from "../../model/canonical-hash.ts";
31
33
  import type { OpcRelationship } from "./part-manifest.ts";
32
34
  import { SCOPE_MARKER_BOOKMARK_PREFIX } from "./parse-scope-markers.ts";
33
35
  import {
@@ -313,8 +315,10 @@ export interface ParsedParagraphNode {
313
315
  styleId?: string;
314
316
  numbering?: {
315
317
  numberingInstanceId: string;
318
+ sourceRef?: CanonicalSourceRef;
316
319
  level: number;
317
320
  };
321
+ sourceRef?: CanonicalSourceRef;
318
322
  alignment?: "left" | "center" | "right" | "both" | "distribute";
319
323
  spacing?: ParagraphSpacing;
320
324
  contextualSpacing?: boolean;
@@ -438,6 +442,16 @@ export interface ParsedImageNode {
438
442
  export interface ParsedHyperlinkNode {
439
443
  type: "hyperlink";
440
444
  href: string;
445
+ sourceRef?: CanonicalSourceRef;
446
+ fieldCarrier?: {
447
+ sourceRef?: CanonicalSourceRef;
448
+ fieldFamily?: ReturnType<typeof classifyFieldInstruction>["family"];
449
+ refreshStatus?: "current" | "stale" | "unresolvable" | "preserve-only";
450
+ locked?: boolean;
451
+ dirty?: boolean;
452
+ instructionHash?: string;
453
+ resultHash?: string;
454
+ };
441
455
  children: Array<ParsedTextNode | ParsedBreakNode | ParsedColumnBreakNode | ParsedPageBreakNode | ParsedTabNode | ParsedSymbolNode>;
442
456
  rawXml: string;
443
457
  }
@@ -545,6 +559,7 @@ export interface ParsedFieldInlineNode {
545
559
  contentXml?: string;
546
560
  children?: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
547
561
  rawXml: string;
562
+ sourceRef?: CanonicalSourceRef;
548
563
  legacyFormField?: import("../../model/canonical-document.ts").LegacyFormFieldNode;
549
564
  }
550
565
 
@@ -724,6 +739,7 @@ const HYPERLINK_RELATIONSHIP_TYPE =
724
739
  * not exist; `parseSdtXml` is only callable externally via `parsePictureSdt`.
725
740
  */
726
741
  let activeChartPartLookup: ChartPartLookup | undefined;
742
+ let activeFieldSequenceOrdinal = 0;
727
743
 
728
744
  /**
729
745
  * Phase 1 telemetry — optional bus installed by the runtime before a load.
@@ -838,6 +854,7 @@ export function parseMainDocumentXml(
838
854
  activeCosmeticStripContext = stripContext;
839
855
  const bus = activeParseTelemetryBus;
840
856
  const started = bus?.isEnabled("parse") ? performanceNow() : 0;
857
+ activeFieldSequenceOrdinal = 0;
841
858
  try {
842
859
  const result = parseMainDocumentXmlInner(xml, relationships, mediaParts, sourcePartPath);
843
860
  if (Object.keys(stripContext.counts).length > 0) {
@@ -850,6 +867,7 @@ export function parseMainDocumentXml(
850
867
  } finally {
851
868
  activeChartPartLookup = undefined;
852
869
  activeCosmeticStripContext = null;
870
+ activeFieldSequenceOrdinal = 0;
853
871
  }
854
872
  }
855
873
 
@@ -1266,7 +1284,7 @@ function parseBodyChild(
1266
1284
  switch (localName(child.name)) {
1267
1285
  case "pPr":
1268
1286
  styleId = readParagraphStyleId(child);
1269
- numbering = readParagraphNumbering(child);
1287
+ numbering = readParagraphNumbering(child, sourcePartPath);
1270
1288
  alignment = readParagraphAlignment(child);
1271
1289
  spacing = readParagraphSpacing(child);
1272
1290
  contextualSpacing = readOptionalOnOffParagraphProperty(child, "contextualSpacing");
@@ -1307,7 +1325,7 @@ function parseBodyChild(
1307
1325
  flushActiveComplexField(children, () => {
1308
1326
  activeComplexField = null;
1309
1327
  }, activeComplexField);
1310
- const hyperlink = parseHyperlink(child, sourceXml, relationshipMap);
1328
+ const hyperlink = parseHyperlink(child, sourceXml, relationshipMap, {}, sourcePartPath);
1311
1329
  children.push(hyperlink);
1312
1330
  break;
1313
1331
  }
@@ -1400,6 +1418,7 @@ function parseBodyChild(
1400
1418
  instruction: fieldInstr,
1401
1419
  contentXml: fieldContentXml,
1402
1420
  rawXml: sourceXml.slice(child.start, child.end),
1421
+ sourceRef: createFieldSequenceSourceRef(sourcePartPath, child),
1403
1422
  } as ParsedFieldInlineNode);
1404
1423
  break;
1405
1424
  }
@@ -1481,6 +1500,7 @@ function parseBodyChild(
1481
1500
 
1482
1501
  return {
1483
1502
  type: "paragraph",
1503
+ sourceRef: createElementSourceRef(sourcePartPath, "p", node),
1484
1504
  styleId,
1485
1505
  ...(numbering ? { numbering } : {}),
1486
1506
  ...(alignment ? { alignment } : {}),
@@ -1514,6 +1534,7 @@ function appendParagraphRunNodes(
1514
1534
  instruction: string;
1515
1535
  children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
1516
1536
  mode: "instruction" | "result";
1537
+ sourceRef?: CanonicalSourceRef;
1517
1538
  legacyFormField?: import("../../model/canonical-document.ts").LegacyFormFieldNode;
1518
1539
  } | null,
1519
1540
  sourceXml: string,
@@ -1524,6 +1545,7 @@ function appendParagraphRunNodes(
1524
1545
  instruction: string;
1525
1546
  children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
1526
1547
  mode: "instruction" | "result";
1548
+ sourceRef?: CanonicalSourceRef;
1527
1549
  legacyFormField?: import("../../model/canonical-document.ts").LegacyFormFieldNode;
1528
1550
  } | null {
1529
1551
  const hasFieldMarkers = node.children.some(
@@ -1565,6 +1587,7 @@ function appendParagraphRunNodes(
1565
1587
  instruction: "",
1566
1588
  children: [],
1567
1589
  mode: "instruction",
1590
+ sourceRef: createFieldSequenceSourceRef(sourcePartPath, node),
1568
1591
  ...(legacyFormField ? { legacyFormField } : {}),
1569
1592
  };
1570
1593
  } else if (fldType === "separate" && activeComplexField) {
@@ -1591,6 +1614,7 @@ function appendParagraphRunNodes(
1591
1614
  instruction,
1592
1615
  children: [],
1593
1616
  rawXml: sourceXml.slice(node.start, node.end),
1617
+ sourceRef: createFieldSequenceSourceRef(sourcePartPath, node),
1594
1618
  });
1595
1619
  }
1596
1620
  continue;
@@ -1607,6 +1631,7 @@ function flushActiveComplexField(
1607
1631
  instruction: string;
1608
1632
  children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
1609
1633
  mode: "instruction" | "result";
1634
+ sourceRef?: CanonicalSourceRef;
1610
1635
  legacyFormField?: import("../../model/canonical-document.ts").LegacyFormFieldNode;
1611
1636
  } | null,
1612
1637
  ): void {
@@ -1620,6 +1645,7 @@ function flushActiveComplexField(
1620
1645
  instruction: activeComplexField.instruction,
1621
1646
  children: activeComplexField.children,
1622
1647
  rawXml: "",
1648
+ ...(activeComplexField.sourceRef ? { sourceRef: activeComplexField.sourceRef } : {}),
1623
1649
  ...(activeComplexField.legacyFormField ? { legacyFormField: activeComplexField.legacyFormField } : {}),
1624
1650
  });
1625
1651
  reset();
@@ -2547,6 +2573,7 @@ function readParagraphStyleId(node: XmlElementNode): string | undefined {
2547
2573
 
2548
2574
  function readParagraphNumbering(
2549
2575
  node: XmlElementNode,
2576
+ sourcePartPath: string,
2550
2577
  ): ParsedParagraphNode["numbering"] | undefined {
2551
2578
  const numberingProperties = node.children.find(
2552
2579
  (child): child is XmlElementNode =>
@@ -2572,10 +2599,50 @@ function readParagraphNumbering(
2572
2599
 
2573
2600
  return {
2574
2601
  numberingInstanceId: toCanonicalNumberingInstanceId(rawInstanceId),
2602
+ sourceRef: createElementSourceRef(sourcePartPath, "numPr", numberingProperties),
2575
2603
  level: Number.parseInt(rawLevel, 10),
2576
2604
  };
2577
2605
  }
2578
2606
 
2607
+ function createElementSourceRef(
2608
+ partPath: string,
2609
+ element: string,
2610
+ node: XmlElementNode,
2611
+ ): CanonicalSourceRef {
2612
+ return {
2613
+ sourceId: `part:${partPath}#${element}:${node.start}`,
2614
+ partPath,
2615
+ storyKind: storyKindFromPartPath(partPath),
2616
+ element,
2617
+ startOffset: node.start,
2618
+ endOffset: node.end,
2619
+ };
2620
+ }
2621
+
2622
+ function createFieldSequenceSourceRef(
2623
+ partPath: string,
2624
+ node: XmlElementNode,
2625
+ ): CanonicalSourceRef {
2626
+ activeFieldSequenceOrdinal += 1;
2627
+ return {
2628
+ sourceId: `part:${partPath}#fieldSequence:${activeFieldSequenceOrdinal}`,
2629
+ partPath,
2630
+ storyKind: storyKindFromPartPath(partPath),
2631
+ element: "fieldSequence",
2632
+ ordinal: activeFieldSequenceOrdinal,
2633
+ startOffset: node.start,
2634
+ endOffset: node.end,
2635
+ };
2636
+ }
2637
+
2638
+ function storyKindFromPartPath(partPath: string): string {
2639
+ if (/\/header\d+\.xml$/i.test(partPath)) return "header";
2640
+ if (/\/footer\d+\.xml$/i.test(partPath)) return "footer";
2641
+ if (/\/footnotes\.xml$/i.test(partPath)) return "footnote";
2642
+ if (/\/endnotes\.xml$/i.test(partPath)) return "endnote";
2643
+ return "main";
2644
+ }
2645
+
2579
2646
  function parseRun(
2580
2647
  node: XmlElementNode,
2581
2648
  sourceXml: string,
@@ -2916,10 +2983,16 @@ function parseRevisionContainer(
2916
2983
  break;
2917
2984
  }
2918
2985
  case "hyperlink": {
2919
- const hyperlink = parseHyperlink(child, sourceXml, relationshipMap, {
2920
- allowDeletedText: allowsDeletedText,
2921
- preserveUnsupportedReviewMarkup: true,
2922
- });
2986
+ const hyperlink = parseHyperlink(
2987
+ child,
2988
+ sourceXml,
2989
+ relationshipMap,
2990
+ {
2991
+ allowDeletedText: allowsDeletedText,
2992
+ preserveUnsupportedReviewMarkup: true,
2993
+ },
2994
+ "/word/document.xml",
2995
+ );
2923
2996
  if (hyperlink.type === "opaque_inline") {
2924
2997
  return [
2925
2998
  {
@@ -3020,6 +3093,7 @@ function parseHyperlink(
3020
3093
  allowDeletedText?: boolean;
3021
3094
  preserveUnsupportedReviewMarkup?: boolean;
3022
3095
  } = {},
3096
+ sourcePartPath = "/word/document.xml",
3023
3097
  ): ParsedHyperlinkNode | ParsedOpaqueInlineNode {
3024
3098
  const relationshipId = node.attributes["r:id"] ?? node.attributes.id;
3025
3099
  const anchor = node.attributes["w:anchor"] ?? node.attributes.anchor;
@@ -3058,6 +3132,10 @@ function parseHyperlink(
3058
3132
  // escape the hyperlink scope.
3059
3133
  type FieldBracketMode = "outside" | "instruction" | "result";
3060
3134
  let bracketMode: FieldBracketMode = "outside";
3135
+ let fieldSourceRef: CanonicalSourceRef | undefined;
3136
+ let fieldInstruction = "";
3137
+ let fieldLocked: boolean | undefined;
3138
+ let fieldDirty: boolean | undefined;
3061
3139
 
3062
3140
  for (const child of node.children) {
3063
3141
  if (child.type !== "element") {
@@ -3081,9 +3159,29 @@ function parseHyperlink(
3081
3159
  if (fldChar) {
3082
3160
  const fldType =
3083
3161
  fldChar.attributes["w:fldCharType"] ?? fldChar.attributes.fldCharType;
3084
- if (fldType === "begin") bracketMode = "instruction";
3085
- else if (fldType === "separate") bracketMode = "result";
3086
- else if (fldType === "end") bracketMode = "outside";
3162
+ if (fldType === "begin") {
3163
+ bracketMode = "instruction";
3164
+ fieldSourceRef = createFieldSequenceSourceRef(sourcePartPath, child);
3165
+ fieldInstruction = "";
3166
+ fieldLocked = readOnOffFieldFlag(fldChar, "fldLock");
3167
+ fieldDirty = readOnOffFieldFlag(fldChar, "dirty");
3168
+ } else if (fldType === "separate") {
3169
+ bracketMode = "result";
3170
+ } else if (fldType === "end") {
3171
+ bracketMode = "outside";
3172
+ }
3173
+ continue;
3174
+ }
3175
+
3176
+ const instrText = child.children.find(
3177
+ (c): c is XmlElementNode =>
3178
+ c.type === "element" && localName(c.name) === "instrText",
3179
+ );
3180
+ if (instrText && bracketMode === "instruction") {
3181
+ fieldInstruction += instrText.children
3182
+ .filter((entry): entry is XmlTextNode => entry.type === "text")
3183
+ .map((entry) => entry.text)
3184
+ .join("");
3087
3185
  continue;
3088
3186
  }
3089
3187
 
@@ -3107,14 +3205,60 @@ function parseHyperlink(
3107
3205
  children.push(...run.nodes);
3108
3206
  }
3109
3207
 
3208
+ const fieldClassification = fieldInstruction.trim().length > 0
3209
+ ? classifyFieldInstruction(fieldInstruction)
3210
+ : undefined;
3110
3211
  return {
3111
3212
  type: "hyperlink",
3112
3213
  href,
3214
+ sourceRef: createElementSourceRef(sourcePartPath, "hyperlink", node),
3215
+ ...(fieldSourceRef
3216
+ ? {
3217
+ fieldCarrier: {
3218
+ sourceRef: fieldSourceRef,
3219
+ ...(fieldClassification ? { fieldFamily: fieldClassification.family } : {}),
3220
+ refreshStatus: fieldClassification?.supported
3221
+ ? "stale"
3222
+ : "preserve-only",
3223
+ ...(fieldInstruction ? { instructionHash: sha256TextHex(fieldInstruction) } : {}),
3224
+ ...(children.length > 0 ? { resultHash: sha256TextHex(hyperlinkFieldResultText(children)) } : {}),
3225
+ ...(fieldLocked !== undefined ? { locked: fieldLocked } : {}),
3226
+ ...(fieldDirty !== undefined ? { dirty: fieldDirty } : {}),
3227
+ },
3228
+ }
3229
+ : {}),
3113
3230
  children,
3114
3231
  rawXml: sourceXml.slice(node.start, node.end),
3115
3232
  };
3116
3233
  }
3117
3234
 
3235
+ function hyperlinkFieldResultText(children: readonly ParsedInlineNode[]): string {
3236
+ return children
3237
+ .map((child) => {
3238
+ switch (child.type) {
3239
+ case "text":
3240
+ return child.text;
3241
+ case "tab":
3242
+ return "\t";
3243
+ case "hard_break":
3244
+ return "\n";
3245
+ case "symbol":
3246
+ return child.char;
3247
+ default:
3248
+ return "";
3249
+ }
3250
+ })
3251
+ .join("");
3252
+ }
3253
+
3254
+ function readOnOffFieldFlag(node: XmlElementNode, name: string): boolean | undefined {
3255
+ const raw = node.attributes[`w:${name}`] ?? node.attributes[name];
3256
+ if (raw === undefined) return undefined;
3257
+ const normalized = raw.toLowerCase();
3258
+ if (normalized === "0" || normalized === "false" || normalized === "off") return false;
3259
+ return true;
3260
+ }
3261
+
3118
3262
  function parseRunContentOnly(
3119
3263
  node: XmlElementNode,
3120
3264
  _sourceXml: string,
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ CanonicalSourceRef,
2
3
  NumberingCatalog,
3
4
  NumberingLevelDefinition,
4
5
  NumberingLevelParagraphGeometry,
@@ -93,6 +94,9 @@ export function parseNumberingXml(xml: string, context?: ParseNumberingContext):
93
94
  const numStyleLink = numStyleLinkEl ? readStringAttr(numStyleLinkEl, "w:val") : undefined;
94
95
  abstractDefinitions[abstractNumberingId] = {
95
96
  abstractNumberingId,
97
+ ...(context?.partPath !== undefined
98
+ ? { sourceRef: createNumberingSourceRef(context.partPath, "abstractNum", rawId) }
99
+ : {}),
96
100
  levels: readLevels(child),
97
101
  ...(nsid ? { nsid } : {}),
98
102
  ...(multiLevelType ? { multiLevelType } : {}),
@@ -114,6 +118,9 @@ export function parseNumberingXml(xml: string, context?: ParseNumberingContext):
114
118
  const numberingInstanceId = toCanonicalNumberingInstanceId(rawId);
115
119
  instances[numberingInstanceId] = {
116
120
  numberingInstanceId,
121
+ ...(context?.partPath !== undefined
122
+ ? { sourceRef: createNumberingSourceRef(context.partPath, "num", rawId) }
123
+ : {}),
117
124
  abstractNumberingId: toCanonicalAbstractNumberingId(rawAbstractId),
118
125
  overrides: readOverrides(child),
119
126
  };
@@ -129,6 +136,19 @@ export function parseNumberingXml(xml: string, context?: ParseNumberingContext):
129
136
  };
130
137
  }
131
138
 
139
+ function createNumberingSourceRef(
140
+ partPath: string,
141
+ element: "abstractNum" | "num",
142
+ rawId: string,
143
+ ): CanonicalSourceRef {
144
+ return {
145
+ sourceId: `part:${partPath}#${element}:${rawId}`,
146
+ partPath,
147
+ storyKind: "numbering",
148
+ element,
149
+ };
150
+ }
151
+
132
152
  /**
133
153
  * Read a `<w:numPicBullet>` element into a catalog entry. Extracts the
134
154
  * inner `wp:extent` when present (drawingML path); the raw XML is
@@ -847,7 +847,7 @@ function parseTableRowAndCellRevisions(
847
847
  const metadata = readRevisionMetadata(trPrChange, state, "property-change");
848
848
  const innerTrPr = findChildElement(trPrChange, "trPr");
849
849
  const beforeXml = innerTrPr ? state.documentXml.slice(innerTrPr.start, innerTrPr.end) : "";
850
- const propertyChangeData: PropertyChangeData = { xmlTag: "tblPrChange", beforeXml };
850
+ const propertyChangeData: PropertyChangeData = { xmlTag: "trPrChange", beforeXml };
851
851
 
852
852
  state.revisions.push(
853
853
  createRevisionRecord({
@@ -861,7 +861,6 @@ function parseTableRowAndCellRevisions(
861
861
  originalRevisionType: "trPrChange",
862
862
  ooxmlRevisionId: metadata.ooxmlRevisionId,
863
863
  propertyChangeData,
864
- preserveOnlyReason: "Row property changes remain preserve-only for export safety.",
865
864
  },
866
865
  }),
867
866
  );
@@ -937,7 +936,7 @@ function parseTableRowAndCellRevisions(
937
936
  const metadata = readRevisionMetadata(tcPrChange, state, "property-change");
938
937
  const innerTcPr = findChildElement(tcPrChange, "tcPr");
939
938
  const beforeXml = innerTcPr ? state.documentXml.slice(innerTcPr.start, innerTcPr.end) : "";
940
- const propertyChangeData: PropertyChangeData = { xmlTag: "tblPrChange", beforeXml };
939
+ const propertyChangeData: PropertyChangeData = { xmlTag: "tcPrChange", beforeXml };
941
940
 
942
941
  state.revisions.push(
943
942
  createRevisionRecord({
@@ -951,7 +950,6 @@ function parseTableRowAndCellRevisions(
951
950
  originalRevisionType: "tcPrChange",
952
951
  ooxmlRevisionId: metadata.ooxmlRevisionId,
953
952
  propertyChangeData,
954
- preserveOnlyReason: "Cell property changes remain preserve-only for export safety.",
955
953
  },
956
954
  }),
957
955
  );
@@ -1148,18 +1146,20 @@ function linkMoveRevisionPairs(state: ParseState): void {
1148
1146
  if (fromRevision && toRevision) {
1149
1147
  fromRevision.metadata.moveData!.linkedRevisionId = toRevision.revisionId;
1150
1148
  toRevision.metadata.moveData!.linkedRevisionId = fromRevision.revisionId;
1149
+ removePreserveOnlyMoveDiagnostic(state, fromRevision.revisionId);
1150
+ removePreserveOnlyMoveDiagnostic(state, toRevision.revisionId);
1151
1151
 
1152
1152
  state.diagnostics.push({
1153
1153
  revisionId: fromRevision.revisionId,
1154
1154
  code: "linked_move_pair",
1155
- message: `Move-from revision linked to move-to revision ${toRevision.revisionId} via moveId ${moveId}.`,
1156
- featureClass: "informational",
1155
+ message: `Move-from revision linked to move-to revision ${toRevision.revisionId} via moveId ${moveId}; linked move pairs are actionable.`,
1156
+ featureClass: "supported",
1157
1157
  });
1158
1158
  state.diagnostics.push({
1159
1159
  revisionId: toRevision.revisionId,
1160
1160
  code: "linked_move_pair",
1161
- message: `Move-to revision linked to move-from revision ${fromRevision.revisionId} via moveId ${moveId}.`,
1162
- featureClass: "informational",
1161
+ message: `Move-to revision linked to move-from revision ${fromRevision.revisionId} via moveId ${moveId}; linked move pairs are actionable.`,
1162
+ featureClass: "supported",
1163
1163
  });
1164
1164
  } else {
1165
1165
  for (const revision of group) {
@@ -1174,6 +1174,17 @@ function linkMoveRevisionPairs(state: ParseState): void {
1174
1174
  }
1175
1175
  }
1176
1176
 
1177
+ function removePreserveOnlyMoveDiagnostic(
1178
+ state: ParseState,
1179
+ revisionId: string,
1180
+ ): void {
1181
+ state.diagnostics = state.diagnostics.filter(
1182
+ (diagnostic) =>
1183
+ diagnostic.revisionId !== revisionId ||
1184
+ diagnostic.code !== "preserve_only_move_revision",
1185
+ );
1186
+ }
1187
+
1177
1188
  function isShallowPropertyChangeNesting(node: XmlElementNode): boolean {
1178
1189
  for (const child of node.children) {
1179
1190
  if (child.type !== "element") {