@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,374 @@
1
+ /**
2
+ * Scope layout evidence projection.
3
+ *
4
+ * Layer 08 only consumes layout evidence supplied by lower layers. When the
5
+ * seam is absent or cold, the bundle reports that explicitly instead of
6
+ * deriving page slices or continuation state from canonical content.
7
+ */
8
+
9
+ import type {
10
+ ScopeLayoutContinuationEvidence,
11
+ ScopeLayoutEvidence,
12
+ ScopeTableFrameEvidence,
13
+ ScopeTableFramePageEvidence,
14
+ } from "./semantic-scope-types.ts";
15
+
16
+ export interface ScopeLayoutEvidenceEntry {
17
+ readonly status?: ScopeLayoutEvidence["status"];
18
+ readonly completeness?: ScopeLayoutEvidence["completeness"];
19
+ readonly reason?: string;
20
+ readonly pageSliceIds?: readonly string[];
21
+ readonly layoutObjectIds?: readonly string[];
22
+ readonly continuationState?: ScopeLayoutContinuationEvidence;
23
+ readonly divergenceIds?: readonly string[];
24
+ readonly tableFrame?: ScopeTableFrameEvidence;
25
+ }
26
+
27
+ export interface ScopeLayoutEvidenceProvider {
28
+ getScopeLayoutEvidence(scopeId: string): ScopeLayoutEvidenceEntry | null;
29
+ }
30
+
31
+ export interface ScopeRuntimeLayoutFragment {
32
+ readonly fragmentId: string;
33
+ readonly blockId: string;
34
+ readonly pageId: string;
35
+ readonly pageIndex: number;
36
+ readonly kind?: "whole" | "paragraph-slice" | "table-slice";
37
+ readonly tableRowRange?: {
38
+ readonly from: number;
39
+ readonly to: number;
40
+ readonly totalRows: number;
41
+ };
42
+ readonly continuation?: {
43
+ readonly kind: string;
44
+ readonly continuesFromPreviousPage?: boolean;
45
+ readonly continuesToNextPage?: boolean;
46
+ readonly repeatedHeaderRowIndexes?: readonly number[];
47
+ readonly splitRowCarry?: readonly {
48
+ readonly rowIndex: number;
49
+ readonly continuesFromPreviousPage: boolean;
50
+ readonly continuesToNextPage: boolean;
51
+ }[];
52
+ readonly verticalMergeCarry?: readonly {
53
+ readonly columnIndex: number;
54
+ readonly restartRowIndex: number;
55
+ }[];
56
+ };
57
+ readonly layoutObject?: {
58
+ readonly objectId?: string;
59
+ };
60
+ }
61
+
62
+ export interface ScopeRuntimeLayoutEvidenceSource {
63
+ getPageCount(): number;
64
+ getFragmentsForPage(pageIndex: number): readonly ScopeRuntimeLayoutFragment[];
65
+ }
66
+
67
+ export interface RuntimeTableFrameLayoutEvidenceOptions {
68
+ readonly layout: ScopeRuntimeLayoutEvidenceSource;
69
+ readonly tableBlockIdsByBlockIndex?: ReadonlyMap<number, string>;
70
+ }
71
+
72
+ export interface ScopeRuntimeRenderSnapshotLike {
73
+ readonly surface?: {
74
+ readonly blocks?: readonly {
75
+ readonly kind?: string;
76
+ readonly blockId?: string;
77
+ }[];
78
+ };
79
+ }
80
+
81
+ function freezeList<T>(values: readonly T[] | undefined): readonly T[] | undefined {
82
+ return values ? Object.freeze([...values]) : undefined;
83
+ }
84
+
85
+ function unique<T>(values: readonly T[]): readonly T[] {
86
+ return Object.freeze([...new Set(values)]);
87
+ }
88
+
89
+ function parseTableFamilyScopeId(scopeId: string):
90
+ | { readonly scopeKind: "table"; readonly blockIndex: number }
91
+ | { readonly scopeKind: "table-row"; readonly blockIndex: number; readonly rowIndex: number }
92
+ | {
93
+ readonly scopeKind: "table-cell";
94
+ readonly blockIndex: number;
95
+ readonly rowIndex: number;
96
+ readonly cellIndex: number;
97
+ }
98
+ | null {
99
+ const table = /^table:(\d+)$/.exec(scopeId);
100
+ if (table) return { scopeKind: "table", blockIndex: Number(table[1]) };
101
+ const row = /^row:(\d+):(\d+)$/.exec(scopeId);
102
+ if (row) {
103
+ return {
104
+ scopeKind: "table-row",
105
+ blockIndex: Number(row[1]),
106
+ rowIndex: Number(row[2]),
107
+ };
108
+ }
109
+ const cell = /^cell:(\d+):(\d+):(\d+)$/.exec(scopeId);
110
+ if (cell) {
111
+ return {
112
+ scopeKind: "table-cell",
113
+ blockIndex: Number(cell[1]),
114
+ rowIndex: Number(cell[2]),
115
+ cellIndex: Number(cell[3]),
116
+ };
117
+ }
118
+ return null;
119
+ }
120
+
121
+ function candidateTableBlockIds(
122
+ blockIndex: number,
123
+ mapped?: ReadonlyMap<number, string>,
124
+ ): readonly string[] {
125
+ const ids: string[] = [];
126
+ const mappedId = mapped?.get(blockIndex);
127
+ if (mappedId) ids.push(mappedId);
128
+ ids.push(`table-${blockIndex}`, `table:${blockIndex}`, `block-${blockIndex}`);
129
+ return unique(ids);
130
+ }
131
+
132
+ function rowInFragment(
133
+ fragment: ScopeRuntimeLayoutFragment,
134
+ rowIndex: number,
135
+ ): boolean {
136
+ const range = fragment.tableRowRange;
137
+ if (range && rowIndex >= range.from && rowIndex < range.to) return true;
138
+ return fragment.continuation?.repeatedHeaderRowIndexes?.includes(rowIndex) === true;
139
+ }
140
+
141
+ function fragmentMatchesScope(
142
+ fragment: ScopeRuntimeLayoutFragment,
143
+ parsed: NonNullable<ReturnType<typeof parseTableFamilyScopeId>>,
144
+ ): boolean {
145
+ if (parsed.scopeKind === "table") return true;
146
+ return rowInFragment(fragment, parsed.rowIndex);
147
+ }
148
+
149
+ function collectTableFragments(
150
+ layout: ScopeRuntimeLayoutEvidenceSource,
151
+ blockIds: readonly string[],
152
+ parsed: NonNullable<ReturnType<typeof parseTableFamilyScopeId>>,
153
+ ): readonly ScopeRuntimeLayoutFragment[] {
154
+ const matches: ScopeRuntimeLayoutFragment[] = [];
155
+ const blockIdSet = new Set(blockIds);
156
+ for (let pageIndex = 0; pageIndex < layout.getPageCount(); pageIndex += 1) {
157
+ for (const fragment of layout.getFragmentsForPage(pageIndex)) {
158
+ if (!blockIdSet.has(fragment.blockId)) continue;
159
+ if (fragment.kind !== undefined && fragment.kind !== "table-slice" && fragment.kind !== "whole") {
160
+ continue;
161
+ }
162
+ if (!fragmentMatchesScope(fragment, parsed)) continue;
163
+ matches.push(fragment);
164
+ }
165
+ }
166
+ return Object.freeze(matches);
167
+ }
168
+
169
+ function projectTableFramePage(
170
+ fragment: ScopeRuntimeLayoutFragment,
171
+ ): ScopeTableFramePageEvidence {
172
+ const repeated = fragment.continuation?.repeatedHeaderRowIndexes;
173
+ const splitRowCarry = fragment.continuation?.splitRowCarry;
174
+ const carry = fragment.continuation?.verticalMergeCarry;
175
+ return {
176
+ pageId: fragment.pageId,
177
+ pageIndex: fragment.pageIndex,
178
+ fragmentId: fragment.fragmentId,
179
+ ...(fragment.tableRowRange ? { rowRange: { ...fragment.tableRowRange } } : {}),
180
+ ...(fragment.continuation?.continuesFromPreviousPage !== undefined
181
+ ? { continuesFromPreviousPage: fragment.continuation.continuesFromPreviousPage }
182
+ : {}),
183
+ ...(fragment.continuation?.continuesToNextPage !== undefined
184
+ ? { continuesToNextPage: fragment.continuation.continuesToNextPage }
185
+ : {}),
186
+ ...(repeated ? { repeatedHeaderRowIndexes: unique(repeated) } : {}),
187
+ ...(splitRowCarry ? { splitRowCarry: Object.freeze(splitRowCarry.map((item) => ({ ...item }))) } : {}),
188
+ ...(carry ? { verticalMergeCarry: Object.freeze(carry.map((item) => ({ ...item }))) } : {}),
189
+ };
190
+ }
191
+
192
+ function projectTableFrame(
193
+ blockId: string,
194
+ parsed: NonNullable<ReturnType<typeof parseTableFamilyScopeId>>,
195
+ fragments: readonly ScopeRuntimeLayoutFragment[],
196
+ ): ScopeTableFrameEvidence {
197
+ const pageIds = unique(fragments.map((fragment) => fragment.pageId));
198
+ const pageSliceIds = unique(fragments.map((fragment) => fragment.fragmentId));
199
+ const layoutObjectIds = unique(
200
+ fragments
201
+ .map((fragment) => fragment.layoutObject?.objectId)
202
+ .filter((objectId): objectId is string => typeof objectId === "string" && objectId.length > 0),
203
+ );
204
+ const rowRangesByPage = Object.freeze(fragments.map(projectTableFramePage));
205
+ const repeatedHeaderRowIndexes = unique(
206
+ fragments.flatMap((fragment) => fragment.continuation?.repeatedHeaderRowIndexes ?? []),
207
+ );
208
+ const verticalMergeCarry = Object.freeze(
209
+ fragments
210
+ .flatMap((fragment) => fragment.continuation?.verticalMergeCarry ?? [])
211
+ .map((item) => ({ ...item })),
212
+ );
213
+ const splitRowCarry = Object.freeze(
214
+ fragments
215
+ .flatMap((fragment) => fragment.continuation?.splitRowCarry ?? [])
216
+ .map((item) => ({ ...item })),
217
+ );
218
+ return {
219
+ source: "runtime.layout.table-frame-continuation",
220
+ blockId,
221
+ scopeKind: parsed.scopeKind,
222
+ ...(parsed.scopeKind === "table-row" || parsed.scopeKind === "table-cell"
223
+ ? { rowIndex: parsed.rowIndex }
224
+ : {}),
225
+ ...(parsed.scopeKind === "table-cell" ? { cellIndex: parsed.cellIndex } : {}),
226
+ ...(pageIds.length > 0 ? { pageIds } : {}),
227
+ ...(pageSliceIds.length > 0 ? { pageSliceIds } : {}),
228
+ ...(layoutObjectIds.length > 0 ? { layoutObjectIds } : {}),
229
+ ...(rowRangesByPage.length > 0 ? { rowRangesByPage } : {}),
230
+ ...(repeatedHeaderRowIndexes.length > 0 ? { repeatedHeaderRowIndexes } : {}),
231
+ ...(splitRowCarry.length > 0 ? { splitRowCarry } : {}),
232
+ ...(verticalMergeCarry.length > 0 ? { verticalMergeCarry } : {}),
233
+ };
234
+ }
235
+
236
+ function continuationFromTableFrame(
237
+ fragments: readonly ScopeRuntimeLayoutFragment[],
238
+ ): ScopeLayoutContinuationEvidence {
239
+ const pageIds = unique(fragments.map((fragment) => fragment.pageId));
240
+ return {
241
+ ...(pageIds.length > 0 ? { pageIds } : {}),
242
+ pageCount: pageIds.length,
243
+ crossesPageBoundary: pageIds.length > 1,
244
+ continuedFromPreviousPage: fragments.some(
245
+ (fragment) => fragment.continuation?.continuesFromPreviousPage === true,
246
+ ),
247
+ continuesToNextPage: fragments.some(
248
+ (fragment) => fragment.continuation?.continuesToNextPage === true,
249
+ ),
250
+ };
251
+ }
252
+
253
+ export function createRuntimeTableFrameLayoutEvidenceProvider(
254
+ options: RuntimeTableFrameLayoutEvidenceOptions,
255
+ ): ScopeLayoutEvidenceProvider {
256
+ return {
257
+ getScopeLayoutEvidence(scopeId) {
258
+ const parsed = parseTableFamilyScopeId(scopeId);
259
+ if (!parsed) return null;
260
+
261
+ const blockIds = candidateTableBlockIds(
262
+ parsed.blockIndex,
263
+ options.tableBlockIdsByBlockIndex,
264
+ );
265
+ const fragments = collectTableFragments(options.layout, blockIds, parsed);
266
+ if (fragments.length === 0) return null;
267
+
268
+ const blockId = fragments[0]?.blockId ?? blockIds[0] ?? `table-${parsed.blockIndex}`;
269
+ const tableFrame = projectTableFrame(blockId, parsed, fragments);
270
+ const pageSliceIds = tableFrame.pageSliceIds;
271
+ const layoutObjectIds = tableFrame.layoutObjectIds;
272
+ const cellScoped = parsed.scopeKind === "table-cell";
273
+ return {
274
+ status: "available",
275
+ completeness: cellScoped ? "partial" : "complete",
276
+ reason: cellScoped
277
+ ? "l04-table-frame-row-level-evidence"
278
+ : "l04-table-frame-continuation",
279
+ ...(pageSliceIds ? { pageSliceIds } : {}),
280
+ ...(layoutObjectIds ? { layoutObjectIds } : {}),
281
+ continuationState: continuationFromTableFrame(fragments),
282
+ tableFrame,
283
+ };
284
+ },
285
+ };
286
+ }
287
+
288
+ export function collectTopLevelTableBlockIdsFromRenderSnapshot(
289
+ snapshot: ScopeRuntimeRenderSnapshotLike | null | undefined,
290
+ ): ReadonlyMap<number, string> {
291
+ const out = new Map<number, string>();
292
+ const blocks = snapshot?.surface?.blocks ?? [];
293
+ blocks.forEach((block, blockIndex) => {
294
+ if (block.kind === "table" && typeof block.blockId === "string") {
295
+ out.set(blockIndex, block.blockId);
296
+ }
297
+ });
298
+ return out;
299
+ }
300
+
301
+ function cloneTableFrame(
302
+ tableFrame: ScopeTableFrameEvidence,
303
+ ): ScopeTableFrameEvidence {
304
+ return {
305
+ ...tableFrame,
306
+ ...(tableFrame.pageIds ? { pageIds: freezeList(tableFrame.pageIds) } : {}),
307
+ ...(tableFrame.pageSliceIds ? { pageSliceIds: freezeList(tableFrame.pageSliceIds) } : {}),
308
+ ...(tableFrame.layoutObjectIds ? { layoutObjectIds: freezeList(tableFrame.layoutObjectIds) } : {}),
309
+ ...(tableFrame.rowRangesByPage
310
+ ? {
311
+ rowRangesByPage: Object.freeze(
312
+ tableFrame.rowRangesByPage.map((row) => ({
313
+ ...row,
314
+ ...(row.rowRange ? { rowRange: { ...row.rowRange } } : {}),
315
+ ...(row.repeatedHeaderRowIndexes
316
+ ? { repeatedHeaderRowIndexes: freezeList(row.repeatedHeaderRowIndexes) }
317
+ : {}),
318
+ ...(row.splitRowCarry
319
+ ? { splitRowCarry: Object.freeze(row.splitRowCarry.map((item) => ({ ...item }))) }
320
+ : {}),
321
+ ...(row.verticalMergeCarry
322
+ ? { verticalMergeCarry: Object.freeze(row.verticalMergeCarry.map((item) => ({ ...item }))) }
323
+ : {}),
324
+ })),
325
+ ),
326
+ }
327
+ : {}),
328
+ ...(tableFrame.repeatedHeaderRowIndexes
329
+ ? { repeatedHeaderRowIndexes: freezeList(tableFrame.repeatedHeaderRowIndexes) }
330
+ : {}),
331
+ ...(tableFrame.splitRowCarry
332
+ ? { splitRowCarry: Object.freeze(tableFrame.splitRowCarry.map((item) => ({ ...item }))) }
333
+ : {}),
334
+ ...(tableFrame.verticalMergeCarry
335
+ ? { verticalMergeCarry: Object.freeze(tableFrame.verticalMergeCarry.map((item) => ({ ...item }))) }
336
+ : {}),
337
+ };
338
+ }
339
+
340
+ export function deriveScopeLayoutEvidence(
341
+ scopeId: string,
342
+ provider?: ScopeLayoutEvidenceProvider,
343
+ ): ScopeLayoutEvidence {
344
+ if (!provider) {
345
+ return {
346
+ status: "unavailable",
347
+ completeness: "unavailable",
348
+ reason: "layout-evidence-provider-unavailable",
349
+ };
350
+ }
351
+
352
+ const entry = provider.getScopeLayoutEvidence(scopeId);
353
+ if (!entry) {
354
+ return {
355
+ status: "requires-rehydration",
356
+ completeness: "requires-rehydration",
357
+ reason: "scope-layout-evidence-unavailable",
358
+ };
359
+ }
360
+
361
+ const status = entry.status ?? "available";
362
+ return {
363
+ status,
364
+ completeness: entry.completeness ?? (status === "available" ? "complete" : status),
365
+ ...(entry.reason ? { reason: entry.reason } : {}),
366
+ ...(entry.pageSliceIds ? { pageSliceIds: freezeList(entry.pageSliceIds) } : {}),
367
+ ...(entry.layoutObjectIds ? { layoutObjectIds: freezeList(entry.layoutObjectIds) } : {}),
368
+ ...(entry.continuationState
369
+ ? { continuationState: { ...entry.continuationState } }
370
+ : {}),
371
+ ...(entry.divergenceIds ? { divergenceIds: freezeList(entry.divergenceIds) } : {}),
372
+ ...(entry.tableFrame ? { tableFrame: cloneTableFrame(entry.tableFrame) } : {}),
373
+ };
374
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared KI-014 refusal facts for marker-backed multi-paragraph scopes.
3
+ *
4
+ * These blockers are evidence only: they describe why broad replacement is
5
+ * still disabled and which facts must exist before L08 can safely lower it.
6
+ */
7
+
8
+ export type MultiParagraphReplacementShape = "text" | "fragment" | "unknown";
9
+
10
+ export const MULTI_PARAGRAPH_REPLACEMENT_REFUSAL =
11
+ "compile-refused:scope:multi-paragraph-replace-not-implemented";
12
+
13
+ function shapeBlocker(shape: MultiParagraphReplacementShape): string {
14
+ switch (shape) {
15
+ case "text":
16
+ return "compile-refused:scope:multi-paragraph-text-replace-not-implemented";
17
+ case "fragment":
18
+ return "compile-refused:scope:multi-paragraph-fragment-replace-not-implemented";
19
+ default:
20
+ return "compile-refused:scope:multi-paragraph-replace-shape-not-implemented";
21
+ }
22
+ }
23
+
24
+ export function multiParagraphReplacementBlockers(
25
+ shape: MultiParagraphReplacementShape = "unknown",
26
+ ): readonly string[] {
27
+ return Object.freeze([
28
+ MULTI_PARAGRAPH_REPLACEMENT_REFUSAL,
29
+ shapeBlocker(shape),
30
+ "capability:scope:block-granular-replacement-lowering-required",
31
+ "capability:scope:provenance:marker-backed-required",
32
+ "capability:scope:layout-completeness-required",
33
+ "capability:scope:geometry-completeness-required",
34
+ "capability:scope:continuation-state-required",
35
+ "capability:scope:preservation-verdict-required",
36
+ ]);
37
+ }
@@ -2,7 +2,7 @@
2
2
  * Layer 08 — preservation-boundary helper (Slice 4 interim).
3
3
  *
4
4
  * Computes whether a scope's canonical range crosses any preserve-only
5
- * boundary that would be destroyed by a replace/split/insert operation.
5
+ * boundary that would be destroyed by a replacement operation.
6
6
  * Sources consulted:
7
7
  *
8
8
  * - `document.preservation.opaqueFragments` — `OpaqueFragmentRecord[]`
@@ -29,6 +29,7 @@
29
29
  import type { CanonicalDocument } from "../../model/canonical-document.ts";
30
30
  import { findOpaqueFragmentsIntersectingRange } from "../../preservation/store.ts";
31
31
 
32
+ import { findContentControlsIntersectingRange } from "./content-control-evidence.ts";
32
33
  import type { ScopePositionMap, ScopePositionRange } from "./position-map.ts";
33
34
 
34
35
  export interface PreservationVerdict {
@@ -69,6 +70,11 @@ export function computePreservationVerdict(
69
70
  );
70
71
  }
71
72
 
73
+ const contentControls = findContentControlsIntersectingRange(document, range);
74
+ for (const control of contentControls) {
75
+ reasons.push(`content-control:${control.evidenceId}`);
76
+ }
77
+
72
78
  if (positionMap) {
73
79
  for (const [scopeId, markerRange] of positionMap.markerScopes) {
74
80
  // A marker range strictly inside the target range would be
@@ -33,6 +33,10 @@ import { enumerateScopes } from "../enumerate-scopes.ts";
33
33
  import type { EnumeratedScope } from "../enumerate-scopes.ts";
34
34
  import { emitScopeActionAudit } from "../audit-bundle.ts";
35
35
  import { compileReplacement } from "./compile.ts";
36
+ import {
37
+ MULTI_PARAGRAPH_REPLACEMENT_REFUSAL,
38
+ multiParagraphReplacementBlockers,
39
+ } from "../multi-paragraph-refusal.ts";
36
40
  import type {
37
41
  ReplacementScope,
38
42
  RuntimeOperationPlan,
@@ -47,6 +51,7 @@ export interface ApplyScopeReplacementSink {
47
51
  readonly getInteractionGuardSnapshot: () => InteractionGuardSnapshot;
48
52
  readonly getCompatibilityReport: () => CompatibilityReport;
49
53
  readonly applyScopeReplacement: (plan: RuntimeOperationPlan) => void;
54
+ readonly verifyReadback?: boolean;
50
55
  }
51
56
 
52
57
  export interface ApplyScopeReplacementInputs {
@@ -80,19 +85,28 @@ export interface ApplyScopeReplacementResult {
80
85
  }
81
86
 
82
87
  function documentHash(doc: CanonicalDocumentEnvelope): string {
83
- // Cheap structural hash block-count + total text length. Good enough
84
- // for the audit's "something changed" signal; later slices can swap in
85
- // a canonical serializer-backed content hash.
86
- const root = doc.content;
87
- let textLength = 0;
88
- for (const block of root.children) {
89
- if (block.type === "paragraph") {
90
- for (const child of block.children) {
91
- if (child.type === "text") textLength += child.text.length;
92
- }
93
- }
88
+ // Structural content hash. This is intentionally stronger than length-only
89
+ // so same-length stale/no-op replacements do not report as successful edits.
90
+ return JSON.stringify(doc.content);
91
+ }
92
+
93
+ function compileScopeById(
94
+ document: CanonicalDocumentEnvelope,
95
+ overlay: WorkflowOverlay | undefined,
96
+ scopeId: string,
97
+ ): { readonly scope: SemanticScope; readonly entry: EnumeratedScope } | null {
98
+ const paragraphIndexByBlockIndex = buildParagraphIndexMap(document);
99
+ const enumerateInputs = overlay ? { overlay } : {};
100
+ for (const entry of enumerateScopes(document, enumerateInputs)) {
101
+ if (entry.handle.scopeId !== scopeId) continue;
102
+ const compiled = compileScope(entry, {
103
+ document,
104
+ ...(overlay ? { overlay } : {}),
105
+ paragraphIndexByBlockIndex,
106
+ });
107
+ if (compiled) return { scope: compiled, entry };
94
108
  }
95
- return `blocks:${root.children.length}|text:${textLength}`;
109
+ return null;
96
110
  }
97
111
 
98
112
  export function applyScopeReplacement(
@@ -102,27 +116,18 @@ export function applyScopeReplacement(
102
116
  const docBefore = inputs.sink.getCanonicalDocument();
103
117
  const overlay = inputs.sink.getWorkflowOverlay() ?? undefined;
104
118
 
105
- const paragraphIndexByBlockIndex = buildParagraphIndexMap(docBefore);
106
- let resolvedScope: SemanticScope | null = null;
107
- let resolvedEnumerated: EnumeratedScope | null = null;
108
- const enumerateInputs = overlay ? { overlay } : {};
109
- for (const entry of enumerateScopes(docBefore, enumerateInputs)) {
110
- if (entry.handle.scopeId !== proposed.targetHandle.scopeId) continue;
111
- const compiled = compileScope(entry, {
112
- document: docBefore,
113
- ...(overlay ? { overlay } : {}),
114
- paragraphIndexByBlockIndex,
115
- });
116
- if (compiled) {
117
- resolvedScope = compiled;
118
- resolvedEnumerated = entry;
119
- break;
120
- }
121
- }
119
+ const resolved = compileScopeById(
120
+ docBefore,
121
+ overlay,
122
+ proposed.targetHandle.scopeId,
123
+ );
124
+ const resolvedScope = resolved?.scope ?? null;
125
+ const resolvedEnumerated = resolved?.entry ?? null;
122
126
 
123
127
  if (!resolvedScope || !resolvedEnumerated) {
124
128
  const validation: ValidationResult = {
125
129
  safe: false,
130
+ posture: "hard-refusal",
126
131
  blockedReasons: Object.freeze([
127
132
  `scope-not-resolvable:${proposed.targetHandle.scopeId}`,
128
133
  ]),
@@ -207,15 +212,30 @@ export function applyScopeReplacement(
207
212
  // `blockers[0]` / `reason` see the actionable sub-reason directly
208
213
  // (rather than the bare `compile-refused:scope`). Grammar matches
209
214
  // §10 `compile-refused:<kind>:<sub-reason>` (74a45eaf, 2026-04-23).
210
- const blocker =
215
+ const blockers =
211
216
  resolvedScope.kind === "scope"
212
- ? "compile-refused:scope:multi-paragraph-replace-not-implemented"
217
+ ? multiParagraphReplacementBlockers(
218
+ proposed.proposedContent.kind === "structured" ? "fragment" : "text",
219
+ )
220
+ : paragraphLike &&
221
+ proposed.operation === "replace" &&
222
+ proposed.preserve?.opaqueFragments === true
223
+ ? [
224
+ `compile-refused:${resolvedScope.kind}:opaque-preserving-text-target-unavailable`,
225
+ ]
213
226
  : paragraphLike && proposed.operation !== "replace"
214
- ? `compile-refused:${resolvedScope.kind}:operation-not-implemented:${proposed.operation}`
215
- : `compile-refused:${resolvedScope.kind}`;
227
+ ? [
228
+ `compile-refused:${resolvedScope.kind}:operation-not-implemented:${proposed.operation}`,
229
+ ]
230
+ : [`compile-refused:${resolvedScope.kind}`];
231
+ const blocker =
232
+ resolvedScope.kind === "scope"
233
+ ? MULTI_PARAGRAPH_REPLACEMENT_REFUSAL
234
+ : blockers[0] ?? `compile-refused:${resolvedScope.kind}`;
216
235
  const refused: ValidationResult = {
217
236
  safe: false,
218
- blockedReasons: Object.freeze([blocker]),
237
+ posture: "hard-refusal",
238
+ blockedReasons: Object.freeze([...blockers]),
219
239
  warnings: verdict.warnings,
220
240
  };
221
241
  // Coord-08 U5 — `reason` mirrors `blockers[0]` for symmetry. Agents
@@ -251,6 +271,49 @@ export function applyScopeReplacement(
251
271
 
252
272
  const docAfter = inputs.sink.getCanonicalDocument();
253
273
  const documentHashAfter = documentHash(docAfter);
274
+ const readback = compileScopeById(
275
+ docAfter,
276
+ inputs.sink.getWorkflowOverlay() ?? undefined,
277
+ proposed.targetHandle.scopeId,
278
+ );
279
+ const proposedText =
280
+ proposed.proposedContent.kind === "text"
281
+ ? proposed.proposedContent.text ?? ""
282
+ : null;
283
+ const shouldVerifyReadback =
284
+ inputs.sink.verifyReadback === true &&
285
+ proposed.preserve?.opaqueFragments === true;
286
+ const readbackFailureReason =
287
+ shouldVerifyReadback &&
288
+ documentHashAfter === documentHashBefore &&
289
+ posture === "direct-edit" &&
290
+ !readback
291
+ ? `apply-readback-unresolvable:${proposed.targetHandle.scopeId}`
292
+ : shouldVerifyReadback &&
293
+ documentHashAfter === documentHashBefore &&
294
+ posture === "direct-edit" &&
295
+ proposed.operation === "replace" &&
296
+ proposedText !== null &&
297
+ proposedText !== resolvedScope.content.text &&
298
+ readback?.scope.content.text === resolvedScope.content.text
299
+ ? `apply-readback-unchanged:${proposed.targetHandle.scopeId}`
300
+ : undefined;
301
+
302
+ if (readbackFailureReason) {
303
+ return {
304
+ applied: false,
305
+ reason: readbackFailureReason,
306
+ validation: {
307
+ safe: false,
308
+ posture: "hard-refusal",
309
+ blockedReasons: Object.freeze([readbackFailureReason]),
310
+ warnings: verdict.warnings,
311
+ },
312
+ plan,
313
+ scope: readback?.scope ?? resolvedScope,
314
+ authoredRevisionIds: Object.freeze([]),
315
+ };
316
+ }
254
317
 
255
318
  const authoredRevisionIds: string[] = [];
256
319
  for (const id of Object.keys(docAfter.review.revisions ?? {})) {