@beyondwork/docx-react-component 1.0.66 → 1.0.69

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 (384) hide show
  1. package/README.md +75 -931
  2. package/package.json +26 -27
  3. package/src/api/anchor-conversion.ts +43 -0
  4. package/src/api/editor-state-types.ts +2 -1
  5. package/src/api/public-types.ts +504 -101
  6. package/src/api/session-state.ts +4 -0
  7. package/src/api/v3/README.md +91 -0
  8. package/src/api/v3/_create.ts +146 -0
  9. package/src/api/v3/_layer-metadata.ts +362 -0
  10. package/src/api/v3/_mocks.ts +84 -0
  11. package/src/api/v3/_runtime-handle.ts +162 -0
  12. package/src/api/v3/_ux-response.ts +73 -0
  13. package/src/api/v3/ai/_metadata-audit.ts +225 -0
  14. package/src/api/v3/ai/attach.ts +235 -0
  15. package/src/api/v3/ai/bundle.ts +132 -0
  16. package/src/api/v3/ai/explain.ts +144 -0
  17. package/src/api/v3/ai/export.ts +54 -0
  18. package/src/api/v3/ai/inspect.ts +118 -0
  19. package/src/api/v3/ai/policy.ts +77 -0
  20. package/src/api/v3/ai/replacement.ts +341 -0
  21. package/src/api/v3/ai/resolve.ts +133 -0
  22. package/src/api/v3/index.ts +79 -0
  23. package/src/api/v3/runtime/chart.ts +310 -0
  24. package/src/api/v3/runtime/clipboard.ts +81 -0
  25. package/src/api/v3/runtime/collab.ts +331 -0
  26. package/src/api/v3/runtime/content.ts +236 -0
  27. package/src/api/v3/runtime/document.ts +282 -0
  28. package/src/api/v3/runtime/formatting.ts +186 -0
  29. package/src/api/v3/runtime/geometry.ts +349 -0
  30. package/src/api/v3/runtime/layout.ts +108 -0
  31. package/src/api/v3/runtime/review.ts +129 -0
  32. package/src/api/v3/runtime/search.ts +74 -0
  33. package/src/api/v3/runtime/table.ts +63 -0
  34. package/src/api/v3/runtime/workflow.ts +434 -0
  35. package/src/api/v3/ui/_context.ts +86 -0
  36. package/src/api/v3/ui/_create.ts +65 -0
  37. package/src/api/v3/ui/_types.ts +520 -0
  38. package/src/api/v3/ui/chrome-composition.ts +342 -0
  39. package/src/{ui-tailwind/chrome → api/v3/ui}/chrome-preset-model.ts +11 -1
  40. package/src/api/v3/ui/chrome.ts +476 -0
  41. package/src/api/v3/ui/debug.ts +124 -0
  42. package/src/api/v3/ui/index.ts +64 -0
  43. package/src/api/v3/ui/overlays-visibility.ts +170 -0
  44. package/src/api/v3/ui/overlays.ts +427 -0
  45. package/src/api/v3/ui/scope.ts +71 -0
  46. package/src/api/v3/ui/session.ts +100 -0
  47. package/src/api/v3/ui/surface.ts +170 -0
  48. package/src/api/v3/ui/viewport.ts +303 -0
  49. package/src/core/commands/index.ts +28 -6
  50. package/src/core/commands/list-commands.ts +3 -2
  51. package/src/core/commands/section-layout-commands.ts +9 -8
  52. package/src/core/schema/text-schema.ts +16 -0
  53. package/src/core/selection/mapping.ts +33 -72
  54. package/src/core/state/editor-state.ts +96 -189
  55. package/src/index.ts +23 -4
  56. package/src/io/chart-preview-resolver.ts +1 -1
  57. package/src/io/docx-session.ts +36 -4795
  58. package/src/io/export/build-app-properties-xml.ts +1 -1
  59. package/src/io/export/serialize-comments.ts +1 -1
  60. package/src/io/export/serialize-headers-footers.ts +6 -1
  61. package/src/io/export/serialize-main-document.ts +45 -0
  62. package/src/io/export/serialize-run-formatting.ts +17 -2
  63. package/src/io/export/twip.ts +1 -1
  64. package/src/io/normalize/normalize-text.ts +27 -20
  65. package/src/io/ooxml/chart/parse-series.ts +1 -1
  66. package/src/io/ooxml/chart/resolve-color.ts +2 -2
  67. package/src/io/ooxml/chart/types.ts +1 -1
  68. package/src/io/ooxml/classify-embedding.ts +83 -33
  69. package/src/io/ooxml/parse-fill.ts +1 -1
  70. package/src/io/ooxml/parse-main-document.ts +71 -1
  71. package/src/io/ooxml/parse-object.ts +14 -10
  72. package/src/io/ooxml/parse-run-formatting.ts +47 -1
  73. package/src/io/ooxml/property-grab-bag.ts +2 -2
  74. package/src/io/ooxml/units.ts +11 -0
  75. package/src/io/ooxml/workflow-payload.ts +282 -7
  76. package/src/model/anchor.ts +85 -0
  77. package/src/model/canonical-document.ts +351 -15
  78. package/src/model/chart-types.ts +1 -1
  79. package/src/model/layout/index.ts +83 -0
  80. package/src/model/layout/page-graph-types.ts +181 -0
  81. package/src/model/layout/page-layout-snapshot.ts +105 -0
  82. package/src/model/layout/resolved-layout-types.ts +47 -0
  83. package/src/model/layout/runtime-page-graph-types.ts +102 -0
  84. package/src/model/paragraph-scope-ids.ts +72 -0
  85. package/src/model/review/comment-types.ts +112 -0
  86. package/src/model/review/index.ts +2 -0
  87. package/src/model/review/revision-types.ts +215 -0
  88. package/src/model/snapshot.ts +32 -0
  89. package/src/review/store/comment-store.ts +21 -47
  90. package/src/review/store/revision-types.ts +40 -198
  91. package/src/runtime/collab/base-doc-fingerprint.ts +6 -1
  92. package/src/runtime/collab/runtime-collab-sync.ts +13 -3
  93. package/src/runtime/collab-session.ts +1 -1
  94. package/src/runtime/debug/build-debug-inspector-snapshot.ts +686 -0
  95. package/src/runtime/debug/event-ring-buffer.ts +64 -0
  96. package/src/runtime/debug/probability-sampler.ts +18 -0
  97. package/src/runtime/debug/runtime-debug-facet.ts +67 -0
  98. package/src/runtime/debug/stage-tokens.ts +31 -0
  99. package/src/runtime/debug/telemetry-bus.ts +271 -0
  100. package/src/runtime/debug/types.ts +275 -0
  101. package/src/runtime/debug/wrap-ref-for-telemetry.ts +118 -0
  102. package/src/runtime/document-layout.ts +8 -6
  103. package/src/runtime/document-runtime.ts +843 -1141
  104. package/src/runtime/document-search.ts +1 -1
  105. package/src/runtime/edit-ops/index.ts +1 -1
  106. package/src/runtime/external-send-runtime.ts +1 -1
  107. package/src/runtime/formatting/document-lookup.ts +235 -0
  108. package/src/runtime/formatting/field/registry.ts +41 -0
  109. package/src/runtime/{field-resolver.ts → formatting/field/resolver.ts} +27 -2
  110. package/src/runtime/formatting/font-resolution.ts +83 -0
  111. package/src/runtime/formatting/formatting-context.ts +903 -0
  112. package/src/runtime/formatting/formatting-types.ts +157 -0
  113. package/src/runtime/{hyperlink-color-resolver.ts → formatting/hyperlink-color.ts} +2 -2
  114. package/src/runtime/formatting/index.ts +125 -0
  115. package/src/runtime/{resolved-numbering-geometry.ts → formatting/numbering/geometry.ts} +1 -1
  116. package/src/runtime/{numbering-prefix.ts → formatting/numbering/prefix.ts} +170 -3
  117. package/src/runtime/formatting/paragraph-style-resolver.ts +92 -0
  118. package/src/runtime/formatting/projector.ts +75 -0
  119. package/src/runtime/formatting/resolve-effective.ts +407 -0
  120. package/src/runtime/formatting/revision-display.ts +105 -0
  121. package/src/runtime/{paragraph-style-resolver.ts → formatting/style-cascade.ts} +84 -141
  122. package/src/runtime/{table-style-resolver.ts → formatting/table-style-resolver.ts} +1 -1
  123. package/src/runtime/formatting/telemetry-bridge.ts +106 -0
  124. package/src/runtime/{theme-color-resolver.ts → formatting/theme-color.ts} +2 -30
  125. package/src/runtime/geometry/caret-geometry.ts +164 -0
  126. package/src/runtime/geometry/geometry-facet.ts +364 -0
  127. package/src/runtime/geometry/geometry-types.ts +256 -0
  128. package/src/runtime/geometry/hit-test.ts +125 -0
  129. package/src/runtime/geometry/index.ts +71 -0
  130. package/src/runtime/geometry/inert-geometry-facet.ts +43 -0
  131. package/src/runtime/geometry/invalidation.ts +35 -0
  132. package/src/runtime/geometry/object-handles.ts +77 -0
  133. package/src/runtime/geometry/overlay-rects.ts +85 -0
  134. package/src/runtime/geometry/project-anchors.ts +100 -0
  135. package/src/runtime/geometry/project-fragments.ts +216 -0
  136. package/src/runtime/geometry/projector.ts +129 -0
  137. package/src/runtime/geometry/replacement-envelope.ts +130 -0
  138. package/src/runtime/geometry/viewport.ts +218 -0
  139. package/src/runtime/layout/compat-input-ledger.ts +211 -0
  140. package/src/runtime/layout/index.ts +6 -1
  141. package/src/runtime/layout/inert-layout-facet.ts +12 -7
  142. package/src/runtime/layout/layout-engine-instance.ts +189 -11
  143. package/src/runtime/layout/layout-engine-version.ts +450 -1
  144. package/src/runtime/layout/layout-facet-types.ts +60 -0
  145. package/src/runtime/layout/layout-measurement-provider.ts +13 -0
  146. package/src/runtime/layout/measurement-backend-canvas.ts +14 -2
  147. package/src/runtime/layout/measurement-backend-empirical.ts +23 -4
  148. package/src/runtime/layout/page-graph.ts +62 -209
  149. package/src/runtime/layout/page-story-resolver.ts +7 -12
  150. package/src/runtime/layout/paginated-layout-engine.ts +186 -11
  151. package/src/runtime/layout/project-block-fragments.ts +11 -0
  152. package/src/runtime/layout/projector.ts +90 -0
  153. package/src/runtime/layout/public-facet.ts +187 -442
  154. package/src/runtime/layout/resolved-formatting-state.ts +158 -26
  155. package/src/runtime/layout/table-render-plan.ts +1 -1
  156. package/src/runtime/prerender/cache-envelope.ts +6 -1
  157. package/src/runtime/prerender/prerender-document.ts +18 -23
  158. package/src/runtime/render/decoration-resolver.ts +1 -1
  159. package/src/runtime/render/render-frame-types.ts +20 -0
  160. package/src/runtime/render/render-kernel.ts +94 -25
  161. package/src/runtime/scopes/_formatting-seam.ts +262 -0
  162. package/src/runtime/scopes/_scope-dependencies.ts +49 -0
  163. package/src/runtime/scopes/action-validation.ts +356 -0
  164. package/src/runtime/scopes/attach-explanation.ts +102 -0
  165. package/src/runtime/scopes/audit-bundle.ts +71 -0
  166. package/src/runtime/scopes/compile-scope-bundle.ts +163 -0
  167. package/src/runtime/scopes/compile-scope.ts +262 -0
  168. package/src/runtime/scopes/compiler-service.ts +431 -0
  169. package/src/runtime/scopes/create-issue.ts +107 -0
  170. package/src/runtime/scopes/enumerate-scopes.ts +543 -0
  171. package/src/runtime/scopes/evidence.ts +233 -0
  172. package/src/runtime/scopes/index.ts +150 -0
  173. package/src/runtime/scopes/position-map.ts +214 -0
  174. package/src/runtime/scopes/preservation-boundary.ts +91 -0
  175. package/src/runtime/scopes/projector.ts +49 -0
  176. package/src/runtime/scopes/replaceability.ts +87 -0
  177. package/src/runtime/scopes/replacement/apply.ts +228 -0
  178. package/src/runtime/scopes/replacement/compile.ts +59 -0
  179. package/src/runtime/scopes/replacement/propose.ts +42 -0
  180. package/src/runtime/scopes/resolve-reference.ts +347 -0
  181. package/src/runtime/scopes/review-bundle.ts +141 -0
  182. package/src/runtime/scopes/scope-kinds/_paragraph-text.ts +57 -0
  183. package/src/runtime/scopes/scope-kinds/_table-text.ts +42 -0
  184. package/src/runtime/scopes/scope-kinds/comment-thread.ts +59 -0
  185. package/src/runtime/scopes/scope-kinds/field.ts +65 -0
  186. package/src/runtime/scopes/scope-kinds/heading.ts +84 -0
  187. package/src/runtime/scopes/scope-kinds/list-item.ts +77 -0
  188. package/src/runtime/scopes/scope-kinds/paragraph.ts +182 -0
  189. package/src/runtime/scopes/scope-kinds/revision.ts +62 -0
  190. package/src/runtime/scopes/scope-kinds/table-cell.ts +57 -0
  191. package/src/runtime/scopes/scope-kinds/table-row.ts +61 -0
  192. package/src/runtime/scopes/scope-kinds/table.ts +55 -0
  193. package/src/runtime/scopes/scope-range.ts +208 -0
  194. package/src/runtime/scopes/semantic-scope-types.ts +454 -0
  195. package/src/runtime/scopes/workflow-overlap.ts +92 -0
  196. package/src/runtime/selection/index.ts +1 -1
  197. package/src/runtime/structure-ops/fragment-insert.ts +1 -1
  198. package/src/runtime/structure-ops/index.ts +1 -1
  199. package/src/runtime/surface-projection.ts +232 -262
  200. package/src/runtime/units.ts +4 -2
  201. package/src/runtime/workflow/coordinator.ts +1348 -0
  202. package/src/runtime/workflow/derived-scope-resolver.ts +125 -0
  203. package/src/runtime/workflow/index.ts +25 -0
  204. package/src/runtime/workflow/markup-mode-policy.ts +98 -0
  205. package/src/runtime/{workflow-markup.ts → workflow/markup.ts} +6 -6
  206. package/src/runtime/workflow/metadata-persistence.ts +306 -0
  207. package/src/runtime/workflow/metadata-writer.ts +123 -0
  208. package/src/runtime/workflow/overlay-store.ts +690 -0
  209. package/src/runtime/workflow/projector.ts +127 -0
  210. package/src/runtime/{query-scopes.ts → workflow/query-scopes.ts} +3 -3
  211. package/src/runtime/{workflow-rail-segments.ts → workflow/rail/compose.ts} +60 -165
  212. package/src/runtime/workflow/rail/types.ts +198 -0
  213. package/src/runtime/workflow/scope-rail-composer.ts +39 -0
  214. package/src/runtime/{scope-resolver.ts → workflow/scope-resolver.ts} +3 -3
  215. package/src/runtime/workflow/scope-writer.ts +188 -0
  216. package/src/runtime/{tamper-gate.ts → workflow/tamper-gate.ts} +1 -1
  217. package/src/runtime/workflow/visibility-policy.ts +129 -0
  218. package/src/session/_sync-legacy.ts +66 -0
  219. package/src/session/export/embedded-reconstitute.ts +104 -0
  220. package/src/session/export/export-diagnostics.ts +85 -0
  221. package/src/session/export/export-validation.ts +110 -0
  222. package/src/session/export/index.ts +34 -0
  223. package/src/session/export/preservation-reattach.ts +30 -0
  224. package/src/session/export/serialize-dispatch.ts +165 -0
  225. package/src/session/export/stateful-export-pipeline.ts +432 -0
  226. package/src/session/export/stateful-export.ts +684 -0
  227. package/src/session/import/canonical-assembly.ts +227 -0
  228. package/src/session/import/diagnostics-session.ts +54 -0
  229. package/src/session/import/embedded-discovery.ts +225 -0
  230. package/src/session/import/embedded-offload.ts +337 -0
  231. package/src/session/import/import-diagnostics.ts +69 -0
  232. package/src/session/import/loader-types.ts +313 -0
  233. package/src/session/import/loader.ts +1834 -0
  234. package/src/session/import/normalize.ts +195 -0
  235. package/src/session/import/package-parts.ts +217 -0
  236. package/src/session/import/package-read.ts +195 -0
  237. package/src/session/import/parse-orchestration.ts +105 -0
  238. package/src/session/import/part-constants.ts +70 -0
  239. package/src/session/import/part-discovery.ts +94 -0
  240. package/src/session/import/preservation-index.ts +46 -0
  241. package/src/{runtime/read-only-diagnostics-runtime.ts → session/import/read-only-diagnostics.ts} +24 -3
  242. package/src/session/import/review-import.ts +508 -0
  243. package/src/session/import/styles-consolidation.ts +281 -0
  244. package/src/session/import/workflow-scope-import.ts +256 -0
  245. package/src/session/index.ts +37 -0
  246. package/src/session/session-state.ts +69 -0
  247. package/src/session/session.ts +532 -0
  248. package/src/session/shared/protection.ts +228 -0
  249. package/src/session/shared/session-utils.ts +82 -0
  250. package/src/session/types.ts +499 -0
  251. package/src/shell/chart-snapshots.ts +96 -0
  252. package/src/shell/media-previews.ts +85 -0
  253. package/src/shell/overlay-anchor-bridge.ts +53 -0
  254. package/src/shell/paste-adapter.ts +23 -0
  255. package/src/shell/ref-commands.ts +1697 -0
  256. package/src/shell/ref-utilities.ts +48 -0
  257. package/src/shell/search.ts +51 -0
  258. package/src/{ui/editor-runtime-boundary.ts → shell/session-bootstrap.ts} +243 -67
  259. package/src/shell/ui-subscriber-channels.ts +81 -0
  260. package/src/shell/use-collab-sync.ts +116 -0
  261. package/src/ui/WordReviewEditor.tsx +496 -2051
  262. package/src/ui/editor-shell-view.tsx +30 -1
  263. package/src/ui/editor-surface-controller.tsx +49 -1
  264. package/src/ui/headless/revision-decoration-model.ts +83 -0
  265. package/src/{ui-tailwind/chrome → ui/headless}/role-action-sets.ts +1 -1
  266. package/src/ui/headless/scoped-chrome-policy.ts +2 -2
  267. package/src/ui/headless/selection-tool-context.ts +1 -1
  268. package/src/ui/headless/selection-tool-resolver.ts +1 -1
  269. package/src/ui/runtime-shortcut-dispatch.ts +46 -1
  270. package/src/ui/ui-controller-factory.ts +221 -0
  271. package/src/ui-tailwind/chart/ChartSurface.tsx +2 -2
  272. package/src/ui-tailwind/chart/layout/legend-layout.ts +1 -1
  273. package/src/ui-tailwind/chart/layout/plot-area.ts +2 -2
  274. package/src/ui-tailwind/chart/layout/title-layout.ts +1 -1
  275. package/src/ui-tailwind/chart/render/area.tsx +3 -3
  276. package/src/ui-tailwind/chart/render/bar-column.tsx +3 -3
  277. package/src/ui-tailwind/chart/render/bubble.tsx +3 -3
  278. package/src/ui-tailwind/chart/render/combo.tsx +2 -2
  279. package/src/ui-tailwind/chart/render/data-labels.tsx +2 -2
  280. package/src/ui-tailwind/chart/render/font-metrics.ts +2 -2
  281. package/src/ui-tailwind/chart/render/line.tsx +3 -3
  282. package/src/ui-tailwind/chart/render/pie.tsx +6 -6
  283. package/src/ui-tailwind/chart/render/scatter.tsx +3 -3
  284. package/src/ui-tailwind/chart/render/svg-primitives.ts +3 -3
  285. package/src/ui-tailwind/chart/render/unsupported.tsx +2 -2
  286. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +88 -0
  287. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +1 -1
  288. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +1 -1
  289. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +1 -1
  290. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +1 -1
  291. package/src/ui-tailwind/chrome/editor-action-registry.ts +553 -0
  292. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +182 -0
  293. package/src/ui-tailwind/chrome/local-surface-arbiter.ts +534 -0
  294. package/src/ui-tailwind/chrome/resolve-target-kind.ts +226 -0
  295. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +38 -4
  296. package/src/ui-tailwind/chrome/tw-context-band.tsx +125 -0
  297. package/src/ui-tailwind/chrome/tw-context-menu-portal.tsx +248 -0
  298. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +42 -1
  299. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +8 -7
  300. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +38 -4
  301. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +104 -6
  302. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +66 -7
  303. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +54 -8
  304. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +7 -1
  305. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +33 -0
  306. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +78 -1
  307. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +16 -8
  308. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +276 -0
  309. package/src/ui-tailwind/chrome/use-context-menu-controller.ts +201 -0
  310. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +1 -1
  311. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +22 -4
  312. package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +1 -1
  313. package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +1 -1
  314. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +11 -5
  315. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +197 -3
  316. package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +1 -1
  317. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +35 -6
  318. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +24 -16
  319. package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +1 -1
  320. package/src/ui-tailwind/debug/README.md +57 -0
  321. package/src/ui-tailwind/debug/index.ts +3 -0
  322. package/src/ui-tailwind/debug/tw-debug-overlay.tsx +186 -0
  323. package/src/ui-tailwind/debug/tw-debug-presentation.tsx +80 -0
  324. package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +83 -0
  325. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +2 -2
  326. package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +1 -1
  327. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +135 -10
  328. package/src/ui-tailwind/editor-surface/pm-decorations.ts +40 -13
  329. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +1 -1
  330. package/src/ui-tailwind/editor-surface/pm-schema.ts +1 -1
  331. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +3 -3
  332. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +1 -1
  333. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +2 -2
  334. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +91 -9
  335. package/src/ui-tailwind/editor-surface/shape-renderer.ts +1 -1
  336. package/src/ui-tailwind/editor-surface/surface-layer.ts +1 -1
  337. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +1 -1
  338. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +23 -6
  339. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -22
  340. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +1 -1
  341. package/src/ui-tailwind/index.ts +0 -5
  342. package/src/ui-tailwind/overlay-anchor-bridge-context.tsx +33 -0
  343. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +66 -29
  344. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +25 -2
  345. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +15 -0
  346. package/src/ui-tailwind/review/tw-review-rail.tsx +92 -4
  347. package/src/ui-tailwind/review/tw-workflow-tab.tsx +1 -1
  348. package/src/ui-tailwind/review-workspace/page-chrome.ts +210 -0
  349. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +101 -0
  350. package/src/ui-tailwind/review-workspace/paragraph-layout.ts +115 -0
  351. package/src/ui-tailwind/review-workspace/selection-toolbar-placement.ts +97 -0
  352. package/src/ui-tailwind/review-workspace/tw-review-workspace-navigator.tsx +130 -0
  353. package/src/ui-tailwind/review-workspace/tw-review-workspace-page-toolbar.tsx +240 -0
  354. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +59 -0
  355. package/src/ui-tailwind/review-workspace/types.ts +408 -0
  356. package/src/ui-tailwind/review-workspace/use-chrome-policy.ts +104 -0
  357. package/src/ui-tailwind/review-workspace/use-derived-view-state.ts +151 -0
  358. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +70 -0
  359. package/src/ui-tailwind/review-workspace/use-grabbed-segment-offsets.ts +40 -0
  360. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +55 -0
  361. package/src/ui-tailwind/review-workspace/use-page-markers.ts +130 -0
  362. package/src/ui-tailwind/review-workspace/use-pm-surface-capture.ts +60 -0
  363. package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +63 -0
  364. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +170 -0
  365. package/src/ui-tailwind/review-workspace/use-scroll-root-capture.ts +28 -0
  366. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +113 -0
  367. package/src/ui-tailwind/review-workspace/use-shell-selection-anchor-bridge.ts +120 -0
  368. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +55 -0
  369. package/src/ui-tailwind/review-workspace/use-viewport-dimensions.ts +43 -0
  370. package/src/ui-tailwind/review-workspace/use-workspace-arbiter.ts +25 -0
  371. package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +86 -0
  372. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +150 -0
  373. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  374. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -2
  375. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +61 -98
  376. package/src/ui-tailwind/tw-review-workspace.tsx +521 -1802
  377. package/src/ui-tailwind/ui-api-context.tsx +43 -0
  378. package/src/ui-tailwind/ui-shell-channels-context.tsx +49 -0
  379. package/src/validation/compatibility-engine.ts +6 -6
  380. package/src/runtime/styles-cascade.ts +0 -33
  381. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +0 -85
  382. /package/src/runtime/{page-number-format.ts → formatting/field/page-number-format.ts} +0 -0
  383. /package/src/runtime/{ai-action-policy.ts → workflow/ai-action-policy.ts} +0 -0
  384. /package/src/runtime/{scope-tag-registry.ts → workflow/scope-tag-registry.ts} +0 -0
@@ -81,6 +81,9 @@ import type {
81
81
  WorkflowCandidateRangeOptions,
82
82
  WorkflowBlockedCommandReason,
83
83
  WorkflowMetadataDefinition,
84
+ OverlayKind,
85
+ OverlayVisibilityPolicy,
86
+ WorkflowMarkupModePolicy,
84
87
  WorkflowMetadataEntry,
85
88
  WorkflowMetadataSnapshot,
86
89
  WorkflowMarkupSnapshot,
@@ -129,7 +132,7 @@ import {
129
132
  snapCommentAnchorAwayFromTable,
130
133
  } from "../core/selection/review-anchors.ts";
131
134
  import { buildBookmarkNameMap } from "../legal/bookmarks.ts";
132
- import { createFieldResolver, type FieldResolver } from "./field-resolver.ts";
135
+ import { createFieldResolver, type FieldResolver } from "./formatting/field/resolver.ts";
133
136
  import { createFootnoteResolver, type FootnoteResolver } from "./footnote-resolver.ts";
134
137
  import {
135
138
  describeOpaqueFragment,
@@ -150,12 +153,30 @@ import {
150
153
  findAllScopesAt,
151
154
  findScopesIntersecting,
152
155
  resolveScope,
153
- } from "./scope-resolver.ts";
156
+ } from "./workflow/scope-resolver.ts";
154
157
  import { buildDiagnosticFromLegacyWarningCode } from "./diagnostics/build-diagnostic.ts";
158
+ import { TelemetryBus } from "./debug/telemetry-bus.ts";
159
+ import { emitStageToken } from "./debug/stage-tokens.ts";
160
+ import {
161
+ createRuntimeDebugFacet,
162
+ type RuntimeDebugFacet,
163
+ } from "./debug/runtime-debug-facet.ts";
164
+ import { setActiveSerializeTelemetryBus } from "../io/export/serialize-main-document.ts";
155
165
  import {
156
166
  projectScopeQueryResults,
157
167
  queryScopes as runQueryScopes,
158
- } from "./query-scopes.ts";
168
+ } from "./workflow/query-scopes.ts";
169
+ import {
170
+ createOverlayStore,
171
+ seedMarkerBackedScopeIds,
172
+ type OverlayStore,
173
+ } from "./workflow/overlay-store.ts";
174
+ import type { RuntimeOperationPlan, ScopeBundle } from "./scopes/semantic-scope-types.ts";
175
+ import { createScopeCompilerService } from "./scopes/compiler-service.ts";
176
+ import {
177
+ createWorkflowCoordinator,
178
+ type WorkflowCoordinator,
179
+ } from "./workflow/coordinator.ts";
159
180
  import {
160
181
  insertScopeMarkers,
161
182
  removeScopeMarkers,
@@ -171,7 +192,12 @@ import {
171
192
  import {
172
193
  collectWorkflowMarkupSnapshot,
173
194
  deriveWorkflowCandidateRangesFromMarkup,
174
- } from "./workflow-markup.ts";
195
+ } from "./workflow/markup.ts";
196
+ import {
197
+ attachScopeCardModel,
198
+ collectScopeRailSegments,
199
+ type ScopeRailSegment,
200
+ } from "./workflow/rail/compose.ts";
175
201
  import {
176
202
  createDocumentNavigationSnapshot,
177
203
  findPageForOffset,
@@ -181,12 +207,18 @@ import {
181
207
  createLayoutEngine,
182
208
  createLayoutFacet,
183
209
  createMeasurementProvider,
210
+ setActiveLayoutWarningEmitter,
184
211
  type DocxFontLoader,
185
212
  type LayoutEngineInstance,
213
+ type LayoutFacet,
186
214
  type LayoutMeasurementProvider,
187
215
  type WordReviewEditorLayoutFacet,
188
216
  } from "./layout/index.ts";
189
217
  import { createRenderKernel, type RenderKernel } from "./render/index.ts";
218
+ import {
219
+ createGeometryFacet,
220
+ type GeometryFacet,
221
+ } from "./geometry/index.ts";
190
222
  import {
191
223
  createDocumentOutlineSnapshot,
192
224
  createDocumentSectionSnapshots,
@@ -237,24 +269,25 @@ import {
237
269
  createEditorViewStateSnapshot,
238
270
  type ViewState,
239
271
  } from "./view-state.ts";
240
- import { ThemeColorResolver } from "./theme-color-resolver.ts";
272
+ import { ThemeColorResolver } from "./formatting/theme-color.ts";
241
273
  import type {
242
274
  BlockNode,
243
275
  CanonicalDocument,
244
276
  FieldNode,
245
277
  FieldRefreshStatus,
246
278
  InlineNode,
279
+ MutableCanonicalDocument,
247
280
  PageMargins,
248
281
  ParagraphNode,
249
282
  SectionProperties,
250
283
  SubPartsCatalog,
251
284
  } from "../model/canonical-document.ts";
252
285
  import {
253
- buildFieldRegistry,
254
286
  isSupportedFieldFamily,
255
287
  parseTocLevelRange,
256
288
  resolveRefFieldText,
257
289
  } from "../io/ooxml/parse-fields.ts";
290
+ import { rebuildFieldRegistry } from "./formatting/field/registry.ts";
258
291
  import {
259
292
  incrementInvalidationCounter,
260
293
  recordPerfSample,
@@ -286,7 +319,7 @@ import {
286
319
  import type { EditorStatePayload } from "../io/ooxml/workflow-payload.ts";
287
320
  import type { SharedWorkflowState } from "./collab/workflow-shared.ts";
288
321
  import { mapLocalSelectionOnRemoteReplay } from "./collab/map-local-selection-on-remote-replay.ts";
289
- import { formatPageNumber } from "./page-number-format.ts";
322
+ import { formatPageNumber } from "./formatting/field/page-number-format.ts";
290
323
 
291
324
  /** Internal extension of ExportDocxOptions that threads the collected
292
325
  * editorState payload from the runtime to the docx serializer. */
@@ -393,6 +426,16 @@ export interface DocumentRuntime {
393
426
  /** R.5.a — `true` when the runtime is inside one or more action brackets. */
394
427
  isInAction(): boolean;
395
428
  applyActiveStoryTextCommand(command: ActiveStoryTextCommand): TextCommandAck;
429
+ /**
430
+ * Layer-08 Slice-5 — apply a compiled `RuntimeOperationPlan` from the
431
+ * scope compiler. Each plan step lowers to one or more existing
432
+ * runtime commands (text.insert, tracked-change ops). Callers route
433
+ * through `src/runtime/scopes/replacement/apply.ts` which validates
434
+ * the plan and emits a single `ScopeActionAudit` on the `scope`
435
+ * telemetry channel — do not call this method directly from agent or
436
+ * UI paths.
437
+ */
438
+ applyScopeReplacement(plan: RuntimeOperationPlan): void;
396
439
  dispatch(command: EditorCommand): void;
397
440
  /**
398
441
  * Apply a command received from a remote collaborator. The command
@@ -469,6 +512,33 @@ export interface DocumentRuntime {
469
512
  editCommentBody(commentId: string, body: string): void;
470
513
  addScope(params: AddScopeParams): AddScopeResult;
471
514
  getScope(scopeId: string): WorkflowScope | null;
515
+ /**
516
+ * Layer-08 scope-bundle lookup keyed by scopeId — read-side join over
517
+ * canonical document + enumerated scopes + workflow metadata entries.
518
+ * `nowUtc` stays caller-supplied (S3 determinism — no `new Date()`
519
+ * fallback). Returns `null` when the id does not enumerate.
520
+ *
521
+ * Exposed on the runtime so v3's `ui.scope.*` family (L10) can reach
522
+ * `ScopeBundle` without importing `src/runtime/scopes/**` (blocked by
523
+ * `ci-check-ui-api-layer-purity`). Parallels `ai.getScopeBundle`
524
+ * (scope-handle keyed) minus the handle-unpack step and the structured
525
+ * not-found discriminator — the L10 seam takes raw scopeIds from UI
526
+ * state so null/nothing semantics match lookup-by-id elsewhere on the
527
+ * handle (e.g. `getScope`).
528
+ */
529
+ compileScopeBundleById(scopeId: string, nowUtc: string): ScopeBundle | null;
530
+ /**
531
+ * Debug projector support — readonly view of scope ids the runtime
532
+ * considers marker-backed (present in both `collectScopeLocations(doc)`
533
+ * and `workflowOverlay.scopes`). Internal-only.
534
+ */
535
+ getMarkerBackedScopeIds(): ReadonlySet<string>;
536
+ /**
537
+ * Debug + telemetry facet. Internal-only. Not forwarded onto the public
538
+ * ref. Phase 2 `services/debug/` accesses it via direct runtime reference
539
+ * (in-process) or via the hidden harness page's `window.__debug` RPC.
540
+ */
541
+ readonly debug: import("./debug/runtime-debug-facet.ts").RuntimeDebugFacet;
472
542
  removeScope(scopeId: string): void;
473
543
  /** §C8 — Add a scope with visibility: "invisible" atomically. */
474
544
  addInvisibleScope(params: Omit<AddScopeParams, "mode"> & { mode?: WorkflowScopeMode }): AddScopeResult;
@@ -517,8 +587,59 @@ export interface DocumentRuntime {
517
587
  * Runtime-owned layout facet. Provides graph-aware queries, fragment
518
588
  * resolution, formatting inspection, and layout events. Prefer this over
519
589
  * the opaque snapshot methods for new integration code.
590
+ *
591
+ * The public-API type name is `WordReviewEditorLayoutFacet`; the
592
+ * refactor-era structural alias is `LayoutFacet` (see
593
+ * `src/runtime/layout/layout-facet-types.ts`). Both resolve to the same
594
+ * interface.
520
595
  */
521
596
  readonly layout: WordReviewEditorLayoutFacet;
597
+ /**
598
+ * Layer-06 (workflow) API surface. Canonical site for scope-rail
599
+ * composition and scope-card projection. Post refactor/06 Slice 4C
600
+ * rail-seam inversion, `runtime.layout` no longer exposes rail/card
601
+ * methods at all — this is the sole seam. See
602
+ * `docs/architecture/06-workflow-review.md` contract W7 + W9.
603
+ */
604
+ readonly workflow: {
605
+ /** Scope rail segments for the given page index. */
606
+ getRailSegments(pageIndex: number): readonly ScopeRailSegment[];
607
+ /** All scope rail segments across every page in the current page graph. */
608
+ getAllRailSegments(): readonly ScopeRailSegment[];
609
+ /**
610
+ * Project current rail segments into per-scope `ScopeCardModel` values,
611
+ * joining issue/review-action metadata, suggestions, and (when a render
612
+ * kernel is available) primary anchor rects.
613
+ */
614
+ getAllScopeCardModels(): readonly import("../api/public-types.ts").ScopeCardModel[];
615
+ };
616
+ /**
617
+ * Runtime-owned geometry facet (Layer 05 · Geometry Projection).
618
+ *
619
+ * Owns concrete visual geometry: rects, hit-tests, caret geometry, anchor
620
+ * rects, selection rects, replacement envelopes, object handles, and the
621
+ * runtime viewport. Projected from the render-kernel frame + layout
622
+ * semantics — never from the DOM (contract G2;
623
+ * `docs/architecture/05-geometry-projection.md`).
624
+ *
625
+ * Construction is wired to the same `renderKernelRef` as the layout facet,
626
+ * so hit-test and anchor queries read from the current render frame
627
+ * directly rather than via the layout-facet compat wrappers that remain on
628
+ * the layout surface for backwards-compat.
629
+ *
630
+ * v3 `runtime.geometry.*` family (`src/api/v3/runtime/geometry.ts`) reads
631
+ * through this facet; the v3 layer does NOT reach geometry through
632
+ * `runtime.layout` any more.
633
+ */
634
+ readonly geometry: GeometryFacet;
635
+ /**
636
+ * Refactor/04 Slice 2 — expose the layout facet via the structural alias
637
+ * `LayoutFacet` for consumers that want to depend on the
638
+ * refactor-era name rather than the public-API name. Returns the same
639
+ * underlying facet as `runtime.layout`; the type narrows to `LayoutFacet`
640
+ * at the return site.
641
+ */
642
+ getLayoutFacet(): LayoutFacet;
522
643
  getCurrentLocation(): DocumentLocationSnapshot | null;
523
644
  getLocationForSelection(selection: SelectionSnapshot): DocumentLocationSnapshot | null;
524
645
  getLocationForAnchor(
@@ -576,6 +697,68 @@ export interface DocumentRuntime {
576
697
  setWorkflowMetadataEntries(entries: WorkflowMetadataEntry[]): void;
577
698
  clearWorkflowMetadataEntries(): void;
578
699
  getWorkflowMetadataSnapshot(): WorkflowMetadataSnapshot;
700
+ /** W10 — class-A overlay-visibility policy (per-kind read). */
701
+ getVisibilityPolicy(kind: OverlayKind): OverlayVisibilityPolicy | null;
702
+ /** W10 — class-A policy set. */
703
+ getVisibilityPolicies(): readonly OverlayVisibilityPolicy[];
704
+ /** W10 — author a class-A policy. Returns true when state changed. */
705
+ setVisibilityPolicy(policy: OverlayVisibilityPolicy): boolean;
706
+ /** W10 — clear a class-A policy. Returns true when state changed. */
707
+ clearVisibilityPolicy(kind: OverlayKind): boolean;
708
+ /**
709
+ * W10 — subscribe to any policy-set change. Listener fires after a
710
+ * successful `setVisibilityPolicy` / `clearVisibilityPolicy` call or
711
+ * any internal bulk-replace (reload / collab sync). Returns an
712
+ * unsubscribe function. Consumed by L10 X3's
713
+ * `ui.overlays.subscribeVisibility` so UI subscribers re-fire when
714
+ * authoring-tool hosts mutate policy mid-session.
715
+ */
716
+ subscribeVisibilityPolicy(listener: () => void): Unsubscribe;
717
+ /** X5 — read the class-A markup-mode policy. `null` means the doc
718
+ * carried no authored mode. */
719
+ getMarkupModePolicy(): WorkflowMarkupModePolicy | null;
720
+ /** X5 — author (or clear with `null`) the class-A markup-mode policy.
721
+ * Returns true when state changed. */
722
+ setMarkupModePolicy(policy: WorkflowMarkupModePolicy | null): boolean;
723
+ /** X5 — subscribe to markup-mode policy changes. Consumed by L10's
724
+ * `ui.viewport` composition (state-classes X5). */
725
+ subscribeMarkupModePolicy(listener: () => void): Unsubscribe;
726
+ /**
727
+ * X5 end-to-end activation — the composition site
728
+ * (`api.ui.viewport.getEffectiveMarkupMode`) hands the runtime a
729
+ * callback that returns the CURRENTLY composed effective markup
730
+ * mode (L06 policy × L10 local preference). When set, the runtime
731
+ * threads it into every `createEditorSurfaceSnapshot` call via
732
+ * `SurfaceProjectionOptions.getEffectiveMarkupMode` so the next
733
+ * projection applies posture without re-parsing the document.
734
+ *
735
+ * Typical wire site: `WordReviewEditor.tsx`, called once right
736
+ * after `createApiV3(runtime, { ui })` so the callback closes over
737
+ * the UI family's closure state (local preference + subscriber
738
+ * chain).
739
+ *
740
+ * Pass `undefined` to clear (teardown). When cleared, surface-
741
+ * projection falls back to the static `revisionMarkupMode` option
742
+ * (if any caller threads one) or to posture-off behavior.
743
+ */
744
+ setEffectiveMarkupModeProvider(
745
+ provider:
746
+ | (() => "clean" | "simple" | "all" | undefined)
747
+ | undefined,
748
+ ): void;
749
+ /**
750
+ * X5 end-to-end activation — the composition site (typically
751
+ * `WordReviewEditor.tsx` subscribing to
752
+ * `api.ui.viewport.subscribeEffectiveMarkupMode`) calls this when
753
+ * the composed mode changes due to a class-C local preference
754
+ * flip. Busts the derived-runtime surface cache + fires a host
755
+ * refresh so the new mode propagates on the next frame.
756
+ *
757
+ * Class-A policy changes already reach the runtime via
758
+ * `setMarkupModePolicy` (which triggers its own invalidation
759
+ * chain), so this entry is for the local-preference-only path.
760
+ */
761
+ invalidateForMarkupModeChange(): void;
579
762
  /**
580
763
  * Phase C §C1 — snapshot-based filter + join projection. See
581
764
  * `WordReviewEditorRef.queryScopes` for contract.
@@ -690,6 +873,14 @@ export interface CreateDocumentRuntimeOptions {
690
873
  defaultAuthorId?: string;
691
874
  fatalError?: EditorError;
692
875
  clock?: () => string;
876
+ /**
877
+ * Optional pre-constructed telemetry bus. When provided, the runtime
878
+ * uses this instance instead of creating a fresh one — lets callers
879
+ * (e.g. `services/debug/lib/session-manager.ts`) share a single bus
880
+ * across the load path (so the parse channel emits via
881
+ * `setActiveParseTelemetryBus`) and the runtime lifetime.
882
+ */
883
+ telemetryBus?: TelemetryBus;
693
884
  exportDocx?: (
694
885
  sessionState: EditorSessionState,
695
886
  options?: ExportDocxOptions,
@@ -734,7 +925,7 @@ export interface CreateDocumentRuntimeOptions {
734
925
  * `LAYOUT_ENGINE_VERSION` (callers rely on the cache-key scheme in
735
926
  * `src/runtime/prerender/cache-key.ts` to guarantee this).
736
927
  *
737
- * See docs/plans/lane-2-render-performance.md §Phase 2.5 §3.7.
928
+ * See CLAUDE.md (lane status table) §Phase 2.5 §3.7.
738
929
  */
739
930
  seedLayoutCache?: import("./layout/page-graph.ts").RuntimePageGraph;
740
931
  }
@@ -747,7 +938,7 @@ interface HistoryState {
747
938
  /**
748
939
  * L7 Phase 1.7.4 — structural-counts hash for the reviewWork cache key.
749
940
  *
750
- * `getCachedWorkflowMarkupSnapshot()` returns a NEW object on every
941
+ * `workflowCoordinator.getWorkflowMarkupSnapshot()` returns a NEW object on every
751
942
  * `revisionToken` bump, so the previous reference-equality check
752
943
  * (`cachedReviewWork.workflowMarkup === wfMarkup`) always failed on pure
753
944
  * text edits — forcing `createReviewWorkSnapshot` to re-walk every comment,
@@ -839,6 +1030,17 @@ export function createDocumentRuntime(
839
1030
  const loadScheduler: LoadScheduler =
840
1031
  options.loadScheduler ?? createLoadScheduler({ backendOverride: "sync" });
841
1032
 
1033
+ // X5 end-to-end activation (coord-03 §5b). Set by the UI API
1034
+ // composition site (`api.ui.viewport.getEffectiveMarkupMode`) via
1035
+ // `setEffectiveMarkupModeProvider` after `createApiV3` constructs
1036
+ // the ui family. When set, every `createEditorSurfaceSnapshot` call
1037
+ // below threads it into `SurfaceProjectionOptions.getEffectiveMarkupMode`
1038
+ // so revision-display posture reflects the composed L06 × L10 mode
1039
+ // without re-parsing the document.
1040
+ let effectiveMarkupModeProvider:
1041
+ | (() => "clean" | "simple" | "all" | undefined)
1042
+ | undefined;
1043
+
842
1044
  // L7 Phase 0 — perf counters. Each `refreshRenderSnapshot` call increments
843
1045
  // `refresh.all`. Phase 1 will add per-facet `facet.<name>.build` counters
844
1046
  // wired to the per-facet cached builders. Cost is one Map.set per call.
@@ -878,27 +1080,46 @@ export function createDocumentRuntime(
878
1080
  enforcedRangeCount: 0,
879
1081
  preservedRangeCount: 0,
880
1082
  };
881
- let workflowOverlay: WorkflowOverlay | null =
882
- structuredClone(
1083
+ // Phase 1 session-scoped telemetry. Hoisted early because the
1084
+ // workflow overlay store uses it for the `scope.marker_backed_delta`
1085
+ // event. All channels default off; cost is one bitmask AND per
1086
+ // hook-point when disabled.
1087
+ //
1088
+ // Accepts an injected bus so the load path (docx-session.parseMainDocumentXml
1089
+ // via setActiveParseTelemetryBus) can share one bus with the runtime it
1090
+ // will eventually back — see CreateDocumentRuntimeOptions.telemetryBus.
1091
+ const telemetryBus = options.telemetryBus ?? new TelemetryBus();
1092
+ let hostAnnotationOverlay: HostAnnotationOverlay | null = null;
1093
+ // Layer-06 workflow state + caches live in the overlay store and
1094
+ // coordinator (see src/runtime/workflow/overlay-store.ts +
1095
+ // coordinator.ts). The `overlayStore` is created here before
1096
+ // `state` so the coordinator can seed marker-backed scope IDs as
1097
+ // soon as the initial document lands. `workflowCoordinator` is
1098
+ // wired after `state` + `viewState` + event emitter are in place
1099
+ // (assignment below the initial render-snapshot setup).
1100
+ const overlayStore: OverlayStore = createOverlayStore({
1101
+ initialOverlay:
883
1102
  options.initialSessionState?.workflowOverlay ??
884
1103
  options.initialSnapshot?.workflowOverlay ??
885
1104
  null,
886
- );
887
- let workflowMetadataDefinitions: WorkflowMetadataDefinition[] =
888
- options.initialSessionState?.workflowMetadata?.definitions
889
- ?? options.initialSnapshot?.workflowMetadata?.definitions
890
- ?? [];
891
- let workflowMetadataEntries: WorkflowMetadataEntry[] =
892
- options.initialSessionState?.workflowMetadata?.entries
893
- ?? options.initialSnapshot?.workflowMetadata?.entries
894
- ?? [];
895
- let markerBackedScopeIds = new Set<string>();
896
- let hostAnnotationOverlay: HostAnnotationOverlay | null = null;
897
- // §C7 — local view-state for scope chrome visibility; never collab-replicated.
898
- let scopeChromeVisibilityState: ScopeChromeVisibilityState = { mode: "all" };
899
- // P13 Slice B: shared workflow state from the collab Y.Map "workflow".
900
- // Set via setSharedWorkflowState(); observed by evaluateWorkflowBlockedReasons.
901
- let sharedWorkflowState: SharedWorkflowState | null = null;
1105
+ initialMetadataDefinitions:
1106
+ options.initialSessionState?.workflowMetadata?.definitions ??
1107
+ options.initialSnapshot?.workflowMetadata?.definitions ??
1108
+ [],
1109
+ initialMetadataEntries:
1110
+ options.initialSessionState?.workflowMetadata?.entries ??
1111
+ options.initialSnapshot?.workflowMetadata?.entries ??
1112
+ [],
1113
+ initialVisibilityPolicies:
1114
+ options.initialSessionState?.visibilityPolicies ??
1115
+ options.initialSnapshot?.visibilityPolicies ??
1116
+ [],
1117
+ initialMarkupModePolicy:
1118
+ options.initialSessionState?.markupModePolicy ??
1119
+ options.initialSnapshot?.markupModePolicy ??
1120
+ null,
1121
+ telemetryBus,
1122
+ });
902
1123
  const initialPersistedSnapshot = options.initialSessionState
903
1124
  ? persistedSnapshotFromEditorSessionState(options.initialSessionState, {
904
1125
  savedAt: options.initialSessionState.updatedAt,
@@ -930,31 +1151,11 @@ export function createDocumentRuntime(
930
1151
  storySelections.set(storyTargetKey(MAIN_STORY_TARGET), state.selection);
931
1152
  lastHeadingFingerprint = computeHeadingFingerprint(state.document);
932
1153
 
933
- function syncMarkerBackedScopeIds(
934
- document: CanonicalDocumentEnvelope,
935
- overlay: WorkflowOverlay | null,
936
- ): void {
937
- const presentScopeIds = new Set(collectScopeLocations(document).keys());
938
- if (!overlay) {
939
- markerBackedScopeIds = presentScopeIds;
940
- return;
941
- }
942
- const overlayScopeIds = new Set(overlay.scopes.map((scope) => scope.scopeId));
943
- const next = new Set<string>();
944
- for (const scopeId of markerBackedScopeIds) {
945
- if (overlayScopeIds.has(scopeId)) {
946
- next.add(scopeId);
947
- }
948
- }
949
- for (const scopeId of presentScopeIds) {
950
- if (overlayScopeIds.has(scopeId)) {
951
- next.add(scopeId);
952
- }
953
- }
954
- markerBackedScopeIds = next;
955
- }
956
-
957
- syncMarkerBackedScopeIds(state.document, workflowOverlay);
1154
+ // Seed marker-backed scope IDs from the initial document. The
1155
+ // overlay store's `replaceOverlay` is the sole writer of that set;
1156
+ // `seedMarkerBackedScopeIds` is an idempotent no-telemetry wrapper
1157
+ // that populates the set from the initial document + overlay.
1158
+ seedMarkerBackedScopeIds(overlayStore, state.document);
958
1159
 
959
1160
  // Runtime-owned paginated layout engine (Phase 1+ of the layout facet work).
960
1161
  // The engine caches graph + resolved-formatting + fragment mapper keyed on
@@ -966,7 +1167,9 @@ export function createDocumentRuntime(
966
1167
  // `createMeasurementProvider({ preference: "auto", fontLoader })` which
967
1168
  // upgrades to the canvas backend once fonts resolve. `swapMeasurementProvider`
968
1169
  // emits `measurement_backend_ready` so chrome consumers can re-read metrics.
969
- const layoutEngine: LayoutEngineInstance = createLayoutEngine();
1170
+ const layoutEngine: LayoutEngineInstance = createLayoutEngine({
1171
+ telemetryBus,
1172
+ });
970
1173
  if (options.seedLayoutCache) {
971
1174
  // L7 Phase 2.5 — seed the cached graph from the prerender envelope so
972
1175
  // the first getPageGraph call skips fullRebuild. Seed is keyed on the
@@ -996,30 +1199,10 @@ export function createDocumentRuntime(
996
1199
  }),
997
1200
  canonicalDocument: () => state.document,
998
1201
  renderKernel: () => renderKernelRef,
999
- getWorkflowRailInput: () => {
1000
- const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
1001
- if (!normalizedWorkflowOverlay) return null;
1002
- const activeWorkItemId = normalizedWorkflowOverlay.activeWorkItemId ?? null;
1003
- const activeWorkItem =
1004
- activeWorkItemId !== null
1005
- ? normalizedWorkflowOverlay.workItems?.find(
1006
- (item) => item.workItemId === activeWorkItemId,
1007
- )
1008
- : undefined;
1009
- return {
1010
- scopes: normalizedWorkflowOverlay.scopes,
1011
- candidates: normalizedWorkflowOverlay.candidates,
1012
- activeWorkItemScopeIds: activeWorkItem?.scopeIds ?? [],
1013
- activeStory,
1014
- };
1015
- },
1016
- // R2 / scope-card-overlay P1 — surface metadata markup so
1017
- // `facet.getAllScopeCardModels()` can attach `IssueMetadataValue`
1018
- // to its scope without the chrome overlay having to re-fetch a
1019
- // separate snapshot. Reads through the same cached snapshot the
1020
- // runtime already builds for comment/revision/search consumers.
1021
- getWorkflowMarkupMetadata: () =>
1022
- getCachedWorkflowMarkupSnapshot().metadata,
1202
+ // Workflow rail / scope-card composition migrated to
1203
+ // `runtime.workflow` in Layer-06 Slice 4. The layout facet no
1204
+ // longer consumes workflow state — it only provides page-graph
1205
+ // input via the getQueryInput + engine accessors above.
1023
1206
  // L7 Phase 2 — delegate viewport culling through the facet so the
1024
1207
  // workspace (which only holds a `layoutFacet` ref) can drive culling
1025
1208
  // without a separate `DocumentRuntime` prop. The lambdas forward to
@@ -1047,10 +1230,31 @@ export function createDocumentRuntime(
1047
1230
  refreshSurfaceOnly();
1048
1231
  },
1049
1232
  });
1233
+ // D1 — wire the layout-guard warning emitter so `return []` guard sites
1234
+ // in public-facet.ts emit on the `warning` channel when it is enabled.
1235
+ setActiveLayoutWarningEmitter((type, guard, inputs) => {
1236
+ if (!telemetryBus.isEnabled("warning")) return;
1237
+ const t = typeof performance !== "undefined" && typeof performance.now === "function"
1238
+ ? performance.now()
1239
+ : Date.now();
1240
+ telemetryBus.emit({ channel: "warning", type, t, payload: { guard, inputs } });
1241
+ });
1050
1242
  renderKernelRef = createRenderKernel({
1051
1243
  facet: layoutFacet,
1052
1244
  getActiveStory: () => activeStory,
1053
1245
  });
1246
+ // Layer 05 · Geometry Projection — construct the geometry facet once
1247
+ // the render kernel exists. The facet's `hitTest` / `getAnchorRects` /
1248
+ // `getBlockRects` / `getPage` / `getBlock` / `getCaret` /
1249
+ // `getSelectionRects` / `getReplacementEnvelope` / `getObjectHandles`
1250
+ // all resolve via the kernel's current frame through the pure helpers
1251
+ // in `src/runtime/geometry/**`. Architecture target:
1252
+ // `docs/architecture/05-geometry-projection.md`.
1253
+ const geometryFacet: GeometryFacet = createGeometryFacet({
1254
+ layoutFacet,
1255
+ renderKernel: () => renderKernelRef,
1256
+ getCanonicalDocument: () => state.document,
1257
+ });
1054
1258
  // L7 Phase 2 — viewport block range for surface culling.
1055
1259
  let viewportBlockRange: { start: number; end: number } | null = null;
1056
1260
 
@@ -1105,7 +1309,7 @@ export function createDocumentRuntime(
1105
1309
  // - `workflowMarkupHash` — structural-counts hash over wfMarkup.
1106
1310
  // Replaces the old `workflowMarkup === wfMarkup` reference check,
1107
1311
  // which always failed on pure text edits because
1108
- // `getCachedWorkflowMarkupSnapshot()` is keyed on revisionToken.
1312
+ // `workflowCoordinator.getWorkflowMarkupSnapshot()` is keyed on revisionToken.
1109
1313
  // Pure text edits keep all per-category item counts stable → hash
1110
1314
  // equal. A comment ADDITION, revision AUTHORED, protected-range added,
1111
1315
  // opaque-fragment added → at least one count changes → hash diverges.
@@ -1179,45 +1383,9 @@ export function createDocumentRuntime(
1179
1383
  snapshot: EditorViewStateSnapshot;
1180
1384
  }
1181
1385
  | undefined;
1182
- let cachedInteractionGuardSnapshot:
1183
- | {
1184
- revisionToken: string;
1185
- activeStoryKey: string;
1186
- selection: EditorState["selection"];
1187
- readOnly: boolean;
1188
- documentMode: DocumentMode;
1189
- protectionSnapshot: ProtectionSnapshot;
1190
- workflowOverlay: WorkflowOverlay | null;
1191
- sharedWorkflowState: SharedWorkflowState | null;
1192
- snapshot: InteractionGuardSnapshot;
1193
- }
1194
- | undefined;
1195
- let cachedWorkflowScopeSnapshot:
1196
- | {
1197
- workflowOverlay: WorkflowOverlay;
1198
- interactionGuardSnapshot: InteractionGuardSnapshot;
1199
- snapshot: WorkflowScopeSnapshot;
1200
- }
1201
- | undefined;
1202
- let cachedNormalizedWorkflowOverlay:
1203
- | {
1204
- document: CanonicalDocumentEnvelope;
1205
- workflowOverlay: WorkflowOverlay;
1206
- normalized: WorkflowOverlay;
1207
- }
1208
- | undefined;
1209
- let cachedWorkflowMarkupSnapshot:
1210
- | {
1211
- revisionToken: string;
1212
- activeStoryKey: string;
1213
- protectionSnapshot: ProtectionSnapshot;
1214
- preservation: CanonicalDocumentEnvelope["preservation"];
1215
- workflowOverlay: WorkflowOverlay | null;
1216
- workflowMetadataDefinitions: WorkflowMetadataDefinition[];
1217
- workflowMetadataEntries: WorkflowMetadataEntry[];
1218
- snapshot: WorkflowMarkupSnapshot;
1219
- }
1220
- | undefined;
1386
+ // Layer-06 workflow/review caches (interaction guard, scope
1387
+ // snapshot, normalized overlay, markup snapshot) live on the
1388
+ // workflow coordinator. See `src/runtime/workflow/coordinator.ts`.
1221
1389
  // Keyed on block count + subParts identity (not revisionToken) — fields only
1222
1390
  // change when blocks are inserted/deleted or subParts (headers/footers) change,
1223
1391
  // NOT on text-only edits. Cleared explicitly by updateFields() for field-refresh.
@@ -1260,7 +1428,12 @@ export function createDocumentRuntime(
1260
1428
  return cachedSurface.snapshot;
1261
1429
  }
1262
1430
 
1263
- const snapshot = createEditorSurfaceSnapshot(document, state.selection, nextActiveStory, { viewportBlockRange });
1431
+ const snapshot = createEditorSurfaceSnapshot(document, state.selection, nextActiveStory, {
1432
+ viewportBlockRange,
1433
+ ...(effectiveMarkupModeProvider
1434
+ ? { getEffectiveMarkupMode: effectiveMarkupModeProvider }
1435
+ : {}),
1436
+ });
1264
1437
  recordPerfSample("snapshot.surface");
1265
1438
  incrementInvalidationCounter("runtime.snapshot.surfaceMisses");
1266
1439
  cachedSurface = {
@@ -1505,197 +1678,13 @@ export function createDocumentRuntime(
1505
1678
  return snapshot;
1506
1679
  }
1507
1680
 
1508
- function evaluateWorkflowBlockedReasons(
1509
- selection: EditorState["selection"],
1510
- commandType?: string,
1511
- ): WorkflowBlockedCommandReason[] {
1512
- const reasons: WorkflowBlockedCommandReason[] = [];
1513
- // P13 Slice B: shared lockedMode overrides all other scope checks when
1514
- // non-editing. Short-circuit: no other scope reason applies when the round
1515
- // is locked (the round state supersedes scope/overlay-level gating).
1516
- // Emit a reason code whose effectiveMode mapping matches the mode intent:
1517
- // "commenting" → workflow_comment_only (maps to effectiveMode: "comment")
1518
- // "viewing" → workflow_view_only (maps to effectiveMode: "view")
1519
- // "suggesting" → workflow_round_locked (no existing mapping; stays "blocked"
1520
- // for this slice — full suggesting-mode semantics will be a
1521
- // future slice that hooks getEffectiveDocumentMode instead).
1522
- if (sharedWorkflowState?.lockedMode && sharedWorkflowState.lockedMode !== "editing") {
1523
- const lockedMode = sharedWorkflowState.lockedMode;
1524
- const code: WorkflowBlockedCommandReason["code"] =
1525
- lockedMode === "commenting"
1526
- ? "workflow_comment_only"
1527
- : lockedMode === "viewing"
1528
- ? "workflow_view_only"
1529
- : "workflow_round_locked";
1530
- reasons.push({
1531
- code,
1532
- message: `Round is locked to ${lockedMode} mode.`,
1533
- });
1534
- return reasons;
1535
- }
1536
- const selectionBounds = {
1537
- from: Math.min(selection.anchor, selection.head),
1538
- to: Math.max(selection.anchor, selection.head),
1539
- };
1540
- const selectionRange = expandSelectionRange(selectionBounds);
1541
- const opaqueReason = deriveOpaqueWorkflowBlockedReason(selectionRange);
1542
-
1543
- if (opaqueReason) {
1544
- reasons.push(opaqueReason);
1545
- }
1546
-
1547
- if (state.readOnly) {
1548
- reasons.push({
1549
- code: "document_read_only",
1550
- message: "Document is in read-only mode.",
1551
- });
1552
- }
1553
-
1554
- if (viewState.documentMode === "viewing" || viewState.documentMode === "commenting") {
1555
- reasons.push({
1556
- code: "document_viewing_mode",
1557
- message: "Document is in viewing mode.",
1558
- });
1559
- }
1560
-
1561
- if (
1562
- isBlockedByProtection(protectionSnapshot, selection)
1563
- ) {
1564
- reasons.push({
1565
- code: "protected_range",
1566
- message: "Selection falls within a protected range.",
1567
- });
1568
- }
1569
-
1570
- const effectiveDocumentMode = getEffectiveDocumentMode(selection);
1571
-
1572
- if (effectiveDocumentMode === "suggesting" && commandType) {
1573
- if (SUGGESTING_UNSUPPORTED_COMMANDS.has(commandType)) {
1574
- reasons.push({
1575
- code: "suggesting_unsupported",
1576
- message: `"${commandType}" is not supported in suggesting mode.`,
1577
- });
1578
- }
1579
- }
1580
-
1581
- const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
1582
- if (normalizedWorkflowOverlay) {
1583
- const matchingScope = getMatchingWorkflowScope(selection);
1584
- const activeScopes = getEffectiveWorkflowScopes(normalizedWorkflowOverlay);
1585
- // §C8: invisible non-view scopes are transparent to the interaction guard.
1586
- // Don't count them toward the "outside_workflow_scope" threshold.
1587
- const guardingScopes = activeScopes.filter(
1588
- (s) => !(s.visibility === "invisible" && s.mode !== "view"),
1589
- );
1590
-
1591
- if (!matchingScope && guardingScopes.length > 0) {
1592
- reasons.push({
1593
- code: "outside_workflow_scope",
1594
- message: "Selection is outside any active workflow scope.",
1595
- });
1596
- } else if (matchingScope) {
1597
- if (matchingScope.mode === "comment") {
1598
- const isCommentCommand =
1599
- commandType?.startsWith("comment.") ?? false;
1600
- if (!isCommentCommand) {
1601
- reasons.push({
1602
- code: "workflow_comment_only",
1603
- message: `Scope "${matchingScope.label ?? matchingScope.scopeId}" allows comments only.`,
1604
- scopeId: matchingScope.scopeId,
1605
- workItemId: matchingScope.workItemId,
1606
- });
1607
- }
1608
- } else if (matchingScope.mode === "view") {
1609
- reasons.push({
1610
- code: "workflow_view_only",
1611
- message: `Scope "${matchingScope.label ?? matchingScope.scopeId}" is view-only.`,
1612
- scopeId: matchingScope.scopeId,
1613
- workItemId: matchingScope.workItemId,
1614
- });
1615
- }
1616
- }
1617
- }
1618
-
1619
- return reasons;
1620
- }
1621
-
1622
- // §C6 — most-restrictive-wins ordering for overlap layering.
1623
- const MODE_RESTRICTIVENESS: Record<WorkflowScopeMode, number> = {
1624
- edit: 0,
1625
- suggest: 1,
1626
- comment: 2,
1627
- view: 3,
1628
- };
1629
-
1630
- /**
1631
- * §C6 — Collect all guard-eligible scopes that contain `selection`,
1632
- * sorted outermost→innermost (startPos ASC, endPos DESC, scopeId ASC).
1633
- * Excludes invisible non-view scopes per §C8.
1634
- */
1635
- function buildMatchingScopeStack(
1636
- selection: EditorState["selection"],
1637
- ): WorkflowOverlay["scopes"] {
1638
- if (!workflowOverlay) return [];
1639
- const selectionBounds = {
1640
- from: Math.min(selection.anchor, selection.head),
1641
- to: Math.max(selection.anchor, selection.head),
1642
- };
1643
- const activeScopes = getEffectiveWorkflowScopes(workflowOverlay);
1644
- const matching = activeScopes.filter((scope) => {
1645
- // §C8
1646
- if (scope.visibility === "invisible" && scope.mode !== "view") return false;
1647
- if (scope.anchor.kind === "detached") return false;
1648
- const scopeFrom = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
1649
- const scopeTo = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
1650
- return selectionBounds.from >= scopeFrom && selectionBounds.to <= scopeTo;
1651
- });
1652
- // Outermost first: startPos ASC, endPos DESC (wider span = outer), scopeId ASC tiebreak
1653
- matching.sort((a, b) => {
1654
- const aFrom = a.anchor.kind === "range" ? a.anchor.from : (a.anchor as { at: number }).at;
1655
- const bFrom = b.anchor.kind === "range" ? b.anchor.from : (b.anchor as { at: number }).at;
1656
- if (aFrom !== bFrom) return aFrom - bFrom;
1657
- const aTo = a.anchor.kind === "range" ? a.anchor.to : (a.anchor as { at: number }).at;
1658
- const bTo = b.anchor.kind === "range" ? b.anchor.to : (b.anchor as { at: number }).at;
1659
- if (aTo !== bTo) return bTo - aTo; // wider first
1660
- return a.scopeId < b.scopeId ? -1 : a.scopeId > b.scopeId ? 1 : 0;
1661
- });
1662
- return matching;
1663
- }
1664
-
1665
- function getMatchingWorkflowScope(
1666
- selection: EditorState["selection"],
1667
- ): WorkflowOverlay["scopes"][number] | null {
1668
- const stack = buildMatchingScopeStack(selection);
1669
- if (stack.length === 0) return null;
1670
- // §C6 — most-restrictive scope wins across all overlapping scopes.
1671
- return stack.reduce((best, scope) =>
1672
- (MODE_RESTRICTIVENESS[scope.mode] ?? 0) > (MODE_RESTRICTIVENESS[best.mode] ?? 0)
1673
- ? scope
1674
- : best,
1675
- );
1676
- }
1677
-
1678
- function getEffectiveDocumentMode(
1679
- selection: EditorState["selection"],
1680
- ): DocumentMode {
1681
- if (viewState.documentMode === "viewing" || viewState.documentMode === "commenting") {
1682
- return viewState.documentMode;
1683
- }
1684
- const matchingScope = getMatchingWorkflowScope(selection);
1685
- if (matchingScope?.mode === "suggest") {
1686
- return "suggesting";
1687
- }
1688
- return viewState.documentMode;
1689
- }
1690
-
1691
- function expandSelectionRange(
1692
- range: { from: number; to: number },
1693
- ): { from: number; to: number } {
1694
- return {
1695
- from: range.from,
1696
- to: range.to > range.from ? range.to : range.from + 1,
1697
- };
1698
- }
1681
+ // Layer-06 blocked-reason composition + scope-matching heuristics
1682
+ // live on the workflow coordinator (see
1683
+ // `src/runtime/workflow/coordinator.ts` — §C6 + §C8 rules, most-
1684
+ // restrictive-wins, invisible-scope carve-out). Runtime call sites
1685
+ // delegate via `workflowCoordinator.evaluateBlockedReasons`,
1686
+ // `workflowCoordinator.getMatchingScope`, and
1687
+ // `workflowCoordinator.getEffectiveDocumentMode`.
1699
1688
 
1700
1689
  function deriveOpaqueWorkflowBlockedReason(
1701
1690
  range: { from: number; to: number },
@@ -1920,265 +1909,71 @@ export function createDocumentRuntime(
1920
1909
  return left.from < right.to && right.from < left.to;
1921
1910
  }
1922
1911
 
1923
- function workflowAnchorsEqual(
1924
- left: EditorAnchorProjection,
1925
- right: EditorAnchorProjection,
1926
- ): boolean {
1927
- if (left.kind !== right.kind) return false;
1928
- switch (left.kind) {
1929
- case "range":
1930
- return (
1931
- right.kind === "range" &&
1932
- left.from === right.from &&
1933
- left.to === right.to &&
1934
- left.assoc.start === right.assoc.start &&
1935
- left.assoc.end === right.assoc.end
1936
- );
1937
- case "node":
1938
- return right.kind === "node" && left.at === right.at;
1939
- case "detached":
1940
- return (
1941
- right.kind === "detached" &&
1942
- left.reason === right.reason &&
1943
- left.lastKnownRange.from === right.lastKnownRange.from &&
1944
- left.lastKnownRange.to === right.lastKnownRange.to
1945
- );
1946
- default:
1947
- return false;
1948
- }
1949
- }
1950
-
1951
- function normalizeWorkflowOverlayForDocument(
1952
- document: CanonicalDocumentEnvelope,
1953
- overlay: WorkflowOverlay,
1954
- ): WorkflowOverlay {
1955
- if (
1956
- cachedNormalizedWorkflowOverlay &&
1957
- cachedNormalizedWorkflowOverlay.document === document &&
1958
- cachedNormalizedWorkflowOverlay.workflowOverlay === overlay
1959
- ) {
1960
- return cachedNormalizedWorkflowOverlay.normalized;
1961
- }
1912
+ // Layer-06 overlay normalization, rail input composition, and
1913
+ // anchor-equality helpers live on the workflow coordinator +
1914
+ // overlay store. See `src/runtime/workflow/overlay-store.ts` and
1915
+ // `coordinator.ts`.
1962
1916
 
1963
- const scopeIdCounts = new Map<string, number>();
1964
- for (const scope of overlay.scopes) {
1965
- scopeIdCounts.set(scope.scopeId, (scopeIdCounts.get(scope.scopeId) ?? 0) + 1);
1966
- }
1967
- const locations = collectScopeLocations(document);
1968
- let changed = false;
1969
- const normalizedScopes = overlay.scopes.map((scope) => {
1970
- if ((scopeIdCounts.get(scope.scopeId) ?? 0) !== 1) {
1971
- return scope;
1972
- }
1973
- const location = locations.get(scope.scopeId);
1974
- const isMarkerBacked = markerBackedScopeIds.has(scope.scopeId);
1975
- let nextAnchor: EditorAnchorProjection | null = null;
1976
- if (
1977
- location &&
1978
- location.startPos !== undefined &&
1979
- location.endPos !== undefined
1980
- ) {
1981
- nextAnchor = {
1982
- kind: "range",
1983
- from: Math.min(location.startPos, location.endPos),
1984
- to: Math.max(location.startPos, location.endPos),
1985
- assoc: { start: -1, end: 1 },
1986
- };
1987
- } else if (isMarkerBacked) {
1988
- const lastKnownRange =
1989
- scope.anchor.kind === "range"
1990
- ? { from: scope.anchor.from, to: scope.anchor.to }
1991
- : scope.anchor.kind === "node"
1992
- ? { from: scope.anchor.at, to: scope.anchor.at }
1993
- : scope.anchor.lastKnownRange;
1994
- nextAnchor = {
1995
- kind: "detached",
1996
- reason:
1997
- location && (location.startPos !== undefined || location.endPos !== undefined)
1998
- ? "deleted"
1999
- : "invalidatedByStructureChange",
2000
- lastKnownRange,
2001
- };
2002
- } else {
2003
- return scope;
2004
- }
2005
- if (workflowAnchorsEqual(scope.anchor, nextAnchor)) {
2006
- return scope;
2007
- }
2008
- changed = true;
2009
- return {
2010
- ...scope,
2011
- anchor: nextAnchor,
2012
- };
2013
- });
2014
-
2015
- const normalized = changed
2016
- ? {
2017
- ...overlay,
2018
- scopes: normalizedScopes,
2019
- }
2020
- : overlay;
2021
- cachedNormalizedWorkflowOverlay = {
2022
- document,
2023
- workflowOverlay: overlay,
2024
- normalized,
2025
- };
2026
- return normalized;
2027
- }
2028
-
2029
- function getNormalizedWorkflowOverlay(): WorkflowOverlay | null {
2030
- if (!workflowOverlay) return null;
2031
- return normalizeWorkflowOverlayForDocument(state.document, workflowOverlay);
2032
- }
2033
-
2034
- function buildWarningSignature(warning: InternalEditorWarning): string {
2035
- return JSON.stringify({
2036
- code: warning.code,
2037
- severity: warning.severity,
2038
- message: warning.message,
2039
- source: warning.source,
2040
- featureEntryId: warning.featureEntryId ?? null,
2041
- details: warning.details ?? null,
2042
- affectedAnchor: warning.affectedAnchor ?? null,
1917
+ function getCurrentPageGraph() {
1918
+ return layoutEngine.getPageGraph({
1919
+ document: state.document,
1920
+ viewState: {
1921
+ activeStory,
1922
+ workspaceMode: viewState.workspaceMode,
1923
+ zoomLevel: viewState.zoomLevel,
1924
+ },
2043
1925
  });
2044
1926
  }
2045
1927
 
2046
- function mergeDetachedWorkflowScopeWarnings(
2047
- overlay: WorkflowOverlay | null,
2048
- existingWarnings: readonly InternalEditorWarning[],
2049
- ): {
2050
- nextWarnings: InternalEditorWarning[];
2051
- added: InternalEditorWarning[];
2052
- cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }>;
2053
- } {
2054
- const detachedScopesById = new Map<string, WorkflowScope>();
2055
- for (const scope of overlay?.scopes ?? []) {
2056
- if (scope.anchor.kind === "detached") {
2057
- detachedScopesById.set(scope.scopeId, scope);
2058
- }
2059
- }
2060
-
2061
- const retainedWarnings = existingWarnings.filter(
2062
- (warning) => warning.code !== "workflow_scope_invalidated",
2063
- );
2064
- const existingDetachedWarnings = existingWarnings.filter(
2065
- (warning) => warning.code === "workflow_scope_invalidated",
2066
- );
2067
- const existingById = new Map(
2068
- existingDetachedWarnings.map((warning) => [warning.warningId, warning] as const),
2069
- );
2070
- const desiredById = new Map(
2071
- [...detachedScopesById.values()].map((scope) => {
2072
- const warning = createInvalidatedWorkflowScopeWarning(scope);
2073
- return [warning.warningId, warning] as const;
2074
- }),
1928
+ // Layer-06 workflow coordinator — owns the orchestration layer
1929
+ // that used to live inline here (scope lifecycle, overlay / metadata
1930
+ // setters, interaction-guard + scope + markup snapshot caches,
1931
+ // blocked-reason composition + scope matching, rail composition,
1932
+ // dispatch-branch handler, warning synthesis). Dependencies are
1933
+ // late-bound via accessor functions — `emit`, `refreshRenderSnapshot`
1934
+ // etc. are function declarations (hoisted) so the accessor closures
1935
+ // remain valid even though the declarations live further down the
1936
+ // file. `dispatchToRuntime` is similarly late-bound to avoid a
1937
+ // circular reference between coordinator and the `runtime` object
1938
+ // literal it gets spliced into.
1939
+ //
1940
+ // The runtime's own public methods forward to `workflowCoordinator`
1941
+ // — see the method definitions on the returned `runtime` object.
1942
+ let dispatchToRuntime: (command: unknown) => void = () => {
1943
+ throw new Error(
1944
+ "workflow coordinator: dispatch called before runtime is fully constructed",
2075
1945
  );
1946
+ };
2076
1947
 
2077
- const added: InternalEditorWarning[] = [];
2078
- const cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }> = [];
2079
-
2080
- for (const [warningId, existingWarning] of existingById) {
2081
- const desiredWarning = desiredById.get(warningId);
2082
- if (!desiredWarning) {
2083
- cleared.push({ warningId, code: existingWarning.code });
2084
- continue;
2085
- }
2086
- if (buildWarningSignature(existingWarning) !== buildWarningSignature(desiredWarning)) {
2087
- cleared.push({ warningId, code: existingWarning.code });
2088
- added.push(desiredWarning);
2089
- }
2090
- }
2091
-
2092
- for (const [warningId, desiredWarning] of desiredById) {
2093
- if (!existingById.has(warningId)) {
2094
- added.push(desiredWarning);
2095
- }
2096
- }
2097
-
2098
- return {
2099
- nextWarnings: [...retainedWarnings, ...desiredById.values()],
2100
- added,
2101
- cleared,
2102
- };
2103
- }
2104
-
2105
- function createInvalidatedWorkflowScopeWarning(
2106
- scope: WorkflowScope,
2107
- ): InternalEditorWarning {
2108
- const anchor = scope.anchor.kind === "detached" ? scope.anchor : null;
2109
- const subject = scope.label
2110
- ? `Workflow scope "${scope.label}" (${scope.scopeId})`
2111
- : `Workflow scope ${scope.scopeId}`;
2112
- const reasonPhrase =
2113
- anchor?.reason === "deleted"
2114
- ? "its anchored text was deleted"
2115
- : anchor?.reason === "invalidatedByStructureChange"
2116
- ? "document structure changed around it"
2117
- : "its anchor could not be resolved unambiguously";
2118
- const modePhrase =
2119
- scope.mode === "view"
2120
- ? "read-only enforcement"
2121
- : `${scope.mode} enforcement`;
2122
-
2123
- return {
2124
- warningId: `warning:workflow-scope-invalidated:${scope.scopeId}`,
2125
- code: "workflow_scope_invalidated",
2126
- severity: "warning",
2127
- message: `${subject} was invalidated because ${reasonPhrase}. Reapply the scope before relying on ${modePhrase}.`,
2128
- source: "runtime",
2129
- affectedAnchor: anchor ? toInternalAnchorProjection(anchor) : undefined,
2130
- diagnostic: buildDiagnosticFromLegacyWarningCode("workflow_scope_invalidated", {
2131
- diagnosticId: `warning-diag:workflow-scope-invalidated:${scope.scopeId}`,
2132
- technical: {
2133
- message: `${subject} lost its trusted anchor and is now detached.`,
2134
- source: "runtime",
2135
- },
2136
- details: {
2137
- scopeId: scope.scopeId,
2138
- label: scope.label,
2139
- mode: scope.mode,
2140
- reason: anchor?.reason,
2141
- lastKnownRange: anchor?.lastKnownRange,
2142
- storyTarget: scope.storyTarget,
2143
- reapplySuggested: true,
2144
- },
2145
- affectedAnchor: anchor ? scope.anchor : undefined,
2146
- llmMetadata: {
2147
- userSummary: `${subject} is no longer attached to trusted document content. Reapply the scope before relying on it.`,
2148
- remediation: {
2149
- kind: "fallback",
2150
- suggestion:
2151
- "Locate the intended text using warning.details.scopeId and warning.details.lastKnownRange, then call addScope again for the repaired range.",
2152
- },
2153
- recoveryClass: "requires-input",
2154
- echoedInput: {
2155
- scopeId: scope.scopeId,
2156
- lastKnownRange: anchor?.lastKnownRange,
2157
- },
2158
- },
2159
- }),
2160
- details: {
2161
- scopeId: scope.scopeId,
2162
- label: scope.label,
2163
- mode: scope.mode,
2164
- reason: anchor?.reason,
2165
- lastKnownRange: anchor?.lastKnownRange,
2166
- storyTarget: scope.storyTarget,
2167
- reapplySuggested: true,
2168
- actionabilityNote:
2169
- "Resolve the intended text again, then reapply the scope; the previous anchor is no longer trusted.",
2170
- },
2171
- };
2172
- }
1948
+ const workflowCoordinator: WorkflowCoordinator = createWorkflowCoordinator({
1949
+ overlayStore,
1950
+ telemetryBus,
1951
+ clock,
1952
+ getDocument: () => state.document,
1953
+ getState: () => state,
1954
+ getActiveStory: () => activeStory,
1955
+ getDocumentMode: () => viewState.documentMode,
1956
+ getProtectionSnapshot: () => protectionSnapshot,
1957
+ getRenderSnapshot: () => cachedRenderSnapshot,
1958
+ getFieldSnapshot: () => getCachedFieldSnapshot(state.document),
1959
+ getSuggestionsSnapshot: () => getCachedSuggestionsSnapshot(state),
1960
+ getRenderFrameAnchorIndex: () =>
1961
+ renderKernelRef?.getRenderFrame?.()?.anchorIndex ?? null,
1962
+ getPageGraph: () => getCurrentPageGraph(),
1963
+ deriveOpaqueWorkflowBlockedReason,
1964
+ isBlockedByProtection,
1965
+ dispatch: (command) => dispatchToRuntime(command as never),
1966
+ emitEvent: (event) => emit(event as DocumentRuntimeEvent),
1967
+ editorStateChannel,
1968
+ suggestingUnsupportedCommands: SUGGESTING_UNSUPPORTED_COMMANDS,
1969
+ });
2173
1970
 
2174
1971
  function syncDetachedWorkflowScopeWarningsInState(): {
2175
1972
  added: InternalEditorWarning[];
2176
1973
  cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }>;
2177
1974
  } {
2178
- const { nextWarnings, added, cleared } = mergeDetachedWorkflowScopeWarnings(
2179
- getNormalizedWorkflowOverlay(),
2180
- state.warnings,
2181
- );
1975
+ const { nextWarnings, added, cleared } =
1976
+ workflowCoordinator.syncDetachedScopeWarnings(state.warnings);
2182
1977
  if (added.length === 0 && cleared.length === 0) {
2183
1978
  return { added, cleared };
2184
1979
  }
@@ -2186,25 +1981,6 @@ export function createDocumentRuntime(
2186
1981
  return { added, cleared };
2187
1982
  }
2188
1983
 
2189
- function deriveWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
2190
- const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
2191
- if (!normalizedWorkflowOverlay) return null;
2192
- const blockedReasons = getCachedInteractionGuardSnapshot().blockedReasons;
2193
- const activeItem = normalizedWorkflowOverlay.activeWorkItemId
2194
- ? normalizedWorkflowOverlay.workItems?.find(
2195
- (item) => item.workItemId === normalizedWorkflowOverlay.activeWorkItemId,
2196
- )
2197
- : undefined;
2198
- return {
2199
- overlayPresent: true,
2200
- activeWorkItemId: normalizedWorkflowOverlay.activeWorkItemId ?? null,
2201
- activeWorkItem: activeItem,
2202
- scopes: normalizedWorkflowOverlay.scopes,
2203
- candidates: normalizedWorkflowOverlay.candidates ?? [],
2204
- blockedReasons,
2205
- };
2206
- }
2207
-
2208
1984
  function deriveHostAnnotationSnapshot(): HostAnnotationSnapshot {
2209
1985
  return {
2210
1986
  totalCount: hostAnnotationOverlay?.annotations.length ?? 0,
@@ -2212,189 +1988,13 @@ export function createDocumentRuntime(
2212
1988
  };
2213
1989
  }
2214
1990
 
2215
- function deriveWorkflowMetadataSnapshot(): WorkflowMetadataSnapshot {
2216
- return {
2217
- definitions: structuredClone(workflowMetadataDefinitions),
2218
- entries: structuredClone(workflowMetadataEntries),
2219
- };
2220
- }
2221
-
2222
- function getEffectiveWorkflowScopes(overlay: WorkflowOverlay): WorkflowOverlay["scopes"] {
2223
- const normalizedOverlay = normalizeWorkflowOverlayForDocument(state.document, overlay);
2224
- const activeWorkItemId = normalizedOverlay.activeWorkItemId ?? null;
2225
- const activeWorkItemScopeIds =
2226
- activeWorkItemId === null
2227
- ? null
2228
- : new Set(
2229
- normalizedOverlay.workItems?.find((item) => item.workItemId === activeWorkItemId)?.scopeIds ?? [],
2230
- );
2231
-
2232
- return normalizedOverlay.scopes.filter((scope) => {
2233
- const scopeStoryTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
2234
- if (!storyTargetsEqual(scopeStoryTarget, activeStory)) {
2235
- return false;
2236
- }
2237
-
2238
- if (activeWorkItemId === null) {
2239
- return true;
2240
- }
2241
-
2242
- return (
2243
- scope.workItemId === activeWorkItemId ||
2244
- activeWorkItemScopeIds?.has(scope.scopeId) === true
2245
- );
2246
- });
2247
- }
2248
-
2249
- function getCachedInteractionGuardSnapshot(): InteractionGuardSnapshot {
2250
- const activeStoryKey = storyTargetKey(activeStory);
2251
- if (
2252
- cachedInteractionGuardSnapshot &&
2253
- cachedInteractionGuardSnapshot.revisionToken === state.revisionToken &&
2254
- cachedInteractionGuardSnapshot.activeStoryKey === activeStoryKey &&
2255
- cachedInteractionGuardSnapshot.selection === state.selection &&
2256
- cachedInteractionGuardSnapshot.readOnly === state.readOnly &&
2257
- cachedInteractionGuardSnapshot.documentMode === viewState.documentMode &&
2258
- cachedInteractionGuardSnapshot.protectionSnapshot === protectionSnapshot &&
2259
- cachedInteractionGuardSnapshot.workflowOverlay === workflowOverlay &&
2260
- cachedInteractionGuardSnapshot.sharedWorkflowState === sharedWorkflowState
2261
- ) {
2262
- return cachedInteractionGuardSnapshot.snapshot;
2263
- }
2264
-
2265
- const blockedReasons = evaluateWorkflowBlockedReasons(state.selection);
2266
- const matchingScope = getMatchingWorkflowScope(state.selection);
2267
- const scopeStack = buildMatchingScopeStack(state.selection);
2268
- const primaryBlockedReason = blockedReasons[0];
2269
- const effectiveMode = primaryBlockedReason
2270
- ? (
2271
- primaryBlockedReason.code === "workflow_comment_only"
2272
- ? "comment"
2273
- : primaryBlockedReason.code === "workflow_view_only"
2274
- ? "view"
2275
- : "blocked"
2276
- )
2277
- : getEffectiveDocumentMode(state.selection) === "suggesting"
2278
- ? "suggest"
2279
- : matchingScope?.mode ?? "edit";
2280
- const matchedScopeStack: InteractionGuardSnapshot["matchedScopeStack"] =
2281
- scopeStack.length > 0
2282
- ? scopeStack.map((s) => ({
2283
- scopeId: s.scopeId,
2284
- mode: s.mode,
2285
- visibility: s.visibility ?? "visible",
2286
- }))
2287
- : undefined;
2288
- const snapshot: InteractionGuardSnapshot = {
2289
- effectiveMode,
2290
- ...(matchingScope?.scopeId ? { matchedScopeId: matchingScope.scopeId } : {}),
2291
- ...(matchingScope?.mode ? { matchedScopeMode: matchingScope.mode } : {}),
2292
- ...(matchedScopeStack ? { matchedScopeStack } : {}),
2293
- targetAccess:
2294
- effectiveMode === "edit"
2295
- ? "direct-edit"
2296
- : effectiveMode === "suggest"
2297
- ? "suggest"
2298
- : effectiveMode === "comment"
2299
- ? "comment-only"
2300
- : effectiveMode === "view"
2301
- ? "view-only"
2302
- : "blocked",
2303
- commandCapabilities: [
2304
- {
2305
- family: "text",
2306
- supported: evaluateWorkflowBlockedReasons(state.selection, "text.insert").length === 0,
2307
- blockedReasons: evaluateWorkflowBlockedReasons(state.selection, "text.insert"),
2308
- },
2309
- {
2310
- family: "formatting",
2311
- supported: evaluateWorkflowBlockedReasons(state.selection, "toggleBold").length === 0,
2312
- blockedReasons: evaluateWorkflowBlockedReasons(state.selection, "toggleBold"),
2313
- },
2314
- {
2315
- family: "structure",
2316
- supported: evaluateWorkflowBlockedReasons(state.selection, "insertTable").length === 0,
2317
- blockedReasons: evaluateWorkflowBlockedReasons(state.selection, "insertTable"),
2318
- },
2319
- ],
2320
- ...(primaryBlockedReason ? { disabledReason: primaryBlockedReason.message } : {}),
2321
- blockedReasons,
2322
- };
2323
- cachedInteractionGuardSnapshot = {
2324
- revisionToken: state.revisionToken,
2325
- activeStoryKey,
2326
- selection: state.selection,
2327
- readOnly: state.readOnly,
2328
- documentMode: viewState.documentMode,
2329
- protectionSnapshot,
2330
- workflowOverlay,
2331
- sharedWorkflowState,
2332
- snapshot,
2333
- };
2334
- return snapshot;
2335
- }
2336
-
2337
- function getCachedWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
2338
- if (!workflowOverlay) {
2339
- return null;
2340
- }
2341
-
2342
- const interactionGuardSnapshot = getCachedInteractionGuardSnapshot();
2343
- if (
2344
- cachedWorkflowScopeSnapshot &&
2345
- cachedWorkflowScopeSnapshot.workflowOverlay === workflowOverlay &&
2346
- cachedWorkflowScopeSnapshot.interactionGuardSnapshot === interactionGuardSnapshot
2347
- ) {
2348
- return cachedWorkflowScopeSnapshot.snapshot;
2349
- }
2350
-
2351
- const snapshot = deriveWorkflowScopeSnapshot()!;
2352
- cachedWorkflowScopeSnapshot = {
2353
- workflowOverlay,
2354
- interactionGuardSnapshot,
2355
- snapshot,
2356
- };
2357
- return snapshot;
2358
- }
2359
-
2360
- function getCachedWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot {
2361
- const activeStoryKey = storyTargetKey(activeStory);
2362
- if (
2363
- cachedWorkflowMarkupSnapshot &&
2364
- cachedWorkflowMarkupSnapshot.revisionToken === state.revisionToken &&
2365
- cachedWorkflowMarkupSnapshot.activeStoryKey === activeStoryKey &&
2366
- cachedWorkflowMarkupSnapshot.protectionSnapshot === protectionSnapshot &&
2367
- cachedWorkflowMarkupSnapshot.preservation === state.document.preservation &&
2368
- cachedWorkflowMarkupSnapshot.workflowOverlay === workflowOverlay &&
2369
- cachedWorkflowMarkupSnapshot.workflowMetadataDefinitions === workflowMetadataDefinitions &&
2370
- cachedWorkflowMarkupSnapshot.workflowMetadataEntries === workflowMetadataEntries
2371
- ) {
2372
- return cachedWorkflowMarkupSnapshot.snapshot;
2373
- }
2374
-
2375
- const snapshot = collectWorkflowMarkupSnapshot({
2376
- renderSnapshot: cachedRenderSnapshot,
2377
- fieldSnapshot: getCachedFieldSnapshot(state.document),
2378
- protectionSnapshot,
2379
- preservation: state.document.preservation,
2380
- workflowMetadataSnapshot: deriveWorkflowMetadataSnapshot(),
2381
- perfStage: (name, durationMs) => {
2382
- perfCounters.increment(`wfMarkup.${name}.us`, Math.round(durationMs * 1000));
2383
- perfCounters.increment(`wfMarkup.${name}.calls`);
2384
- },
2385
- });
2386
- cachedWorkflowMarkupSnapshot = {
2387
- revisionToken: state.revisionToken,
2388
- activeStoryKey,
2389
- protectionSnapshot,
2390
- preservation: state.document.preservation,
2391
- workflowOverlay,
2392
- workflowMetadataDefinitions,
2393
- workflowMetadataEntries,
2394
- snapshot,
2395
- };
2396
- return snapshot;
2397
- }
1991
+ // Layer-06 metadata snapshot derivation + getEffectiveWorkflowScopes
1992
+ // + cached interaction-guard / scope / markup snapshots live on the
1993
+ // workflow coordinator. Runtime call sites delegate via
1994
+ // `workflowCoordinator.getInteractionGuardSnapshot()`,
1995
+ // `workflowCoordinator.getWorkflowScopeSnapshot()`,
1996
+ // `workflowCoordinator.getWorkflowMarkupSnapshot()`, and
1997
+ // `workflowCoordinator.getWorkflowMetadataSnapshot()`.
2398
1998
 
2399
1999
  function getCachedRuntimeContextAnalytics(
2400
2000
  query?: RuntimeContextAnalyticsQuery,
@@ -2419,7 +2019,7 @@ export function createDocumentRuntime(
2419
2019
  cachedEntry.selection === selectionCacheKey &&
2420
2020
  cachedEntry.readOnly === state.readOnly &&
2421
2021
  cachedEntry.documentMode === viewState.documentMode &&
2422
- cachedEntry.workflowOverlay === workflowOverlay &&
2022
+ cachedEntry.workflowOverlay === overlayStore.getOverlay() &&
2423
2023
  cachedEntry.protectionSnapshot === protectionSnapshot &&
2424
2024
  cachedEntry.warnings === state.warnings &&
2425
2025
  cachedEntry.fatalError === state.fatalError
@@ -2428,9 +2028,9 @@ export function createDocumentRuntime(
2428
2028
  }
2429
2029
 
2430
2030
  const tWf = performance.now();
2431
- const wfScope = getCachedWorkflowScopeSnapshot();
2432
- const wfGuard = getCachedInteractionGuardSnapshot();
2433
- const wfMarkup = getCachedWorkflowMarkupSnapshot();
2031
+ const wfScope = workflowCoordinator.getWorkflowScopeSnapshot();
2032
+ const wfGuard = workflowCoordinator.getInteractionGuardSnapshot();
2033
+ const wfMarkup = workflowCoordinator.getWorkflowMarkupSnapshot();
2434
2034
  perfCounters.increment("ctxa.workflow.us", Math.round((performance.now() - tWf) * 1000));
2435
2035
 
2436
2036
  const tSugg = performance.now();
@@ -2485,7 +2085,7 @@ export function createDocumentRuntime(
2485
2085
  const tCompat = performance.now();
2486
2086
  const compat = toPublicCompatibilityReport(createDerivedCompatibility(state));
2487
2087
  perfCounters.increment("ctxa.compat.us", Math.round((performance.now() - tCompat) * 1000));
2488
- const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
2088
+ const normalizedWorkflowOverlay = workflowCoordinator.getWorkflowOverlay();
2489
2089
 
2490
2090
  const tBuild = performance.now();
2491
2091
  const snapshot = createRuntimeContextAnalyticsSnapshot({
@@ -2509,7 +2109,7 @@ export function createDocumentRuntime(
2509
2109
  selection: selectionCacheKey,
2510
2110
  readOnly: state.readOnly,
2511
2111
  documentMode: viewState.documentMode,
2512
- workflowOverlay,
2112
+ workflowOverlay: overlayStore.getOverlay(),
2513
2113
  protectionSnapshot,
2514
2114
  warnings: state.warnings,
2515
2115
  fatalError: state.fatalError,
@@ -2531,7 +2131,7 @@ export function createDocumentRuntime(
2531
2131
  undefined,
2532
2132
  { scopeKind: "document" },
2533
2133
  ];
2534
- const workflowScopeSnapshot = getCachedWorkflowScopeSnapshot();
2134
+ const workflowScopeSnapshot = workflowCoordinator.getWorkflowScopeSnapshot();
2535
2135
  const seenScopeIds = new Set<string>();
2536
2136
  for (const scope of workflowScopeSnapshot?.scopes ?? []) {
2537
2137
  if (seenScopeIds.has(scope.scopeId)) {
@@ -2544,7 +2144,7 @@ export function createDocumentRuntime(
2544
2144
  });
2545
2145
  }
2546
2146
  const seenWorkItemIds = new Set<string>();
2547
- for (const workItem of workflowOverlay?.workItems ?? []) {
2147
+ for (const workItem of overlayStore.getOverlay()?.workItems ?? []) {
2548
2148
  if (seenWorkItemIds.has(workItem.workItemId)) {
2549
2149
  continue;
2550
2150
  }
@@ -2602,9 +2202,12 @@ export function createDocumentRuntime(
2602
2202
  }
2603
2203
 
2604
2204
  function refreshRenderSnapshot(): RuntimeRenderSnapshot {
2205
+ emitStageToken(telemetryBus, "layout", "layout.refresh.start", {
2206
+ revisionToken: state.revisionToken,
2207
+ });
2605
2208
  perfCounters.increment("refresh.all");
2606
2209
  const surface = timeFacet("surface", () => getCachedSurface(state.document, activeStory));
2607
- return {
2210
+ const snapshot = {
2608
2211
  documentId: state.documentId,
2609
2212
  sessionId: state.sessionId,
2610
2213
  sourceLabel: state.sourceLabel,
@@ -2631,6 +2234,10 @@ export function createDocumentRuntime(
2631
2234
  protectionSnapshot,
2632
2235
  grabbedObjectId: grabState.objectId,
2633
2236
  };
2237
+ emitStageToken(telemetryBus, "layout", "layout.refresh.complete", {
2238
+ revisionToken: state.revisionToken,
2239
+ });
2240
+ return snapshot;
2634
2241
  }
2635
2242
 
2636
2243
  /**
@@ -2644,7 +2251,12 @@ export function createDocumentRuntime(
2644
2251
  state.document,
2645
2252
  state.selection,
2646
2253
  activeStory,
2647
- { viewportBlockRange },
2254
+ {
2255
+ viewportBlockRange,
2256
+ ...(effectiveMarkupModeProvider
2257
+ ? { getEffectiveMarkupMode: effectiveMarkupModeProvider }
2258
+ : {}),
2259
+ },
2648
2260
  );
2649
2261
  // Refresh the cache with the just-built snapshot so a subsequent
2650
2262
  // refreshRenderSnapshot (same revisionToken + activeStoryKey) hits cache.
@@ -2675,10 +2287,7 @@ export function createDocumentRuntime(
2675
2287
  cachedPageLayout = undefined;
2676
2288
  cachedNavigation = undefined;
2677
2289
  cachedViewStateSnapshot = undefined;
2678
- cachedInteractionGuardSnapshot = undefined;
2679
- cachedWorkflowScopeSnapshot = undefined;
2680
- cachedNormalizedWorkflowOverlay = undefined;
2681
- cachedWorkflowMarkupSnapshot = undefined;
2290
+ workflowCoordinator.invalidateCachesForDocumentMutation();
2682
2291
  cachedFieldSnapshotEntry = null;
2683
2292
  cachedContextAnalyticsSnapshots.clear();
2684
2293
  lastEmittedContextAnalyticsSnapshots = undefined;
@@ -2820,6 +2429,11 @@ export function createDocumentRuntime(
2820
2429
  const r5ScratchReplayState: typeof state = { ...state };
2821
2430
  const r5ScratchReplaySnapshot: typeof cachedRenderSnapshot = { ...cachedRenderSnapshot };
2822
2431
 
2432
+ // `telemetryBus` is declared earlier (near `markerBackedScopeIds`) so
2433
+ // early-hoisted calls to `syncMarkerBackedScopeIds` can emit on it. This
2434
+ // is the facet consumed by `runtime.debug`.
2435
+ const debugFacet = createRuntimeDebugFacet(() => runtime, telemetryBus);
2436
+
2823
2437
  const runtime: DocumentRuntime & {
2824
2438
  hydrateCanonicalDocumentInternally(
2825
2439
  document: CanonicalDocumentEnvelope,
@@ -2838,6 +2452,9 @@ export function createDocumentRuntime(
2838
2452
  };
2839
2453
  },
2840
2454
  getRenderSnapshot() {
2455
+ emitStageToken(telemetryBus, "render", "render.snapshot.read", {
2456
+ revisionToken: cachedRenderSnapshot?.revisionToken ?? null,
2457
+ });
2841
2458
  return cachedRenderSnapshot;
2842
2459
  },
2843
2460
  getCanonicalDocument() {
@@ -2872,7 +2489,7 @@ export function createDocumentRuntime(
2872
2489
  dispatch(command) {
2873
2490
  const commandSelection = getCommandSelection(command, state.selection);
2874
2491
  if (isMutationCommand(command)) {
2875
- const blockedReasons = evaluateWorkflowBlockedReasons(
2492
+ const blockedReasons = workflowCoordinator.evaluateBlockedReasons(
2876
2493
  commandSelection,
2877
2494
  command.type,
2878
2495
  );
@@ -2903,7 +2520,7 @@ export function createDocumentRuntime(
2903
2520
  applyRuntimeStateOverlayCommand(command);
2904
2521
  const context = {
2905
2522
  timestamp: normalizeCommandTimestamp(command.origin?.timestamp) ?? clock(),
2906
- documentMode: getEffectiveDocumentMode(commandSelection),
2523
+ documentMode: workflowCoordinator.getEffectiveDocumentMode(commandSelection),
2907
2524
  defaultAuthorId: defaultAuthorId ?? undefined,
2908
2525
  } as const;
2909
2526
  const noopTransaction: EditorTransaction = {
@@ -2924,7 +2541,7 @@ export function createDocumentRuntime(
2924
2541
  try {
2925
2542
  const context = {
2926
2543
  timestamp: normalizeCommandTimestamp(command.origin?.timestamp) ?? clock(),
2927
- documentMode: getEffectiveDocumentMode(commandSelection),
2544
+ documentMode: workflowCoordinator.getEffectiveDocumentMode(commandSelection),
2928
2545
  defaultAuthorId: defaultAuthorId ?? undefined,
2929
2546
  renderSnapshot: cachedRenderSnapshot,
2930
2547
  } as const;
@@ -3000,7 +2617,7 @@ export function createDocumentRuntime(
3000
2617
  // at the same logical content. Do NOT adopt
3001
2618
  // `transaction.nextState.selection` — that is the remote
3002
2619
  // author's caret position, not B's. Without this, B would see
3003
- // their own cursor jump whenever A types.
2620
+ // their own cursor jump whenever A types. (PR #243 fix.)
3004
2621
  const mappedLocalSelection = mapLocalSelectionOnRemoteReplay(
3005
2622
  state.selection,
3006
2623
  transaction.mapping,
@@ -3038,7 +2655,7 @@ export function createDocumentRuntime(
3038
2655
  // Running local selection: seeded from CURRENT local state, then
3039
2656
  // mapped forward through each same-story envelope's mapping.
3040
2657
  // Cross-story envelopes do not touch this value — B's caret
3041
- // should never move from a story B isn't looking at.
2658
+ // should never move from a story B isn't looking at. (PR #243 fix.)
3042
2659
  let runningLocalSelection: typeof state.selection = state.selection;
3043
2660
  const warningsAdded: import("../core/state/editor-state.ts").EditorWarning[] = [];
3044
2661
  const warningsCleared: Array<{ warningId: string; code: import("../core/state/editor-state.ts").EditorWarning["code"] }> = [];
@@ -3204,6 +2821,143 @@ export function createDocumentRuntime(
3204
2821
  emitError(toRuntimeError(error));
3205
2822
  }
3206
2823
  },
2824
+ applyScopeReplacement(plan: RuntimeOperationPlan) {
2825
+ // Layer-08 Slice 5. Each step lowers to an existing command; the
2826
+ // apply pipeline (`src/runtime/scopes/replacement/apply.ts`) owns
2827
+ // the validation + audit emission around this call — keep the
2828
+ // implementation strictly mechanical.
2829
+ for (const step of plan.steps) {
2830
+ if (step.kind === "text-replace" && step.range && typeof step.text === "string") {
2831
+ const anchor: EditorAnchorProjection = {
2832
+ kind: "range",
2833
+ from: step.range.from,
2834
+ to: step.range.to,
2835
+ assoc: { start: -1, end: 1 },
2836
+ };
2837
+ const timestamp = clock();
2838
+ try {
2839
+ applyTextCommandInActiveStory(
2840
+ {
2841
+ type: "text.insert",
2842
+ text: step.text,
2843
+ origin: createOrigin("api", timestamp),
2844
+ },
2845
+ {
2846
+ selection: createSelectionFromPublicAnchor(anchor),
2847
+ blockedCommandName: "applyScopeReplacement",
2848
+ },
2849
+ );
2850
+ } catch (error) {
2851
+ emitError(toRuntimeError(error));
2852
+ }
2853
+ } else if (
2854
+ step.kind === "fragment-replace" &&
2855
+ step.range &&
2856
+ step.fragment &&
2857
+ Array.isArray((step.fragment as { blocks?: unknown }).blocks)
2858
+ ) {
2859
+ // Structured-fragment path (coord item 8 unblocked by L02
2860
+ // shipping CanonicalDocumentFragment, 2026-04-22).
2861
+ // Route through the existing fragment.insert command with
2862
+ // the block range as selection — runtime command handler
2863
+ // (src/core/commands/index.ts → applyFragmentInsert) replaces
2864
+ // the selected content with the fragment's blocks.
2865
+ const anchor: EditorAnchorProjection = {
2866
+ kind: "range",
2867
+ from: step.range.from,
2868
+ to: step.range.to,
2869
+ assoc: { start: -1, end: 1 },
2870
+ };
2871
+ const timestamp = clock();
2872
+ try {
2873
+ applyTextCommandInActiveStory(
2874
+ {
2875
+ type: "fragment.insert",
2876
+ fragment: step.fragment as CanonicalDocumentFragment,
2877
+ origin: createOrigin("api", timestamp),
2878
+ },
2879
+ {
2880
+ selection: createSelectionFromPublicAnchor(anchor),
2881
+ blockedCommandName: "applyScopeReplacement",
2882
+ },
2883
+ );
2884
+ } catch (error) {
2885
+ emitError(toRuntimeError(error));
2886
+ }
2887
+ }
2888
+ else if (
2889
+ step.kind === "text-insert-tracked" &&
2890
+ step.range &&
2891
+ typeof step.text === "string"
2892
+ ) {
2893
+ // L08 coord-08 §2 — suggest-mode tracked-insert dispatch primitive.
2894
+ // The compiler emits `text-insert-tracked` only when apply posture
2895
+ // is `"suggest-mode"`; we force the command context's documentMode
2896
+ // to `"suggesting"` so `executeEditorCommand` routes the text.insert
2897
+ // through `applySuggestingInsert` (which produces the insertion
2898
+ // revision record) regardless of what the scope's auto-derived mode
2899
+ // would be. For replacement ranges (from !== to), the tracked
2900
+ // insert handler's non-collapsed branch emits a tracked deletion
2901
+ // for the existing range and a tracked insertion for the new text
2902
+ // in a single review-store update.
2903
+ const anchor: EditorAnchorProjection = {
2904
+ kind: "range",
2905
+ from: step.range.from,
2906
+ to: step.range.to,
2907
+ assoc: { start: -1, end: 1 },
2908
+ };
2909
+ const timestamp = clock();
2910
+ try {
2911
+ applyTextCommandInActiveStory(
2912
+ {
2913
+ type: "text.insert",
2914
+ text: step.text,
2915
+ origin: createOrigin("api", timestamp),
2916
+ },
2917
+ {
2918
+ selection: createSelectionFromPublicAnchor(anchor),
2919
+ blockedCommandName: "applyScopeReplacement",
2920
+ documentModeOverride: "suggesting",
2921
+ },
2922
+ );
2923
+ } catch (error) {
2924
+ emitError(toRuntimeError(error));
2925
+ }
2926
+ } else if (
2927
+ step.kind === "text-delete-tracked" &&
2928
+ step.range &&
2929
+ step.range.to > step.range.from
2930
+ ) {
2931
+ // L08 coord-08 §2 — tracked deletion. Non-collapsed delete-forward
2932
+ // with documentMode override routes into `applySuggestingDelete`,
2933
+ // which appends a tracked deletion revision over the range without
2934
+ // removing the content (matching the ledger-ish invariant of
2935
+ // suggesting mode: accept → gone, reject → stays).
2936
+ const anchor: EditorAnchorProjection = {
2937
+ kind: "range",
2938
+ from: step.range.from,
2939
+ to: step.range.to,
2940
+ assoc: { start: -1, end: 1 },
2941
+ };
2942
+ const timestamp = clock();
2943
+ try {
2944
+ applyTextCommandInActiveStory(
2945
+ {
2946
+ type: "text.delete-forward",
2947
+ origin: createOrigin("api", timestamp),
2948
+ },
2949
+ {
2950
+ selection: createSelectionFromPublicAnchor(anchor),
2951
+ blockedCommandName: "applyScopeReplacement",
2952
+ documentModeOverride: "suggesting",
2953
+ },
2954
+ );
2955
+ } catch (error) {
2956
+ emitError(toRuntimeError(error));
2957
+ }
2958
+ }
2959
+ }
2960
+ },
3207
2961
  insertFragment(fragment, target) {
3208
2962
  // I2 Tier B Slice 1 — dispatch `fragment.insert` against the active story. The
3209
2963
  // runtime command handler routes into `applyFragmentInsert` (structure-ops).
@@ -3526,196 +3280,36 @@ export function createDocumentRuntime(
3526
3280
  origin: createOrigin("api", clock()),
3527
3281
  });
3528
3282
  },
3529
- addScope(params): AddScopeResult {
3530
- const scopeId =
3531
- params.scopeId ??
3532
- `scope-${clock().replace(/[^0-9]/gu, "")}-${Math.floor(Math.random() * 1e6)}`;
3533
- const anchor =
3534
- params.anchor.kind === "range"
3535
- ? { from: params.anchor.from, to: params.anchor.to }
3536
- : null;
3537
-
3538
- if (!anchor) {
3539
- return {
3540
- scopeId,
3541
- anchor: params.anchor,
3542
- };
3543
- }
3544
-
3545
- const { document: nextDocument } = insertScopeMarkers(state.document, {
3546
- scopeId,
3547
- from: anchor.from,
3548
- to: anchor.to,
3549
- });
3550
-
3551
- if (nextDocument !== state.document) {
3552
- this.dispatch({
3553
- type: "document.replace",
3554
- document: nextDocument,
3555
- origin: createOrigin("api", clock()),
3556
- });
3557
- }
3558
-
3559
- const resolved = resolveScope(nextDocument, scopeId);
3560
- const publicAnchor: EditorAnchorProjection =
3561
- resolved && resolved.kind === "range"
3562
- ? resolved
3563
- : {
3564
- kind: "range",
3565
- from: anchor.from,
3566
- to: anchor.to,
3567
- assoc: { start: -1, end: 1 },
3568
- };
3569
-
3570
- const currentOverlay: WorkflowOverlay = workflowOverlay ?? {
3571
- overlayVersion: "workflow-overlay/1",
3572
- scopes: [],
3573
- };
3574
- const existingScopes = currentOverlay.scopes.filter(
3575
- (existing) => existing.scopeId !== scopeId,
3576
- );
3577
- const scope: WorkflowScope = {
3578
- scopeId,
3579
- mode: params.mode ?? "comment",
3580
- anchor: publicAnchor,
3581
- ...(params.storyTarget ? { storyTarget: params.storyTarget } : {}),
3582
- ...(params.label ? { label: params.label } : {}),
3583
- };
3584
- this.dispatch({
3585
- type: "workflow.set-overlay",
3586
- overlay: {
3587
- ...currentOverlay,
3588
- scopes: [...existingScopes, scope],
3589
- },
3590
- origin: createOrigin("api", clock()),
3591
- });
3592
-
3593
- if (params.persistence && params.persistence !== "runtime-only") {
3594
- const requestedMetadata = params.metadata ?? {};
3595
- const entryPersistence =
3596
- requestedMetadata.metadataPersistence ??
3597
- (params.persistence === "session" ? "external" : "internal");
3598
- const entry: WorkflowMetadataEntry = {
3599
- entryId: requestedMetadata.entryId ?? `scope-metadata-${scopeId}`,
3600
- metadataId: requestedMetadata.metadataId ?? "workflow.scope",
3601
- anchor: publicAnchor,
3602
- ...(params.storyTarget ? { storyTarget: params.storyTarget } : {}),
3603
- scopeId,
3604
- ...(requestedMetadata.workItemId ? { workItemId: requestedMetadata.workItemId } : {}),
3605
- ...(requestedMetadata.value !== undefined
3606
- ? { value: requestedMetadata.value }
3607
- : params.persistence === "document-metadata" && params.label
3608
- ? { value: { label: params.label } }
3609
- : {}),
3610
- metadataPersistence: entryPersistence,
3611
- ...(requestedMetadata.storageRef !== undefined
3612
- ? { storageRef: requestedMetadata.storageRef }
3613
- : {}),
3614
- ...(requestedMetadata.metadataVersion !== undefined
3615
- ? { metadataVersion: requestedMetadata.metadataVersion }
3616
- : {}),
3617
- };
3618
- this.dispatch({
3619
- type: "workflow.set-metadata-entries",
3620
- entries: [...(workflowMetadataEntries ?? []), entry],
3621
- origin: createOrigin("api", clock()),
3622
- });
3623
- }
3624
-
3625
- return {
3626
- scopeId,
3627
- anchor: publicAnchor,
3628
- };
3283
+ addScope(params) {
3284
+ return workflowCoordinator.addScope(params);
3629
3285
  },
3630
3286
  getScope(scopeId) {
3631
- const normalizedScope =
3632
- getNormalizedWorkflowOverlay()?.scopes.find((scope) => scope.scopeId === scopeId) ??
3633
- null;
3634
- if (normalizedScope) {
3635
- return normalizedScope;
3636
- }
3637
- const resolved = resolveScope(state.document, scopeId);
3638
- if (!resolved) {
3639
- return null;
3640
- }
3641
- return {
3642
- scopeId,
3643
- mode: "comment",
3644
- anchor: resolved,
3645
- };
3287
+ return workflowCoordinator.getScope(scopeId);
3288
+ },
3289
+ compileScopeBundleById(scopeId, nowUtc) {
3290
+ return createScopeCompilerService(runtime).compileBundleById(scopeId, nowUtc);
3646
3291
  },
3292
+ getMarkerBackedScopeIds() {
3293
+ return workflowCoordinator.getMarkerBackedScopeIds();
3294
+ },
3295
+ debug: debugFacet as RuntimeDebugFacet,
3647
3296
  removeScope(scopeId) {
3648
- // Step 1: drop the scope from the overlay FIRST. If the scope's mode was
3649
- // "comment" / "view" the workflow-blocked-reasons gate in `dispatch`
3650
- // would otherwise refuse the subsequent `document.replace` with
3651
- // `workflow_comment_only` / `workflow_view_only`. Overlay commands are
3652
- // routed through `applyRuntimeStateOverlayCommand` and bypass that gate.
3653
- if (workflowOverlay) {
3654
- const nextScopes = workflowOverlay.scopes.filter(
3655
- (scope) => scope.scopeId !== scopeId,
3656
- );
3657
- if (nextScopes.length !== workflowOverlay.scopes.length) {
3658
- this.dispatch({
3659
- type: "workflow.set-overlay",
3660
- overlay: { ...workflowOverlay, scopes: nextScopes },
3661
- origin: createOrigin("api", clock()),
3662
- });
3663
- }
3664
- }
3665
- // Step 2: now that the scope is gone, strip the markers from the doc.
3666
- const nextDocument = removeScopeMarkers(state.document, scopeId);
3667
- if (nextDocument !== state.document) {
3668
- this.dispatch({
3669
- type: "document.replace",
3670
- document: nextDocument,
3671
- origin: createOrigin("api", clock()),
3672
- });
3673
- }
3674
- // Step 3: clear any customXml-persisted metadata entries.
3675
- if (workflowMetadataEntries) {
3676
- const nextEntries = workflowMetadataEntries.filter(
3677
- (entry) => entry.scopeId !== scopeId,
3678
- );
3679
- if (nextEntries.length !== workflowMetadataEntries.length) {
3680
- this.dispatch({
3681
- type: "workflow.set-metadata-entries",
3682
- entries: nextEntries,
3683
- origin: createOrigin("api", clock()),
3684
- });
3685
- }
3686
- }
3297
+ workflowCoordinator.removeScope(scopeId);
3687
3298
  },
3688
3299
  addInvisibleScope(params) {
3689
- const result = this.addScope({
3690
- ...params,
3691
- mode: params.mode ?? "comment",
3692
- });
3693
- this.setScopeVisibility(result.scopeId, "invisible");
3694
- return result;
3300
+ return workflowCoordinator.addInvisibleScope(params);
3695
3301
  },
3696
3302
  setScopeVisibility(scopeId, visibility) {
3697
- if (!workflowOverlay) return;
3698
- const idx = workflowOverlay.scopes.findIndex((s) => s.scopeId === scopeId);
3699
- if (idx === -1) return;
3700
- const nextScopes = workflowOverlay.scopes.map((s) =>
3701
- s.scopeId === scopeId ? { ...s, visibility } : s,
3702
- );
3703
- this.dispatch({
3704
- type: "workflow.set-overlay",
3705
- overlay: { ...workflowOverlay, scopes: nextScopes },
3706
- origin: createOrigin("api", clock()),
3707
- });
3303
+ workflowCoordinator.setScopeVisibility(scopeId, visibility);
3708
3304
  },
3709
- getScopeVisibility(scopeId): ScopeVisibility {
3710
- if (!workflowOverlay) return "visible";
3711
- const scope = workflowOverlay.scopes.find((s) => s.scopeId === scopeId);
3712
- return scope?.visibility ?? "visible";
3305
+ getScopeVisibility(scopeId) {
3306
+ return workflowCoordinator.getScopeVisibility(scopeId);
3713
3307
  },
3714
- setScopeChromeVisibility(state) {
3715
- scopeChromeVisibilityState = state;
3308
+ setScopeChromeVisibility(chromeVisibility) {
3309
+ workflowCoordinator.setScopeChromeVisibility(chromeVisibility);
3716
3310
  },
3717
- getScopeChromeVisibility(): ScopeChromeVisibilityState {
3718
- return scopeChromeVisibilityState;
3311
+ getScopeChromeVisibility() {
3312
+ return workflowCoordinator.getScopeChromeVisibility();
3719
3313
  },
3720
3314
  acceptChange(changeId) {
3721
3315
  this.dispatch({
@@ -3808,7 +3402,7 @@ export function createDocumentRuntime(
3808
3402
  };
3809
3403
 
3810
3404
  const suggesting =
3811
- getEffectiveDocumentMode(state.selection) === "suggesting";
3405
+ workflowCoordinator.getEffectiveDocumentMode(state.selection) === "suggesting";
3812
3406
  if (suggesting) {
3813
3407
  if (activeStory.kind !== "main") {
3814
3408
  this.emitBlockedCommand("clearHighlight", [{
@@ -3980,7 +3574,7 @@ export function createDocumentRuntime(
3980
3574
  bookmarkMap,
3981
3575
  paragraphOffsets,
3982
3576
  styles: state.document.styles,
3983
- contentRoot: state.document.content as unknown as import("./field-resolver.ts").DocumentContainerNode,
3577
+ contentRoot: state.document.content as unknown as import("./formatting/field/resolver.ts").DocumentContainerNode,
3984
3578
  });
3985
3579
  },
3986
3580
  getFootnoteResolver(): FootnoteResolver | undefined {
@@ -3994,6 +3588,21 @@ export function createDocumentRuntime(
3994
3588
  );
3995
3589
  },
3996
3590
  layout: layoutFacet,
3591
+ workflow: {
3592
+ getRailSegments(pageIndex: number) {
3593
+ return workflowCoordinator.getRailSegments(pageIndex);
3594
+ },
3595
+ getAllRailSegments() {
3596
+ return workflowCoordinator.getAllRailSegments();
3597
+ },
3598
+ getAllScopeCardModels() {
3599
+ return workflowCoordinator.getAllScopeCardModels();
3600
+ },
3601
+ },
3602
+ geometry: geometryFacet,
3603
+ getLayoutFacet() {
3604
+ return layoutFacet;
3605
+ },
3997
3606
  getCurrentLocation() {
3998
3607
  const navigation = getCachedDocumentNavigationSnapshot(state, activeStory);
3999
3608
  return createCurrentLocation({
@@ -4179,7 +3788,9 @@ export function createDocumentRuntime(
4179
3788
  },
4180
3789
  getSessionState() {
4181
3790
  const compatibility = createDerivedCompatibility(state);
4182
- const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
3791
+ const normalizedWorkflowOverlay = workflowCoordinator.getWorkflowOverlay();
3792
+ const visibilityPolicies = workflowCoordinator.getVisibilityPolicies();
3793
+ const markupModePolicy = workflowCoordinator.getMarkupModePolicy();
4183
3794
  return editorSessionStateFromPersistedSnapshot(
4184
3795
  {
4185
3796
  ...(createPersistedEditorSnapshot(state, {
@@ -4189,7 +3800,9 @@ export function createDocumentRuntime(
4189
3800
  protectionSnapshot,
4190
3801
  }) as unknown as PersistedEditorSnapshot),
4191
3802
  workflowOverlay: normalizedWorkflowOverlay ?? undefined,
4192
- workflowMetadata: deriveWorkflowMetadataSnapshot(),
3803
+ workflowMetadata: workflowCoordinator.getWorkflowMetadataSnapshot(),
3804
+ ...(visibilityPolicies.length > 0 ? { visibilityPolicies } : {}),
3805
+ ...(markupModePolicy ? { markupModePolicy } : {}),
4193
3806
  },
4194
3807
  );
4195
3808
  },
@@ -4234,17 +3847,25 @@ export function createDocumentRuntime(
4234
3847
  return { schemaVersion: "host-annotation-overlay/1", data: snap };
4235
3848
  }
4236
3849
  case "workflowOverlay": {
4237
- const ov = getNormalizedWorkflowOverlay();
3850
+ const ov = workflowCoordinator.getWorkflowOverlay();
4238
3851
  if (!ov) return null;
4239
3852
  return { schemaVersion: "workflow-overlay/1", data: ov };
4240
3853
  }
4241
3854
  case "workflowMetadata": {
4242
- const meta = deriveWorkflowMetadataSnapshot();
3855
+ const meta = workflowCoordinator.getWorkflowMetadataSnapshot();
4243
3856
  if (!meta || (meta.definitions.length === 0 && meta.entries.length === 0)) return null;
4244
3857
  return { schemaVersion: "workflow-metadata/1", data: meta };
4245
3858
  }
4246
3859
  case "workItems":
4247
3860
  return null;
3861
+ case "embeddings":
3862
+ // P8 Step 7: the session layer owns the `embeddings`
3863
+ // namespace (load-time offload + export-time
3864
+ // reconstitute) via
3865
+ // `src/session/import/embedded-offload.ts` +
3866
+ // `src/session/export/embedded-reconstitute.ts`.
3867
+ // The runtime has no per-doc state to serialize here.
3868
+ return null;
4248
3869
  }
4249
3870
  },
4250
3871
  });
@@ -4253,7 +3874,37 @@ export function createDocumentRuntime(
4253
3874
  _editorState: collectedEditorState,
4254
3875
  };
4255
3876
 
4256
- const result = await options.exportDocx(this.getSessionState(), internalOptions);
3877
+ // Phase 1 io + serialize channels — bracket the exporter call so the
3878
+ // `serialize.main_document.completed` emit runs with this runtime's bus.
3879
+ setActiveSerializeTelemetryBus(telemetryBus);
3880
+ let exportStart = 0;
3881
+ if (telemetryBus.isEnabled("io")) {
3882
+ exportStart = performance.now();
3883
+ telemetryBus.emit({
3884
+ channel: "io",
3885
+ type: "io.export.start",
3886
+ t: 0,
3887
+ payload: { documentId: state.documentId },
3888
+ });
3889
+ }
3890
+ let result: Awaited<ReturnType<typeof options.exportDocx>>;
3891
+ try {
3892
+ result = await options.exportDocx(this.getSessionState(), internalOptions);
3893
+ } finally {
3894
+ setActiveSerializeTelemetryBus(undefined);
3895
+ }
3896
+ if (telemetryBus.isEnabled("io")) {
3897
+ telemetryBus.emit({
3898
+ channel: "io",
3899
+ type: "io.export.completed",
3900
+ t: 0,
3901
+ payload: {
3902
+ documentId: state.documentId,
3903
+ bytes: result.bytes?.byteLength ?? 0,
3904
+ ms: performance.now() - exportStart,
3905
+ },
3906
+ });
3907
+ }
4257
3908
 
4258
3909
  emit({
4259
3910
  type: "export_completed",
@@ -4264,87 +3915,81 @@ export function createDocumentRuntime(
4264
3915
  return result;
4265
3916
  },
4266
3917
  setWorkflowOverlay(overlay) {
4267
- this.dispatch({
4268
- type: "workflow.set-overlay",
4269
- overlay,
4270
- origin: createOrigin("api", clock()),
4271
- });
4272
- const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
4273
- editorStateChannel.recordMutation("workflowOverlay", {
4274
- namespace: "workflowOverlay",
4275
- schemaVersion: "workflow-overlay/1",
4276
- data: normalizedWorkflowOverlay ?? overlay,
4277
- });
3918
+ workflowCoordinator.setWorkflowOverlay(overlay);
4278
3919
  },
4279
3920
  clearWorkflowOverlay() {
4280
- this.dispatch({
4281
- type: "workflow.clear-overlay",
4282
- origin: createOrigin("api", clock()),
4283
- });
3921
+ workflowCoordinator.clearWorkflowOverlay();
4284
3922
  },
4285
3923
  getWorkflowOverlay() {
4286
- return getNormalizedWorkflowOverlay();
3924
+ return workflowCoordinator.getWorkflowOverlay();
4287
3925
  },
4288
- setSharedWorkflowState(state) {
4289
- if (state === sharedWorkflowState) return;
4290
- sharedWorkflowState = state;
4291
- // Invalidate guard/scope caches so next derivation reflects the new state.
4292
- cachedInteractionGuardSnapshot = undefined;
4293
- cachedWorkflowScopeSnapshot = undefined;
3926
+ setSharedWorkflowState(sharedState) {
3927
+ workflowCoordinator.setSharedWorkflowState(sharedState);
4294
3928
  },
4295
3929
  getWorkflowScopeSnapshot() {
4296
- return getCachedWorkflowScopeSnapshot();
3930
+ return workflowCoordinator.getWorkflowScopeSnapshot();
4297
3931
  },
4298
3932
  getInteractionGuardSnapshot() {
4299
- return getCachedInteractionGuardSnapshot();
3933
+ return workflowCoordinator.getInteractionGuardSnapshot();
4300
3934
  },
4301
3935
  getWorkflowMarkupSnapshot() {
4302
- return getCachedWorkflowMarkupSnapshot();
3936
+ return workflowCoordinator.getWorkflowMarkupSnapshot();
4303
3937
  },
4304
3938
  setWorkflowMetadataDefinitions(definitions) {
4305
- this.dispatch({
4306
- type: "workflow.set-metadata-definitions",
4307
- definitions,
4308
- origin: createOrigin("api", clock()),
4309
- });
3939
+ workflowCoordinator.setWorkflowMetadataDefinitions(definitions);
4310
3940
  },
4311
3941
  clearWorkflowMetadataDefinitions() {
4312
- this.dispatch({
4313
- type: "workflow.clear-metadata-definitions",
4314
- origin: createOrigin("api", clock()),
4315
- });
3942
+ workflowCoordinator.clearWorkflowMetadataDefinitions();
4316
3943
  },
4317
3944
  setWorkflowMetadataEntries(entries) {
4318
- this.dispatch({
4319
- type: "workflow.set-metadata-entries",
4320
- entries,
4321
- origin: createOrigin("api", clock()),
4322
- });
4323
- editorStateChannel.recordMutation("workflowMetadata", {
4324
- namespace: "workflowMetadata",
4325
- schemaVersion: "workflow-metadata/1",
4326
- data: entries,
4327
- });
3945
+ workflowCoordinator.setWorkflowMetadataEntries(entries);
4328
3946
  },
4329
3947
  clearWorkflowMetadataEntries() {
4330
- this.dispatch({
4331
- type: "workflow.clear-metadata-entries",
4332
- origin: createOrigin("api", clock()),
4333
- });
3948
+ workflowCoordinator.clearWorkflowMetadataEntries();
4334
3949
  },
4335
3950
  getWorkflowMetadataSnapshot() {
4336
- return deriveWorkflowMetadataSnapshot();
3951
+ return workflowCoordinator.getWorkflowMetadataSnapshot();
3952
+ },
3953
+ getVisibilityPolicy(kind) {
3954
+ return workflowCoordinator.getVisibilityPolicy(kind);
3955
+ },
3956
+ getVisibilityPolicies() {
3957
+ return workflowCoordinator.getVisibilityPolicies();
3958
+ },
3959
+ setVisibilityPolicy(policy) {
3960
+ return workflowCoordinator.setVisibilityPolicy(policy);
3961
+ },
3962
+ clearVisibilityPolicy(kind) {
3963
+ return workflowCoordinator.clearVisibilityPolicy(kind);
3964
+ },
3965
+ subscribeVisibilityPolicy(listener) {
3966
+ return workflowCoordinator.subscribeVisibilityPolicy(listener);
3967
+ },
3968
+ getMarkupModePolicy() {
3969
+ return workflowCoordinator.getMarkupModePolicy();
3970
+ },
3971
+ setMarkupModePolicy(policy) {
3972
+ return workflowCoordinator.setMarkupModePolicy(policy);
3973
+ },
3974
+ subscribeMarkupModePolicy(listener) {
3975
+ return workflowCoordinator.subscribeMarkupModePolicy(listener);
3976
+ },
3977
+ setEffectiveMarkupModeProvider(provider) {
3978
+ effectiveMarkupModeProvider = provider ?? undefined;
3979
+ // Re-project the surface so `cachedRenderSnapshot` reflects the
3980
+ // new mode on the next `getRenderSnapshot()`. `refreshSurfaceOnly`
3981
+ // also notifies listeners — React trees that have subscribed
3982
+ // re-read synchronously.
3983
+ refreshSurfaceOnly();
3984
+ },
3985
+ invalidateForMarkupModeChange() {
3986
+ // Fast path for class-C local-preference flips. Provider is
3987
+ // stable; cache invalidation is what propagates the new value.
3988
+ // `refreshSurfaceOnly` notifies listeners as part of its work.
3989
+ refreshSurfaceOnly();
4337
3990
  },
4338
3991
  queryScopes(filter) {
4339
- return runQueryScopes(
4340
- {
4341
- overlay: workflowOverlay,
4342
- entries: workflowMetadataEntries,
4343
- document: state.document,
4344
- markerBackedScopeIds,
4345
- },
4346
- filter,
4347
- );
3992
+ return workflowCoordinator.queryScopes(filter);
4348
3993
  },
4349
3994
  subscribeToScopeQuery(filter, callback) {
4350
3995
  const buildAnchorKey = (anchor: EditorAnchorProjection): string => {
@@ -4444,10 +4089,10 @@ export function createDocumentRuntime(
4444
4089
  const hits = findAllScopesAt(state.document, pos);
4445
4090
  return projectScopeQueryResults(
4446
4091
  {
4447
- overlay: workflowOverlay,
4448
- entries: workflowMetadataEntries,
4092
+ overlay: overlayStore.getOverlay(),
4093
+ entries: overlayStore.getMetadataEntries(),
4449
4094
  document: state.document,
4450
- markerBackedScopeIds,
4095
+ markerBackedScopeIds: overlayStore.getMarkerBackedScopeIds(),
4451
4096
  },
4452
4097
  hits.map((h) => h.scopeId),
4453
4098
  options,
@@ -4458,10 +4103,10 @@ export function createDocumentRuntime(
4458
4103
  const hits = findScopesIntersecting(state.document, range.from, range.to, options?.mode);
4459
4104
  return projectScopeQueryResults(
4460
4105
  {
4461
- overlay: workflowOverlay,
4462
- entries: workflowMetadataEntries,
4106
+ overlay: overlayStore.getOverlay(),
4107
+ entries: overlayStore.getMetadataEntries(),
4463
4108
  document: state.document,
4464
- markerBackedScopeIds,
4109
+ markerBackedScopeIds: overlayStore.getMarkerBackedScopeIds(),
4465
4110
  },
4466
4111
  hits.map((h) => h.scopeId),
4467
4112
  options,
@@ -4530,14 +4175,14 @@ export function createDocumentRuntime(
4530
4175
  return createWorkflowChunks({
4531
4176
  document: state.document,
4532
4177
  navigation: getCachedDocumentNavigationSnapshot(state, activeStory),
4533
- workflowMarkup: getCachedWorkflowMarkupSnapshot(),
4178
+ workflowMarkup: workflowCoordinator.getWorkflowMarkupSnapshot(),
4534
4179
  });
4535
4180
  },
4536
4181
  getReviewWorkSnapshot() {
4537
4182
  return createReviewWorkSnapshot({
4538
4183
  comments: cachedRenderSnapshot.comments,
4539
4184
  trackedChanges: cachedRenderSnapshot.trackedChanges,
4540
- workflowMarkup: getCachedWorkflowMarkupSnapshot(),
4185
+ workflowMarkup: workflowCoordinator.getWorkflowMarkupSnapshot(),
4541
4186
  document: state.document,
4542
4187
  navigation: getCachedDocumentNavigationSnapshot(state, activeStory),
4543
4188
  });
@@ -4590,6 +4235,17 @@ export function createDocumentRuntime(
4590
4235
  },
4591
4236
  };
4592
4237
 
4238
+ // Late-bind the workflow coordinator's dispatch accessor now that
4239
+ // the runtime object exists. addScope / removeScope / setWorkflowOverlay /
4240
+ // setWorkflowMetadataEntries and other coordinator methods route
4241
+ // through this. Calling any of those before this line is a bug
4242
+ // (guarded by the throw in the placeholder).
4243
+ dispatchToRuntime = (command: unknown) => {
4244
+ runtime.dispatch(
4245
+ command as Parameters<typeof runtime.dispatch>[0],
4246
+ );
4247
+ };
4248
+
4593
4249
  return runtime;
4594
4250
 
4595
4251
  function applyHistory(direction: "undo" | "redo"): void {
@@ -4667,6 +4323,12 @@ export function createDocumentRuntime(
4667
4323
  return;
4668
4324
  }
4669
4325
 
4326
+ emitStageToken(telemetryBus, "commit", "commit.apply.start", {
4327
+ revisionToken: state.revisionToken,
4328
+ markDirty: transaction.markDirty,
4329
+ stepCount: transaction.mapping?.steps?.length ?? 0,
4330
+ });
4331
+
4670
4332
  const previous = state;
4671
4333
 
4672
4334
  const tApply0 = performance.now();
@@ -4675,7 +4337,10 @@ export function createDocumentRuntime(
4675
4337
  state = finalizeState(transaction.nextState, transaction.markDirty, clock());
4676
4338
  perfCounters.increment("commit.finalizeState.us", Math.round((performance.now() - tFinalize0) * 1000));
4677
4339
  storySelections.set(storyTargetKey(activeStory), state.selection);
4678
- syncMarkerBackedScopeIds(state.document, workflowOverlay);
4340
+ // Re-sync marker-backed scope IDs against the new document; the
4341
+ // store handles set computation + telemetry. `replaceOverlay(current, doc)`
4342
+ // is idempotent on state and re-derives the marker-backed set.
4343
+ overlayStore.replaceOverlay(overlayStore.getOverlay(), state.document);
4679
4344
  const detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
4680
4345
 
4681
4346
  const tInvalidate0 = performance.now();
@@ -4763,6 +4428,9 @@ export function createDocumentRuntime(
4763
4428
  });
4764
4429
  perfCounters.increment("commit.notify.us", Math.round((performance.now() - tNotify0) * 1000));
4765
4430
  perfCounters.increment("commit.total.us", Math.round((performance.now() - tApply0) * 1000));
4431
+ emitStageToken(telemetryBus, "commit", "commit.apply.complete", {
4432
+ revisionToken: state.revisionToken,
4433
+ });
4766
4434
  }
4767
4435
 
4768
4436
  function notify(
@@ -4986,13 +4654,42 @@ export function createDocumentRuntime(
4986
4654
  textOptions: {
4987
4655
  selection?: EditorState["selection"];
4988
4656
  blockedCommandName?: string;
4657
+ /**
4658
+ * Force the command-execution `documentMode` for this dispatch
4659
+ * regardless of what the workflow coordinator would infer from
4660
+ * the selection's scope stack. Used by
4661
+ * `applyScopeReplacement` for the `text-insert-tracked` +
4662
+ * `text-delete-tracked` step kinds emitted by the L08 compiler:
4663
+ * the compiler has already decided this step should produce
4664
+ * tracked insertion/deletion revisions, so the runtime must
4665
+ * route through the suggesting-mode branch unconditionally.
4666
+ *
4667
+ * Coord-08 §2 ("Suggest-mode tracked-insert dispatch primitive")
4668
+ * asks for this verb; the implementation is a single-field
4669
+ * override to the context that `executeEditorCommand` reads.
4670
+ */
4671
+ documentModeOverride?: DocumentMode;
4989
4672
  } = {},
4990
4673
  ): TextCommandAck {
4674
+ emitStageToken(telemetryBus, "command", "command.dispatch.start", {
4675
+ commandType: command.type,
4676
+ });
4677
+ // Every exit of this function must emit a matching `.complete` stage
4678
+ // token so the `command` channel carries start/complete pairs just like
4679
+ // the `commit` channel does. D1 lineage reads the pair as proof no
4680
+ // silent short-circuit swallowed the dispatch.
4681
+ const completeDispatch = (ack: TextCommandAck): TextCommandAck => {
4682
+ emitStageToken(telemetryBus, "command", "command.dispatch.complete", {
4683
+ commandType: command.type,
4684
+ outcome: ack.kind,
4685
+ });
4686
+ return ack;
4687
+ };
4991
4688
  const opId = (command.origin as { opId?: string } | undefined)?.opId;
4992
4689
  const selection = textOptions.selection ?? state.selection;
4993
4690
  if (
4994
4691
  activeStory.kind !== "main" &&
4995
- getEffectiveDocumentMode(selection) === "suggesting" &&
4692
+ workflowCoordinator.getEffectiveDocumentMode(selection) === "suggesting" &&
4996
4693
  command.type === "paragraph.split"
4997
4694
  ) {
4998
4695
  const message = `"${command.type}" is not supported in suggesting mode for this story.`;
@@ -5006,14 +4703,14 @@ export function createDocumentRuntime(
5006
4703
  storyTarget: activeStory,
5007
4704
  }],
5008
4705
  });
5009
- return {
4706
+ return completeDispatch({
5010
4707
  kind: "rejected",
5011
4708
  opId,
5012
4709
  newRevisionToken: "",
5013
4710
  blockedReasons: [{ code: "suggesting_unsupported", message }],
5014
- };
4711
+ });
5015
4712
  }
5016
- const blockedReasons = evaluateWorkflowBlockedReasons(selection, command.type);
4713
+ const blockedReasons = workflowCoordinator.evaluateBlockedReasons(selection, command.type);
5017
4714
  if (blockedReasons.length > 0) {
5018
4715
  emit({
5019
4716
  type: "command_blocked",
@@ -5021,18 +4718,20 @@ export function createDocumentRuntime(
5021
4718
  command: textOptions.blockedCommandName ?? command.type,
5022
4719
  reasons: blockedReasons,
5023
4720
  });
5024
- return {
4721
+ return completeDispatch({
5025
4722
  kind: "rejected",
5026
4723
  opId,
5027
4724
  newRevisionToken: "",
5028
4725
  blockedReasons: blockedReasons.map((r) => ({ code: r.code, message: r.message })),
5029
- };
4726
+ });
5030
4727
  }
5031
4728
 
5032
4729
  const timestamp = normalizeCommandTimestamp(command.origin?.timestamp) ?? clock();
5033
4730
  const context = {
5034
4731
  timestamp,
5035
- documentMode: getEffectiveDocumentMode(selection),
4732
+ documentMode:
4733
+ textOptions.documentModeOverride ??
4734
+ workflowCoordinator.getEffectiveDocumentMode(selection),
5036
4735
  defaultAuthorId: defaultAuthorId ?? undefined,
5037
4736
  renderSnapshot: cachedRenderSnapshot,
5038
4737
  } as const;
@@ -5054,13 +4753,13 @@ export function createDocumentRuntime(
5054
4753
  activeStory: preActiveStory,
5055
4754
  priorDocument,
5056
4755
  });
5057
- return classifyAck({
4756
+ return completeDispatch(classifyAck({
5058
4757
  command,
5059
4758
  opId,
5060
4759
  priorState: baseState,
5061
4760
  transaction: mainTransaction,
5062
4761
  newRevisionToken: state.revisionToken,
5063
- });
4762
+ }));
5064
4763
  }
5065
4764
 
5066
4765
  const localState = createEditorState({
@@ -5091,11 +4790,11 @@ export function createDocumentRuntime(
5091
4790
  historyBoundary: "skip",
5092
4791
  markDirty: false,
5093
4792
  });
5094
- return {
4793
+ return completeDispatch({
5095
4794
  kind: "equivalent",
5096
4795
  opId,
5097
4796
  newRevisionToken: state.revisionToken,
5098
- };
4797
+ });
5099
4798
  }
5100
4799
 
5101
4800
  const nextDocument = replaceStoryBlocks(
@@ -5136,13 +4835,13 @@ export function createDocumentRuntime(
5136
4835
  activeStory: preActiveStory,
5137
4836
  priorDocument,
5138
4837
  });
5139
- return classifyAck({
4838
+ return completeDispatch(classifyAck({
5140
4839
  command,
5141
4840
  opId,
5142
4841
  priorState: baseState,
5143
4842
  transaction: mergedTransaction,
5144
4843
  newRevisionToken: state.revisionToken,
5145
- });
4844
+ }));
5146
4845
  }
5147
4846
 
5148
4847
  function classifyAck(params: {
@@ -5346,7 +5045,7 @@ export function createDocumentRuntime(
5346
5045
  // text is not heading text), so this does not loop.
5347
5046
  const ctx = {
5348
5047
  timestamp: clock(),
5349
- documentMode: getEffectiveDocumentMode(state.selection),
5048
+ documentMode: workflowCoordinator.getEffectiveDocumentMode(state.selection),
5350
5049
  defaultAuthorId: defaultAuthorId ?? undefined,
5351
5050
  renderSnapshot: cachedRenderSnapshot,
5352
5051
  } as const;
@@ -5381,6 +5080,54 @@ export function createDocumentRuntime(
5381
5080
  const t0 = performance.now();
5382
5081
  emitInternal(event);
5383
5082
  perfCounters.increment(`emit.${event.type}.internal.us`, Math.round((performance.now() - t0) * 1000));
5083
+ // Phase 1 — telemetry fan-out. One central emit covering warning / scope /
5084
+ // commit / selection / collab channels. Each branch early-returns if the
5085
+ // corresponding channel is off (one bitmask AND). Zero alloc when all off.
5086
+ if (telemetryBus.isEnabled("warning") && event.type === "warning_added") {
5087
+ telemetryBus.emit({
5088
+ channel: "warning",
5089
+ type: `warning.${event.warning.code}`,
5090
+ t: 0,
5091
+ payload: {
5092
+ warningId: event.warning.warningId,
5093
+ code: event.warning.code,
5094
+ severity: event.warning.severity,
5095
+ source: event.warning.source,
5096
+ scopeId: (event.warning as { scopeId?: string }).scopeId,
5097
+ },
5098
+ });
5099
+ } else if (telemetryBus.isEnabled("warning") && event.type === "warning_cleared") {
5100
+ telemetryBus.emit({
5101
+ channel: "warning",
5102
+ type: "warning.cleared",
5103
+ t: 0,
5104
+ payload: { warningId: event.warningId, code: event.code },
5105
+ });
5106
+ }
5107
+ if (
5108
+ telemetryBus.isEnabled("scope") &&
5109
+ event.type === "workflow_overlay_changed"
5110
+ ) {
5111
+ telemetryBus.emitLazy("scope", () => {
5112
+ const overlay = overlayStore.getOverlay();
5113
+ return {
5114
+ type: "scope.overlay_changed",
5115
+ payload: {
5116
+ markerBackedCount: overlayStore.getMarkerBackedScopeIds().size,
5117
+ overlayScopeCount: overlay?.scopes.length ?? 0,
5118
+ activeWorkItemId: overlay?.activeWorkItemId ?? null,
5119
+ },
5120
+ };
5121
+ });
5122
+ }
5123
+ if (telemetryBus.isEnabled("selection") && event.type === "selection_changed") {
5124
+ telemetryBus.emit({
5125
+ channel: "selection",
5126
+ type: "selection.changed",
5127
+ t: 0,
5128
+ payload: { isCollapsed: (event as { selection?: { isCollapsed?: boolean } }).selection?.isCollapsed ?? true },
5129
+ });
5130
+ }
5384
5131
  if (shouldEmitContextAnalyticsChanged(event)) {
5385
5132
  scheduleContextAnalyticsEmit();
5386
5133
  }
@@ -5396,85 +5143,43 @@ export function createDocumentRuntime(
5396
5143
  }
5397
5144
  | null = null;
5398
5145
  switch (command.type) {
5399
- case "workflow.set-overlay": {
5400
- workflowOverlay = structuredClone(command.overlay);
5401
- syncMarkerBackedScopeIds(state.document, workflowOverlay);
5402
- cachedNormalizedWorkflowOverlay = undefined;
5403
- detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
5404
- cachedRenderSnapshot = refreshRenderSnapshot();
5405
- const snapshot = deriveWorkflowScopeSnapshot()!;
5406
- emit({
5407
- type: "workflow_overlay_changed",
5408
- documentId: state.documentId,
5409
- snapshot,
5410
- });
5411
- if (workflowOverlay.activeWorkItemId !== undefined) {
5412
- emit({
5413
- type: "workflow_active_work_item_changed",
5414
- documentId: state.documentId,
5415
- activeWorkItemId: workflowOverlay.activeWorkItemId ?? null,
5416
- });
5417
- }
5418
- break;
5419
- }
5420
- case "workflow.clear-overlay": {
5421
- workflowOverlay = null;
5422
- syncMarkerBackedScopeIds(state.document, workflowOverlay);
5423
- cachedNormalizedWorkflowOverlay = undefined;
5424
- detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
5425
- cachedRenderSnapshot = refreshRenderSnapshot();
5426
- emit({
5427
- type: "workflow_active_work_item_changed",
5428
- documentId: state.documentId,
5429
- activeWorkItemId: null,
5430
- });
5431
- emit({
5432
- type: "workflow_overlay_changed",
5433
- documentId: state.documentId,
5434
- snapshot: {
5435
- overlayPresent: false,
5436
- activeWorkItemId: null,
5437
- scopes: [],
5438
- candidates: [],
5439
- blockedReasons: [],
5440
- },
5441
- });
5442
- break;
5443
- }
5444
- case "workflow.set-metadata-definitions": {
5445
- workflowMetadataDefinitions = structuredClone(command.definitions);
5446
- emit({
5447
- type: "workflow_metadata_changed",
5448
- documentId: state.documentId,
5449
- snapshot: deriveWorkflowMetadataSnapshot(),
5450
- });
5451
- break;
5452
- }
5453
- case "workflow.clear-metadata-definitions": {
5454
- workflowMetadataDefinitions = [];
5455
- emit({
5456
- type: "workflow_metadata_changed",
5457
- documentId: state.documentId,
5458
- snapshot: deriveWorkflowMetadataSnapshot(),
5459
- });
5460
- break;
5461
- }
5462
- case "workflow.set-metadata-entries": {
5463
- workflowMetadataEntries = structuredClone(command.entries);
5464
- emit({
5465
- type: "workflow_metadata_changed",
5466
- documentId: state.documentId,
5467
- snapshot: deriveWorkflowMetadataSnapshot(),
5468
- });
5469
- break;
5470
- }
5146
+ case "workflow.set-overlay":
5147
+ case "workflow.clear-overlay":
5148
+ case "workflow.set-metadata-definitions":
5149
+ case "workflow.clear-metadata-definitions":
5150
+ case "workflow.set-metadata-entries":
5471
5151
  case "workflow.clear-metadata-entries": {
5472
- workflowMetadataEntries = [];
5473
- emit({
5474
- type: "workflow_metadata_changed",
5475
- documentId: state.documentId,
5476
- snapshot: deriveWorkflowMetadataSnapshot(),
5477
- });
5152
+ // Delegate all workflow overlay / metadata commands to the
5153
+ // coordinator. It owns state mutation (via the overlay store),
5154
+ // cache invalidation, and event emission. The returned
5155
+ // warning-delta (if any) is rewritten into the runtime's
5156
+ // state.warnings array by `syncDetachedScopeWarnings` called
5157
+ // from inside the coordinator, then surfaced through the
5158
+ // `warning_added` / `warning_cleared` emit loop below.
5159
+ const warningDelta = workflowCoordinator.applyOverlayCommand(
5160
+ command as Parameters<typeof workflowCoordinator.applyOverlayCommand>[0],
5161
+ () => {
5162
+ cachedRenderSnapshot = refreshRenderSnapshot();
5163
+ },
5164
+ );
5165
+ if (warningDelta) {
5166
+ const priorWarnings = state.warnings;
5167
+ if (
5168
+ warningDelta.nextWarnings !== priorWarnings &&
5169
+ warningDelta.nextWarnings.length + priorWarnings.length > 0
5170
+ ) {
5171
+ state = { ...state, warnings: warningDelta.nextWarnings };
5172
+ }
5173
+ if (warningDelta.added.length > 0 || warningDelta.cleared.length > 0) {
5174
+ detachedWorkflowScopeWarnings = {
5175
+ added: warningDelta.added,
5176
+ cleared: warningDelta.cleared.map((c) => ({
5177
+ warningId: c.warningId,
5178
+ code: c.code,
5179
+ })),
5180
+ };
5181
+ }
5182
+ }
5478
5183
  break;
5479
5184
  }
5480
5185
  case "host-annotation.set-overlay": {
@@ -5591,8 +5296,8 @@ export function createDocumentRuntime(
5591
5296
  trackedSnapshots.get(
5592
5297
  getRuntimeContextAnalyticsQueryKey(
5593
5298
  resolveCurrentContextAnalyticsQuery({
5594
- workflowScopeSnapshot: getCachedWorkflowScopeSnapshot(),
5595
- interactionGuardSnapshot: getCachedInteractionGuardSnapshot(),
5299
+ workflowScopeSnapshot: workflowCoordinator.getWorkflowScopeSnapshot(),
5300
+ interactionGuardSnapshot: workflowCoordinator.getInteractionGuardSnapshot(),
5596
5301
  }),
5597
5302
  ),
5598
5303
  ) ?? null,
@@ -5994,15 +5699,17 @@ function toPublicCompatibilityReport(
5994
5699
  function toPublicCompatibilityFeatureEntry(
5995
5700
  entry: InternalCompatibilityFeatureEntry,
5996
5701
  ) {
5997
- return {
5998
- ...entry,
5999
- affectedAnchor: entry.affectedAnchor
6000
- ? toPublicAnchorProjection(entry.affectedAnchor)
6001
- : undefined,
6002
- };
5702
+ // Internal* collapsed to public-type aliases 2026-04-22 — the record
5703
+ // `affectedAnchor` is already the public flat shape end-to-end (see
5704
+ // `docs/plans/cross-layer-coord-02.md §8`).
5705
+ return { ...entry };
6003
5706
  }
6004
5707
 
6005
5708
  function toPublicWarning(warning: InternalEditorWarning): EditorWarning {
5709
+ // Internal* collapsed to public-type aliases 2026-04-22; anchor is
5710
+ // already public flat. The function remains meaningful — it still
5711
+ // synthesises the v2.0.0 `diagnostic` composite from legacy warning
5712
+ // codes when absent. Anchor handling is now identity.
6006
5713
  const diagnostic =
6007
5714
  warning.diagnostic ??
6008
5715
  (() => {
@@ -6025,9 +5732,7 @@ function toPublicWarning(warning: InternalEditorWarning): EditorWarning {
6025
5732
  source: warning.source,
6026
5733
  },
6027
5734
  details: warning.details,
6028
- affectedAnchor: warning.affectedAnchor
6029
- ? toPublicAnchorProjection(warning.affectedAnchor)
6030
- : undefined,
5735
+ affectedAnchor: warning.affectedAnchor,
6031
5736
  });
6032
5737
  default:
6033
5738
  return undefined;
@@ -6035,9 +5740,6 @@ function toPublicWarning(warning: InternalEditorWarning): EditorWarning {
6035
5740
  })();
6036
5741
  return {
6037
5742
  ...warning,
6038
- affectedAnchor: warning.affectedAnchor
6039
- ? toPublicAnchorProjection(warning.affectedAnchor)
6040
- : undefined,
6041
5743
  diagnostic,
6042
5744
  };
6043
5745
  }
@@ -6771,7 +6473,7 @@ function refreshDocumentFields(
6771
6473
  return { document, updatedCount, changed: false };
6772
6474
  }
6773
6475
 
6774
- const nextDocument: CanonicalDocumentEnvelope = {
6476
+ const nextDocument: MutableCanonicalDocument = {
6775
6477
  ...document,
6776
6478
  content: {
6777
6479
  ...document.content,
@@ -6779,7 +6481,7 @@ function refreshDocumentFields(
6779
6481
  },
6780
6482
  ...(nextSubParts ? { subParts: nextSubParts } : {}),
6781
6483
  };
6782
- const nextRegistry = buildFieldRegistry({
6484
+ const nextRegistry = rebuildFieldRegistry({
6783
6485
  content: nextDocument.content,
6784
6486
  styles: nextDocument.styles,
6785
6487
  subParts: nextDocument.subParts,
@@ -6883,14 +6585,14 @@ function refreshDocumentTableOfContents(
6883
6585
  };
6884
6586
  }
6885
6587
 
6886
- const nextDocument: CanonicalDocumentEnvelope = {
6588
+ const nextDocument: MutableCanonicalDocument = {
6887
6589
  ...document,
6888
6590
  content: {
6889
6591
  ...document.content,
6890
6592
  children: nextChildren,
6891
6593
  },
6892
6594
  };
6893
- const nextRegistry = buildFieldRegistry({
6595
+ const nextRegistry = rebuildFieldRegistry({
6894
6596
  content: nextDocument.content,
6895
6597
  styles: nextDocument.styles,
6896
6598
  subParts: nextDocument.subParts,