@beyondwork/docx-react-component 1.0.106 → 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 (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
@@ -88,10 +88,23 @@ export interface RuntimeResolvedRegions {
88
88
  export interface RuntimeExclusionZone {
89
89
  rectTwips: RuntimeTwipsRect;
90
90
  objectId: string;
91
- wrapMode: string;
91
+ wrapMode: RuntimeAnchorWrapMode;
92
92
  layoutInCell?: boolean;
93
93
  }
94
94
 
95
+ /**
96
+ * Project-canonical wrap mode union. Mirrors `AnchorGeometry["wrapMode"]` and
97
+ * `FloatingImageProperties["wrap"]` in `src/model/canonical-document.ts`.
98
+ * `behindText` / `inFrontOfText` are *not* wrap modes here — they live as the
99
+ * `behindDoc?: boolean` flag on the canonical floating shape.
100
+ */
101
+ export type RuntimeAnchorWrapMode =
102
+ | "none"
103
+ | "square"
104
+ | "tight"
105
+ | "through"
106
+ | "topAndBottom";
107
+
95
108
  export type RuntimeLayoutDivergenceKind =
96
109
  | "frame-collision"
97
110
  | "intentional-overflow"
@@ -116,6 +129,14 @@ export interface RuntimePageFrame {
116
129
  pageIndex: number;
117
130
  sectionIndex: number;
118
131
  displayPageNumber: number;
132
+ /**
133
+ * PE2 v73 — explicit page-frame realization state for downstream runtime
134
+ * readers. L04-built frames are "complete" when every emitted region has a
135
+ * twips rect, "partial" when the graph exists but any region rect is absent,
136
+ * and "absent" is reserved for adapters describing a page without an L04
137
+ * frame.
138
+ */
139
+ completeness: RuntimePageFrameCompleteness;
119
140
  physicalBoundsTwips: RuntimeTwipsRect;
120
141
  regions: RuntimeResolvedRegions;
121
142
  pageLocalStories: RuntimePageLocalStoryInstance[];
@@ -123,6 +144,8 @@ export interface RuntimePageFrame {
123
144
  signature: string;
124
145
  }
125
146
 
147
+ export type RuntimePageFrameCompleteness = "complete" | "partial" | "absent";
148
+
126
149
  export interface RuntimePageLocalStoryInstance {
127
150
  instanceId: string;
128
151
  storyKey: string;
@@ -156,11 +179,18 @@ export interface RuntimeStoryAnchoredObject {
156
179
  | "ole-embed"
157
180
  | "opaque-inline";
158
181
  display: "inline" | "floating" | "unknown";
182
+ /**
183
+ * Source wrap mode for floating objects when L04 can read it. This stays as
184
+ * source/layout truth in twips-space; exact projected object bboxes remain
185
+ * a Layer 05 geometry concern.
186
+ */
187
+ wrapMode?: RuntimeAnchorWrapMode;
159
188
  extentTwips?: {
160
189
  widthTwips: number;
161
190
  heightTwips: number;
162
191
  };
163
192
  relationshipIds?: readonly string[];
193
+ mediaIds?: readonly string[];
164
194
  preserveOnly: boolean;
165
195
  divergenceIds: string[];
166
196
  }
@@ -194,9 +224,16 @@ export interface RuntimeTableContinuationCursor {
194
224
  continuesFromPreviousPage: boolean;
195
225
  continuesToNextPage: boolean;
196
226
  repeatedHeaderRowIndexes: readonly number[];
227
+ splitRowCarry?: readonly RuntimeTableSplitRowCarry[];
197
228
  verticalMergeCarry: readonly RuntimeTableVerticalMergeCarry[];
198
229
  }
199
230
 
231
+ export interface RuntimeTableSplitRowCarry {
232
+ rowIndex: number;
233
+ continuesFromPreviousPage: boolean;
234
+ continuesToNextPage: boolean;
235
+ }
236
+
200
237
  export interface RuntimeTableVerticalMergeCarry {
201
238
  columnIndex: number;
202
239
  restartRowIndex: number;
@@ -221,6 +258,86 @@ export interface RuntimeFragmentLayoutObject {
221
258
  widthTwips?: number;
222
259
  };
223
260
  fieldFamilies?: readonly string[];
261
+ /**
262
+ * Field-region identities whose visible result participates in this
263
+ * page-local fragment. L04 owns the fragment/page membership; L01/L02 own the
264
+ * source and canonical ids copied here for Debug/L05 joins.
265
+ */
266
+ fieldRegions?: readonly RuntimeFieldRegionLayoutFacts[];
267
+ /**
268
+ * KI-005 — source numbering facts for geometry/debug consumers.
269
+ * L04 owns the page-local fragment and measured extent; this payload carries
270
+ * the resolved marker/text-column facts that L05 can project without
271
+ * re-reading the surface snapshot, DOM, or numbering resolver.
272
+ */
273
+ numbering?: RuntimeNumberingLayoutFacts;
274
+ /**
275
+ * Joinable numbering facts for every numbered paragraph represented by this
276
+ * fragment, including paragraphs nested inside tables and block content
277
+ * controls. `numbering` remains the direct-paragraph compatibility field;
278
+ * consumers that need runtime fragment/page joins should prefer this list.
279
+ */
280
+ numberingRows?: readonly RuntimeNumberingLayoutFacts[];
281
+ }
282
+
283
+ export interface RuntimeFieldRegionLayoutFacts {
284
+ fieldRegionId: string;
285
+ regionId: string;
286
+ canonicalFieldId: string;
287
+ regionKind: "field" | "toc-region" | "hyperlink-field";
288
+ storyKey: string;
289
+ fieldIndex: number;
290
+ paragraphIndex: number;
291
+ fieldFamily: string;
292
+ refreshStatus: string;
293
+ sourceRef?: {
294
+ sourceId: string;
295
+ partPath?: string;
296
+ storyKind?: string;
297
+ element?: string;
298
+ ordinal?: number;
299
+ startOffset?: number;
300
+ endOffset?: number;
301
+ };
302
+ fieldEvidence?: {
303
+ sourceRef?: RuntimeFieldRegionLayoutFacts["sourceRef"];
304
+ instructionHash?: string;
305
+ resultHash?: string;
306
+ locked?: boolean;
307
+ dirty?: boolean;
308
+ };
309
+ }
310
+
311
+ export interface RuntimeNumberingLayoutFacts {
312
+ numberingLayoutId?: string;
313
+ numberingKey?: string;
314
+ sourceBlockPath?: string;
315
+ sourceBlockId?: string;
316
+ paragraphIndex?: number;
317
+ numberingInstanceId?: string;
318
+ level?: number;
319
+ format?: string;
320
+ markerText?: string;
321
+ markerSuffix?: "tab" | "space" | "nothing";
322
+ markerJustification?: "left" | "center" | "right" | "both" | "distribute";
323
+ markerLane?: {
324
+ startTwips: number;
325
+ widthTwips: number;
326
+ textStartTwips: number;
327
+ };
328
+ textColumn?: {
329
+ startTwips: number;
330
+ rightTwips?: number;
331
+ firstLineTwips?: number;
332
+ hangingTwips?: number;
333
+ };
334
+ tabStops?: readonly RuntimeNumberingTabStopFacts[];
335
+ }
336
+
337
+ export interface RuntimeNumberingTabStopFacts {
338
+ positionTwips: number;
339
+ align?: string;
340
+ leader?: string;
224
341
  }
225
342
 
226
343
  // ---------------------------------------------------------------------------
@@ -29,7 +29,13 @@ export type RevisionActionability = "actionable" | "preserve-only";
29
29
  export type RevisionAnchorState = "active" | "detached";
30
30
 
31
31
  export interface PropertyChangeData {
32
- xmlTag: "pPrChange" | "sectPrChange" | "tblPrChange" | "rPrChange";
32
+ xmlTag:
33
+ | "pPrChange"
34
+ | "sectPrChange"
35
+ | "tblPrChange"
36
+ | "trPrChange"
37
+ | "tcPrChange"
38
+ | "rPrChange";
33
39
  beforeXml: string;
34
40
  }
35
41
 
@@ -152,8 +158,9 @@ export function getRevisionActionability(
152
158
  // preserve-only (see revision-store.ts), which covers both `kind: "move"`
153
159
  // and `kind: "formatting"`. The overrides below flip the default back to
154
160
  // actionable for the specific shapes that Lane 7b promoted: cellIns
155
- // structural-table revisions, and linked-pair move revisions. Do not add a
156
- // `&& !preserveOnlyReason` guard here that would re-lock these shapes
161
+ // structural-table revisions, linked container-form move revisions, and
162
+ // linked range-marker move revisions. Do not add a
163
+ // `&& !preserveOnlyReason` guard here — that would re-lock promoted shapes
157
164
  // against the injected default and regress 7b.
158
165
  if (
159
166
  typeof revision !== "string" &&
@@ -167,6 +174,10 @@ export function getRevisionActionability(
167
174
  if (
168
175
  typeof revision !== "string" &&
169
176
  revision.kind === "move" &&
177
+ (revision.metadata.originalRevisionType === "moveFrom" ||
178
+ revision.metadata.originalRevisionType === "moveTo" ||
179
+ revision.metadata.originalRevisionType === "moveFromRangeStart" ||
180
+ revision.metadata.originalRevisionType === "moveToRangeStart") &&
170
181
  typeof revision.metadata.moveData?.linkedRevisionId === "string" &&
171
182
  revision.metadata.moveData.linkedRevisionId.length > 0
172
183
  ) {
@@ -22,6 +22,14 @@ export interface OpaqueFragmentDescriptor {
22
22
  detail: string;
23
23
  }
24
24
 
25
+ export interface StructuredWrapperDescriptorOptions {
26
+ /**
27
+ * TOC doc-part SDTs are preserve-only workflow objects, but the surface
28
+ * projection must keep their cached result paragraphs visible.
29
+ */
30
+ projectVisibleTocContentControls?: boolean;
31
+ }
32
+
25
33
  const BLOCKED_IMPORT_FEATURE_KEYS = new Set<OpaqueFragmentDescriptor["featureKey"]>([
26
34
  "alt-chunk",
27
35
  "custom-xml",
@@ -160,13 +168,21 @@ export function describeOpaqueFragment(
160
168
 
161
169
  export function describeStructuredWrapperBlock(
162
170
  block: BlockNode,
171
+ options: StructuredWrapperDescriptorOptions = {},
163
172
  ): OpaqueFragmentDescriptor | null {
164
173
  if (block.type === "sdt") {
165
174
  if (isTocContentControl(block)) {
166
- // CCEP SOW templates wrap the visible cached TOC result paragraphs in a
167
- // docPartObj SDT. Collapsing that wrapper hides LibreOffice-visible
168
- // content, so keep the wrapper in the surface tree and project children.
169
- return null;
175
+ if (options.projectVisibleTocContentControls) {
176
+ // CCEP SOW templates wrap the visible cached TOC result paragraphs in
177
+ // a docPartObj SDT. Collapsing that wrapper hides visible content, so
178
+ // callers that project the editing surface keep the wrapper recursive.
179
+ return null;
180
+ }
181
+ return {
182
+ featureKey: "content-controls",
183
+ label: "TOC content control",
184
+ detail: "Table of Contents doc-part content control remains package-backed and read-only.",
185
+ };
170
186
  }
171
187
  if (block.properties.sdtType === "docPartObj") {
172
188
  // coord-02 §11 P0 — a Template content control that wraps a
@@ -9,7 +9,7 @@ Frozen Wave 1 contract:
9
9
  - all review mutations flow through runtime commands and transactions
10
10
  - detached comments or revisions remain addressable with reason metadata; they are not silently dropped
11
11
  - v1 authoring is limited to single-paragraph comments, thread replies, tracked insertions, tracked deletions, and accept/reject flows
12
- - preserve-only review structures such as multi-paragraph editable comment ranges, tracked moves, and structural list/table revisions stay locked and warning-backed
12
+ - preserve-only review structures such as multi-paragraph editable comment ranges, unlinked move revisions, and unsupported structural list/table revisions stay locked and warning-backed
13
13
 
14
14
  Key subdirectories:
15
15
 
@@ -111,6 +111,7 @@ export function applyRevisionAction(
111
111
 
112
112
  if (
113
113
  revision.kind === "move" &&
114
+ isLinkedMoveActionRevision(revision) &&
114
115
  typeof revision.metadata.moveData?.linkedRevisionId === "string" &&
115
116
  revision.metadata.moveData.linkedRevisionId.length > 0
116
117
  ) {
@@ -212,7 +213,9 @@ export function applyRevisionAction(
212
213
  if (
213
214
  slice.some(
214
215
  (unit) =>
215
- unit.kind === "paragraph_break" || unit.kind === "opaque_block",
216
+ unit.kind === "paragraph_break" ||
217
+ unit.kind === "opaque_block" ||
218
+ unit.kind === "structural_block",
216
219
  )
217
220
  ) {
218
221
  return skippedResult(
@@ -312,6 +315,15 @@ function applyPairedMoveAction(
312
315
  };
313
316
  }
314
317
 
318
+ function isLinkedMoveActionRevision(revision: RevisionRecord): boolean {
319
+ return (
320
+ revision.metadata.originalRevisionType === "moveFrom" ||
321
+ revision.metadata.originalRevisionType === "moveTo" ||
322
+ revision.metadata.originalRevisionType === "moveFromRangeStart" ||
323
+ revision.metadata.originalRevisionType === "moveToRangeStart"
324
+ );
325
+ }
326
+
315
327
  function applyRowStructuralAction(
316
328
  options: ApplyRevisionActionOptions,
317
329
  revision: RevisionRecord,
@@ -862,7 +874,7 @@ function mapParagraphRanges(
862
874
  continue;
863
875
  }
864
876
 
865
- if (unit.kind === "opaque_block") {
877
+ if (unit.kind === "opaque_block" || unit.kind === "structural_block") {
866
878
  if (hasPendingParagraph) {
867
879
  paragraphs.push({ start, end: index });
868
880
  }
@@ -1,4 +1,4 @@
1
- import type { EditorCommand } from "../../core/commands/index.ts";
1
+ import type { CommandOrigin, EditorCommand } from "../../core/commands/index.ts";
2
2
  import type { SelectionSnapshot } from "../../core/state/editor-state.ts";
3
3
  import type { EditorStoryTarget } from "../../api/public-types.ts";
4
4
 
@@ -63,6 +63,8 @@ export interface CommandEvent {
63
63
  authorId: string;
64
64
  /** ISO-8601 UTC timestamp at origin. */
65
65
  timestamp: string;
66
+ /** Normalized origin classification for remote replay/audit consumers. */
67
+ origin: CommandEventOriginMetadata;
66
68
  /** The exact `EditorCommand` that produced this delta. */
67
69
  command: EditorCommand;
68
70
  /** Context required for deterministic replay on remote clients. */
@@ -86,6 +88,23 @@ export interface CommandEvent {
86
88
  };
87
89
  }
88
90
 
91
+ export type CommandEventOriginKind =
92
+ | "local-typing"
93
+ | "local-command"
94
+ | "runtime"
95
+ | "agent"
96
+ | "import"
97
+ | "field-refresh"
98
+ | "unknown";
99
+
100
+ export type CommandEventOriginSource = CommandOrigin["source"] | "agent" | "import" | "field_refresh" | "unspecified";
101
+
102
+ export interface CommandEventOriginMetadata {
103
+ readonly channel: "runtime-command-event";
104
+ readonly source: CommandEventOriginSource;
105
+ readonly kind: CommandEventOriginKind;
106
+ }
107
+
89
108
  /**
90
109
  * Command types that are broadcast to all collaborators.
91
110
  *
@@ -184,6 +203,52 @@ export function isBroadcastCommand(command: EditorCommand): boolean {
184
203
  return BROADCAST_COMMAND_TYPES.has(command.type);
185
204
  }
186
205
 
206
+ const TYPING_COMMAND_TYPES: ReadonlySet<EditorCommand["type"]> = new Set<EditorCommand["type"]>([
207
+ "text.insert",
208
+ "text.delete-backward",
209
+ "text.delete-forward",
210
+ "text.insert-tab",
211
+ "text.insert-hard-break",
212
+ "text.outdent-tab",
213
+ "paragraph.split",
214
+ ]);
215
+
216
+ export function classifyCommandEventOriginSource(
217
+ source: CommandEventOriginSource,
218
+ commandType: EditorCommand["type"],
219
+ ): CommandEventOriginKind {
220
+ if (source === "agent") return "agent";
221
+ if (source === "import") return "import";
222
+ if (source === "field_refresh") return "field-refresh";
223
+ if (source === "runtime") return "runtime";
224
+ if (source === "keyboard" && TYPING_COMMAND_TYPES.has(commandType)) return "local-typing";
225
+ if (
226
+ source === "keyboard" ||
227
+ source === "toolbar" ||
228
+ source === "context_menu" ||
229
+ source === "comment_panel" ||
230
+ source === "review_panel" ||
231
+ source === "api"
232
+ ) {
233
+ return "local-command";
234
+ }
235
+ if (TYPING_COMMAND_TYPES.has(commandType)) return "local-typing";
236
+ return "unknown";
237
+ }
238
+
239
+ export function classifyCommandEventOrigin(command: EditorCommand): CommandEventOriginKind {
240
+ return classifyCommandEventOriginSource(command.origin?.source ?? "unspecified", command.type);
241
+ }
242
+
243
+ export function commandEventOriginMetadata(command: EditorCommand): CommandEventOriginMetadata {
244
+ const source = command.origin?.source ?? "unspecified";
245
+ return {
246
+ channel: "runtime-command-event",
247
+ source,
248
+ kind: classifyCommandEventOriginSource(source, command.type),
249
+ };
250
+ }
251
+
187
252
  export interface CreateCommandEventInput {
188
253
  command: EditorCommand;
189
254
  originClientId: number;
@@ -203,6 +268,7 @@ export function createCommandEvent(input: CreateCommandEventInput): CommandEvent
203
268
  originClientId: input.originClientId,
204
269
  authorId: input.authorId,
205
270
  timestamp: input.timestamp,
271
+ origin: commandEventOriginMetadata(input.command),
206
272
  command: input.command,
207
273
  context: {
208
274
  documentMode: input.context?.documentMode,
@@ -11,6 +11,11 @@ import type {
11
11
  BlockNode,
12
12
  MutableCanonicalDocument,
13
13
  } from "../../model/canonical-document.ts";
14
+ import type {
15
+ Assoc,
16
+ BoundaryAssoc,
17
+ InternalEditorAnchorProjection,
18
+ } from "../../model/anchor.ts";
14
19
  import type {
15
20
  CommandAppliedMeta,
16
21
  DocumentRuntime,
@@ -19,6 +24,7 @@ import type {
19
24
  } from "../document-runtime.ts";
20
25
  import {
21
26
  createCommandEvent,
27
+ commandEventOriginMetadata,
22
28
  isBroadcastCommand,
23
29
  isLocalOnlyCommand,
24
30
  COMMAND_EVENT_SCHEMA_VERSION,
@@ -784,11 +790,58 @@ function asCommandEvent(value: unknown): CommandEvent | null {
784
790
  originClientId: value.originClientId,
785
791
  authorId: value.authorId,
786
792
  timestamp,
793
+ origin: asCommandEventOrigin(value.origin, command),
787
794
  command,
788
795
  context,
789
796
  };
790
797
  }
791
798
 
799
+ function asCommandEventOrigin(value: unknown, command: EditorCommand): CommandEvent["origin"] {
800
+ if (!isRecord(value)) {
801
+ return commandEventOriginMetadata(command);
802
+ }
803
+ if (
804
+ value.channel === "runtime-command-event" &&
805
+ isCommandEventOriginSource(value.source) &&
806
+ isCommandEventOriginKind(value.kind)
807
+ ) {
808
+ return {
809
+ channel: "runtime-command-event",
810
+ source: value.source,
811
+ kind: value.kind,
812
+ };
813
+ }
814
+ return commandEventOriginMetadata(command);
815
+ }
816
+
817
+ function isCommandEventOriginSource(value: unknown): value is CommandEvent["origin"]["source"] {
818
+ return (
819
+ value === "keyboard" ||
820
+ value === "toolbar" ||
821
+ value === "context_menu" ||
822
+ value === "comment_panel" ||
823
+ value === "review_panel" ||
824
+ value === "api" ||
825
+ value === "runtime" ||
826
+ value === "agent" ||
827
+ value === "import" ||
828
+ value === "field_refresh" ||
829
+ value === "unspecified"
830
+ );
831
+ }
832
+
833
+ function isCommandEventOriginKind(value: unknown): value is CommandEvent["origin"]["kind"] {
834
+ return (
835
+ value === "local-typing" ||
836
+ value === "local-command" ||
837
+ value === "runtime" ||
838
+ value === "agent" ||
839
+ value === "import" ||
840
+ value === "field-refresh" ||
841
+ value === "unknown"
842
+ );
843
+ }
844
+
792
845
  function normalizeTimestamp(value: unknown): string | null {
793
846
  if (typeof value === "string" && value.length > 0) {
794
847
  return value;
@@ -824,16 +877,131 @@ function asCommandEventContext(value: unknown): CommandEvent["context"] | null {
824
877
 
825
878
  function asSelectionSnapshot(value: unknown): CommandEvent["context"]["preSelection"] {
826
879
  if (!isRecord(value)) return undefined;
827
- if (typeof value.anchor !== "number" || typeof value.head !== "number") return undefined;
880
+ if (!isFiniteNumber(value.anchor) || !isFiniteNumber(value.head)) return undefined;
828
881
  if (typeof value.isCollapsed !== "boolean") return undefined;
829
- if (!isRecord(value.activeRange)) return undefined;
830
- return value as unknown as CommandEvent["context"]["preSelection"];
882
+ const activeRange = asEditorAnchorProjection(value.activeRange);
883
+ if (!activeRange) return undefined;
884
+ return {
885
+ anchor: value.anchor,
886
+ head: value.head,
887
+ isCollapsed: value.isCollapsed,
888
+ activeRange,
889
+ };
831
890
  }
832
891
 
833
892
  function asStoryTarget(value: unknown): CommandEvent["context"]["activeStory"] {
834
893
  if (!isRecord(value)) return undefined;
835
- if (typeof value.kind !== "string") return undefined;
836
- return value as unknown as CommandEvent["context"]["activeStory"];
894
+ switch (value.kind) {
895
+ case "main":
896
+ return { kind: "main" };
897
+ case "header":
898
+ case "footer": {
899
+ if (typeof value.relationshipId !== "string" || value.relationshipId.length === 0) {
900
+ return undefined;
901
+ }
902
+ if (!isHeaderFooterVariant(value.variant)) {
903
+ return undefined;
904
+ }
905
+ if (value.sectionIndex != null && !isFiniteNumber(value.sectionIndex)) {
906
+ return undefined;
907
+ }
908
+ return value.sectionIndex == null
909
+ ? {
910
+ kind: value.kind,
911
+ relationshipId: value.relationshipId,
912
+ variant: value.variant,
913
+ }
914
+ : {
915
+ kind: value.kind,
916
+ relationshipId: value.relationshipId,
917
+ variant: value.variant,
918
+ sectionIndex: value.sectionIndex,
919
+ };
920
+ }
921
+ case "footnote":
922
+ case "endnote":
923
+ if (typeof value.noteId !== "string" || value.noteId.length === 0) {
924
+ return undefined;
925
+ }
926
+ return {
927
+ kind: value.kind,
928
+ noteId: value.noteId,
929
+ };
930
+ default:
931
+ return undefined;
932
+ }
933
+ }
934
+
935
+ function asEditorAnchorProjection(value: unknown): InternalEditorAnchorProjection | undefined {
936
+ if (!isRecord(value)) return undefined;
937
+ if (value.kind == null && isFiniteNumber(value.from) && isFiniteNumber(value.to)) {
938
+ return {
939
+ kind: "range",
940
+ from: value.from,
941
+ to: value.to,
942
+ assoc: { start: -1, end: 1 },
943
+ };
944
+ }
945
+
946
+ switch (value.kind) {
947
+ case "range": {
948
+ if (!isFiniteNumber(value.from) || !isFiniteNumber(value.to)) return undefined;
949
+ const assoc = asBoundaryAssoc(value.assoc);
950
+ if (!assoc) return undefined;
951
+ const range = value.range == null ? undefined : asDocRange(value.range);
952
+ if (value.range != null && !range) return undefined;
953
+ return range == null
954
+ ? { kind: "range", from: value.from, to: value.to, assoc }
955
+ : { kind: "range", from: value.from, to: value.to, range, assoc };
956
+ }
957
+ case "node": {
958
+ if (!isFiniteNumber(value.at)) return undefined;
959
+ const assoc = asAssoc(value.assoc);
960
+ if (assoc == null) return undefined;
961
+ return { kind: "node", at: value.at, assoc };
962
+ }
963
+ case "detached": {
964
+ const lastKnownRange = asDocRange(value.lastKnownRange);
965
+ if (!lastKnownRange) return undefined;
966
+ if (!isDetachedReason(value.reason)) return undefined;
967
+ return { kind: "detached", lastKnownRange, reason: value.reason };
968
+ }
969
+ default:
970
+ return undefined;
971
+ }
972
+ }
973
+
974
+ function asDocRange(value: unknown): { from: number; to: number } | undefined {
975
+ if (!isRecord(value)) return undefined;
976
+ if (!isFiniteNumber(value.from) || !isFiniteNumber(value.to)) return undefined;
977
+ return { from: value.from, to: value.to };
978
+ }
979
+
980
+ function asBoundaryAssoc(value: unknown): BoundaryAssoc | undefined {
981
+ if (isAssoc(value)) {
982
+ return { start: value, end: value };
983
+ }
984
+ if (!isRecord(value)) return undefined;
985
+ if (!isAssoc(value.start) || !isAssoc(value.end)) return undefined;
986
+ return { start: value.start, end: value.end };
987
+ }
988
+
989
+ function asAssoc(value: unknown): Assoc | undefined {
990
+ return isAssoc(value) ? value : undefined;
991
+ }
992
+
993
+ function isAssoc(value: unknown): value is Assoc {
994
+ return value === -1 || value === 1;
995
+ }
996
+
997
+ function isDetachedReason(
998
+ value: unknown,
999
+ ): value is Extract<InternalEditorAnchorProjection, { kind: "detached" }>["reason"] {
1000
+ return value === "deleted" || value === "invalidatedByStructureChange" || value === "importAmbiguity";
1001
+ }
1002
+
1003
+ function isHeaderFooterVariant(value: unknown): value is "default" | "first" | "even" {
1004
+ return value === "default" || value === "first" || value === "even";
837
1005
  }
838
1006
 
839
1007
  function isCommandDocumentMode(
@@ -848,3 +1016,7 @@ function isCommandDocumentMode(
848
1016
  function isRecord(value: unknown): value is Record<string, unknown> {
849
1017
  return typeof value === "object" && value !== null;
850
1018
  }
1019
+
1020
+ function isFiniteNumber(value: unknown): value is number {
1021
+ return typeof value === "number" && Number.isFinite(value);
1022
+ }
@@ -0,0 +1,18 @@
1
+ export type LayoutWarningEmitter = (
2
+ type: string,
3
+ guard: string,
4
+ inputs: Record<string, unknown>,
5
+ ) => void;
6
+
7
+ let activeLayoutWarningEmitter: LayoutWarningEmitter | undefined;
8
+
9
+ export function setActiveLayoutWarningEmitter(fn: LayoutWarningEmitter | undefined): void {
10
+ activeLayoutWarningEmitter = fn;
11
+ }
12
+
13
+ export function emitLayoutGuardWarning(
14
+ guard: string,
15
+ inputs: Record<string, unknown>,
16
+ ): void {
17
+ activeLayoutWarningEmitter?.("layout.guard.return-empty", guard, inputs);
18
+ }