@beyondwork/docx-react-component 1.0.105 → 1.0.108

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 (193) 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 +10 -2
  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-reference.ts +28 -0
  11. package/src/api/v3/ai/_audit-time.ts +5 -0
  12. package/src/api/v3/ai/_pe2-evidence.ts +310 -6
  13. package/src/api/v3/ai/attach.ts +29 -4
  14. package/src/api/v3/ai/bundle.ts +6 -2
  15. package/src/api/v3/ai/inspect.ts +6 -2
  16. package/src/api/v3/ai/replacement.ts +112 -18
  17. package/src/api/v3/ai/resolve.ts +2 -2
  18. package/src/api/v3/ai/review.ts +177 -3
  19. package/src/api/v3/index.ts +8 -0
  20. package/src/api/v3/runtime/collab.ts +462 -0
  21. package/src/api/v3/runtime/document.ts +503 -20
  22. package/src/api/v3/runtime/geometry.ts +97 -0
  23. package/src/api/v3/runtime/layout.ts +744 -0
  24. package/src/api/v3/runtime/perf-probe.ts +14 -0
  25. package/src/api/v3/runtime/viewport.ts +9 -8
  26. package/src/api/v3/ui/_types.ts +202 -55
  27. package/src/api/v3/ui/chrome-preset-model.ts +5 -5
  28. package/src/api/v3/ui/debug.ts +115 -2
  29. package/src/api/v3/ui/index.ts +17 -0
  30. package/src/api/v3/ui/overlays.ts +0 -8
  31. package/src/api/v3/ui/surface.ts +56 -0
  32. package/src/api/v3/ui/viewport.ts +119 -9
  33. package/src/core/commands/image-commands.ts +1 -0
  34. package/src/core/commands/index.ts +6 -0
  35. package/src/core/schema/text-schema.ts +43 -5
  36. package/src/core/selection/mapping.ts +8 -1
  37. package/src/core/selection/review-anchors.ts +5 -1
  38. package/src/core/state/text-transaction.ts +8 -2
  39. package/src/io/export/serialize-revisions.ts +149 -1
  40. package/src/io/normalize/normalize-text.ts +6 -0
  41. package/src/io/ooxml/parse-bookmark-references.ts +55 -0
  42. package/src/io/ooxml/parse-fields.ts +24 -2
  43. package/src/io/ooxml/parse-headers-footers.ts +38 -5
  44. package/src/io/ooxml/parse-main-document.ts +153 -9
  45. package/src/io/ooxml/parse-numbering.ts +20 -0
  46. package/src/io/ooxml/parse-revisions.ts +19 -8
  47. package/src/io/opc/package-reader.ts +98 -8
  48. package/src/model/anchor.ts +4 -3
  49. package/src/model/canonical-document.ts +220 -2
  50. package/src/model/canonical-hash.ts +221 -0
  51. package/src/model/canonical-layout-inputs.ts +245 -6
  52. package/src/model/layout/index.ts +1 -0
  53. package/src/model/layout/page-graph-types.ts +147 -1
  54. package/src/model/review/revision-types.ts +14 -3
  55. package/src/preservation/store.ts +20 -4
  56. package/src/review/README.md +1 -1
  57. package/src/review/store/revision-actions.ts +14 -2
  58. package/src/runtime/collab/event-types.ts +67 -1
  59. package/src/runtime/collab/runtime-collab-sync.ts +177 -5
  60. package/src/runtime/diagnostics/layout-guard-warning.ts +18 -0
  61. package/src/runtime/document-heading-outline.ts +147 -0
  62. package/src/runtime/document-navigation.ts +8 -243
  63. package/src/runtime/document-runtime.ts +279 -115
  64. package/src/runtime/edit-dispatch/dispatch-text-command.ts +11 -0
  65. package/src/runtime/formatting/layout-inputs.ts +38 -5
  66. package/src/runtime/formatting/numbering/geometry.ts +28 -2
  67. package/src/runtime/geometry/adjacent-geometry-intake.ts +835 -0
  68. package/src/runtime/geometry/caret-geometry.ts +5 -6
  69. package/src/runtime/geometry/geometry-facet.ts +60 -10
  70. package/src/runtime/geometry/geometry-index.ts +661 -16
  71. package/src/runtime/geometry/geometry-types.ts +59 -0
  72. package/src/runtime/geometry/hit-test.ts +11 -1
  73. package/src/runtime/geometry/overlay-rects.ts +5 -3
  74. package/src/runtime/geometry/project-anchors.ts +1 -1
  75. package/src/runtime/geometry/word-layout-v2-line-intake.ts +323 -0
  76. package/src/runtime/layout/index.ts +6 -0
  77. package/src/runtime/layout/layout-engine-instance.ts +6 -1
  78. package/src/runtime/layout/layout-engine-version.ts +188 -16
  79. package/src/runtime/layout/layout-facet-types.ts +6 -0
  80. package/src/runtime/layout/page-graph.ts +23 -4
  81. package/src/runtime/layout/paginated-layout-engine.ts +149 -15
  82. package/src/runtime/layout/project-block-fragments.ts +351 -14
  83. package/src/runtime/layout/public-facet.ts +162 -24
  84. package/src/runtime/layout/table-row-continuation-contract.ts +107 -0
  85. package/src/runtime/layout/table-row-split.ts +92 -35
  86. package/src/runtime/prerender/cache-envelope.ts +2 -2
  87. package/src/runtime/prerender/cache-key.ts +5 -4
  88. package/src/runtime/prerender/customxml-cache.ts +0 -1
  89. package/src/runtime/render/render-kernel.ts +1 -1
  90. package/src/runtime/revision-runtime.ts +112 -10
  91. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  92. package/src/runtime/scopes/action-validation.ts +22 -2
  93. package/src/runtime/scopes/capabilities.ts +316 -0
  94. package/src/runtime/scopes/compile-scope-bundle.ts +14 -0
  95. package/src/runtime/scopes/compiler-service.ts +108 -4
  96. package/src/runtime/scopes/content-control-evidence.ts +79 -0
  97. package/src/runtime/scopes/create-issue.ts +5 -5
  98. package/src/runtime/scopes/evidence.ts +91 -0
  99. package/src/runtime/scopes/formatting/apply.ts +2 -0
  100. package/src/runtime/scopes/geometry-evidence.ts +130 -0
  101. package/src/runtime/scopes/index.ts +54 -0
  102. package/src/runtime/scopes/issue-lifecycle.ts +224 -0
  103. package/src/runtime/scopes/layout-evidence.ts +374 -0
  104. package/src/runtime/scopes/multi-paragraph-refusal.ts +37 -0
  105. package/src/runtime/scopes/preservation-boundary.ts +7 -1
  106. package/src/runtime/scopes/replacement/apply.ts +97 -34
  107. package/src/runtime/scopes/scope-kinds/paragraph.ts +108 -12
  108. package/src/runtime/scopes/semantic-scope-types.ts +242 -3
  109. package/src/runtime/scopes/visualization.ts +28 -0
  110. package/src/runtime/surface-projection.ts +44 -5
  111. package/src/runtime/telemetry/perf-probe.ts +216 -0
  112. package/src/runtime/virtualized-rendering.ts +36 -1
  113. package/src/runtime/workflow/ai-issue-lifecycle.ts +253 -0
  114. package/src/runtime/workflow/coordinator.ts +39 -11
  115. package/src/runtime/workflow/derived-scope-resolver.ts +63 -9
  116. package/src/runtime/workflow/index.ts +4 -0
  117. package/src/runtime/workflow/overlay-lane-types.ts +58 -0
  118. package/src/runtime/workflow/overlay-lanes.ts +386 -0
  119. package/src/runtime/workflow/overlay-store.ts +2 -2
  120. package/src/runtime/workflow/redline-posture-calibration.ts +257 -0
  121. package/src/runtime/workflow/word-field-matrix-calibration.ts +231 -0
  122. package/src/session/_sync-legacy.ts +17 -27
  123. package/src/session/import/loader.ts +6 -4
  124. package/src/session/import/source-package-evidence.ts +186 -2
  125. package/src/session/index.ts +5 -6
  126. package/src/session/session.ts +30 -56
  127. package/src/session/types.ts +8 -13
  128. package/src/shell/session-bootstrap.ts +155 -81
  129. package/src/ui/WordReviewEditor.tsx +520 -12
  130. package/src/ui/editor-shell-view.tsx +14 -4
  131. package/src/ui/editor-surface-controller.tsx +5 -3
  132. package/src/ui/headless/selection-tool-resolver.ts +1 -2
  133. package/src/ui/presence-overlay-lane.ts +130 -0
  134. package/src/ui/ui-controller-factory.ts +17 -0
  135. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +5 -1
  136. package/src/ui-tailwind/chrome/editor-action-registry.ts +105 -5
  137. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +7 -0
  138. package/src/ui-tailwind/chrome/layer-debug-contracts.ts +208 -0
  139. package/src/ui-tailwind/chrome/resolve-target-kind.ts +13 -0
  140. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +11 -3
  141. package/src/ui-tailwind/chrome/tw-command-palette.tsx +36 -6
  142. package/src/ui-tailwind/chrome/tw-context-menu.tsx +6 -1
  143. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +42 -109
  144. package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +26 -6
  145. package/src/ui-tailwind/chrome/tw-navigation-command-bar.tsx +328 -0
  146. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +8 -4
  147. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +129 -1
  148. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +19 -5
  149. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
  150. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +28 -12
  151. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +30 -3
  152. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +116 -10
  153. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +223 -94
  154. package/src/ui-tailwind/chrome-overlay/tw-presence-overlay-lane.tsx +157 -0
  155. package/src/ui-tailwind/chrome-overlay/tw-review-overlay-lane-markers.tsx +259 -0
  156. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +5 -2
  157. package/src/ui-tailwind/chrome-overlay/tw-substrate-overlay-lanes.tsx +314 -0
  158. package/src/ui-tailwind/debug/README.md +4 -1
  159. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +272 -0
  160. package/src/ui-tailwind/debug/layer11-word-field-matrix-evidence.ts +160 -0
  161. package/src/ui-tailwind/editor-surface/perf-probe.ts +14 -215
  162. package/src/ui-tailwind/editor-surface/pm-decorations.ts +42 -0
  163. package/src/ui-tailwind/editor-surface/pm-position-map.ts +38 -2
  164. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -4
  165. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +34 -5
  166. package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +9 -19
  167. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -2
  168. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +145 -0
  169. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +16 -11
  170. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +8 -10
  171. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +3 -0
  172. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +4 -2
  173. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +60 -20
  174. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +16 -11
  175. package/src/ui-tailwind/review/tw-health-panel.tsx +36 -17
  176. package/src/ui-tailwind/review/tw-review-rail.tsx +7 -4
  177. package/src/ui-tailwind/review-workspace/diagnostics-visibility.ts +44 -0
  178. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +11 -0
  179. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +16 -1
  180. package/src/ui-tailwind/review-workspace/types.ts +26 -12
  181. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +40 -11
  182. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +2 -1
  183. package/src/ui-tailwind/review-workspace/use-page-markers.ts +15 -26
  184. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +35 -18
  185. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +41 -32
  186. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +2 -1
  187. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +2 -1
  188. package/src/ui-tailwind/status/tw-status-bar.tsx +6 -5
  189. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +52 -80
  190. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +12 -48
  191. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +9 -4
  192. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +328 -361
  193. package/src/ui-tailwind/tw-review-workspace.tsx +152 -286
@@ -24,6 +24,7 @@ import type { SelectionSnapshot } from "../../public-types.ts";
24
24
  import type {
25
25
  SelectionRangeInput,
26
26
  ScrollTarget,
27
+ UiObjectDragStartInput,
27
28
  ViewportState,
28
29
  } from "./_types.ts";
29
30
  import type { UiApiContext } from "./_context.ts";
@@ -105,6 +106,31 @@ export const scrollToMetadata: ApiV3FnMetadata = {
105
106
  rwdReference: "§UI API § ui.surface.scrollTo. Adapter delegates to UiController.dispatchScroll. Slice 3 adds geometry-backed target resolution for block/scope/comment/revision kinds prior to dispatch.",
106
107
  };
107
108
 
109
+ export const beginObjectDragMetadata: ApiV3FnMetadata = {
110
+ name: "ui.surface.beginObjectDrag",
111
+ status: "live-with-adapter",
112
+ sourceLayer: "presentation",
113
+ liveEvidence: {
114
+ runnerTest: "test/api/v3/ui/object-drag.test.ts",
115
+ commit: "refactor-10-object-drag-lifecycle",
116
+ },
117
+ uxIntent: {
118
+ uiVisible: true,
119
+ expectsUxResponse: "surface-refresh",
120
+ expectedDelta: "object drag lifecycle starts for a mounted object handle",
121
+ },
122
+ agentMetadata: {
123
+ readOrMutate: "mutate",
124
+ boundedScope: "selection",
125
+ auditCategory: "ui-surface-object-drag",
126
+ },
127
+ stateClass: "C-local",
128
+ persistsTo: "none",
129
+ bidirectional: true,
130
+ rwdReference:
131
+ "§UI API § object handles. Adapter delegates mounted object drag to UiController.beginObjectDrag; commit routes through host-owned runtime mutation and layout invalidation paths.",
132
+ };
133
+
108
134
  export function createSurfaceFamily(ctx: UiApiContext) {
109
135
  return {
110
136
  getSelection(): SelectionSnapshot | null {
@@ -166,5 +192,35 @@ export function createSurfaceFamily(ctx: UiApiContext) {
166
192
  actualDelta: { kind: "surface-refresh", payload: { kind: target.kind, value: target.kind === "scope" || target.kind === "comment" || target.kind === "revision" || target.kind === "block" ? target.value : undefined } },
167
193
  });
168
194
  },
195
+ beginObjectDrag(input: UiObjectDragStartInput) {
196
+ const controller = ctx.binding?.controller;
197
+ if (!controller) {
198
+ throw new Error(
199
+ "ui.surface.beginObjectDrag: no controller bound — call ui.session.bind(controller) first",
200
+ );
201
+ }
202
+ if (!controller.beginObjectDrag) {
203
+ throw new Error(
204
+ `ui.surface.beginObjectDrag: controller of kind "${controller.kind}" did not provide a beginObjectDrag hook`,
205
+ );
206
+ }
207
+ const session = controller.beginObjectDrag(input);
208
+ emitUxResponse(ctx.handle, {
209
+ apiFn: beginObjectDragMetadata.name,
210
+ intent: beginObjectDragMetadata.uxIntent.expectedDelta ?? "",
211
+ mockOrLive: "live-with-adapter",
212
+ uiVisible: true,
213
+ expectedDelta: beginObjectDragMetadata.uxIntent.expectedDelta,
214
+ actualDelta: {
215
+ kind: "surface-refresh",
216
+ payload: {
217
+ objectId: input.objectId,
218
+ handle: input.handle,
219
+ sessionId: session.id,
220
+ },
221
+ },
222
+ });
223
+ return session;
224
+ },
169
225
  };
170
226
  }
@@ -24,6 +24,7 @@
24
24
  import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
25
25
  import type {
26
26
  ViewportState,
27
+ PageResidencySnapshot,
27
28
  UiListener,
28
29
  UiUnsubscribe,
29
30
  WorkflowMarkupMode,
@@ -33,6 +34,7 @@ import type {
33
34
  import type { UiApiContext } from "./_context.ts";
34
35
  import { readComposedViewport } from "./_context.ts";
35
36
  import { emitUxResponse } from "../_ux-response.ts";
37
+ import { buildPageAnchorElementId } from "../_page-anchor-id.ts";
36
38
 
37
39
  export const getMetadata: ApiV3FnMetadata = {
38
40
  name: "ui.viewport.get",
@@ -86,6 +88,52 @@ export const subscribeMetadata: ApiV3FnMetadata = {
86
88
  rwdReference: "§UI API § ui.viewport.subscribe. Adapter delegates to UiController.subscribeViewport; throws when the active binding has no hook. Subscribe call emits one `ux.response.ui.viewport.subscribe` acknowledgement; per-tick ViewportState deliveries flow through the listener (rAF-coalesced, U7).",
87
89
  };
88
90
 
91
+ export const getPageResidencyMetadata: ApiV3FnMetadata = {
92
+ name: "ui.viewport.getPageResidency",
93
+ status: "live-with-adapter",
94
+ sourceLayer: "presentation",
95
+ liveEvidence: {
96
+ runnerTest: "test/api/v3/ui/viewport-residency.test.ts",
97
+ commit: "refactor-10-pe2-residency",
98
+ },
99
+ mockShape: {
100
+ deterministic: true,
101
+ seededFrom: "fixed",
102
+ shapeDescription: "Mock PageResidencySnapshot with residency='evicted' and status='unavailable' when no mounted controller exposes L10 residency policy.",
103
+ carriesMockFlag: true,
104
+ },
105
+ uxIntent: { uiVisible: false },
106
+ agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "ui-viewport-read" },
107
+ stateClass: "C-local",
108
+ persistsTo: "none",
109
+ rwdReference: "§PE2 § Virtual Page Windowing. Reads L10 page residency policy (realized/cold/evicted) without probing L05 geometry or realizing L11 DOM.",
110
+ };
111
+
112
+ export const subscribePageResidencyMetadata: ApiV3FnMetadata = {
113
+ name: "ui.viewport.subscribePageResidency",
114
+ status: "live-with-adapter",
115
+ sourceLayer: "presentation",
116
+ liveEvidence: {
117
+ runnerTest: "test/api/v3/ui/viewport-residency.test.ts",
118
+ commit: "refactor-10-pe2-residency",
119
+ },
120
+ uxIntent: {
121
+ uiVisible: true,
122
+ expectsUxResponse: "surface-refresh",
123
+ expectedDelta: "page-residency subscriber attached; future realized/cold/evicted changes propagate through the listener",
124
+ },
125
+ agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "ui-viewport-subscribe" },
126
+ stateClass: "C-local",
127
+ persistsTo: "none",
128
+ bidirectional: true,
129
+ subscriptionShape: {
130
+ eventType: "ui.viewport.page_residency_changed",
131
+ payloadType: "PageResidencySnapshot",
132
+ coalescing: "raf",
133
+ },
134
+ rwdReference: "§PE2 § Virtual Page Windowing. Adapter delegates to UiController.subscribePageResidency; emissions are plain snapshots and do not dispatch PM transactions or force geometry rehydration.",
135
+ };
136
+
89
137
  // ----- scrollToPage (coord-10 §γ — visual-fidelity / Go-to-page UX) -----
90
138
 
91
139
  export const scrollToPageMetadata: ApiV3FnMetadata = {
@@ -112,7 +160,7 @@ export const scrollToPageMetadata: ApiV3FnMetadata = {
112
160
  stateClass: "C-local",
113
161
  persistsTo: "none",
114
162
  rwdReference:
115
- "§UI API § ui.viewport.scrollToPage. Resolves pageNumber → scrollY via handle.geometry.getPage(pageIndex); dispatches through controller.dispatchScroll({ kind:'page', value, behavior }); returns the settled {actualPage, scrollY}. 1-based page numbers; clamps to [1, pageCount]. First-class API for visual-fidelity harness + 'Go to page N' UX — replaces DOM-scrape fallback (coord-10 §γ). Parity note: reads the same `handle.geometry.getPage(i).frame.topPx` source as `runtime.viewport.getPageAnchor` (L07 coord-07 §2.9, shipped 2026-04-24 in `src/api/v3/runtime/viewport.ts`), so `actualPage + scrollY` here and `{scrollY, pageRect}` on the runtime side stay consistent by construction. No direct delegation today because `scripts/ci-check-ui-api-layer-purity.mjs` restricts `src/api/v3/ui/**` from importing `src/api/v3/runtime/**`; both surfaces are thin wrappers over the shared geometry facet.",
163
+ "§UI API § ui.viewport.scrollToPage. Resolves pageNumber → scrollY + elementId via handle.geometry.getPage(pageIndex); dispatches through controller.dispatchScroll({ kind:'page', value, behavior }); returns the settled {actualPage, scrollY, elementId}. 1-based page numbers; clamps to [1, pageCount]. First-class API for visual-fidelity harness + 'Go to page N' UX — replaces DOM-scrape fallback (coord-10 §γ). Parity note: reads the same `handle.geometry.getPage(i).frame.topPx` source as `runtime.viewport.getPageAnchor` (L07 coord-07 §2.9, shipped 2026-04-24 in `src/api/v3/runtime/viewport.ts`), so `actualPage + scrollY + elementId` here and `{scrollY, pageRect, elementId}` on the runtime side stay consistent by construction. No direct delegation today because `scripts/ci-check-ui-api-layer-purity.mjs` restricts `src/api/v3/ui/**` from importing `src/api/v3/runtime/**`; both surfaces are thin wrappers over the shared geometry facet.",
116
164
  };
117
165
 
118
166
  // ----- X5 markup-mode metadata (state-classes cross-cutting Slice X5) -----
@@ -245,6 +293,23 @@ export function createViewportFamily(ctx: UiApiContext) {
245
293
  notifyMarkupModeSubscribers();
246
294
  });
247
295
 
296
+ function normalizePageIndex(pageIndex: number): number {
297
+ if (!Number.isFinite(pageIndex)) return 0;
298
+ return Math.max(0, Math.floor(pageIndex));
299
+ }
300
+
301
+ function unavailableResidency(pageIndex: number): PageResidencySnapshot {
302
+ return Object.freeze({
303
+ __mock: true,
304
+ pageIndex: normalizePageIndex(pageIndex),
305
+ residency: "evicted",
306
+ status: "unavailable",
307
+ revision: 0,
308
+ source: "unavailable",
309
+ reason: "page-residency policy is not wired on the active controller",
310
+ } as const);
311
+ }
312
+
248
313
  return {
249
314
  // Slice 11 — composes scroll/dpr/zoom from `handle.geometry` with
250
315
  // width/height from the bound controller (see
@@ -279,6 +344,39 @@ export function createViewportFamily(ctx: UiApiContext) {
279
344
  });
280
345
  return unsubscribe;
281
346
  },
347
+ getPageResidency(pageIndex: number): PageResidencySnapshot {
348
+ const normalized = normalizePageIndex(pageIndex);
349
+ const resolver = ctx.binding?.controller.getPageResidency;
350
+ if (!resolver) return unavailableResidency(normalized);
351
+ return resolver(normalized);
352
+ },
353
+ subscribePageResidency(
354
+ pageIndex: number,
355
+ listener: UiListener<PageResidencySnapshot>,
356
+ ): UiUnsubscribe {
357
+ const controller = ctx.binding?.controller;
358
+ if (!controller) {
359
+ throw new Error(
360
+ "ui.viewport.subscribePageResidency: no controller bound — call ui.session.bind(controller) first",
361
+ );
362
+ }
363
+ if (!controller.subscribePageResidency) {
364
+ throw new Error(
365
+ `ui.viewport.subscribePageResidency: controller of kind "${controller.kind}" did not provide a subscribePageResidency hook`,
366
+ );
367
+ }
368
+ const normalized = normalizePageIndex(pageIndex);
369
+ const unsubscribe = controller.subscribePageResidency(normalized, listener);
370
+ emitUxResponse(ctx.handle, {
371
+ apiFn: subscribePageResidencyMetadata.name,
372
+ intent: subscribePageResidencyMetadata.uxIntent.expectedDelta ?? "",
373
+ mockOrLive: "live-with-adapter",
374
+ uiVisible: true,
375
+ expectedDelta: subscribePageResidencyMetadata.uxIntent.expectedDelta,
376
+ actualDelta: { kind: "surface-refresh", payload: { subscribed: "ui.viewport.pageResidency", pageIndex: normalized } },
377
+ });
378
+ return unsubscribe;
379
+ },
282
380
 
283
381
  // ----- scrollToPage (coord-10 §γ) -----
284
382
 
@@ -295,10 +393,10 @@ export function createViewportFamily(ctx: UiApiContext) {
295
393
  * the document's page count returns the last valid page's scrollY;
296
394
  * `actualPage` reflects the clamp so callers can detect it.
297
395
  *
298
- * Returns `null` when (a) no controller is bound, (b) the controller
299
- * has no `dispatchScroll` hook, or (c) geometry cannot resolve any
300
- * page (pre-paint / empty doc). Callers that need explicit failure
301
- * handling check `result !== null` before trusting the scroll.
396
+ * Throws when controller wiring is missing. Returns `null` only when
397
+ * geometry cannot resolve any page (pre-paint / empty doc). Callers
398
+ * that need explicit failure handling check `result !== null` before
399
+ * trusting the scroll.
302
400
  */
303
401
  async scrollToPage(
304
402
  pageNumber: number,
@@ -328,11 +426,15 @@ export function createViewportFamily(ctx: UiApiContext) {
328
426
  // Try the requested page; if null, scan downward through lower
329
427
  // indices to land on the largest resolvable page (the doc's last
330
428
  // populated page). If nothing resolves, return null.
331
- let resolved: { pageIndex: number; scrollY: number } | null = null;
429
+ let resolved: { pageIndex: number; scrollY: number; elementId: string } | null = null;
332
430
  for (let i = requestedClampedLow - 1; i >= 0; i--) {
333
431
  const page = getPage.call(ctx.handle.geometry, i);
334
432
  if (page) {
335
- resolved = { pageIndex: i, scrollY: page.frame.topPx };
433
+ resolved = {
434
+ pageIndex: i,
435
+ scrollY: page.frame.topPx,
436
+ elementId: buildPageAnchorElementId(page.pageId, i),
437
+ };
336
438
  break;
337
439
  }
338
440
  }
@@ -354,11 +456,19 @@ export function createViewportFamily(ctx: UiApiContext) {
354
456
  expectedDelta: scrollToPageMetadata.uxIntent.expectedDelta,
355
457
  actualDelta: {
356
458
  kind: "surface-refresh",
357
- payload: { page: actualPage, scrollY: resolved.scrollY },
459
+ payload: {
460
+ page: actualPage,
461
+ scrollY: resolved.scrollY,
462
+ elementId: resolved.elementId,
463
+ },
358
464
  },
359
465
  });
360
466
 
361
- return { actualPage, scrollY: resolved.scrollY };
467
+ return {
468
+ actualPage,
469
+ scrollY: resolved.scrollY,
470
+ elementId: resolved.elementId,
471
+ };
362
472
  },
363
473
 
364
474
  // ----- X5 markup-mode (state-classes cross-cutting Slice X5) -----
@@ -139,6 +139,7 @@ function insertImageIntoParagraph(
139
139
  (unit) =>
140
140
  unit.kind === "opaque_inline" ||
141
141
  unit.kind === "opaque_block" ||
142
+ unit.kind === "structural_block" ||
142
143
  unit.kind === "image",
143
144
  );
144
145
  if (protectedUnit) {
@@ -15,7 +15,9 @@ import {
15
15
  createNodeAnchor,
16
16
  createRangeAnchor,
17
17
  DEFAULT_BOUNDARY_ASSOC,
18
+ MAIN_STORY_TARGET,
18
19
  mapAnchor,
20
+ storyTargetsEqual,
19
21
  type BoundaryAssoc,
20
22
  type EditorAnchorProjection,
21
23
  type MappingStep,
@@ -1925,6 +1927,10 @@ function applyReviewCommand(
1925
1927
  continue;
1926
1928
  }
1927
1929
 
1930
+ if (!storyTargetsEqual(entry.storyTarget, MAIN_STORY_TARGET)) {
1931
+ continue;
1932
+ }
1933
+
1928
1934
  selection = remapSelection(selection, entry.mapping);
1929
1935
  mappingSteps.push(...entry.mapping.steps);
1930
1936
 
@@ -8,6 +8,11 @@ import type {
8
8
  TextMark,
9
9
  } from "../../model/canonical-document.ts";
10
10
 
11
+ type PreservedStructuralBlockNode = Exclude<
12
+ DocumentRootNode["children"][number],
13
+ ParagraphNode | OpaqueBlockNode
14
+ >;
15
+
11
16
  export interface ParagraphProperties {
12
17
  styleId?: string;
13
18
  numbering?: ParagraphNode["numbering"];
@@ -33,6 +38,7 @@ export type StoryUnit =
33
38
  | ImageUnit
34
39
  | OpaqueInlineUnit
35
40
  | OpaqueBlockUnit
41
+ | PreservedStructuralBlockUnit
36
42
  | ScopeMarkerUnit
37
43
  | ParagraphBreakUnit;
38
44
 
@@ -72,6 +78,12 @@ export interface OpaqueBlockUnit {
72
78
  nextParagraph?: ParagraphProperties;
73
79
  }
74
80
 
81
+ export interface PreservedStructuralBlockUnit {
82
+ kind: "structural_block";
83
+ block: PreservedStructuralBlockNode;
84
+ nextParagraph?: ParagraphProperties;
85
+ }
86
+
75
87
  export interface ParagraphBreakUnit {
76
88
  kind: "paragraph_break";
77
89
  nextParagraph: ParagraphProperties;
@@ -114,14 +126,21 @@ export function parseTextStory(content: unknown): TextStory {
114
126
  continue;
115
127
  }
116
128
 
117
- if (block.type !== "opaque_block") {
129
+ if (block.type === "opaque_block") {
130
+ units.push({
131
+ kind: "opaque_block",
132
+ fragmentId: block.fragmentId,
133
+ warningId: block.warningId,
134
+ ...(isParagraphNode(nextBlock)
135
+ ? { nextParagraph: extractParagraphProperties(nextBlock) }
136
+ : {}),
137
+ });
118
138
  continue;
119
139
  }
120
140
 
121
141
  units.push({
122
- kind: "opaque_block",
123
- fragmentId: block.fragmentId,
124
- warningId: block.warningId,
142
+ kind: "structural_block",
143
+ block,
125
144
  ...(isParagraphNode(nextBlock)
126
145
  ? { nextParagraph: extractParagraphProperties(nextBlock) }
127
146
  : {}),
@@ -186,7 +205,7 @@ export function logicalPositionToUnitIndex(
186
205
  }
187
206
 
188
207
  export function serializeTextStory(story: TextStory): DocumentRootNode {
189
- const blocks: Array<ParagraphNode | OpaqueBlockNode> = [];
208
+ const blocks: DocumentRootNode["children"] = [];
190
209
  let currentParagraph: ParagraphNode | undefined = createParagraph(story.firstParagraph);
191
210
  let currentHyperlink: HyperlinkNode | undefined;
192
211
  let activeTextBuffer:
@@ -301,6 +320,15 @@ export function serializeTextStory(story: TextStory): DocumentRootNode {
301
320
  continue;
302
321
  }
303
322
 
323
+ if (unit.kind === "structural_block") {
324
+ flushParagraph();
325
+ blocks.push(unit.block);
326
+ currentParagraph = unit.nextParagraph
327
+ ? createParagraph(unit.nextParagraph)
328
+ : undefined;
329
+ continue;
330
+ }
331
+
304
332
  if (unit.kind === "text") {
305
333
  const shouldExtendBuffer =
306
334
  activeTextBuffer &&
@@ -384,6 +412,8 @@ export function createPlainText(story: TextStory): string {
384
412
  return "\uFFF9";
385
413
  case "opaque_block":
386
414
  return "\uFFFA";
415
+ case "structural_block":
416
+ return "\uFFFA";
387
417
  case "scope_marker":
388
418
  return "";
389
419
  }
@@ -431,6 +461,14 @@ export function cloneStoryUnit(unit: StoryUnit): StoryUnit {
431
461
  ? { nextParagraph: cloneParagraphProperties(unit.nextParagraph) }
432
462
  : {}),
433
463
  };
464
+ case "structural_block":
465
+ return {
466
+ kind: "structural_block",
467
+ block: structuredClone(unit.block) as PreservedStructuralBlockNode,
468
+ ...(unit.nextParagraph
469
+ ? { nextParagraph: cloneParagraphProperties(unit.nextParagraph) }
470
+ : {}),
471
+ };
434
472
  case "paragraph_break":
435
473
  return {
436
474
  kind: "paragraph_break",
@@ -67,12 +67,19 @@ export function createRangeAnchor(
67
67
  assoc: BoundaryAssoc = DEFAULT_BOUNDARY_ASSOC,
68
68
  ): RangeAnchor {
69
69
  const normalized = normalizeRange({ from, to });
70
- return {
70
+ const anchor = {
71
71
  kind: "range",
72
72
  from: normalized.from,
73
73
  to: normalized.to,
74
74
  assoc,
75
75
  };
76
+ Object.defineProperty(anchor, "range", {
77
+ value: normalized,
78
+ enumerable: false,
79
+ configurable: false,
80
+ writable: false,
81
+ });
82
+ return anchor as RangeAnchor;
76
83
  }
77
84
 
78
85
  export function createNodeAnchor(at: Position, assoc: Assoc = 1): NodeAnchor {
@@ -95,7 +95,11 @@ export function rangeStaysWithinSingleParagraph(
95
95
  continue;
96
96
  }
97
97
 
98
- if (unit.kind === "paragraph_break" || unit.kind === "opaque_block") {
98
+ if (
99
+ unit.kind === "paragraph_break" ||
100
+ unit.kind === "opaque_block" ||
101
+ unit.kind === "structural_block"
102
+ ) {
99
103
  return false;
100
104
  }
101
105
  }
@@ -914,6 +914,8 @@ function resolveParagraphPropertiesAtPosition(
914
914
  current = cloneParagraphProperties(unit.nextParagraph);
915
915
  } else if (unit.kind === "opaque_block" && unit.nextParagraph) {
916
916
  current = cloneParagraphProperties(unit.nextParagraph);
917
+ } else if (unit.kind === "structural_block" && unit.nextParagraph) {
918
+ current = cloneParagraphProperties(unit.nextParagraph);
917
919
  }
918
920
  }
919
921
 
@@ -947,7 +949,11 @@ function normalizeStoryUnits(units: StoryUnit[]): StoryUnit[] {
947
949
 
948
950
  function ensureEditableRange(units: StoryUnit[]): void {
949
951
  const protectedUnit = units.find(
950
- (unit) => unit.kind === "opaque_inline" || unit.kind === "opaque_block" || unit.kind === "image",
952
+ (unit) =>
953
+ unit.kind === "opaque_inline" ||
954
+ unit.kind === "opaque_block" ||
955
+ unit.kind === "structural_block" ||
956
+ unit.kind === "image",
951
957
  );
952
958
 
953
959
  if (!protectedUnit) {
@@ -971,5 +977,5 @@ function containsParagraphBoundaryChange(
971
977
 
972
978
  return story.units
973
979
  .slice(range.from, range.to)
974
- .some((unit) => unit.kind === "paragraph_break");
980
+ .some((unit) => unit.kind === "paragraph_break" || unit.kind === "structural_block");
975
981
  }
@@ -40,12 +40,22 @@ export function serializeRevisionsIntoDocumentXml(
40
40
  const boundaries = options.boundaries ?? mapRevisionBoundaries(documentXml);
41
41
  const replacements: XmlReplacement[] = [];
42
42
  const consumedRevisionIds = new Set<string>();
43
+ const consumedMarkupRevisionIds = new Set<string>();
43
44
  const paragraphDecisions = collectParagraphMarkupDecisions(
44
45
  preservedMarkup,
45
46
  revisionById,
46
47
  boundaries,
47
48
  );
48
49
 
50
+ replacements.push(
51
+ ...createMoveRangeMarkerReplacements(
52
+ preservedMarkup,
53
+ revisionById,
54
+ consumedRevisionIds,
55
+ consumedMarkupRevisionIds,
56
+ ),
57
+ );
58
+
49
59
  replacements.push(
50
60
  ...createParagraphStructuralReplacements(
51
61
  documentXml,
@@ -56,7 +66,10 @@ export function serializeRevisionsIntoDocumentXml(
56
66
  );
57
67
 
58
68
  for (const markup of preservedMarkup) {
59
- if (consumedRevisionIds.has(markup.revisionId)) {
69
+ if (
70
+ consumedRevisionIds.has(markup.revisionId) ||
71
+ consumedMarkupRevisionIds.has(markup.revisionId)
72
+ ) {
60
73
  continue;
61
74
  }
62
75
 
@@ -87,6 +100,141 @@ export function serializeRevisionsIntoDocumentXml(
87
100
  return applyReplacements(documentXml, replacements);
88
101
  }
89
102
 
103
+ function createMoveRangeMarkerReplacements(
104
+ preservedMarkup: readonly PreservedRevisionMarkup[],
105
+ revisionById: ReadonlyMap<string, RevisionRecord>,
106
+ consumedRevisionIds: Set<string>,
107
+ consumedMarkupRevisionIds: Set<string>,
108
+ ): XmlReplacement[] {
109
+ const replacements: XmlReplacement[] = [];
110
+ const rangeMarkers = collectMoveRangeMarkers(preservedMarkup);
111
+
112
+ for (const marker of rangeMarkers) {
113
+ const revision = revisionById.get(marker.start.revisionId);
114
+ if (
115
+ !revision ||
116
+ revision.status === "active" ||
117
+ revision.status === "detached" ||
118
+ revision.kind !== "move"
119
+ ) {
120
+ continue;
121
+ }
122
+
123
+ const direction = revision.metadata.moveData?.direction;
124
+ if (direction !== marker.direction) {
125
+ continue;
126
+ }
127
+
128
+ const shouldDropSpan =
129
+ (revision.status === "accepted" && marker.direction === "from") ||
130
+ (revision.status === "rejected" && marker.direction === "to");
131
+
132
+ if (shouldDropSpan) {
133
+ replacements.push({
134
+ start: marker.start.xmlStart,
135
+ end: marker.end.xmlEnd,
136
+ replacement: "",
137
+ });
138
+ } else {
139
+ replacements.push(
140
+ {
141
+ start: marker.start.xmlStart,
142
+ end: marker.start.xmlEnd,
143
+ replacement: "",
144
+ },
145
+ {
146
+ start: marker.end.xmlStart,
147
+ end: marker.end.xmlEnd,
148
+ replacement: "",
149
+ },
150
+ );
151
+ }
152
+
153
+ consumedRevisionIds.add(marker.start.revisionId);
154
+ consumedMarkupRevisionIds.add(marker.end.revisionId);
155
+ }
156
+
157
+ return replacements;
158
+ }
159
+
160
+ function collectMoveRangeMarkers(
161
+ preservedMarkup: readonly PreservedRevisionMarkup[],
162
+ ): Array<{
163
+ direction: "from" | "to";
164
+ moveId: string;
165
+ start: PreservedRevisionMarkup;
166
+ end: PreservedRevisionMarkup;
167
+ }> {
168
+ const starts = new Map<string, PreservedRevisionMarkup>();
169
+ const ends = new Map<string, PreservedRevisionMarkup>();
170
+
171
+ for (const markup of preservedMarkup) {
172
+ const direction = getMoveRangeMarkerDirection(markup.originalRevisionType);
173
+ if (!direction) {
174
+ continue;
175
+ }
176
+ const moveId = readMoveRangeMarkerId(markup);
177
+ if (!moveId) {
178
+ continue;
179
+ }
180
+ const key = `${direction}:${moveId}`;
181
+ if (isMoveRangeStart(markup.originalRevisionType)) {
182
+ starts.set(key, markup);
183
+ } else if (isMoveRangeEnd(markup.originalRevisionType)) {
184
+ ends.set(key, markup);
185
+ }
186
+ }
187
+
188
+ const markers: Array<{
189
+ direction: "from" | "to";
190
+ moveId: string;
191
+ start: PreservedRevisionMarkup;
192
+ end: PreservedRevisionMarkup;
193
+ }> = [];
194
+
195
+ for (const [key, start] of starts.entries()) {
196
+ const end = ends.get(key);
197
+ if (!end) {
198
+ continue;
199
+ }
200
+ const [direction, moveId] = key.split(":");
201
+ if ((direction === "from" || direction === "to") && moveId) {
202
+ markers.push({ direction, moveId, start, end });
203
+ }
204
+ }
205
+
206
+ return markers;
207
+ }
208
+
209
+ function isMoveRangeStart(originalRevisionType: string): boolean {
210
+ return originalRevisionType === "moveFromRangeStart" ||
211
+ originalRevisionType === "moveToRangeStart";
212
+ }
213
+
214
+ function isMoveRangeEnd(originalRevisionType: string): boolean {
215
+ return originalRevisionType === "moveFromRangeEnd" ||
216
+ originalRevisionType === "moveToRangeEnd";
217
+ }
218
+
219
+ function getMoveRangeMarkerDirection(
220
+ originalRevisionType: string,
221
+ ): "from" | "to" | undefined {
222
+ if (originalRevisionType === "moveFromRangeStart" ||
223
+ originalRevisionType === "moveFromRangeEnd") {
224
+ return "from";
225
+ }
226
+ if (originalRevisionType === "moveToRangeStart" ||
227
+ originalRevisionType === "moveToRangeEnd") {
228
+ return "to";
229
+ }
230
+ return undefined;
231
+ }
232
+
233
+ function readMoveRangeMarkerId(markup: PreservedRevisionMarkup): string | undefined {
234
+ const match = /\b(?:w:)?id=(["'])([^"']+)\1/.exec(markup.rawXml);
235
+ return match?.[2];
236
+ }
237
+
90
238
  function serializeMarkup(
91
239
  markup: PreservedRevisionMarkup,
92
240
  revision: RevisionRecord | undefined,
@@ -245,6 +245,7 @@ function normalizeParagraph(
245
245
  const children = normalizeInlineChildren(paragraph.children, state, packagePartName);
246
246
  return {
247
247
  type: "paragraph",
248
+ ...(paragraph.sourceRef ? { sourceRef: paragraph.sourceRef } : {}),
248
249
  ...(paragraph.styleId ? { styleId: paragraph.styleId } : {}),
249
250
  ...(paragraph.numbering ? { numbering: paragraph.numbering } : {}),
250
251
  ...(paragraph.alignment ? { alignment: paragraph.alignment } : {}),
@@ -603,6 +604,7 @@ function normalizeInlineChildren(
603
604
  ...(classification.target ? { fieldTarget: classification.target } : {}),
604
605
  ...(classification.switches ? { switches: classification.switches } : {}),
605
606
  refreshStatus: classification.supported ? "stale" : "preserve-only",
607
+ ...(node.sourceRef ? { sourceRef: node.sourceRef } : {}),
606
608
  ...(node.legacyFormField ? { legacyFormField: node.legacyFormField } : {}),
607
609
  });
608
610
  state.cursor += renderedLength > 0 ? renderedLength : 1;
@@ -720,6 +722,8 @@ function registerComplexPreviewMedia(
720
722
  function normalizeHyperlink(node: ParsedHyperlinkNode): {
721
723
  type: "hyperlink";
722
724
  href: string;
725
+ sourceRef?: ParsedHyperlinkNode["sourceRef"];
726
+ fieldCarrier?: ParsedHyperlinkNode["fieldCarrier"];
723
727
  children: Array<
724
728
  | TextNode
725
729
  | { type: "hard_break" }
@@ -789,6 +793,8 @@ function normalizeHyperlink(node: ParsedHyperlinkNode): {
789
793
  return {
790
794
  type: "hyperlink",
791
795
  href: node.href,
796
+ ...(node.sourceRef ? { sourceRef: node.sourceRef } : {}),
797
+ ...(node.fieldCarrier ? { fieldCarrier: node.fieldCarrier } : {}),
792
798
  children,
793
799
  };
794
800
  }