@beyondwork/docx-react-component 1.0.106 → 1.0.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/package.json +19 -5
  2. package/src/api/geometry-overlay-rects.ts +5 -0
  3. package/src/api/package-version.ts +1 -1
  4. package/src/api/page-anchor-id.ts +5 -0
  5. package/src/api/public-types.ts +16 -9
  6. package/src/api/table-node-specs.ts +6 -0
  7. package/src/api/v3/_create.ts +2 -1
  8. package/src/api/v3/_page-anchor-id.ts +52 -0
  9. package/src/api/v3/_runtime-handle.ts +92 -1
  10. package/src/api/v3/ai/_audit-time.ts +5 -0
  11. package/src/api/v3/ai/_pe2-evidence.ts +38 -0
  12. package/src/api/v3/ai/attach.ts +7 -2
  13. package/src/api/v3/ai/replacement.ts +101 -18
  14. package/src/api/v3/ai/resolve.ts +2 -2
  15. package/src/api/v3/ai/review.ts +177 -3
  16. package/src/api/v3/index.ts +1 -0
  17. package/src/api/v3/runtime/collab.ts +462 -0
  18. package/src/api/v3/runtime/document.ts +503 -20
  19. package/src/api/v3/runtime/geometry.ts +97 -0
  20. package/src/api/v3/runtime/layout.ts +744 -0
  21. package/src/api/v3/runtime/perf-probe.ts +14 -0
  22. package/src/api/v3/runtime/viewport.ts +9 -8
  23. package/src/api/v3/ui/_types.ts +149 -55
  24. package/src/api/v3/ui/chrome-preset-model.ts +5 -5
  25. package/src/api/v3/ui/debug.ts +115 -2
  26. package/src/api/v3/ui/index.ts +13 -0
  27. package/src/api/v3/ui/overlays.ts +0 -8
  28. package/src/api/v3/ui/surface.ts +56 -0
  29. package/src/api/v3/ui/viewport.ts +22 -9
  30. package/src/core/commands/image-commands.ts +1 -0
  31. package/src/core/commands/index.ts +6 -0
  32. package/src/core/schema/text-schema.ts +43 -5
  33. package/src/core/selection/mapping.ts +8 -1
  34. package/src/core/selection/review-anchors.ts +5 -1
  35. package/src/core/state/text-transaction.ts +8 -2
  36. package/src/io/export/serialize-revisions.ts +149 -1
  37. package/src/io/normalize/normalize-text.ts +6 -0
  38. package/src/io/ooxml/parse-bookmark-references.ts +55 -0
  39. package/src/io/ooxml/parse-fields.ts +24 -2
  40. package/src/io/ooxml/parse-headers-footers.ts +38 -5
  41. package/src/io/ooxml/parse-main-document.ts +153 -9
  42. package/src/io/ooxml/parse-numbering.ts +20 -0
  43. package/src/io/ooxml/parse-revisions.ts +19 -8
  44. package/src/io/opc/package-reader.ts +98 -8
  45. package/src/model/anchor.ts +4 -3
  46. package/src/model/canonical-document.ts +220 -2
  47. package/src/model/canonical-hash.ts +221 -0
  48. package/src/model/canonical-layout-inputs.ts +245 -6
  49. package/src/model/layout/index.ts +1 -0
  50. package/src/model/layout/page-graph-types.ts +118 -1
  51. package/src/model/review/revision-types.ts +14 -3
  52. package/src/preservation/store.ts +20 -4
  53. package/src/review/README.md +1 -1
  54. package/src/review/store/revision-actions.ts +14 -2
  55. package/src/runtime/collab/event-types.ts +67 -1
  56. package/src/runtime/collab/runtime-collab-sync.ts +177 -5
  57. package/src/runtime/diagnostics/layout-guard-warning.ts +18 -0
  58. package/src/runtime/document-heading-outline.ts +147 -0
  59. package/src/runtime/document-navigation.ts +8 -243
  60. package/src/runtime/document-runtime.ts +240 -97
  61. package/src/runtime/edit-dispatch/dispatch-text-command.ts +11 -0
  62. package/src/runtime/formatting/layout-inputs.ts +38 -5
  63. package/src/runtime/formatting/numbering/geometry.ts +28 -2
  64. package/src/runtime/geometry/adjacent-geometry-intake.ts +835 -0
  65. package/src/runtime/geometry/caret-geometry.ts +5 -6
  66. package/src/runtime/geometry/geometry-facet.ts +60 -10
  67. package/src/runtime/geometry/geometry-index.ts +591 -20
  68. package/src/runtime/geometry/geometry-types.ts +59 -0
  69. package/src/runtime/geometry/hit-test.ts +11 -1
  70. package/src/runtime/geometry/overlay-rects.ts +5 -3
  71. package/src/runtime/geometry/project-anchors.ts +1 -1
  72. package/src/runtime/geometry/word-layout-v2-line-intake.ts +323 -0
  73. package/src/runtime/layout/index.ts +6 -0
  74. package/src/runtime/layout/layout-engine-instance.ts +6 -1
  75. package/src/runtime/layout/layout-engine-version.ts +181 -16
  76. package/src/runtime/layout/layout-facet-types.ts +6 -0
  77. package/src/runtime/layout/page-graph.ts +21 -4
  78. package/src/runtime/layout/paginated-layout-engine.ts +139 -15
  79. package/src/runtime/layout/project-block-fragments.ts +265 -7
  80. package/src/runtime/layout/public-facet.ts +78 -24
  81. package/src/runtime/layout/table-row-continuation-contract.ts +107 -0
  82. package/src/runtime/layout/table-row-split.ts +92 -35
  83. package/src/runtime/prerender/cache-envelope.ts +2 -2
  84. package/src/runtime/prerender/cache-key.ts +5 -4
  85. package/src/runtime/prerender/customxml-cache.ts +0 -1
  86. package/src/runtime/render/render-kernel.ts +1 -1
  87. package/src/runtime/revision-runtime.ts +112 -10
  88. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  89. package/src/runtime/scopes/action-validation.ts +22 -2
  90. package/src/runtime/scopes/capabilities.ts +316 -0
  91. package/src/runtime/scopes/compile-scope-bundle.ts +14 -0
  92. package/src/runtime/scopes/compiler-service.ts +108 -4
  93. package/src/runtime/scopes/content-control-evidence.ts +79 -0
  94. package/src/runtime/scopes/create-issue.ts +5 -5
  95. package/src/runtime/scopes/evidence.ts +91 -0
  96. package/src/runtime/scopes/formatting/apply.ts +2 -0
  97. package/src/runtime/scopes/geometry-evidence.ts +130 -0
  98. package/src/runtime/scopes/index.ts +54 -0
  99. package/src/runtime/scopes/issue-lifecycle.ts +224 -0
  100. package/src/runtime/scopes/layout-evidence.ts +374 -0
  101. package/src/runtime/scopes/multi-paragraph-refusal.ts +37 -0
  102. package/src/runtime/scopes/preservation-boundary.ts +7 -1
  103. package/src/runtime/scopes/replacement/apply.ts +97 -34
  104. package/src/runtime/scopes/scope-kinds/paragraph.ts +108 -12
  105. package/src/runtime/scopes/semantic-scope-types.ts +242 -3
  106. package/src/runtime/scopes/visualization.ts +28 -0
  107. package/src/runtime/surface-projection.ts +44 -5
  108. package/src/runtime/telemetry/perf-probe.ts +216 -0
  109. package/src/runtime/virtualized-rendering.ts +36 -1
  110. package/src/runtime/workflow/ai-issue-lifecycle.ts +253 -0
  111. package/src/runtime/workflow/coordinator.ts +39 -11
  112. package/src/runtime/workflow/derived-scope-resolver.ts +63 -9
  113. package/src/runtime/workflow/index.ts +3 -0
  114. package/src/runtime/workflow/overlay-lane-types.ts +58 -0
  115. package/src/runtime/workflow/overlay-lanes.ts +168 -10
  116. package/src/runtime/workflow/overlay-store.ts +2 -2
  117. package/src/runtime/workflow/redline-posture-calibration.ts +257 -0
  118. package/src/runtime/workflow/word-field-matrix-calibration.ts +231 -0
  119. package/src/session/_sync-legacy.ts +17 -27
  120. package/src/session/import/loader.ts +6 -4
  121. package/src/session/import/source-package-evidence.ts +186 -2
  122. package/src/session/index.ts +5 -6
  123. package/src/session/session.ts +30 -56
  124. package/src/session/types.ts +8 -13
  125. package/src/shell/session-bootstrap.ts +155 -81
  126. package/src/ui/WordReviewEditor.tsx +520 -12
  127. package/src/ui/editor-shell-view.tsx +14 -4
  128. package/src/ui/editor-surface-controller.tsx +5 -3
  129. package/src/ui/headless/selection-tool-resolver.ts +1 -2
  130. package/src/ui/presence-overlay-lane.ts +0 -1
  131. package/src/ui/ui-controller-factory.ts +7 -0
  132. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +5 -1
  133. package/src/ui-tailwind/chrome/editor-action-registry.ts +105 -5
  134. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +7 -0
  135. package/src/ui-tailwind/chrome/layer-debug-contracts.ts +208 -0
  136. package/src/ui-tailwind/chrome/resolve-target-kind.ts +13 -0
  137. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +11 -3
  138. package/src/ui-tailwind/chrome/tw-command-palette.tsx +36 -6
  139. package/src/ui-tailwind/chrome/tw-context-menu.tsx +6 -1
  140. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +42 -109
  141. package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +26 -6
  142. package/src/ui-tailwind/chrome/tw-navigation-command-bar.tsx +328 -0
  143. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +8 -4
  144. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +129 -1
  145. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +19 -5
  146. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
  147. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +28 -12
  148. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +30 -3
  149. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +116 -10
  150. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +223 -94
  151. package/src/ui-tailwind/chrome-overlay/tw-presence-overlay-lane.tsx +157 -0
  152. package/src/ui-tailwind/chrome-overlay/tw-review-overlay-lane-markers.tsx +259 -0
  153. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +5 -2
  154. package/src/ui-tailwind/chrome-overlay/tw-substrate-overlay-lanes.tsx +314 -0
  155. package/src/ui-tailwind/debug/README.md +4 -1
  156. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +272 -0
  157. package/src/ui-tailwind/debug/layer11-word-field-matrix-evidence.ts +160 -0
  158. package/src/ui-tailwind/editor-surface/perf-probe.ts +14 -215
  159. package/src/ui-tailwind/editor-surface/pm-decorations.ts +42 -0
  160. package/src/ui-tailwind/editor-surface/pm-position-map.ts +38 -2
  161. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -4
  162. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +34 -5
  163. package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +9 -19
  164. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -2
  165. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +145 -0
  166. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +16 -11
  167. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +8 -10
  168. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +3 -0
  169. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +4 -2
  170. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +60 -20
  171. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +16 -11
  172. package/src/ui-tailwind/review/tw-health-panel.tsx +36 -17
  173. package/src/ui-tailwind/review/tw-review-rail.tsx +7 -4
  174. package/src/ui-tailwind/review-workspace/diagnostics-visibility.ts +44 -0
  175. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +11 -0
  176. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +16 -1
  177. package/src/ui-tailwind/review-workspace/types.ts +26 -12
  178. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +40 -11
  179. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +2 -1
  180. package/src/ui-tailwind/review-workspace/use-page-markers.ts +15 -26
  181. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +35 -18
  182. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +41 -32
  183. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +2 -1
  184. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +2 -1
  185. package/src/ui-tailwind/status/tw-status-bar.tsx +6 -5
  186. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +52 -80
  187. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +12 -48
  188. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +9 -4
  189. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +328 -361
  190. package/src/ui-tailwind/tw-review-workspace.tsx +152 -286
@@ -0,0 +1,216 @@
1
+ export type PerfProbeKind =
2
+ | "typing"
3
+ | "typing.predicted"
4
+ | "typing.reconcile"
5
+ | "typing.divergence"
6
+ | "selection"
7
+ | "runtime.create"
8
+ | "loadSession.laycacheProbe"
9
+ | "loadSession.compatReportCached"
10
+ | "snapshot.surface"
11
+ | "snapshot.compatibility"
12
+ | "snapshot.navigation"
13
+ | "pm.rebuild"
14
+ | "pm.decorations"
15
+ | "pm.decorations.apply"
16
+ | "pm.decorations.comments"
17
+ | "pm.decorations.revisions"
18
+ | "pm.decorations.workflow"
19
+ | "pm.mount"
20
+ | "shell.render"
21
+ | "workspace.chrome"
22
+ | "selection.sync"
23
+ | "layout.incremental"
24
+ | "layout.full"
25
+ | "render.frame_build"
26
+ | "render.frame_diff"
27
+ | "render.decoration_resolve"
28
+ | "chrome.overlay_reposition"
29
+ | "chrome.hit_test"
30
+ | "rail.segment_project";
31
+
32
+ /**
33
+ * Counter names the FastTextEditLane emits via `incrementInvalidationCounter`.
34
+ * Expose them as a const so integrators can read the shape without duplicating
35
+ * strings.
36
+ */
37
+ export const PREDICTED_LANE_COUNTERS = {
38
+ applied: "predictions.applied",
39
+ equivalent: "predictions.equivalent",
40
+ adjusted: "predictions.adjusted",
41
+ rejected: "predictions.rejected",
42
+ rollback: "predictions.rollback",
43
+ structuralDivergence: "predictions.structuralDivergence",
44
+ bailBeforePredict: "predictions.bailBeforePredict",
45
+ refreshSelectionOnly: "predictions.refresh.selectionOnly",
46
+ refreshLocalTextEquivalent: "predictions.refresh.localTextEquivalent",
47
+ refreshSurfaceOnly: "predictions.refresh.surfaceOnly",
48
+ refreshFullProjection: "predictions.refresh.fullProjection",
49
+ refreshBlocked: "predictions.refresh.blocked",
50
+ } as const;
51
+
52
+ export interface PerfProbeSample {
53
+ token: string;
54
+ kind: PerfProbeKind;
55
+ durationMs: number;
56
+ recordedAt: number;
57
+ }
58
+
59
+ interface PendingProbe {
60
+ kind: PerfProbeKind;
61
+ startedAt: number;
62
+ }
63
+
64
+ interface PerfProbeState {
65
+ enabled?: boolean;
66
+ nextToken?: number;
67
+ pending?: Record<string, PendingProbe>;
68
+ samples?: PerfProbeSample[];
69
+ maxSamples?: number;
70
+ invalidationCounts?: Record<string, number>;
71
+ }
72
+
73
+ export interface PerfProbeSummary {
74
+ samples: PerfProbeSample[];
75
+ latest: Partial<Record<PerfProbeKind, PerfProbeSample | null>>;
76
+ invalidationCounts: Record<string, number>;
77
+ }
78
+
79
+ declare global {
80
+ interface Window {
81
+ __DOCX_REACT_PERF_PROBE__?: PerfProbeState;
82
+ }
83
+ }
84
+
85
+ export function startPerfProbe(kind: PerfProbeKind): string | null {
86
+ const state = getEnabledState();
87
+ if (!state) {
88
+ return null;
89
+ }
90
+
91
+ const token = `${kind}-${state.nextToken ?? 0}`;
92
+ state.nextToken = (state.nextToken ?? 0) + 1;
93
+ state.pending ??= {};
94
+ state.pending[token] = {
95
+ kind,
96
+ startedAt: performance.now(),
97
+ };
98
+ return token;
99
+ }
100
+
101
+ export function finishPerfProbe(token: string | null | undefined): PerfProbeSample | null {
102
+ if (!token) {
103
+ return null;
104
+ }
105
+ const state = getEnabledState();
106
+ if (!state?.pending?.[token]) {
107
+ return null;
108
+ }
109
+
110
+ const pending = state.pending[token];
111
+ delete state.pending[token];
112
+
113
+ const sample: PerfProbeSample = {
114
+ token,
115
+ kind: pending.kind,
116
+ durationMs: performance.now() - pending.startedAt,
117
+ recordedAt: Date.now(),
118
+ };
119
+
120
+ pushSample(state, sample);
121
+
122
+ return sample;
123
+ }
124
+
125
+ export function recordPerfSample(
126
+ kind: PerfProbeKind,
127
+ durationMs = 0,
128
+ ): PerfProbeSample | null {
129
+ const state = getEnabledState();
130
+ if (!state) {
131
+ return null;
132
+ }
133
+
134
+ const token = `${kind}-${state.nextToken ?? 0}`;
135
+ state.nextToken = (state.nextToken ?? 0) + 1;
136
+ const sample: PerfProbeSample = {
137
+ token,
138
+ kind,
139
+ durationMs,
140
+ recordedAt: Date.now(),
141
+ };
142
+ pushSample(state, sample);
143
+ return sample;
144
+ }
145
+
146
+ export function incrementInvalidationCounter(
147
+ counter: string,
148
+ amount = 1,
149
+ ): number {
150
+ const state = getEnabledState();
151
+ if (!state) {
152
+ return 0;
153
+ }
154
+
155
+ state.invalidationCounts ??= {};
156
+ state.invalidationCounts[counter] =
157
+ (state.invalidationCounts[counter] ?? 0) + amount;
158
+ return state.invalidationCounts[counter]!;
159
+ }
160
+
161
+ export function getLatestPerfSummary(): PerfProbeSummary | null {
162
+ const state = getEnabledState();
163
+ const samples = state?.samples ?? [];
164
+ if (!state || samples.length === 0) {
165
+ return null;
166
+ }
167
+
168
+ return {
169
+ samples: [...samples],
170
+ latest: buildLatestSampleMap(samples),
171
+ invalidationCounts: { ...(state.invalidationCounts ?? {}) },
172
+ };
173
+ }
174
+
175
+ export function resetPerfProbeState(): void {
176
+ const state = getEnabledState();
177
+ if (!state) {
178
+ return;
179
+ }
180
+ state.nextToken = 0;
181
+ state.pending = {};
182
+ state.samples = [];
183
+ state.invalidationCounts = {};
184
+ }
185
+
186
+ function getEnabledState(): PerfProbeState | null {
187
+ if (typeof window === "undefined") {
188
+ return null;
189
+ }
190
+ const state = window.__DOCX_REACT_PERF_PROBE__;
191
+ if (!state?.enabled) {
192
+ return null;
193
+ }
194
+ return state;
195
+ }
196
+
197
+ function pushSample(state: PerfProbeState, sample: PerfProbeSample): void {
198
+ state.samples ??= [];
199
+ state.samples.push(sample);
200
+ const maxSamples = state.maxSamples ?? 20;
201
+ if (state.samples.length > maxSamples) {
202
+ state.samples.splice(0, state.samples.length - maxSamples);
203
+ }
204
+ }
205
+
206
+ function buildLatestSampleMap(
207
+ samples: PerfProbeSample[],
208
+ ): Partial<Record<PerfProbeKind, PerfProbeSample | null>> {
209
+ const latest: Partial<Record<PerfProbeKind, PerfProbeSample | null>> = {};
210
+ for (const sample of [...samples].reverse()) {
211
+ if (latest[sample.kind] === undefined) {
212
+ latest[sample.kind] = sample;
213
+ }
214
+ }
215
+ return latest;
216
+ }
@@ -35,6 +35,21 @@ export interface VirtualizedViewport {
35
35
  overscanHeight?: number;
36
36
  }
37
37
 
38
+ export interface VirtualizedBlockGeometryProvider {
39
+ getBlock(blockId: string): { rects: readonly { topPx: number }[] } | null;
40
+ }
41
+
42
+ export interface ComputeVirtualizedWindowOptions {
43
+ /**
44
+ * Optional L05 geometry source. When present, `offsetTop` is derived from
45
+ * `GeometryFacet.getBlock(firstVisibleBlockId).rects[0].topPx` instead of
46
+ * the synthetic virtualization estimate. This keeps DOM-free consumers on
47
+ * geometry truth while preserving the legacy estimate for unwired tests and
48
+ * pre-paint sessions.
49
+ */
50
+ geometryFacet?: VirtualizedBlockGeometryProvider;
51
+ }
52
+
38
53
  export interface VirtualizedWindow {
39
54
  startIndex: number;
40
55
  endIndex: number;
@@ -121,6 +136,7 @@ export function createVirtualizedRenderingSession(
121
136
  export function computeVirtualizedWindow(
122
137
  session: VirtualizedRenderingSession,
123
138
  viewport: VirtualizedViewport,
139
+ options: ComputeVirtualizedWindowOptions = {},
124
140
  ): VirtualizedWindow {
125
141
  if (session.metrics.length === 0) {
126
142
  return {
@@ -145,19 +161,38 @@ export function computeVirtualizedWindow(
145
161
  const safeEndIndex = Math.max(startIndex + 1, endIndexExclusive);
146
162
  const visibleMetrics = session.metrics.slice(startIndex, safeEndIndex);
147
163
  const visibleBlocks = session.blocks.slice(startIndex, safeEndIndex);
164
+ const firstMetric = visibleMetrics[0];
165
+ const firstBlock = visibleBlocks[0];
148
166
  const lastMetric = visibleMetrics[visibleMetrics.length - 1]!;
149
167
 
150
168
  return {
151
169
  startIndex,
152
170
  endIndex: safeEndIndex,
153
171
  totalHeight: session.totalHeight,
154
- offsetTop: visibleMetrics[0]?.top ?? 0,
172
+ offsetTop: resolveWindowOffsetTop(
173
+ firstBlock?.blockId ?? firstMetric?.blockId,
174
+ firstMetric,
175
+ options,
176
+ ),
155
177
  offsetBottom: Math.max(0, session.totalHeight - lastMetric.bottom),
156
178
  visibleBlocks,
157
179
  visibleMetrics,
158
180
  };
159
181
  }
160
182
 
183
+ function resolveWindowOffsetTop(
184
+ blockId: string | undefined,
185
+ fallbackMetric: VirtualizedRenderingMetrics | undefined,
186
+ options: ComputeVirtualizedWindowOptions,
187
+ ): number {
188
+ const geometryTop = blockId
189
+ ? options.geometryFacet?.getBlock(blockId)?.rects[0]?.topPx
190
+ : undefined;
191
+ return typeof geometryTop === "number" && Number.isFinite(geometryTop)
192
+ ? geometryTop
193
+ : (fallbackMetric?.top ?? 0);
194
+ }
195
+
161
196
  function findFirstVisibleIndex(
162
197
  metrics: VirtualizedRenderingMetrics[],
163
198
  top: number,
@@ -0,0 +1,253 @@
1
+ import type {
2
+ WorkflowEventOrigin,
3
+ WorkflowMetadataEntry,
4
+ WorkflowMetadataSnapshot,
5
+ } from "../../api/public-types.ts";
6
+
7
+ export const AI_ISSUE_METADATA_ID = "ai.issue" as const;
8
+
9
+ export type AIIssueStatus = "open" | "resolved";
10
+ export type AIIssueLifecycleAction = "resolve" | "reopen";
11
+
12
+ export interface AIIssueLifecycleAuditEntry {
13
+ readonly action: AIIssueLifecycleAction;
14
+ readonly actorId: string;
15
+ readonly at: string;
16
+ readonly origin: string;
17
+ readonly fromStatus: AIIssueStatus;
18
+ readonly toStatus: AIIssueStatus;
19
+ }
20
+
21
+ export interface AIIssueLifecycleValue {
22
+ readonly issueId: string;
23
+ readonly summary: string;
24
+ readonly severity?: string;
25
+ readonly status: AIIssueStatus;
26
+ readonly createdAtUtc?: string;
27
+ readonly statusUpdatedAtUtc?: string;
28
+ readonly resolvedAtUtc?: string;
29
+ readonly resolvedBy?: string;
30
+ readonly reopenedAtUtc?: string;
31
+ readonly reopenedBy?: string;
32
+ readonly lifecycle?: readonly AIIssueLifecycleAuditEntry[];
33
+ }
34
+
35
+ export interface AIIssueLifecycleReadback {
36
+ readonly issueId: string;
37
+ readonly entryId: string;
38
+ readonly scopeId?: string;
39
+ readonly summary: string;
40
+ readonly severity?: string;
41
+ readonly status: AIIssueStatus;
42
+ readonly canResolve: boolean;
43
+ readonly canReopen: boolean;
44
+ readonly createdAtUtc?: string;
45
+ readonly statusUpdatedAtUtc?: string;
46
+ readonly lastTransition?: AIIssueLifecycleAuditEntry;
47
+ }
48
+
49
+ export interface AIIssueLifecycleRuntime {
50
+ getWorkflowMetadataSnapshot(): WorkflowMetadataSnapshot;
51
+ setWorkflowMetadataEntries(
52
+ entries: readonly WorkflowMetadataEntry[],
53
+ origin?: WorkflowEventOrigin,
54
+ ): void;
55
+ }
56
+
57
+ export interface TransitionAIIssueLifecycleInput {
58
+ readonly issueId: string;
59
+ readonly action: AIIssueLifecycleAction;
60
+ readonly actorId: string;
61
+ readonly at: string;
62
+ readonly origin?: WorkflowEventOrigin;
63
+ }
64
+
65
+ export type TransitionAIIssueLifecycleResult =
66
+ | {
67
+ readonly transitioned: true;
68
+ readonly issueId: string;
69
+ readonly entryId: string;
70
+ readonly fromStatus: AIIssueStatus;
71
+ readonly toStatus: AIIssueStatus;
72
+ readonly audit: AIIssueLifecycleAuditEntry;
73
+ }
74
+ | {
75
+ readonly transitioned: false;
76
+ readonly issueId: string;
77
+ readonly reason:
78
+ | "issue-not-found"
79
+ | "issue-value-malformed"
80
+ | "already-resolved"
81
+ | "already-open";
82
+ };
83
+
84
+ export function projectAIIssueLifecycleReadback(
85
+ entries: readonly WorkflowMetadataEntry[],
86
+ ): AIIssueLifecycleReadback[] {
87
+ return entries
88
+ .filter((entry) => entry.metadataId === AI_ISSUE_METADATA_ID)
89
+ .map(projectEntry)
90
+ .filter((entry): entry is AIIssueLifecycleReadback => entry !== null)
91
+ .sort((a, b) => a.issueId.localeCompare(b.issueId));
92
+ }
93
+
94
+ export function transitionAIIssueLifecycle(
95
+ runtime: AIIssueLifecycleRuntime,
96
+ input: TransitionAIIssueLifecycleInput,
97
+ ): TransitionAIIssueLifecycleResult {
98
+ const snapshot = runtime.getWorkflowMetadataSnapshot();
99
+ const target = snapshot.entries.find((entry) => {
100
+ if (entry.metadataId !== AI_ISSUE_METADATA_ID) return false;
101
+ const value = coerceIssueValue(entry.value);
102
+ return value?.issueId === input.issueId || entry.entryId === input.issueId;
103
+ });
104
+
105
+ if (!target) {
106
+ return {
107
+ transitioned: false,
108
+ issueId: input.issueId,
109
+ reason: "issue-not-found",
110
+ };
111
+ }
112
+
113
+ const currentValue = coerceIssueValue(target.value);
114
+ if (!currentValue) {
115
+ return {
116
+ transitioned: false,
117
+ issueId: input.issueId,
118
+ reason: "issue-value-malformed",
119
+ };
120
+ }
121
+
122
+ const fromStatus = currentValue.status;
123
+ const toStatus = input.action === "resolve" ? "resolved" : "open";
124
+ if (fromStatus === toStatus) {
125
+ return {
126
+ transitioned: false,
127
+ issueId: currentValue.issueId,
128
+ reason: toStatus === "resolved" ? "already-resolved" : "already-open",
129
+ };
130
+ }
131
+
132
+ const audit: AIIssueLifecycleAuditEntry = {
133
+ action: input.action,
134
+ actorId: input.actorId,
135
+ at: input.at,
136
+ origin: input.origin?.source ?? "agent",
137
+ fromStatus,
138
+ toStatus,
139
+ };
140
+ const nextValue = nextIssueValue(currentValue, audit);
141
+ const nextEntry: WorkflowMetadataEntry = {
142
+ ...target,
143
+ value: nextValue as unknown as Record<string, unknown>,
144
+ };
145
+ const nextEntries = snapshot.entries.map((entry) =>
146
+ entry.entryId === target.entryId ? nextEntry : entry,
147
+ );
148
+ runtime.setWorkflowMetadataEntries(nextEntries, input.origin ?? {
149
+ source: audit.origin,
150
+ at: input.at,
151
+ });
152
+
153
+ return {
154
+ transitioned: true,
155
+ issueId: currentValue.issueId,
156
+ entryId: target.entryId,
157
+ fromStatus,
158
+ toStatus,
159
+ audit,
160
+ };
161
+ }
162
+
163
+ function projectEntry(entry: WorkflowMetadataEntry): AIIssueLifecycleReadback | null {
164
+ const value = coerceIssueValue(entry.value);
165
+ if (!value) return null;
166
+ const lastTransition = value.lifecycle?.[value.lifecycle.length - 1];
167
+ return {
168
+ issueId: value.issueId,
169
+ entryId: entry.entryId,
170
+ ...(entry.scopeId ? { scopeId: entry.scopeId } : {}),
171
+ summary: value.summary,
172
+ ...(value.severity ? { severity: value.severity } : {}),
173
+ status: value.status,
174
+ canResolve: value.status === "open",
175
+ canReopen: value.status === "resolved",
176
+ ...(value.createdAtUtc ? { createdAtUtc: value.createdAtUtc } : {}),
177
+ ...(value.statusUpdatedAtUtc ? { statusUpdatedAtUtc: value.statusUpdatedAtUtc } : {}),
178
+ ...(lastTransition ? { lastTransition } : {}),
179
+ };
180
+ }
181
+
182
+ function nextIssueValue(
183
+ value: AIIssueLifecycleValue,
184
+ audit: AIIssueLifecycleAuditEntry,
185
+ ): AIIssueLifecycleValue {
186
+ const lifecycle = [...(value.lifecycle ?? []), audit];
187
+ const base =
188
+ audit.action === "resolve"
189
+ ? omitReopenFields(value)
190
+ : omitResolveFields(value);
191
+ return {
192
+ ...base,
193
+ status: audit.toStatus,
194
+ statusUpdatedAtUtc: audit.at,
195
+ ...(audit.action === "resolve"
196
+ ? { resolvedAtUtc: audit.at, resolvedBy: audit.actorId }
197
+ : { reopenedAtUtc: audit.at, reopenedBy: audit.actorId }),
198
+ lifecycle,
199
+ };
200
+ }
201
+
202
+ function omitReopenFields(value: AIIssueLifecycleValue): AIIssueLifecycleValue {
203
+ const { reopenedAtUtc: _reopenedAtUtc, reopenedBy: _reopenedBy, ...base } = value;
204
+ return base;
205
+ }
206
+
207
+ function omitResolveFields(value: AIIssueLifecycleValue): AIIssueLifecycleValue {
208
+ const { resolvedAtUtc: _resolvedAtUtc, resolvedBy: _resolvedBy, ...base } = value;
209
+ return base;
210
+ }
211
+
212
+ function coerceIssueValue(value: unknown): AIIssueLifecycleValue | null {
213
+ if (!isRecord(value)) return null;
214
+ if (typeof value.issueId !== "string" || value.issueId.length === 0) return null;
215
+ if (typeof value.summary !== "string" || value.summary.length === 0) return null;
216
+ const status = value.status === "resolved" ? "resolved" : value.status === "open" ? "open" : null;
217
+ if (!status) return null;
218
+ const lifecycle = Array.isArray(value.lifecycle)
219
+ ? value.lifecycle.filter(isLifecycleAuditEntry)
220
+ : undefined;
221
+ return {
222
+ ...value,
223
+ issueId: value.issueId,
224
+ summary: value.summary,
225
+ ...(typeof value.severity === "string" ? { severity: value.severity } : {}),
226
+ status,
227
+ ...(typeof value.createdAtUtc === "string" ? { createdAtUtc: value.createdAtUtc } : {}),
228
+ ...(typeof value.statusUpdatedAtUtc === "string"
229
+ ? { statusUpdatedAtUtc: value.statusUpdatedAtUtc }
230
+ : {}),
231
+ ...(typeof value.resolvedAtUtc === "string" ? { resolvedAtUtc: value.resolvedAtUtc } : {}),
232
+ ...(typeof value.resolvedBy === "string" ? { resolvedBy: value.resolvedBy } : {}),
233
+ ...(typeof value.reopenedAtUtc === "string" ? { reopenedAtUtc: value.reopenedAtUtc } : {}),
234
+ ...(typeof value.reopenedBy === "string" ? { reopenedBy: value.reopenedBy } : {}),
235
+ ...(lifecycle ? { lifecycle } : {}),
236
+ };
237
+ }
238
+
239
+ function isLifecycleAuditEntry(value: unknown): value is AIIssueLifecycleAuditEntry {
240
+ return (
241
+ isRecord(value) &&
242
+ (value.action === "resolve" || value.action === "reopen") &&
243
+ typeof value.actorId === "string" &&
244
+ typeof value.at === "string" &&
245
+ typeof value.origin === "string" &&
246
+ (value.fromStatus === "open" || value.fromStatus === "resolved") &&
247
+ (value.toStatus === "open" || value.toStatus === "resolved")
248
+ );
249
+ }
250
+
251
+ function isRecord(value: unknown): value is Record<string, unknown> {
252
+ return typeof value === "object" && value !== null && !Array.isArray(value);
253
+ }
@@ -17,9 +17,9 @@
17
17
  *
18
18
  * - W3 (single interaction-guard verdict) — `getInteractionGuardSnapshot`
19
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).
20
+ * snapshot caches against structural workflow inputs (activeStory,
21
+ * selection, readOnly, documentMode, protectionSnapshot, overlay
22
+ * reference, sharedWorkflowState).
23
23
  *
24
24
  * - W4 (AI action policy orthogonal to guard) — this coordinator does
25
25
  * not re-implement AI policy (see `ai-action-policy.ts`); guard
@@ -95,6 +95,11 @@ import {
95
95
  import { resolveScope } from "./scope-resolver.ts";
96
96
  import { insertScopeMarkers, removeScopeMarkers } from "../../core/commands/add-scope.ts";
97
97
  import type { OverlayStore, MergeDetachedWarningsResult } from "./overlay-store.ts";
98
+ import {
99
+ projectWorkflowReviewOverlayLane,
100
+ type WorkflowReviewOverlayLaneKind,
101
+ type WorkflowReviewOverlayLaneSnapshot,
102
+ } from "./overlay-lanes.ts";
98
103
  import {
99
104
  type OverlayKind,
100
105
  type OverlayVisibilityPolicy,
@@ -252,7 +257,10 @@ export interface WorkflowCoordinator {
252
257
  definitions: readonly WorkflowMetadataDefinition[],
253
258
  ): void;
254
259
  clearWorkflowMetadataDefinitions(): void;
255
- setWorkflowMetadataEntries(entries: readonly WorkflowMetadataEntry[]): void;
260
+ setWorkflowMetadataEntries(
261
+ entries: readonly WorkflowMetadataEntry[],
262
+ origin?: CoordinatorCommandOrigin,
263
+ ): void;
256
264
  clearWorkflowMetadataEntries(): void;
257
265
  getWorkflowMetadataSnapshot(): WorkflowMetadataSnapshot;
258
266
  /* --- W10 overlay-visibility policy (class-A state) --- */
@@ -290,6 +298,7 @@ export interface WorkflowCoordinator {
290
298
  getInteractionGuardSnapshot(): InteractionGuardSnapshot;
291
299
  getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
292
300
  getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
301
+ getReviewOverlayLane(kind: WorkflowReviewOverlayLaneKind): WorkflowReviewOverlayLaneSnapshot;
293
302
  /* --- scope matching / blocked reasons --- */
294
303
  evaluateBlockedReasons(
295
304
  selection: EditorState["selection"],
@@ -350,7 +359,6 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
350
359
 
351
360
  let cachedInteractionGuardSnapshot:
352
361
  | {
353
- revisionToken: string;
354
362
  activeStoryKey: string;
355
363
  selection: EditorState["selection"];
356
364
  readOnly: boolean;
@@ -372,7 +380,6 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
372
380
 
373
381
  let cachedWorkflowMarkupSnapshot:
374
382
  | {
375
- revisionToken: string;
376
383
  activeStoryKey: string;
377
384
  protectionSnapshot: ProtectionSnapshot;
378
385
  preservation: CanonicalDocumentEnvelope["preservation"];
@@ -631,7 +638,6 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
631
638
 
632
639
  if (
633
640
  cachedInteractionGuardSnapshot &&
634
- cachedInteractionGuardSnapshot.revisionToken === state.revisionToken &&
635
641
  cachedInteractionGuardSnapshot.activeStoryKey === activeStoryKey &&
636
642
  cachedInteractionGuardSnapshot.selection === state.selection &&
637
643
  cachedInteractionGuardSnapshot.readOnly === state.readOnly &&
@@ -706,7 +712,6 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
706
712
  blockedReasons,
707
713
  };
708
714
  cachedInteractionGuardSnapshot = {
709
- revisionToken: state.revisionToken,
710
715
  activeStoryKey,
711
716
  selection: state.selection,
712
717
  readOnly: state.readOnly,
@@ -751,7 +756,6 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
751
756
 
752
757
  if (
753
758
  cachedWorkflowMarkupSnapshot &&
754
- cachedWorkflowMarkupSnapshot.revisionToken === state.revisionToken &&
755
759
  cachedWorkflowMarkupSnapshot.activeStoryKey === activeStoryKey &&
756
760
  cachedWorkflowMarkupSnapshot.protectionSnapshot === protectionSnapshot &&
757
761
  cachedWorkflowMarkupSnapshot.preservation === preservation &&
@@ -770,7 +774,6 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
770
774
  workflowMetadataSnapshot: overlayStore.getMetadataSnapshot(),
771
775
  });
772
776
  cachedWorkflowMarkupSnapshot = {
773
- revisionToken: state.revisionToken,
774
777
  activeStoryKey,
775
778
  protectionSnapshot,
776
779
  preservation,
@@ -1118,11 +1121,12 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
1118
1121
 
1119
1122
  function setWorkflowMetadataEntries(
1120
1123
  entries: readonly WorkflowMetadataEntry[],
1124
+ origin: CoordinatorCommandOrigin = { source: "api", at: clock() },
1121
1125
  ): void {
1122
1126
  deps.dispatch({
1123
1127
  type: "workflow.set-metadata-entries",
1124
1128
  entries,
1125
- origin: { source: "api", at: clock() },
1129
+ origin,
1126
1130
  });
1127
1131
  deps.editorStateChannel.recordMutation("workflowMetadata", {
1128
1132
  namespace: "workflowMetadata",
@@ -1146,6 +1150,11 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
1146
1150
  state: SharedWorkflowState | null,
1147
1151
  origin: CoordinatorCommandOrigin = { source: "api", at: clock() },
1148
1152
  ): void {
1153
+ if (isAwarenessWorkflowOrigin(origin)) {
1154
+ throw new Error(
1155
+ "runtime.workflow.setSharedWorkflowState: awareness is transient presence state and cannot enforce durable workflow policy",
1156
+ );
1157
+ }
1149
1158
  const prior = overlayStore.getSharedWorkflowState();
1150
1159
  if (state === prior) return;
1151
1160
  overlayStore.replaceSharedWorkflowState(state);
@@ -1265,6 +1274,10 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
1265
1274
  });
1266
1275
  }
1267
1276
 
1277
+ function isAwarenessWorkflowOrigin(origin: CoordinatorCommandOrigin): boolean {
1278
+ return origin.source === "awareness";
1279
+ }
1280
+
1268
1281
  /* -------- queries + rail -------- */
1269
1282
 
1270
1283
  function queryScopes(filter?: ScopeQueryFilter): ScopeQueryResult[] {
@@ -1318,6 +1331,20 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
1318
1331
  });
1319
1332
  }
1320
1333
 
1334
+ function getReviewOverlayLane(
1335
+ kind: WorkflowReviewOverlayLaneKind,
1336
+ ): WorkflowReviewOverlayLaneSnapshot {
1337
+ const renderSnapshot = deps.getRenderSnapshot();
1338
+ return projectWorkflowReviewOverlayLane(kind, {
1339
+ comments: renderSnapshot.comments,
1340
+ trackedChanges: renderSnapshot.trackedChanges,
1341
+ suggestions: deps.getSuggestionsSnapshot() as never,
1342
+ workflowScope: getCachedWorkflowScopeSnapshot() ?? undefined,
1343
+ workflowMarkup: getCachedWorkflowMarkupSnapshot(),
1344
+ revision: deps.getState().revision,
1345
+ });
1346
+ }
1347
+
1321
1348
  /* -------- dispatch branch handler -------- */
1322
1349
 
1323
1350
  function applyOverlayCommand(
@@ -1483,6 +1510,7 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
1483
1510
  getInteractionGuardSnapshot: getCachedInteractionGuardSnapshot,
1484
1511
  getWorkflowScopeSnapshot: getCachedWorkflowScopeSnapshot,
1485
1512
  getWorkflowMarkupSnapshot: getCachedWorkflowMarkupSnapshot,
1513
+ getReviewOverlayLane,
1486
1514
  evaluateBlockedReasons,
1487
1515
  getMatchingScope: getMatchingWorkflowScope,
1488
1516
  getMatchingScopeStack: buildMatchingScopeStack,