@beyondwork/docx-react-component 1.0.67 → 1.0.70

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 -932
  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 -4797
  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
@@ -0,0 +1,1348 @@
1
+ /**
2
+ * Layer 06 — Workflow coordinator.
3
+ *
4
+ * Owns the workflow/review orchestration that used to live inline in
5
+ * `document-runtime.ts`. The overlay store (`overlay-store.ts`) holds
6
+ * state + pure helpers; the coordinator plumbs that state through
7
+ * runtime-scoped dependencies (selection, revision token, render
8
+ * snapshot, page graph, dispatch, event emission) and exposes the
9
+ * full workflow public API plus the dispatch-branch handler.
10
+ *
11
+ * ## Contracts
12
+ *
13
+ * - W1 (marker-backed scopes) — scope marker pairs remain canonical
14
+ * truth for scope location. This coordinator does not own markers;
15
+ * it reads them through `collectScopeLocations` (via overlay-store
16
+ * + query-scopes) and resolves anchors through `resolveScope`.
17
+ *
18
+ * - W3 (single interaction-guard verdict) — `getInteractionGuardSnapshot`
19
+ * is the sole authority for effective mode + blocked reasons. The
20
+ * snapshot caches against (revisionToken, activeStory, selection,
21
+ * readOnly, documentMode, protectionSnapshot, overlay reference,
22
+ * sharedWorkflowState).
23
+ *
24
+ * - W4 (AI action policy orthogonal to guard) — this coordinator does
25
+ * not re-implement AI policy (see `ai-action-policy.ts`); guard
26
+ * evaluation is input to AI policy, not vice versa.
27
+ *
28
+ * - W7 (rail is page-anchored) — `getRailSegments` / `getAllRailSegments`
29
+ * / `getAllScopeCardModels` delegate to `collectScopeRailSegments`
30
+ * over the coordinator's page-graph accessor. Layout is a
31
+ * page-graph input source, not the owner.
32
+ *
33
+ * - W8 (tamper gate blocks on failure) — the coordinator reads
34
+ * `sharedWorkflowState.lockedMode` through the overlay store and
35
+ * short-circuits blocked-reasons evaluation when the round is
36
+ * locked (matches the pre-extraction behavior).
37
+ *
38
+ * - W9 (no upward imports) — this file must not import from
39
+ * `../document-runtime.ts`, `../render`, `../../ui*`, `../../api/v*`,
40
+ * or `../../io`. Enforced by
41
+ * `scripts/ci-check-workflow-layer-purity.mjs`.
42
+ */
43
+
44
+ import type {
45
+ AddScopeParams,
46
+ AddScopeResult,
47
+ DocumentMode,
48
+ EditorAnchorProjection,
49
+ EditorStoryTarget,
50
+ FieldSnapshot,
51
+ InteractionGuardSnapshot,
52
+ ProtectionSnapshot,
53
+ RuntimeRenderSnapshot,
54
+ ScopeCardModel,
55
+ ScopeChromeVisibilityState,
56
+ ScopeQueryFilter,
57
+ ScopeQueryResult,
58
+ ScopeVisibility,
59
+ WorkflowBlockedCommandReason,
60
+ WorkflowCandidateRange,
61
+ WorkflowMarkupSnapshot,
62
+ WorkflowMetadataDefinition,
63
+ WorkflowMetadataEntry,
64
+ WorkflowMetadataSnapshot,
65
+ WorkflowOverlay,
66
+ WorkflowScope,
67
+ WorkflowScopeMode,
68
+ WorkflowScopeSnapshot,
69
+ } from "../../api/public-types.ts";
70
+ import {
71
+ type CanonicalDocumentEnvelope,
72
+ type EditorState,
73
+ type EditorWarning as InternalEditorWarning,
74
+ } from "../../core/state/editor-state.ts";
75
+ import type { SharedWorkflowState } from "../collab/workflow-shared.ts";
76
+ import type { TelemetryBus } from "../debug/telemetry-bus.ts";
77
+ import {
78
+ MAIN_STORY_TARGET,
79
+ storyTargetsEqual,
80
+ } from "../../core/selection/mapping.ts";
81
+ import { storyTargetKey } from "../story-targeting.ts";
82
+ import { collectWorkflowMarkupSnapshot } from "./markup.ts";
83
+ import {
84
+ collectScopeRailSegments,
85
+ attachScopeCardModel,
86
+ type CollectScopeRailSegmentsInput,
87
+ } from "./rail/compose.ts";
88
+ import type { ScopeRailSegment } from "./rail/types.ts";
89
+ import {
90
+ queryScopes as runQueryScopes,
91
+ type ScopeQueryInputs,
92
+ } from "./query-scopes.ts";
93
+ import { resolveScope } from "./scope-resolver.ts";
94
+ import { insertScopeMarkers, removeScopeMarkers } from "../../core/commands/add-scope.ts";
95
+ import type { OverlayStore, MergeDetachedWarningsResult } from "./overlay-store.ts";
96
+ import {
97
+ type OverlayKind,
98
+ type OverlayVisibilityPolicy,
99
+ } from "./visibility-policy.ts";
100
+ import { type WorkflowMarkupModePolicy } from "./markup-mode-policy.ts";
101
+
102
+ /** Shape of origin metadata attached to commands. The runtime has a
103
+ * richer `CommandOrigin` type; we only pass it through, so we keep an
104
+ * open shape here to avoid coupling. */
105
+ type CoordinatorCommandOrigin = { readonly source: string; readonly at?: string };
106
+
107
+ /** Document-mutation commands the coordinator emits through `deps.dispatch`. */
108
+ type CoordinatorDispatchedCommand =
109
+ | {
110
+ type: "workflow.set-overlay";
111
+ overlay: WorkflowOverlay;
112
+ origin: CoordinatorCommandOrigin;
113
+ }
114
+ | {
115
+ type: "workflow.clear-overlay";
116
+ origin: CoordinatorCommandOrigin;
117
+ }
118
+ | {
119
+ type: "workflow.set-metadata-definitions";
120
+ definitions: readonly WorkflowMetadataDefinition[];
121
+ origin: CoordinatorCommandOrigin;
122
+ }
123
+ | {
124
+ type: "workflow.clear-metadata-definitions";
125
+ origin: CoordinatorCommandOrigin;
126
+ }
127
+ | {
128
+ type: "workflow.set-metadata-entries";
129
+ entries: readonly WorkflowMetadataEntry[];
130
+ origin: CoordinatorCommandOrigin;
131
+ }
132
+ | {
133
+ type: "workflow.clear-metadata-entries";
134
+ origin: CoordinatorCommandOrigin;
135
+ }
136
+ | {
137
+ type: "document.replace";
138
+ document: CanonicalDocumentEnvelope;
139
+ origin: CoordinatorCommandOrigin;
140
+ };
141
+
142
+ /** Editor-state channel — the coordinator records overlay + metadata
143
+ * mutations through this for collab broadcast. Matches the runtime's
144
+ * `editorStateChannel.recordMutation` signature. */
145
+ export interface EditorStateChannel {
146
+ recordMutation(
147
+ namespace: string,
148
+ payload: { namespace: string; schemaVersion: string; data: unknown },
149
+ ): void;
150
+ }
151
+
152
+ /** Event-emission callback — the coordinator fires
153
+ * `workflow_overlay_changed`, `workflow_metadata_changed`, etc. via
154
+ * this. The runtime owns the event bus; the coordinator does not. */
155
+ export interface WorkflowEmittableEvent {
156
+ readonly type:
157
+ | "workflow_overlay_changed"
158
+ | "workflow_active_work_item_changed"
159
+ | "workflow_metadata_changed";
160
+ readonly documentId: string;
161
+ readonly snapshot?: WorkflowScopeSnapshot | WorkflowMetadataSnapshot | null;
162
+ readonly activeWorkItemId?: string | null;
163
+ }
164
+
165
+ export interface PerfCounters {
166
+ increment(key: string, n?: number): void;
167
+ }
168
+
169
+ /** Runtime-scoped dependencies the coordinator needs. These are
170
+ * read-only accessors (pure functions of the runtime's current state)
171
+ * plus callbacks to dispatch commands, emit events, and record
172
+ * mutations — everything required to preserve current behavior. */
173
+ export interface CoordinatorDeps {
174
+ readonly overlayStore: OverlayStore;
175
+ readonly telemetryBus: TelemetryBus;
176
+ readonly clock: () => string;
177
+ /** Current canonical document envelope. */
178
+ readonly getDocument: () => CanonicalDocumentEnvelope;
179
+ /** Current editor state (selection + revisionToken + readOnly + warnings). */
180
+ readonly getState: () => EditorState;
181
+ /** Active story target (main / header / footer / footnote / endnote). */
182
+ readonly getActiveStory: () => EditorStoryTarget;
183
+ /** View state accessor (documentMode used for guard composition). */
184
+ readonly getDocumentMode: () => DocumentMode;
185
+ /** Current protection snapshot (document protection + preserved ranges). */
186
+ readonly getProtectionSnapshot: () => ProtectionSnapshot;
187
+ /** Current render snapshot — used by markup + rail derivations. */
188
+ readonly getRenderSnapshot: () => RuntimeRenderSnapshot;
189
+ /** Field snapshot (used by markup). Runtime owns the cache; we just read. */
190
+ readonly getFieldSnapshot: () => FieldSnapshot;
191
+ /** Suggestions snapshot — used by scope-card composition. */
192
+ readonly getSuggestionsSnapshot: () => unknown;
193
+ /** Render-frame anchor index — used by scope-card composition. */
194
+ readonly getRenderFrameAnchorIndex: () => unknown;
195
+ /** Page graph — used by rail composition. */
196
+ readonly getPageGraph: () => CollectScopeRailSegmentsInput["pageGraph"];
197
+ /** Runtime's opaque-workflow-blocked-reason probe (preservation +
198
+ * blocked-import gating). Owned by the runtime because it reads
199
+ * preservation store and content-range overlap. */
200
+ readonly deriveOpaqueWorkflowBlockedReason: (
201
+ range: { from: number; to: number },
202
+ ) => WorkflowBlockedCommandReason | null;
203
+ /** Protection-blocked selection probe. Owned by the runtime. */
204
+ readonly isBlockedByProtection: (
205
+ protection: ProtectionSnapshot,
206
+ selection: EditorState["selection"],
207
+ ) => boolean;
208
+ /** Dispatch a runtime command. Used by addScope / removeScope /
209
+ * setWorkflowOverlay / etc. which route through the standard
210
+ * dispatch gate. */
211
+ readonly dispatch: (command: CoordinatorDispatchedCommand) => void;
212
+ /** Emit a workflow_* event to the runtime's event bus. */
213
+ readonly emitEvent: (event: WorkflowEmittableEvent) => void;
214
+ /** Runtime's editor-state channel (collab mutation log). */
215
+ readonly editorStateChannel: EditorStateChannel;
216
+ /** Set of command types that are unsupported in suggesting mode. */
217
+ readonly suggestingUnsupportedCommands: ReadonlySet<string>;
218
+ }
219
+
220
+ /** The coordinator's public API surface. The runtime forwards its
221
+ * public methods to these 1:1; cache invalidation flows through
222
+ * `invalidateCachesForDocumentMutation` + `invalidateGuardAndScopeCaches`. */
223
+ export interface WorkflowCoordinator {
224
+ /* --- scope lifecycle (mutating, dispatch-backed) --- */
225
+ addScope(params: AddScopeParams): AddScopeResult;
226
+ removeScope(scopeId: string): void;
227
+ addInvisibleScope(params: AddScopeParams): AddScopeResult;
228
+ setScopeVisibility(scopeId: string, visibility: ScopeVisibility): void;
229
+ getScopeVisibility(scopeId: string): ScopeVisibility;
230
+ getScope(scopeId: string): WorkflowScope | null;
231
+ getMarkerBackedScopeIds(): ReadonlySet<string>;
232
+ /* --- scope chrome visibility (local view state) --- */
233
+ setScopeChromeVisibility(state: ScopeChromeVisibilityState): void;
234
+ getScopeChromeVisibility(): ScopeChromeVisibilityState;
235
+ /* --- overlay / metadata (mutating, dispatch-backed) --- */
236
+ setWorkflowOverlay(overlay: WorkflowOverlay): void;
237
+ clearWorkflowOverlay(): void;
238
+ getWorkflowOverlay(): WorkflowOverlay | null;
239
+ setWorkflowMetadataDefinitions(
240
+ definitions: readonly WorkflowMetadataDefinition[],
241
+ ): void;
242
+ clearWorkflowMetadataDefinitions(): void;
243
+ setWorkflowMetadataEntries(entries: readonly WorkflowMetadataEntry[]): void;
244
+ clearWorkflowMetadataEntries(): void;
245
+ getWorkflowMetadataSnapshot(): WorkflowMetadataSnapshot;
246
+ /* --- W10 overlay-visibility policy (class-A state) --- */
247
+ getVisibilityPolicy(kind: OverlayKind): OverlayVisibilityPolicy | null;
248
+ getVisibilityPolicies(): readonly OverlayVisibilityPolicy[];
249
+ setVisibilityPolicy(policy: OverlayVisibilityPolicy): boolean;
250
+ clearVisibilityPolicy(kind: OverlayKind): boolean;
251
+ /** Bulk replace — used by reload + collab state sync. */
252
+ replaceVisibilityPolicies(
253
+ policies: readonly OverlayVisibilityPolicy[],
254
+ ): boolean;
255
+ /** W10 — subscribe to any policy-set change. Consumed by L10 X3's
256
+ * `ui.overlays.subscribeVisibility` so authoring-tool mutations
257
+ * re-fire UI subscribers without a document reload. Returns an
258
+ * unsubscribe function. */
259
+ subscribeVisibilityPolicy(listener: () => void): () => void;
260
+ /* --- X5 · class-A markup-mode policy --- */
261
+ /** X5 — class-A markup-mode policy. `null` means the document carried
262
+ * no authored mode (consumer falls back to class-C local preference). */
263
+ getMarkupModePolicy(): WorkflowMarkupModePolicy | null;
264
+ /** X5 — author (or clear with `null`) the class-A markup-mode policy.
265
+ * Returns `true` when the record actually changed. Persists via
266
+ * customXml on next export + fires subscribers. */
267
+ setMarkupModePolicy(policy: WorkflowMarkupModePolicy | null): boolean;
268
+ /** X5 — subscribe to markup-mode policy changes. Consumed by L10's
269
+ * X5 consumer (`ui.viewport.getEffectiveMarkupMode` + its subscribe
270
+ * seam) so UI surfaces re-fire when policy changes mid-session. */
271
+ subscribeMarkupModePolicy(listener: () => void): () => void;
272
+ /* --- collab shared state --- */
273
+ setSharedWorkflowState(state: SharedWorkflowState | null): void;
274
+ /* --- snapshots (cached) --- */
275
+ getInteractionGuardSnapshot(): InteractionGuardSnapshot;
276
+ getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
277
+ getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
278
+ /* --- scope matching / blocked reasons --- */
279
+ evaluateBlockedReasons(
280
+ selection: EditorState["selection"],
281
+ commandType?: string,
282
+ ): WorkflowBlockedCommandReason[];
283
+ getMatchingScope(selection: EditorState["selection"]): WorkflowScope | null;
284
+ getMatchingScopeStack(
285
+ selection: EditorState["selection"],
286
+ ): readonly WorkflowScope[];
287
+ getEffectiveDocumentMode(selection: EditorState["selection"]): DocumentMode;
288
+ /* --- queries --- */
289
+ queryScopes(filter?: ScopeQueryFilter): ScopeQueryResult[];
290
+ /* --- rail / card (runtime.workflow facet backing) --- */
291
+ getRailSegments(pageIndex: number): readonly ScopeRailSegment[];
292
+ getAllRailSegments(): readonly ScopeRailSegment[];
293
+ getAllScopeCardModels(): readonly ScopeCardModel[];
294
+ /* --- dispatch branch handler (called from runtime's dispatch loop) --- */
295
+ applyOverlayCommand(
296
+ command: CoordinatorDispatchedCommand,
297
+ onRefreshRender: () => void,
298
+ ): MergeDetachedWarningsResult | null;
299
+ /* --- warning synthesis (called after document.replace) --- */
300
+ syncDetachedScopeWarnings(
301
+ existing: readonly InternalEditorWarning[],
302
+ ): MergeDetachedWarningsResult;
303
+ /* --- cache invalidation (called from runtime's cache-bust routines) --- */
304
+ invalidateCachesForDocumentMutation(): void;
305
+ invalidateGuardAndScopeCaches(): void;
306
+ }
307
+
308
+ /* ---------------------------------------------------------------- *\
309
+ * Factory
310
+ \* ---------------------------------------------------------------- */
311
+
312
+ const MODE_RESTRICTIVENESS: Record<WorkflowScopeMode, number> = {
313
+ edit: 0,
314
+ suggest: 1,
315
+ comment: 2,
316
+ view: 3,
317
+ };
318
+
319
+ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordinator {
320
+ const { overlayStore, clock } = deps;
321
+
322
+ /* -------- snapshot caches (runtime-scope) -------- */
323
+
324
+ let cachedInteractionGuardSnapshot:
325
+ | {
326
+ revisionToken: string;
327
+ activeStoryKey: string;
328
+ selection: EditorState["selection"];
329
+ readOnly: boolean;
330
+ documentMode: DocumentMode;
331
+ protectionSnapshot: ProtectionSnapshot;
332
+ overlay: WorkflowOverlay | null;
333
+ sharedWorkflowState: SharedWorkflowState | null;
334
+ snapshot: InteractionGuardSnapshot;
335
+ }
336
+ | undefined;
337
+
338
+ let cachedWorkflowScopeSnapshot:
339
+ | {
340
+ overlay: WorkflowOverlay;
341
+ interactionGuardSnapshot: InteractionGuardSnapshot;
342
+ snapshot: WorkflowScopeSnapshot;
343
+ }
344
+ | undefined;
345
+
346
+ let cachedWorkflowMarkupSnapshot:
347
+ | {
348
+ revisionToken: string;
349
+ activeStoryKey: string;
350
+ protectionSnapshot: ProtectionSnapshot;
351
+ preservation: CanonicalDocumentEnvelope["preservation"];
352
+ overlay: WorkflowOverlay | null;
353
+ metadataDefinitions: readonly WorkflowMetadataDefinition[];
354
+ metadataEntries: readonly WorkflowMetadataEntry[];
355
+ snapshot: WorkflowMarkupSnapshot;
356
+ }
357
+ | undefined;
358
+
359
+ /* -------- normalized-overlay accessor (thin shim over store) -------- */
360
+
361
+ function getNormalizedOverlay(): WorkflowOverlay | null {
362
+ return overlayStore.getNormalizedOverlay(deps.getDocument());
363
+ }
364
+
365
+ /* -------- scope-matching (selection-scoped) -------- */
366
+
367
+ function getEffectiveWorkflowScopes(
368
+ overlay: WorkflowOverlay,
369
+ ): readonly WorkflowScope[] {
370
+ const activeStory = deps.getActiveStory();
371
+ const normalized = overlayStore.getNormalizedOverlay(deps.getDocument());
372
+ const source = normalized ?? overlay;
373
+ const activeWorkItemId = source.activeWorkItemId ?? null;
374
+ const activeWorkItemScopeIds =
375
+ activeWorkItemId === null
376
+ ? null
377
+ : new Set(
378
+ source.workItems?.find((item) => item.workItemId === activeWorkItemId)
379
+ ?.scopeIds ?? [],
380
+ );
381
+
382
+ return source.scopes.filter((scope) => {
383
+ const scopeStoryTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
384
+ if (!storyTargetsEqual(scopeStoryTarget, activeStory)) {
385
+ return false;
386
+ }
387
+ if (activeWorkItemId === null) return true;
388
+ return (
389
+ scope.workItemId === activeWorkItemId ||
390
+ activeWorkItemScopeIds?.has(scope.scopeId) === true
391
+ );
392
+ });
393
+ }
394
+
395
+ function expandSelectionRange(range: {
396
+ from: number;
397
+ to: number;
398
+ }): { from: number; to: number } {
399
+ return {
400
+ from: range.from,
401
+ to: range.to > range.from ? range.to : range.from + 1,
402
+ };
403
+ }
404
+
405
+ function buildMatchingScopeStack(
406
+ selection: EditorState["selection"],
407
+ ): WorkflowScope[] {
408
+ const overlay = overlayStore.getOverlay();
409
+ if (!overlay) return [];
410
+ const selectionBounds = {
411
+ from: Math.min(selection.anchor, selection.head),
412
+ to: Math.max(selection.anchor, selection.head),
413
+ };
414
+ const activeScopes = getEffectiveWorkflowScopes(overlay);
415
+ const matching = activeScopes.filter((scope) => {
416
+ // §C8: invisible non-view scopes are transparent to the guard.
417
+ if (scope.visibility === "invisible" && scope.mode !== "view") return false;
418
+ if (scope.anchor.kind === "detached") return false;
419
+ const scopeFrom =
420
+ scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
421
+ const scopeTo =
422
+ scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
423
+ return selectionBounds.from >= scopeFrom && selectionBounds.to <= scopeTo;
424
+ });
425
+ // §C6 — outermost first: startPos ASC, endPos DESC, scopeId ASC tiebreak.
426
+ matching.sort((a, b) => {
427
+ const aFrom =
428
+ a.anchor.kind === "range"
429
+ ? a.anchor.from
430
+ : (a.anchor as { at: number }).at;
431
+ const bFrom =
432
+ b.anchor.kind === "range"
433
+ ? b.anchor.from
434
+ : (b.anchor as { at: number }).at;
435
+ if (aFrom !== bFrom) return aFrom - bFrom;
436
+ const aTo =
437
+ a.anchor.kind === "range" ? a.anchor.to : (a.anchor as { at: number }).at;
438
+ const bTo =
439
+ b.anchor.kind === "range" ? b.anchor.to : (b.anchor as { at: number }).at;
440
+ if (aTo !== bTo) return bTo - aTo;
441
+ return a.scopeId < b.scopeId ? -1 : a.scopeId > b.scopeId ? 1 : 0;
442
+ });
443
+ return matching;
444
+ }
445
+
446
+ function getMatchingWorkflowScope(
447
+ selection: EditorState["selection"],
448
+ ): WorkflowScope | null {
449
+ const stack = buildMatchingScopeStack(selection);
450
+ if (stack.length === 0) return null;
451
+ // §C6 — most-restrictive-wins across overlapping scopes.
452
+ return stack.reduce((best, scope) =>
453
+ (MODE_RESTRICTIVENESS[scope.mode] ?? 0) >
454
+ (MODE_RESTRICTIVENESS[best.mode] ?? 0)
455
+ ? scope
456
+ : best,
457
+ );
458
+ }
459
+
460
+ function getEffectiveDocumentMode(
461
+ selection: EditorState["selection"],
462
+ ): DocumentMode {
463
+ const mode = deps.getDocumentMode();
464
+ if (mode === "viewing" || mode === "commenting") return mode;
465
+ const matchingScope = getMatchingWorkflowScope(selection);
466
+ if (matchingScope?.mode === "suggest") return "suggesting";
467
+ return mode;
468
+ }
469
+
470
+ function evaluateBlockedReasons(
471
+ selection: EditorState["selection"],
472
+ commandType?: string,
473
+ ): WorkflowBlockedCommandReason[] {
474
+ const reasons: WorkflowBlockedCommandReason[] = [];
475
+ const sharedWorkflowState = overlayStore.getSharedWorkflowState();
476
+ if (
477
+ sharedWorkflowState?.lockedMode &&
478
+ sharedWorkflowState.lockedMode !== "editing"
479
+ ) {
480
+ const lockedMode = sharedWorkflowState.lockedMode;
481
+ const code: WorkflowBlockedCommandReason["code"] =
482
+ lockedMode === "commenting"
483
+ ? "workflow_comment_only"
484
+ : lockedMode === "viewing"
485
+ ? "workflow_view_only"
486
+ : "workflow_round_locked";
487
+ reasons.push({
488
+ code,
489
+ message: `Round is locked to ${lockedMode} mode.`,
490
+ });
491
+ return reasons;
492
+ }
493
+
494
+ const selectionBounds = {
495
+ from: Math.min(selection.anchor, selection.head),
496
+ to: Math.max(selection.anchor, selection.head),
497
+ };
498
+ const selectionRange = expandSelectionRange(selectionBounds);
499
+ const opaqueReason = deps.deriveOpaqueWorkflowBlockedReason(selectionRange);
500
+ if (opaqueReason) reasons.push(opaqueReason);
501
+
502
+ const state = deps.getState();
503
+ if (state.readOnly) {
504
+ reasons.push({
505
+ code: "document_read_only",
506
+ message: "Document is in read-only mode.",
507
+ });
508
+ }
509
+
510
+ const documentMode = deps.getDocumentMode();
511
+ if (documentMode === "viewing" || documentMode === "commenting") {
512
+ reasons.push({
513
+ code: "document_viewing_mode",
514
+ message: "Document is in viewing mode.",
515
+ });
516
+ }
517
+
518
+ if (deps.isBlockedByProtection(deps.getProtectionSnapshot(), selection)) {
519
+ reasons.push({
520
+ code: "protected_range",
521
+ message: "Selection falls within a protected range.",
522
+ });
523
+ }
524
+
525
+ const effectiveDocumentMode = getEffectiveDocumentMode(selection);
526
+ if (effectiveDocumentMode === "suggesting" && commandType) {
527
+ if (deps.suggestingUnsupportedCommands.has(commandType)) {
528
+ reasons.push({
529
+ code: "suggesting_unsupported",
530
+ message: `"${commandType}" is not supported in suggesting mode.`,
531
+ });
532
+ }
533
+ }
534
+
535
+ const normalized = getNormalizedOverlay();
536
+ if (normalized) {
537
+ const matchingScope = getMatchingWorkflowScope(selection);
538
+ const activeScopes = getEffectiveWorkflowScopes(normalized);
539
+ const guardingScopes = activeScopes.filter(
540
+ (s) => !(s.visibility === "invisible" && s.mode !== "view"),
541
+ );
542
+
543
+ if (!matchingScope && guardingScopes.length > 0) {
544
+ reasons.push({
545
+ code: "outside_workflow_scope",
546
+ message: "Selection is outside any active workflow scope.",
547
+ });
548
+ } else if (matchingScope) {
549
+ if (matchingScope.mode === "comment") {
550
+ const isCommentCommand = commandType?.startsWith("comment.") ?? false;
551
+ if (!isCommentCommand) {
552
+ reasons.push({
553
+ code: "workflow_comment_only",
554
+ message: `Scope "${matchingScope.label ?? matchingScope.scopeId}" allows comments only.`,
555
+ scopeId: matchingScope.scopeId,
556
+ workItemId: matchingScope.workItemId,
557
+ });
558
+ }
559
+ } else if (matchingScope.mode === "view") {
560
+ reasons.push({
561
+ code: "workflow_view_only",
562
+ message: `Scope "${matchingScope.label ?? matchingScope.scopeId}" is view-only.`,
563
+ scopeId: matchingScope.scopeId,
564
+ workItemId: matchingScope.workItemId,
565
+ });
566
+ }
567
+ }
568
+ }
569
+
570
+ return reasons;
571
+ }
572
+
573
+ /* -------- snapshot derivations (cached) -------- */
574
+
575
+ function deriveWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
576
+ const normalized = getNormalizedOverlay();
577
+ if (!normalized) return null;
578
+ const guardSnapshot = getCachedInteractionGuardSnapshot();
579
+ const activeItem = normalized.activeWorkItemId
580
+ ? normalized.workItems?.find(
581
+ (item) => item.workItemId === normalized.activeWorkItemId,
582
+ )
583
+ : undefined;
584
+ return {
585
+ overlayPresent: true,
586
+ activeWorkItemId: normalized.activeWorkItemId ?? null,
587
+ activeWorkItem: activeItem,
588
+ scopes: normalized.scopes,
589
+ candidates: normalized.candidates ?? [],
590
+ blockedReasons: guardSnapshot.blockedReasons,
591
+ };
592
+ }
593
+
594
+ function getCachedInteractionGuardSnapshot(): InteractionGuardSnapshot {
595
+ const state = deps.getState();
596
+ const activeStory = deps.getActiveStory();
597
+ const activeStoryKey = storyTargetKey(activeStory);
598
+ const protectionSnapshot = deps.getProtectionSnapshot();
599
+ const documentMode = deps.getDocumentMode();
600
+ const overlay = overlayStore.getOverlay();
601
+ const sharedWorkflowState = overlayStore.getSharedWorkflowState();
602
+
603
+ if (
604
+ cachedInteractionGuardSnapshot &&
605
+ cachedInteractionGuardSnapshot.revisionToken === state.revisionToken &&
606
+ cachedInteractionGuardSnapshot.activeStoryKey === activeStoryKey &&
607
+ cachedInteractionGuardSnapshot.selection === state.selection &&
608
+ cachedInteractionGuardSnapshot.readOnly === state.readOnly &&
609
+ cachedInteractionGuardSnapshot.documentMode === documentMode &&
610
+ cachedInteractionGuardSnapshot.protectionSnapshot === protectionSnapshot &&
611
+ cachedInteractionGuardSnapshot.overlay === overlay &&
612
+ cachedInteractionGuardSnapshot.sharedWorkflowState === sharedWorkflowState
613
+ ) {
614
+ return cachedInteractionGuardSnapshot.snapshot;
615
+ }
616
+
617
+ const blockedReasons = evaluateBlockedReasons(state.selection);
618
+ const matchingScope = getMatchingWorkflowScope(state.selection);
619
+ const scopeStack = buildMatchingScopeStack(state.selection);
620
+ const primaryBlockedReason = blockedReasons[0];
621
+ const effectiveMode = primaryBlockedReason
622
+ ? primaryBlockedReason.code === "workflow_comment_only"
623
+ ? "comment"
624
+ : primaryBlockedReason.code === "workflow_view_only"
625
+ ? "view"
626
+ : "blocked"
627
+ : getEffectiveDocumentMode(state.selection) === "suggesting"
628
+ ? "suggest"
629
+ : matchingScope?.mode ?? "edit";
630
+ const matchedScopeStack: InteractionGuardSnapshot["matchedScopeStack"] =
631
+ scopeStack.length > 0
632
+ ? scopeStack.map((s) => ({
633
+ scopeId: s.scopeId,
634
+ mode: s.mode,
635
+ visibility: s.visibility ?? "visible",
636
+ }))
637
+ : undefined;
638
+ const snapshot: InteractionGuardSnapshot = {
639
+ effectiveMode,
640
+ ...(matchingScope?.scopeId ? { matchedScopeId: matchingScope.scopeId } : {}),
641
+ ...(matchingScope?.mode ? { matchedScopeMode: matchingScope.mode } : {}),
642
+ ...(matchedScopeStack ? { matchedScopeStack } : {}),
643
+ targetAccess:
644
+ effectiveMode === "edit"
645
+ ? "direct-edit"
646
+ : effectiveMode === "suggest"
647
+ ? "suggest"
648
+ : effectiveMode === "comment"
649
+ ? "comment-only"
650
+ : effectiveMode === "view"
651
+ ? "view-only"
652
+ : "blocked",
653
+ commandCapabilities: [
654
+ {
655
+ family: "text",
656
+ supported:
657
+ evaluateBlockedReasons(state.selection, "text.insert").length === 0,
658
+ blockedReasons: evaluateBlockedReasons(state.selection, "text.insert"),
659
+ },
660
+ {
661
+ family: "formatting",
662
+ supported:
663
+ evaluateBlockedReasons(state.selection, "toggleBold").length === 0,
664
+ blockedReasons: evaluateBlockedReasons(state.selection, "toggleBold"),
665
+ },
666
+ {
667
+ family: "structure",
668
+ supported:
669
+ evaluateBlockedReasons(state.selection, "insertTable").length === 0,
670
+ blockedReasons: evaluateBlockedReasons(state.selection, "insertTable"),
671
+ },
672
+ ],
673
+ ...(primaryBlockedReason ? { disabledReason: primaryBlockedReason.message } : {}),
674
+ blockedReasons,
675
+ };
676
+ cachedInteractionGuardSnapshot = {
677
+ revisionToken: state.revisionToken,
678
+ activeStoryKey,
679
+ selection: state.selection,
680
+ readOnly: state.readOnly,
681
+ documentMode,
682
+ protectionSnapshot,
683
+ overlay,
684
+ sharedWorkflowState,
685
+ snapshot,
686
+ };
687
+ return snapshot;
688
+ }
689
+
690
+ function getCachedWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
691
+ const overlay = overlayStore.getOverlay();
692
+ if (!overlay) return null;
693
+ const interactionGuardSnapshot = getCachedInteractionGuardSnapshot();
694
+ if (
695
+ cachedWorkflowScopeSnapshot &&
696
+ cachedWorkflowScopeSnapshot.overlay === overlay &&
697
+ cachedWorkflowScopeSnapshot.interactionGuardSnapshot === interactionGuardSnapshot
698
+ ) {
699
+ return cachedWorkflowScopeSnapshot.snapshot;
700
+ }
701
+ const snapshot = deriveWorkflowScopeSnapshot()!;
702
+ cachedWorkflowScopeSnapshot = {
703
+ overlay,
704
+ interactionGuardSnapshot,
705
+ snapshot,
706
+ };
707
+ return snapshot;
708
+ }
709
+
710
+ function getCachedWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot {
711
+ const state = deps.getState();
712
+ const activeStory = deps.getActiveStory();
713
+ const activeStoryKey = storyTargetKey(activeStory);
714
+ const protectionSnapshot = deps.getProtectionSnapshot();
715
+ const preservation = deps.getDocument().preservation;
716
+ const overlay = overlayStore.getOverlay();
717
+ const metadataDefinitions = overlayStore.getMetadataDefinitions();
718
+ const metadataEntries = overlayStore.getMetadataEntries();
719
+
720
+ if (
721
+ cachedWorkflowMarkupSnapshot &&
722
+ cachedWorkflowMarkupSnapshot.revisionToken === state.revisionToken &&
723
+ cachedWorkflowMarkupSnapshot.activeStoryKey === activeStoryKey &&
724
+ cachedWorkflowMarkupSnapshot.protectionSnapshot === protectionSnapshot &&
725
+ cachedWorkflowMarkupSnapshot.preservation === preservation &&
726
+ cachedWorkflowMarkupSnapshot.overlay === overlay &&
727
+ cachedWorkflowMarkupSnapshot.metadataDefinitions === metadataDefinitions &&
728
+ cachedWorkflowMarkupSnapshot.metadataEntries === metadataEntries
729
+ ) {
730
+ return cachedWorkflowMarkupSnapshot.snapshot;
731
+ }
732
+
733
+ const snapshot = collectWorkflowMarkupSnapshot({
734
+ renderSnapshot: deps.getRenderSnapshot(),
735
+ fieldSnapshot: deps.getFieldSnapshot(),
736
+ protectionSnapshot,
737
+ preservation,
738
+ workflowMetadataSnapshot: overlayStore.getMetadataSnapshot(),
739
+ });
740
+ cachedWorkflowMarkupSnapshot = {
741
+ revisionToken: state.revisionToken,
742
+ activeStoryKey,
743
+ protectionSnapshot,
744
+ preservation,
745
+ overlay,
746
+ metadataDefinitions,
747
+ metadataEntries,
748
+ snapshot,
749
+ };
750
+ return snapshot;
751
+ }
752
+
753
+ /* -------- rail composition -------- */
754
+
755
+ function buildRailInput(): {
756
+ scopes: readonly WorkflowScope[];
757
+ candidates?: readonly WorkflowCandidateRange[];
758
+ activeWorkItemScopeIds: readonly string[];
759
+ activeStory: EditorStoryTarget;
760
+ } | null {
761
+ const normalized = getNormalizedOverlay();
762
+ if (!normalized) return null;
763
+ const activeWorkItemId = normalized.activeWorkItemId ?? null;
764
+ const activeWorkItem =
765
+ activeWorkItemId !== null
766
+ ? normalized.workItems?.find(
767
+ (item) => item.workItemId === activeWorkItemId,
768
+ )
769
+ : undefined;
770
+ return {
771
+ scopes: normalized.scopes,
772
+ candidates: normalized.candidates,
773
+ activeWorkItemScopeIds: activeWorkItem?.scopeIds ?? [],
774
+ activeStory: deps.getActiveStory(),
775
+ };
776
+ }
777
+
778
+ /* -------- scope lifecycle (mutating) -------- */
779
+
780
+ function addScope(params: AddScopeParams): AddScopeResult {
781
+ const state = deps.getState();
782
+ const scopeId =
783
+ params.scopeId ??
784
+ `scope-${clock().replace(/[^0-9]/gu, "")}-${Math.floor(Math.random() * 1e6)}`;
785
+ const anchor =
786
+ params.anchor.kind === "range"
787
+ ? { from: params.anchor.from, to: params.anchor.to }
788
+ : null;
789
+
790
+ if (!anchor) {
791
+ return { scopeId, anchor: params.anchor };
792
+ }
793
+
794
+ const { document: nextDocument } = insertScopeMarkers(
795
+ deps.getDocument(),
796
+ { scopeId, from: anchor.from, to: anchor.to },
797
+ );
798
+
799
+ if (nextDocument !== deps.getDocument()) {
800
+ deps.dispatch({
801
+ type: "document.replace",
802
+ document: nextDocument,
803
+ origin: { source: "api", at: clock() },
804
+ });
805
+ }
806
+
807
+ // Coord-06 §13d — preserve the caller's assoc on the public anchor.
808
+ // resolveScope re-derives the range from the inserted markers but emits
809
+ // a hardcoded { start: -1, end: 1 }; without this override the caller's
810
+ // per-scope edge-stickiness choice is silently dropped before the scope
811
+ // ever lands on the overlay.
812
+ const callerAssoc: { readonly start: -1 | 1; readonly end: -1 | 1 } =
813
+ params.anchor.kind === "range"
814
+ ? params.anchor.assoc
815
+ : { start: -1, end: 1 };
816
+ const resolved = resolveScope(nextDocument, scopeId);
817
+ const publicAnchor: EditorAnchorProjection =
818
+ resolved && resolved.kind === "range"
819
+ ? { ...resolved, assoc: callerAssoc }
820
+ : {
821
+ kind: "range",
822
+ from: anchor.from,
823
+ to: anchor.to,
824
+ assoc: callerAssoc,
825
+ };
826
+
827
+ const currentOverlay: WorkflowOverlay =
828
+ overlayStore.getOverlay() ?? {
829
+ overlayVersion: "workflow-overlay/1",
830
+ scopes: [],
831
+ };
832
+ const existingScopes = currentOverlay.scopes.filter(
833
+ (existing) => existing.scopeId !== scopeId,
834
+ );
835
+ const scope: WorkflowScope = {
836
+ scopeId,
837
+ mode: params.mode ?? "comment",
838
+ anchor: publicAnchor,
839
+ ...(params.storyTarget ? { storyTarget: params.storyTarget } : {}),
840
+ ...(params.label ? { label: params.label } : {}),
841
+ };
842
+
843
+ deps.dispatch({
844
+ type: "workflow.set-overlay",
845
+ overlay: { ...currentOverlay, scopes: [...existingScopes, scope] },
846
+ origin: { source: "api", at: clock() },
847
+ });
848
+
849
+ if (params.persistence && params.persistence !== "runtime-only") {
850
+ const requestedMetadata = params.metadata ?? {};
851
+ const entryPersistence =
852
+ requestedMetadata.metadataPersistence ??
853
+ (params.persistence === "session" ? "external" : "internal");
854
+ const entry: WorkflowMetadataEntry = {
855
+ entryId: requestedMetadata.entryId ?? `scope-metadata-${scopeId}`,
856
+ metadataId: requestedMetadata.metadataId ?? "workflow.scope",
857
+ anchor: publicAnchor,
858
+ ...(params.storyTarget ? { storyTarget: params.storyTarget } : {}),
859
+ scopeId,
860
+ ...(requestedMetadata.workItemId
861
+ ? { workItemId: requestedMetadata.workItemId }
862
+ : {}),
863
+ ...(requestedMetadata.value !== undefined
864
+ ? { value: requestedMetadata.value }
865
+ : params.persistence === "document-metadata" && params.label
866
+ ? { value: { label: params.label } }
867
+ : {}),
868
+ metadataPersistence: entryPersistence,
869
+ ...(requestedMetadata.storageRef !== undefined
870
+ ? { storageRef: requestedMetadata.storageRef }
871
+ : {}),
872
+ ...(requestedMetadata.metadataVersion !== undefined
873
+ ? { metadataVersion: requestedMetadata.metadataVersion }
874
+ : {}),
875
+ };
876
+ const priorEntries = overlayStore.getMetadataEntries();
877
+ deps.dispatch({
878
+ type: "workflow.set-metadata-entries",
879
+ entries: [...priorEntries, entry],
880
+ origin: { source: "api", at: clock() },
881
+ });
882
+ }
883
+
884
+ return { scopeId, anchor: publicAnchor };
885
+ }
886
+
887
+ function removeScope(scopeId: string): void {
888
+ // Step 1: drop from overlay first (the workflow-blocked-reasons
889
+ // gate in dispatch would otherwise refuse the subsequent
890
+ // document.replace on comment/view scopes).
891
+ const overlay = overlayStore.getOverlay();
892
+ if (overlay) {
893
+ const nextScopes = overlay.scopes.filter((s) => s.scopeId !== scopeId);
894
+ if (nextScopes.length !== overlay.scopes.length) {
895
+ deps.dispatch({
896
+ type: "workflow.set-overlay",
897
+ overlay: { ...overlay, scopes: nextScopes },
898
+ origin: { source: "api", at: clock() },
899
+ });
900
+ }
901
+ }
902
+ // Step 2: strip markers from doc.
903
+ const nextDocument = removeScopeMarkers(deps.getDocument(), scopeId);
904
+ if (nextDocument !== deps.getDocument()) {
905
+ deps.dispatch({
906
+ type: "document.replace",
907
+ document: nextDocument,
908
+ origin: { source: "api", at: clock() },
909
+ });
910
+ }
911
+ // Step 3: clear customXml-persisted metadata entries.
912
+ const entries = overlayStore.getMetadataEntries();
913
+ const nextEntries = entries.filter((e) => e.scopeId !== scopeId);
914
+ if (nextEntries.length !== entries.length) {
915
+ deps.dispatch({
916
+ type: "workflow.set-metadata-entries",
917
+ entries: nextEntries,
918
+ origin: { source: "api", at: clock() },
919
+ });
920
+ }
921
+ }
922
+
923
+ function addInvisibleScope(params: AddScopeParams): AddScopeResult {
924
+ const result = addScope({ ...params, mode: params.mode ?? "comment" });
925
+ setScopeVisibility(result.scopeId, "invisible");
926
+ return result;
927
+ }
928
+
929
+ function setScopeVisibility(
930
+ scopeId: string,
931
+ visibility: ScopeVisibility,
932
+ ): void {
933
+ const overlay = overlayStore.getOverlay();
934
+ if (!overlay) return;
935
+ const idx = overlay.scopes.findIndex((s) => s.scopeId === scopeId);
936
+ if (idx === -1) return;
937
+ const nextScopes = overlay.scopes.map((s) =>
938
+ s.scopeId === scopeId ? { ...s, visibility } : s,
939
+ );
940
+ deps.dispatch({
941
+ type: "workflow.set-overlay",
942
+ overlay: { ...overlay, scopes: nextScopes },
943
+ origin: { source: "api", at: clock() },
944
+ });
945
+ }
946
+
947
+ function getScopeVisibility(scopeId: string): ScopeVisibility {
948
+ const overlay = overlayStore.getOverlay();
949
+ if (!overlay) return "visible";
950
+ const scope = overlay.scopes.find((s) => s.scopeId === scopeId);
951
+ return scope?.visibility ?? "visible";
952
+ }
953
+
954
+ function getScope(scopeId: string): WorkflowScope | null {
955
+ const normalized = getNormalizedOverlay();
956
+ const fromOverlay = normalized?.scopes.find((s) => s.scopeId === scopeId);
957
+ if (fromOverlay) return fromOverlay;
958
+ const resolved = resolveScope(deps.getDocument(), scopeId);
959
+ if (!resolved) return null;
960
+ return { scopeId, mode: "comment", anchor: resolved };
961
+ }
962
+
963
+ /* -------- overlay / metadata setters (dispatch-backed) -------- */
964
+
965
+ function setWorkflowOverlay(overlay: WorkflowOverlay): void {
966
+ deps.dispatch({
967
+ type: "workflow.set-overlay",
968
+ overlay,
969
+ origin: { source: "api", at: clock() },
970
+ });
971
+ const normalized = getNormalizedOverlay();
972
+ deps.editorStateChannel.recordMutation("workflowOverlay", {
973
+ namespace: "workflowOverlay",
974
+ schemaVersion: "workflow-overlay/1",
975
+ data: normalized ?? overlay,
976
+ });
977
+ }
978
+
979
+ function clearWorkflowOverlay(): void {
980
+ deps.dispatch({
981
+ type: "workflow.clear-overlay",
982
+ origin: { source: "api", at: clock() },
983
+ });
984
+ }
985
+
986
+ function getWorkflowOverlay(): WorkflowOverlay | null {
987
+ return getNormalizedOverlay();
988
+ }
989
+
990
+ function setWorkflowMetadataDefinitions(
991
+ definitions: readonly WorkflowMetadataDefinition[],
992
+ ): void {
993
+ deps.dispatch({
994
+ type: "workflow.set-metadata-definitions",
995
+ definitions,
996
+ origin: { source: "api", at: clock() },
997
+ });
998
+ }
999
+
1000
+ function clearWorkflowMetadataDefinitions(): void {
1001
+ deps.dispatch({
1002
+ type: "workflow.clear-metadata-definitions",
1003
+ origin: { source: "api", at: clock() },
1004
+ });
1005
+ }
1006
+
1007
+ function setWorkflowMetadataEntries(
1008
+ entries: readonly WorkflowMetadataEntry[],
1009
+ ): void {
1010
+ deps.dispatch({
1011
+ type: "workflow.set-metadata-entries",
1012
+ entries,
1013
+ origin: { source: "api", at: clock() },
1014
+ });
1015
+ deps.editorStateChannel.recordMutation("workflowMetadata", {
1016
+ namespace: "workflowMetadata",
1017
+ schemaVersion: "workflow-metadata/1",
1018
+ data: entries,
1019
+ });
1020
+ }
1021
+
1022
+ function clearWorkflowMetadataEntries(): void {
1023
+ deps.dispatch({
1024
+ type: "workflow.clear-metadata-entries",
1025
+ origin: { source: "api", at: clock() },
1026
+ });
1027
+ }
1028
+
1029
+ function getWorkflowMetadataSnapshot(): WorkflowMetadataSnapshot {
1030
+ return overlayStore.getMetadataSnapshot();
1031
+ }
1032
+
1033
+ function setSharedWorkflowState(state: SharedWorkflowState | null): void {
1034
+ const prior = overlayStore.getSharedWorkflowState();
1035
+ if (state === prior) return;
1036
+ overlayStore.replaceSharedWorkflowState(state);
1037
+ cachedInteractionGuardSnapshot = undefined;
1038
+ cachedWorkflowScopeSnapshot = undefined;
1039
+ }
1040
+
1041
+ function setScopeChromeVisibility(state: ScopeChromeVisibilityState): void {
1042
+ overlayStore.replaceScopeChromeVisibility(state);
1043
+ }
1044
+
1045
+ function getScopeChromeVisibility(): ScopeChromeVisibilityState {
1046
+ return overlayStore.getScopeChromeVisibility();
1047
+ }
1048
+
1049
+ /* -------- W10 overlay-visibility policy (class-A) -------- */
1050
+
1051
+ function getVisibilityPolicy(kind: OverlayKind): OverlayVisibilityPolicy | null {
1052
+ return overlayStore.getVisibilityPolicy(kind);
1053
+ }
1054
+
1055
+ function getVisibilityPolicies(): readonly OverlayVisibilityPolicy[] {
1056
+ return overlayStore.getVisibilityPolicies();
1057
+ }
1058
+
1059
+ function setVisibilityPolicy(policy: OverlayVisibilityPolicy): boolean {
1060
+ const changed = overlayStore.replaceVisibilityPolicy(policy.kind, policy);
1061
+ if (changed) emitVisibilityPolicyChanged();
1062
+ return changed;
1063
+ }
1064
+
1065
+ function clearVisibilityPolicy(kind: OverlayKind): boolean {
1066
+ const changed = overlayStore.replaceVisibilityPolicy(kind, null);
1067
+ if (changed) emitVisibilityPolicyChanged();
1068
+ return changed;
1069
+ }
1070
+
1071
+ function replaceVisibilityPoliciesBulk(
1072
+ policies: readonly OverlayVisibilityPolicy[],
1073
+ ): boolean {
1074
+ const changed = overlayStore.replaceVisibilityPolicies(policies);
1075
+ if (changed) emitVisibilityPolicyChanged();
1076
+ return changed;
1077
+ }
1078
+
1079
+ function subscribeVisibilityPolicy(listener: () => void): () => void {
1080
+ return overlayStore.subscribeVisibilityPolicy(listener);
1081
+ }
1082
+
1083
+ /* -------- X5 class-A markup-mode policy -------- */
1084
+
1085
+ function getMarkupModePolicy(): WorkflowMarkupModePolicy | null {
1086
+ return overlayStore.getMarkupModePolicy();
1087
+ }
1088
+
1089
+ function setMarkupModePolicy(
1090
+ policy: WorkflowMarkupModePolicy | null,
1091
+ ): boolean {
1092
+ const changed = overlayStore.replaceMarkupModePolicy(policy);
1093
+ if (changed) {
1094
+ overlayStore.notifyMarkupModePolicyChanged();
1095
+ if (deps.telemetryBus.isEnabled("scope")) {
1096
+ deps.telemetryBus.emit({
1097
+ channel: "scope",
1098
+ type: "scope.markup_mode_policy_changed",
1099
+ t: 0,
1100
+ payload: { hasPolicy: policy !== null },
1101
+ });
1102
+ }
1103
+ }
1104
+ return changed;
1105
+ }
1106
+
1107
+ function subscribeMarkupModePolicy(listener: () => void): () => void {
1108
+ return overlayStore.subscribeMarkupModePolicy(listener);
1109
+ }
1110
+
1111
+ function emitVisibilityPolicyChanged(): void {
1112
+ // Notify direct subscribers first — L10 X3's ui.overlays.subscribeVisibility
1113
+ // chains onto this signal so UI surfaces re-fire when an authoring tool
1114
+ // mutates policy mid-session.
1115
+ overlayStore.notifyVisibilityPolicyChanged();
1116
+ if (!deps.telemetryBus.isEnabled("scope")) return;
1117
+ deps.telemetryBus.emit({
1118
+ channel: "scope",
1119
+ type: "scope.visibility_policy_changed",
1120
+ t: 0,
1121
+ payload: {
1122
+ total: overlayStore.getVisibilityPolicies().length,
1123
+ },
1124
+ });
1125
+ }
1126
+
1127
+ /* -------- queries + rail -------- */
1128
+
1129
+ function queryScopes(filter?: ScopeQueryFilter): ScopeQueryResult[] {
1130
+ const inputs: ScopeQueryInputs = {
1131
+ overlay: overlayStore.getOverlay(),
1132
+ entries: overlayStore.getMetadataEntries(),
1133
+ document: deps.getDocument(),
1134
+ markerBackedScopeIds: overlayStore.getMarkerBackedScopeIds(),
1135
+ };
1136
+ return runQueryScopes(inputs, filter);
1137
+ }
1138
+
1139
+ function getRailSegments(pageIndex: number): readonly ScopeRailSegment[] {
1140
+ const input = buildRailInput();
1141
+ if (!input) return [];
1142
+ return collectScopeRailSegments({
1143
+ ...input,
1144
+ pageGraph: deps.getPageGraph(),
1145
+ }).filter((segment) => segment.pageIndex === pageIndex);
1146
+ }
1147
+
1148
+ function getAllRailSegments(): readonly ScopeRailSegment[] {
1149
+ const input = buildRailInput();
1150
+ if (!input) return [];
1151
+ return collectScopeRailSegments({
1152
+ ...input,
1153
+ pageGraph: deps.getPageGraph(),
1154
+ });
1155
+ }
1156
+
1157
+ function getAllScopeCardModels(): readonly ScopeCardModel[] {
1158
+ const railInput = buildRailInput();
1159
+ if (!railInput) return [];
1160
+ const segments = collectScopeRailSegments({
1161
+ ...railInput,
1162
+ pageGraph: deps.getPageGraph(),
1163
+ });
1164
+ if (segments.length === 0) return [];
1165
+ const metadata = getCachedWorkflowMarkupSnapshot().metadata;
1166
+ // deps returns `unknown` for suggestions / anchor-index — cast at
1167
+ // the composition boundary because attachScopeCardModel's types
1168
+ // accept the runtime-shaped values verbatim.
1169
+ return attachScopeCardModel({
1170
+ segments,
1171
+ scopes: railInput.scopes ?? [],
1172
+ metadata,
1173
+ anchorIndex: deps.getRenderFrameAnchorIndex() as never,
1174
+ suggestions: deps.getSuggestionsSnapshot() as never,
1175
+ reviewActionMetadata: metadata,
1176
+ candidates: railInput.candidates,
1177
+ });
1178
+ }
1179
+
1180
+ /* -------- dispatch branch handler -------- */
1181
+
1182
+ function applyOverlayCommand(
1183
+ command: CoordinatorDispatchedCommand,
1184
+ onRefreshRender: () => void,
1185
+ ): MergeDetachedWarningsResult | null {
1186
+ const documentId = deps.getState().documentId;
1187
+ switch (command.type) {
1188
+ case "workflow.set-overlay": {
1189
+ overlayStore.replaceOverlay(command.overlay, deps.getDocument());
1190
+ const state = deps.getState();
1191
+ const warningDelta = overlayStore.mergeDetachedScopeWarnings(
1192
+ getNormalizedOverlay(),
1193
+ state.warnings,
1194
+ );
1195
+ onRefreshRender();
1196
+ const snapshot = deriveWorkflowScopeSnapshot()!;
1197
+ deps.emitEvent({
1198
+ type: "workflow_overlay_changed",
1199
+ documentId,
1200
+ snapshot,
1201
+ });
1202
+ if (command.overlay.activeWorkItemId !== undefined) {
1203
+ deps.emitEvent({
1204
+ type: "workflow_active_work_item_changed",
1205
+ documentId,
1206
+ activeWorkItemId: command.overlay.activeWorkItemId ?? null,
1207
+ });
1208
+ }
1209
+ return warningDelta;
1210
+ }
1211
+ case "workflow.clear-overlay": {
1212
+ overlayStore.replaceOverlay(null, deps.getDocument());
1213
+ const state = deps.getState();
1214
+ const warningDelta = overlayStore.mergeDetachedScopeWarnings(
1215
+ null,
1216
+ state.warnings,
1217
+ );
1218
+ onRefreshRender();
1219
+ deps.emitEvent({
1220
+ type: "workflow_active_work_item_changed",
1221
+ documentId,
1222
+ activeWorkItemId: null,
1223
+ });
1224
+ deps.emitEvent({
1225
+ type: "workflow_overlay_changed",
1226
+ documentId,
1227
+ snapshot: {
1228
+ overlayPresent: false,
1229
+ activeWorkItemId: null,
1230
+ scopes: [],
1231
+ candidates: [],
1232
+ blockedReasons: [],
1233
+ },
1234
+ });
1235
+ return warningDelta;
1236
+ }
1237
+ case "workflow.set-metadata-definitions": {
1238
+ overlayStore.replaceMetadataDefinitions(command.definitions);
1239
+ deps.emitEvent({
1240
+ type: "workflow_metadata_changed",
1241
+ documentId,
1242
+ snapshot: overlayStore.getMetadataSnapshot(),
1243
+ });
1244
+ return null;
1245
+ }
1246
+ case "workflow.clear-metadata-definitions": {
1247
+ overlayStore.replaceMetadataDefinitions([]);
1248
+ deps.emitEvent({
1249
+ type: "workflow_metadata_changed",
1250
+ documentId,
1251
+ snapshot: overlayStore.getMetadataSnapshot(),
1252
+ });
1253
+ return null;
1254
+ }
1255
+ case "workflow.set-metadata-entries": {
1256
+ overlayStore.replaceMetadataEntries(command.entries);
1257
+ deps.emitEvent({
1258
+ type: "workflow_metadata_changed",
1259
+ documentId,
1260
+ snapshot: overlayStore.getMetadataSnapshot(),
1261
+ });
1262
+ return null;
1263
+ }
1264
+ case "workflow.clear-metadata-entries": {
1265
+ overlayStore.replaceMetadataEntries([]);
1266
+ deps.emitEvent({
1267
+ type: "workflow_metadata_changed",
1268
+ documentId,
1269
+ snapshot: overlayStore.getMetadataSnapshot(),
1270
+ });
1271
+ return null;
1272
+ }
1273
+ default:
1274
+ return null;
1275
+ }
1276
+ }
1277
+
1278
+ /* -------- warning synthesis (called after document mutations) -------- */
1279
+
1280
+ function syncDetachedScopeWarnings(
1281
+ existing: readonly InternalEditorWarning[],
1282
+ ): MergeDetachedWarningsResult {
1283
+ return overlayStore.mergeDetachedScopeWarnings(
1284
+ getNormalizedOverlay(),
1285
+ existing,
1286
+ );
1287
+ }
1288
+
1289
+ /* -------- cache invalidation -------- */
1290
+
1291
+ function invalidateCachesForDocumentMutation(): void {
1292
+ cachedInteractionGuardSnapshot = undefined;
1293
+ cachedWorkflowScopeSnapshot = undefined;
1294
+ cachedWorkflowMarkupSnapshot = undefined;
1295
+ }
1296
+
1297
+ function invalidateGuardAndScopeCaches(): void {
1298
+ cachedInteractionGuardSnapshot = undefined;
1299
+ cachedWorkflowScopeSnapshot = undefined;
1300
+ }
1301
+
1302
+ /* -------- public API surface -------- */
1303
+
1304
+ return {
1305
+ addScope,
1306
+ removeScope,
1307
+ addInvisibleScope,
1308
+ setScopeVisibility,
1309
+ getScopeVisibility,
1310
+ getScope,
1311
+ getMarkerBackedScopeIds: () => overlayStore.getMarkerBackedScopeIds(),
1312
+ setScopeChromeVisibility,
1313
+ getScopeChromeVisibility,
1314
+ getVisibilityPolicy,
1315
+ getVisibilityPolicies,
1316
+ setVisibilityPolicy,
1317
+ clearVisibilityPolicy,
1318
+ replaceVisibilityPolicies: replaceVisibilityPoliciesBulk,
1319
+ subscribeVisibilityPolicy,
1320
+ getMarkupModePolicy,
1321
+ setMarkupModePolicy,
1322
+ subscribeMarkupModePolicy,
1323
+ setWorkflowOverlay,
1324
+ clearWorkflowOverlay,
1325
+ getWorkflowOverlay,
1326
+ setWorkflowMetadataDefinitions,
1327
+ clearWorkflowMetadataDefinitions,
1328
+ setWorkflowMetadataEntries,
1329
+ clearWorkflowMetadataEntries,
1330
+ getWorkflowMetadataSnapshot,
1331
+ setSharedWorkflowState,
1332
+ getInteractionGuardSnapshot: getCachedInteractionGuardSnapshot,
1333
+ getWorkflowScopeSnapshot: getCachedWorkflowScopeSnapshot,
1334
+ getWorkflowMarkupSnapshot: getCachedWorkflowMarkupSnapshot,
1335
+ evaluateBlockedReasons,
1336
+ getMatchingScope: getMatchingWorkflowScope,
1337
+ getMatchingScopeStack: buildMatchingScopeStack,
1338
+ getEffectiveDocumentMode,
1339
+ queryScopes,
1340
+ getRailSegments,
1341
+ getAllRailSegments,
1342
+ getAllScopeCardModels,
1343
+ applyOverlayCommand,
1344
+ syncDetachedScopeWarnings,
1345
+ invalidateCachesForDocumentMutation,
1346
+ invalidateGuardAndScopeCaches,
1347
+ };
1348
+ }