@beyondwork/docx-react-component 1.0.103 → 1.0.105

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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +66 -1
  3. package/src/api/v3/_runtime-handle.ts +2 -0
  4. package/src/api/v3/ai/_pe2-evidence.ts +153 -0
  5. package/src/api/v3/ai/bundle.ts +13 -5
  6. package/src/api/v3/ai/inspect.ts +7 -1
  7. package/src/api/v3/ai/outline.ts +2 -7
  8. package/src/api/v3/ai/replacement.ts +113 -0
  9. package/src/api/v3/runtime/geometry.ts +79 -0
  10. package/src/api/v3/ui/_types.ts +86 -0
  11. package/src/api/v3/ui/index.ts +5 -0
  12. package/src/api/v3/ui/overlays.ts +104 -0
  13. package/src/io/ooxml/parse-drawing.ts +99 -1
  14. package/src/io/ooxml/parse-fields.ts +27 -6
  15. package/src/io/ooxml/parse-shapes.ts +130 -0
  16. package/src/model/canonical-document.ts +34 -3
  17. package/src/model/canonical-layout-inputs.ts +979 -0
  18. package/src/model/layout/index.ts +9 -0
  19. package/src/model/layout/page-graph-types.ts +150 -0
  20. package/src/model/layout/runtime-page-graph-types.ts +23 -0
  21. package/src/runtime/collab/runtime-collab-sync.ts +3 -3
  22. package/src/runtime/debug/build-debug-inspector-snapshot.ts +17 -4
  23. package/src/runtime/document-runtime.ts +30 -14
  24. package/src/runtime/event-refresh-hints.ts +35 -5
  25. package/src/runtime/formatting/formatting-context.ts +110 -9
  26. package/src/runtime/formatting/index.ts +2 -0
  27. package/src/runtime/formatting/layout-inputs.ts +67 -3
  28. package/src/runtime/geometry/caret-geometry.ts +82 -10
  29. package/src/runtime/geometry/geometry-facet.ts +44 -0
  30. package/src/runtime/geometry/geometry-index.ts +1268 -0
  31. package/src/runtime/geometry/geometry-types.ts +227 -1
  32. package/src/runtime/geometry/index.ts +26 -0
  33. package/src/runtime/geometry/inert-geometry-facet.ts +3 -0
  34. package/src/runtime/geometry/object-handles.ts +7 -4
  35. package/src/runtime/geometry/replacement-envelope.ts +41 -2
  36. package/src/runtime/layout/layout-engine-instance.ts +2 -0
  37. package/src/runtime/layout/layout-engine-version.ts +44 -1
  38. package/src/runtime/layout/page-graph.ts +877 -2
  39. package/src/runtime/layout/project-block-fragments.ts +101 -1
  40. package/src/runtime/layout/public-facet.ts +152 -0
  41. package/src/runtime/prerender/graph-canonicalize.ts +44 -0
  42. package/src/runtime/surface-projection.ts +43 -3
  43. package/src/runtime/workflow/coordinator.ts +57 -11
  44. package/src/ui/ui-controller-factory.ts +11 -0
  45. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +3 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.103",
4
+ "version": "1.0.105",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
6
  "type": "module",
7
7
  "sideEffects": [
@@ -177,9 +177,11 @@ export type {
177
177
  PublicBlockMeasurement,
178
178
  PublicFieldDirtinessReport,
179
179
  PublicLineBox,
180
+ PublicLayoutDivergence,
180
181
  PublicMeasurementFidelity,
181
182
  PublicNoteAllocation,
182
183
  PublicPageAnchor,
184
+ PublicPageFrame,
183
185
  PublicPageNode,
184
186
  PublicPageRegion,
185
187
  PublicPageRegions,
@@ -190,6 +192,7 @@ export type {
190
192
  PublicResolvedParagraphFormatting,
191
193
  PublicResolvedRunFormatting,
192
194
  PublicSectionNode,
195
+ PublicTwipsRect,
193
196
  // R0.5: named page-format + margin-preset catalogs
194
197
  PageFormatDefinition,
195
198
  ActivePageFormat,
@@ -1223,6 +1226,23 @@ export interface SurfacePictureEffects {
1223
1226
  glow?: { radius: number; color: string; colorType: "srgbClr" | "schemeClr" };
1224
1227
  }
1225
1228
 
1229
+ export interface SurfacePreserveOnlyObjectSizing {
1230
+ sourceId?: string;
1231
+ display: "inline" | "floating" | "unknown";
1232
+ extentEmu?: { widthEmu: number; heightEmu: number };
1233
+ fallbackHint:
1234
+ | "drawing-inline"
1235
+ | "drawing-floating"
1236
+ | "chart"
1237
+ | "smartart"
1238
+ | "shape"
1239
+ | "wordart"
1240
+ | "vml-shape"
1241
+ | "ole-object"
1242
+ | "opaque-object";
1243
+ relationshipIds?: string[];
1244
+ }
1245
+
1226
1246
  export type SurfaceInlineSegment =
1227
1247
  | {
1228
1248
  segmentId: string;
@@ -1338,6 +1358,20 @@ export type SurfaceInlineSegment =
1338
1358
  * can render `ChartSurface` instead of the fallback bitmap or badge.
1339
1359
  */
1340
1360
  parsedChartId?: string;
1361
+ /**
1362
+ * PE2 preserve-only object handoff. Present for unsupported drawing,
1363
+ * chart, SmartArt, WordArt, VML, and opaque-object previews when import
1364
+ * could recover source ids, relationship ids, fallback hints, or
1365
+ * extents. Layout consumers may size placeholders from this metadata;
1366
+ * it is not a layout computation.
1367
+ */
1368
+ preserveOnlyObject?: SurfacePreserveOnlyObjectSizing;
1369
+ /**
1370
+ * Drawing anchor geometry for preserve-only objects that originated in a
1371
+ * `drawing_frame`. Mirrors image/shape anchor semantics so L04 can join
1372
+ * the object to source/canonical/layout rows without re-parsing OOXML.
1373
+ */
1374
+ anchor?: SurfaceDrawingAnchor;
1341
1375
  state: "locked-preserve-only";
1342
1376
  }
1343
1377
  | {
@@ -1403,6 +1437,7 @@ export type SurfaceInlineSegment =
1403
1437
  isTextBox?: boolean;
1404
1438
  /** Text box body layout from `wps:bodyPr` / `a:bodyPr`. */
1405
1439
  textBoxBody?: SurfaceTextBoxBodyProperties;
1440
+ preserveOnlyObject?: SurfacePreserveOnlyObjectSizing;
1406
1441
  /** First-paragraph plain-text preview when `isTextBox` is true. */
1407
1442
  txbxText?: string;
1408
1443
  /** Run-level marks from the first text-box text run, when available. */
@@ -3609,6 +3644,11 @@ export interface WordReviewEditorPasteEvent {
3609
3644
  source: "paste" | "drop";
3610
3645
  }
3611
3646
 
3647
+ export interface WorkflowEventOrigin {
3648
+ readonly source: "api" | "runtime" | "collab" | string;
3649
+ readonly at?: string;
3650
+ }
3651
+
3612
3652
  export type WordReviewEditorEvent =
3613
3653
  | {
3614
3654
  type: "ready";
@@ -3724,16 +3764,38 @@ export type WordReviewEditorEvent =
3724
3764
  type: "workflow_overlay_changed";
3725
3765
  documentId: string;
3726
3766
  snapshot: WorkflowScopeSnapshot;
3767
+ origin?: WorkflowEventOrigin;
3727
3768
  }
3728
3769
  | {
3729
3770
  type: "workflow_active_work_item_changed";
3730
3771
  documentId: string;
3731
3772
  activeWorkItemId: string | null;
3773
+ origin?: WorkflowEventOrigin;
3732
3774
  }
3733
3775
  | {
3734
3776
  type: "workflow_metadata_changed";
3735
3777
  documentId: string;
3736
3778
  snapshot: WorkflowMetadataSnapshot;
3779
+ origin?: WorkflowEventOrigin;
3780
+ }
3781
+ | {
3782
+ type: "workflow_shared_state_changed";
3783
+ documentId: string;
3784
+ state: SharedWorkflowState | null;
3785
+ origin: WorkflowEventOrigin;
3786
+ }
3787
+ | {
3788
+ type: "workflow_visibility_policy_changed";
3789
+ documentId: string;
3790
+ kind?: OverlayKind;
3791
+ policy?: OverlayVisibilityPolicy | null;
3792
+ origin: WorkflowEventOrigin;
3793
+ }
3794
+ | {
3795
+ type: "workflow_markup_mode_policy_changed";
3796
+ documentId: string;
3797
+ policy: WorkflowMarkupModePolicy | null;
3798
+ origin: WorkflowEventOrigin;
3737
3799
  }
3738
3800
  | {
3739
3801
  type: "host_annotation_overlay_changed";
@@ -5010,7 +5072,10 @@ export interface WordReviewEditorRef {
5010
5072
  * `replaceText` calls to block with `workflow_comment_only`.
5011
5073
  */
5012
5074
  getWorkflowOverlay(): WorkflowOverlay | null;
5013
- setSharedWorkflowState(state: SharedWorkflowState | null): void;
5075
+ setSharedWorkflowState(
5076
+ state: SharedWorkflowState | null,
5077
+ origin?: WorkflowEventOrigin,
5078
+ ): void;
5014
5079
  getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
5015
5080
  getInteractionGuardSnapshot(): InteractionGuardSnapshot;
5016
5081
  getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
@@ -48,6 +48,7 @@ export type RuntimeApiHandle = Pick<
48
48
  | "getCompatibilityReport"
49
49
  | "getWarnings"
50
50
  | "getRenderSnapshot"
51
+ | "getDocumentNavigationSnapshot"
51
52
  // Canonical document read (ai.inspect + ai.bundle + ai.resolve families,
52
53
  // added in refactor/08 Slice 2/3 graduations)
53
54
  | "getCanonicalDocument"
@@ -155,6 +156,7 @@ export const RUNTIME_API_HANDLE_SHAPE_CHECK: Record<keyof RuntimeApiHandle, true
155
156
  getCompatibilityReport: true,
156
157
  getWarnings: true,
157
158
  getRenderSnapshot: true,
159
+ getDocumentNavigationSnapshot: true,
158
160
  getCanonicalDocument: true,
159
161
  findAllText: true,
160
162
  replaceText: true,
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Layer 09 PE2 evidence projection.
3
+ *
4
+ * The AI API should not leak the full runtime geometry index. Agents need a
5
+ * small, JSON-stable read model that says whether PE2 geometry evidence is
6
+ * available and, for scope reads, whether a replacement envelope exists for
7
+ * the requested scope.
8
+ */
9
+
10
+ import type { RuntimeApiHandle } from "../_runtime-handle.ts";
11
+ import type {
12
+ GeometryIndexCoverage,
13
+ GeometryPrecision,
14
+ GeometryRehydrationStatus,
15
+ GeometryReplacementEnvelopeEntry,
16
+ GeometrySourceIdentity,
17
+ } from "../../../runtime/geometry/index.ts";
18
+
19
+ export interface AiPe2GeometryCoverageEvidence {
20
+ readonly status: GeometryRehydrationStatus;
21
+ readonly pageCount: number;
22
+ readonly regionCount: number;
23
+ readonly sliceCount: number;
24
+ readonly lineCount: number;
25
+ readonly anchorCount: number;
26
+ readonly hitTargetCount: number;
27
+ readonly semanticEntryCount: number;
28
+ readonly replacementEnvelopeCount: number;
29
+ readonly objectHandleCount: number;
30
+ readonly precision: {
31
+ readonly exact: number;
32
+ readonly "within-tolerance": number;
33
+ readonly heuristic: number;
34
+ };
35
+ }
36
+
37
+ export interface AiPe2DocumentEvidence {
38
+ readonly geometry: AiPe2GeometryCoverageEvidence;
39
+ }
40
+
41
+ export interface AiPe2ScopeReplacementEnvelopeEvidence {
42
+ readonly status: GeometryRehydrationStatus;
43
+ readonly precision: GeometryPrecision;
44
+ readonly rectCount: number;
45
+ readonly pageIds?: readonly string[];
46
+ readonly sourceIdentity?: GeometrySourceIdentity;
47
+ }
48
+
49
+ export interface AiPe2ScopeEvidence {
50
+ readonly geometry: {
51
+ readonly coverage: AiPe2GeometryCoverageEvidence;
52
+ readonly replacementEnvelope?: AiPe2ScopeReplacementEnvelopeEvidence;
53
+ readonly reason?: "geometry-index-unavailable" | "scope-envelope-not-found";
54
+ };
55
+ }
56
+
57
+ function copyCoverage(coverage: GeometryIndexCoverage): AiPe2GeometryCoverageEvidence {
58
+ return {
59
+ status: coverage.status,
60
+ pageCount: coverage.pageCount,
61
+ regionCount: coverage.regionCount,
62
+ sliceCount: coverage.sliceCount,
63
+ lineCount: coverage.lineCount,
64
+ anchorCount: coverage.anchorCount,
65
+ hitTargetCount: coverage.hitTargetCount,
66
+ semanticEntryCount: coverage.semanticEntryCount,
67
+ replacementEnvelopeCount: coverage.replacementEnvelopeCount,
68
+ objectHandleCount: coverage.objectHandleCount,
69
+ precision: {
70
+ exact: coverage.precision.exact,
71
+ "within-tolerance": coverage.precision["within-tolerance"],
72
+ heuristic: coverage.precision.heuristic,
73
+ },
74
+ };
75
+ }
76
+
77
+ function copyEnvelope(
78
+ envelope: GeometryReplacementEnvelopeEntry,
79
+ ): AiPe2ScopeReplacementEnvelopeEvidence {
80
+ return {
81
+ status: envelope.status,
82
+ precision: envelope.precision,
83
+ rectCount: envelope.rects.length,
84
+ ...(envelope.pageIds ? { pageIds: [...envelope.pageIds] } : {}),
85
+ ...(envelope.sourceIdentity ? { sourceIdentity: { ...envelope.sourceIdentity } } : {}),
86
+ };
87
+ }
88
+
89
+ const UNAVAILABLE_COVERAGE: AiPe2GeometryCoverageEvidence = {
90
+ status: "unavailable",
91
+ pageCount: 0,
92
+ regionCount: 0,
93
+ sliceCount: 0,
94
+ lineCount: 0,
95
+ anchorCount: 0,
96
+ hitTargetCount: 0,
97
+ semanticEntryCount: 0,
98
+ replacementEnvelopeCount: 0,
99
+ objectHandleCount: 0,
100
+ precision: { exact: 0, "within-tolerance": 0, heuristic: 0 },
101
+ };
102
+
103
+ export function projectDocumentPe2Evidence(
104
+ runtime: RuntimeApiHandle,
105
+ ): AiPe2DocumentEvidence {
106
+ if (!runtime.geometry) {
107
+ return { geometry: UNAVAILABLE_COVERAGE };
108
+ }
109
+ return {
110
+ geometry: copyCoverage(runtime.geometry.getGeometryCoverage()),
111
+ };
112
+ }
113
+
114
+ export function projectScopePe2Evidence(
115
+ runtime: RuntimeApiHandle,
116
+ scopeId: string,
117
+ ): AiPe2ScopeEvidence {
118
+ if (!runtime.geometry) {
119
+ return {
120
+ geometry: {
121
+ coverage: UNAVAILABLE_COVERAGE,
122
+ reason: "geometry-index-unavailable",
123
+ },
124
+ };
125
+ }
126
+ const coverage = copyCoverage(runtime.geometry.getGeometryCoverage());
127
+ const index = runtime.geometry.getGeometryIndex();
128
+ if (!index) {
129
+ return {
130
+ geometry: {
131
+ coverage,
132
+ reason: "geometry-index-unavailable",
133
+ },
134
+ };
135
+ }
136
+
137
+ const envelope = index.replacementEnvelopes.find((entry) => entry.scopeId === scopeId);
138
+ if (!envelope) {
139
+ return {
140
+ geometry: {
141
+ coverage,
142
+ reason: "scope-envelope-not-found",
143
+ },
144
+ };
145
+ }
146
+
147
+ return {
148
+ geometry: {
149
+ coverage,
150
+ replacementEnvelope: copyEnvelope(envelope),
151
+ },
152
+ };
153
+ }
@@ -24,6 +24,10 @@ import {
24
24
  type SemanticScope,
25
25
  } from "../../../runtime/scopes/index.ts";
26
26
  import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
27
+ import {
28
+ projectScopePe2Evidence,
29
+ type AiPe2ScopeEvidence,
30
+ } from "./_pe2-evidence.ts";
27
31
 
28
32
  /**
29
33
  * Handle-shaped input (architecture A3 — scope-handle-first targeting).
@@ -42,8 +46,9 @@ export interface GetScopeBundleInput {
42
46
  readonly nowUtc: string;
43
47
  }
44
48
 
45
- /** Re-export the runtime-side `ScopeBundle` so v3 consumers get the rich shape. */
46
- export type { RuntimeScopeBundle as ScopeBundle };
49
+ export type ScopeBundle = RuntimeScopeBundle & {
50
+ readonly pe2Evidence: AiPe2ScopeEvidence;
51
+ };
47
52
 
48
53
  export interface ScopeBundleNotFound {
49
54
  readonly notFound: true;
@@ -91,7 +96,7 @@ export const getScopeBundleMetadata: ApiV3FnMetadata = {
91
96
  boundedScope: "scope",
92
97
  auditCategory: "scope-bundle",
93
98
  contextPromptShape:
94
- "Bundle content + formatting + layout + geometry + workflow + replaceability for prompt context.",
99
+ "Bundle content + formatting + layout + geometry + workflow + replaceability + PE2 geometry coverage/envelope evidence for prompt context.",
95
100
  },
96
101
  stateClass: "A-canonical",
97
102
  persistsTo: "canonical",
@@ -110,7 +115,7 @@ export function createBundleFamily(runtime: RuntimeApiHandle) {
110
115
  return compiled?.scope ?? null;
111
116
  },
112
117
 
113
- getScopeBundle(input: GetScopeBundleInput): RuntimeScopeBundle | ScopeBundleNotFound {
118
+ getScopeBundle(input: GetScopeBundleInput): ScopeBundle | ScopeBundleNotFound {
114
119
  // @endStateApi — live-with-adapter. Routes through the compiler-
115
120
  // service facade.
116
121
  //
@@ -126,7 +131,10 @@ export function createBundleFamily(runtime: RuntimeApiHandle) {
126
131
  reason: "no scope with this id in canonical document or review store",
127
132
  };
128
133
  }
129
- return bundle;
134
+ return {
135
+ ...bundle,
136
+ pe2Evidence: projectScopePe2Evidence(runtime, scopeId),
137
+ };
130
138
  },
131
139
  };
132
140
  }
@@ -16,6 +16,10 @@ import {
16
16
  type SemanticScopeKind,
17
17
  } from "../../../runtime/scopes/index.ts";
18
18
  import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
19
+ import {
20
+ projectDocumentPe2Evidence,
21
+ type AiPe2DocumentEvidence,
22
+ } from "./_pe2-evidence.ts";
19
23
 
20
24
  export interface InspectDocumentResult {
21
25
  readonly documentId: string;
@@ -23,6 +27,7 @@ export interface InspectDocumentResult {
23
27
  readonly pageCount?: number;
24
28
  readonly semanticSummary: string;
25
29
  readonly kindDistribution: Readonly<Partial<Record<SemanticScopeKind, number>>>;
30
+ readonly pe2Evidence: AiPe2DocumentEvidence;
26
31
  }
27
32
 
28
33
  export const inspectDocumentMetadata: ApiV3FnMetadata = {
@@ -40,7 +45,7 @@ export const inspectDocumentMetadata: ApiV3FnMetadata = {
40
45
  boundedScope: "document",
41
46
  auditCategory: "document-inspect",
42
47
  contextPromptShape:
43
- "Summarize document by scope count + page count + semantic kind distribution.",
48
+ "Summarize document by scope count + page count + semantic kind distribution + PE2 geometry coverage evidence.",
44
49
  },
45
50
  stateClass: "A-canonical",
46
51
  persistsTo: "canonical",
@@ -97,6 +102,7 @@ export function createInspectFamily(runtime: RuntimeApiHandle) {
97
102
  scopeCount: scopes.length,
98
103
  semanticSummary: summary,
99
104
  kindDistribution,
105
+ pe2Evidence: projectDocumentPe2Evidence(runtime),
100
106
  };
101
107
  },
102
108
 
@@ -19,7 +19,6 @@ import type {
19
19
  DocumentOutlineHeadingSnapshot,
20
20
  DocumentOutlineSnapshot,
21
21
  } from "../../public-types.ts";
22
- import { createDocumentNavigationSnapshot } from "../../../runtime/document-navigation.ts";
23
22
  import { createDocumentOutlineSnapshot } from "../../../runtime/document-outline.ts";
24
23
  import {
25
24
  computeBlockPositions,
@@ -68,7 +67,7 @@ export const getDocumentOutlineMetadata: ApiV3FnMetadata = {
68
67
  stateClass: "A-canonical",
69
68
  persistsTo: "canonical",
70
69
  rwdReference:
71
- "§AI API § ai.getDocumentOutline. Composes createDocumentNavigationSnapshot (L04/07) with createDocumentOutlineSnapshot (L07) to surface the heading tree. Read-only; no audit emission.",
70
+ "§AI API § ai.getDocumentOutline. Composes runtime.getDocumentNavigationSnapshot() (L04 graph-derived navigation) with createDocumentOutlineSnapshot (L07) to surface the heading tree. Read-only; no audit emission.",
72
71
  };
73
72
 
74
73
  export function createOutlineFamily(runtime: RuntimeApiHandle) {
@@ -82,11 +81,7 @@ export function createOutlineFamily(runtime: RuntimeApiHandle) {
82
81
  const snapshot = runtime.getRenderSnapshot();
83
82
  const document = runtime.getCanonicalDocument();
84
83
  const selectionHead = snapshot.selection.head;
85
- const navigation = createDocumentNavigationSnapshot(
86
- document,
87
- selectionHead,
88
- snapshot.activeStory,
89
- );
84
+ const navigation = runtime.getDocumentNavigationSnapshot();
90
85
  const outline = createDocumentOutlineSnapshot({
91
86
  navigation,
92
87
  activeStory: snapshot.activeStory,
@@ -123,6 +123,7 @@ export interface ApplyResult {
123
123
  readonly applied: boolean;
124
124
  readonly reason?: string;
125
125
  readonly blockers?: readonly string[];
126
+ readonly blockerDetails?: readonly ActionBlockerDetail[];
126
127
  readonly auditHint?: string;
127
128
  /**
128
129
  * Gap A (post-Slice-7 integration) — revision IDs authored during
@@ -133,6 +134,19 @@ export interface ApplyResult {
133
134
  readonly authoredRevisionIds: readonly string[];
134
135
  }
135
136
 
137
+ export interface ActionBlockerDetail {
138
+ readonly code: string;
139
+ readonly category:
140
+ | "unsupported-scope-kind"
141
+ | "unsupported-operation"
142
+ | "unresolved-scope"
143
+ | "policy-or-guard";
144
+ readonly message: string;
145
+ readonly nextStep: string;
146
+ readonly scopeKind?: string;
147
+ readonly operation?: string;
148
+ }
149
+
136
150
  export interface ApplyReplacementScopeInput {
137
151
  readonly targetScopeId: string;
138
152
  readonly operation?: ReplacementOperationKind;
@@ -263,6 +277,97 @@ function projectValidationResult(
263
277
  };
264
278
  }
265
279
 
280
+ function blockerDetailFor(code: string): ActionBlockerDetail | null {
281
+ if (code.startsWith("scope-not-resolvable:")) {
282
+ return {
283
+ code,
284
+ category: "unresolved-scope",
285
+ message: "The target scope no longer resolves in the current document.",
286
+ nextStep:
287
+ "Call ai.resolveReference or ai.queryScopeAtPosition again, then retry with the returned handle's scopeId.",
288
+ };
289
+ }
290
+
291
+ if (!code.startsWith("compile-refused:")) return null;
292
+
293
+ const [, scopeKind = "unknown", ...rest] = code.split(":");
294
+ const suffix = rest.join(":");
295
+ if (suffix.startsWith("operation-not-implemented:")) {
296
+ const operation = suffix.slice("operation-not-implemented:".length);
297
+ return {
298
+ code,
299
+ category: "unsupported-operation",
300
+ scopeKind,
301
+ operation,
302
+ message: `The ${operation} operation is not implemented for ${scopeKind} scopes.`,
303
+ nextStep:
304
+ "Retry with operation:\"replace\" when that is acceptable, or attach an explanation/issue instead of mutating.",
305
+ };
306
+ }
307
+
308
+ if (scopeKind === "scope" && suffix === "multi-paragraph-replace-not-implemented") {
309
+ return {
310
+ code,
311
+ category: "unsupported-scope-kind",
312
+ scopeKind,
313
+ message: "Multi-paragraph scope replacement is not implemented.",
314
+ nextStep:
315
+ "Split the request into paragraph-scoped replacements, or attach an explanation/issue until the multi-paragraph planner ships.",
316
+ };
317
+ }
318
+
319
+ if (scopeKind === "table" || scopeKind === "table-row" || scopeKind === "table-cell") {
320
+ return {
321
+ code,
322
+ category: "unsupported-scope-kind",
323
+ scopeKind,
324
+ message: `Flat text replacement is not implemented for ${scopeKind} scopes because it can break table structure.`,
325
+ nextStep:
326
+ "Use ai.attachExplanation or ai.createIssue for now, or wait for the Layer 08 table-family replacement planner.",
327
+ };
328
+ }
329
+
330
+ if (scopeKind === "field") {
331
+ return {
332
+ code,
333
+ category: "unsupported-scope-kind",
334
+ scopeKind,
335
+ message: "Field result replacement is preserve-only because field text is computed from instructions.",
336
+ nextStep:
337
+ "Attach an explanation/issue, or use a future field-aware edit path that updates field instructions safely.",
338
+ };
339
+ }
340
+
341
+ if (scopeKind === "image" || scopeKind === "note") {
342
+ return {
343
+ code,
344
+ category: "unsupported-scope-kind",
345
+ scopeKind,
346
+ message: `Replacement is not implemented for ${scopeKind} scopes.`,
347
+ nextStep:
348
+ "Attach an explanation/issue rather than mutating until a scope-specific planner exists.",
349
+ };
350
+ }
351
+
352
+ return {
353
+ code,
354
+ category: "unsupported-scope-kind",
355
+ scopeKind,
356
+ message: `Replacement is not implemented for ${scopeKind} scopes.`,
357
+ nextStep:
358
+ "Use a supported paragraph-like scope, or attach an explanation/issue and route the unsupported planner to the owning layer.",
359
+ };
360
+ }
361
+
362
+ function projectBlockerDetails(
363
+ blockers: readonly string[],
364
+ ): readonly ActionBlockerDetail[] | undefined {
365
+ const details = blockers
366
+ .map((code) => blockerDetailFor(code))
367
+ .filter((detail): detail is ActionBlockerDetail => detail !== null);
368
+ return details.length > 0 ? Object.freeze(details) : undefined;
369
+ }
370
+
266
371
  export function createReplacementFamily(runtime: RuntimeApiHandle) {
267
372
  const compiler = createScopeCompilerService(runtime);
268
373
  return {
@@ -371,6 +476,9 @@ export function createReplacementFamily(runtime: RuntimeApiHandle) {
371
476
  expectedDelta: applyReplacementScopeMetadata.uxIntent.expectedDelta,
372
477
  });
373
478
 
479
+ const blockerDetails = projectBlockerDetails(
480
+ result.validation.blockedReasons,
481
+ );
374
482
  return {
375
483
  proposalId,
376
484
  applied: result.applied,
@@ -378,6 +486,7 @@ export function createReplacementFamily(runtime: RuntimeApiHandle) {
378
486
  ...(result.validation.blockedReasons.length > 0
379
487
  ? { blockers: Object.freeze([...result.validation.blockedReasons]) }
380
488
  : {}),
489
+ ...(blockerDetails ? { blockerDetails } : {}),
381
490
  ...(result.audit ? { auditHint: result.audit.actionId } : {}),
382
491
  authoredRevisionIds: result.authoredRevisionIds,
383
492
  };
@@ -412,6 +521,9 @@ export function createReplacementFamily(runtime: RuntimeApiHandle) {
412
521
  expectedDelta: applyScopeActionMetadata.uxIntent.expectedDelta,
413
522
  });
414
523
 
524
+ const blockerDetails = projectBlockerDetails(
525
+ result.validation.blockedReasons,
526
+ );
415
527
  return {
416
528
  proposalId,
417
529
  applied: result.applied,
@@ -419,6 +531,7 @@ export function createReplacementFamily(runtime: RuntimeApiHandle) {
419
531
  ...(result.validation.blockedReasons.length > 0
420
532
  ? { blockers: Object.freeze([...result.validation.blockedReasons]) }
421
533
  : {}),
534
+ ...(blockerDetails ? { blockerDetails } : {}),
422
535
  ...(result.audit ? { auditHint: result.audit.actionId } : {}),
423
536
  authoredRevisionIds: result.authoredRevisionIds,
424
537
  };
@@ -15,6 +15,26 @@ import type { RuntimeApiHandle } from "../_runtime-handle.ts";
15
15
  import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
16
16
  import { mockPayload } from "../_mocks.ts";
17
17
  import type { MockPayload } from "../_layer-metadata.ts";
18
+ import type {
19
+ GeometryIndex,
20
+ GeometryIndexCoverage,
21
+ } from "../../../runtime/geometry/index.ts";
22
+
23
+ function createUnavailableGeometryCoverage(): GeometryIndexCoverage {
24
+ return {
25
+ status: "unavailable",
26
+ pageCount: 0,
27
+ regionCount: 0,
28
+ sliceCount: 0,
29
+ lineCount: 0,
30
+ anchorCount: 0,
31
+ hitTargetCount: 0,
32
+ semanticEntryCount: 0,
33
+ replacementEnvelopeCount: 0,
34
+ objectHandleCount: 0,
35
+ precision: { exact: 0, "within-tolerance": 0, heuristic: 0 },
36
+ };
37
+ }
18
38
 
19
39
  export interface BlockRectEntry {
20
40
  readonly blockId: string;
@@ -57,6 +77,50 @@ export interface HitTestResult {
57
77
  readonly __mock?: true;
58
78
  }
59
79
 
80
+ /* ================================================================== */
81
+ /* PE2 geometry index + coverage */
82
+ /* ================================================================== */
83
+
84
+ export const getGeometryIndexMetadata: ApiV3FnMetadata = {
85
+ name: "runtime.geometry.getGeometryIndex",
86
+ status: "live-with-adapter",
87
+ sourceLayer: "geometry-projection",
88
+ liveEvidence: {
89
+ runnerTest: "test/api/v3/geometry-uses-geometry-handle.test.ts",
90
+ commit: "refactor-07-pe2-geometry-index-read-surface",
91
+ },
92
+ uxIntent: { uiVisible: false, expectsUxResponse: "none" },
93
+ agentMetadata: {
94
+ readOrMutate: "read",
95
+ boundedScope: "document",
96
+ auditCategory: "geometry-read",
97
+ },
98
+ stateClass: "A-canonical",
99
+ persistsTo: "canonical",
100
+ rwdReference:
101
+ "§Runtime API § runtime.geometry.getGeometryIndex. PE2 read surface over GeometryFacet.getGeometryIndex(): renderer-neutral pages, regions, slices, lines, anchors, hit targets, semantic entries, replacement envelopes, object handles, and coverage. Returns null when no geometry facet or render frame is warm. Promotes to live when L05 projects directly from L04 page slices instead of the render-frame adapter.",
102
+ };
103
+
104
+ export const getGeometryCoverageMetadata: ApiV3FnMetadata = {
105
+ name: "runtime.geometry.getGeometryCoverage",
106
+ status: "live-with-adapter",
107
+ sourceLayer: "geometry-projection",
108
+ liveEvidence: {
109
+ runnerTest: "test/api/v3/geometry-uses-geometry-handle.test.ts",
110
+ commit: "refactor-07-pe2-geometry-index-read-surface",
111
+ },
112
+ uxIntent: { uiVisible: false, expectsUxResponse: "none" },
113
+ agentMetadata: {
114
+ readOrMutate: "read",
115
+ boundedScope: "document",
116
+ auditCategory: "geometry-read",
117
+ },
118
+ stateClass: "A-canonical",
119
+ persistsTo: "canonical",
120
+ rwdReference:
121
+ "§Runtime API § runtime.geometry.getGeometryCoverage. PE2 lightweight status read over GeometryFacet.getGeometryCoverage(). Always returns a coverage object; unwired/pre-paint states report status:'unavailable' with zero counts so agents and evidence runners can distinguish no geometry from empty geometry.",
122
+ };
123
+
60
124
  /* ================================================================== */
61
125
  /* getCaret — Slice-4 caret geometry (Slice X3 of §4 of coord-05) */
62
126
  /* ================================================================== */
@@ -224,6 +288,21 @@ export function createGeometryFamily(runtime: RuntimeApiHandle) {
224
288
  const geometry = runtime.geometry;
225
289
 
226
290
  return {
291
+ getGeometryIndex(): GeometryIndex | null {
292
+ // @endStateApi — live-with-adapter. Exposes the Layer-05 PE2
293
+ // geometry index through the v3 runtime seam. The current L05
294
+ // index is projected from the render frame; callers still get
295
+ // renderer-neutral value objects and no DOM/PM/runtime instances.
296
+ return geometry?.getGeometryIndex() ?? null;
297
+ },
298
+
299
+ getGeometryCoverage(): GeometryIndexCoverage {
300
+ // @endStateApi — live-with-adapter. Coverage is intentionally
301
+ // total: an unwired/pre-paint handle reports an unavailable zero
302
+ // summary instead of null so evidence code can branch on status.
303
+ return geometry?.getGeometryCoverage() ?? createUnavailableGeometryCoverage();
304
+ },
305
+
227
306
  getBlockRects(blockIds: readonly string[]): ReadonlyArray<BlockRectEntry & Partial<MockPayload>> {
228
307
  // @endStateApi — live-with-adapter. Routes directly through the
229
308
  // Layer-05 GeometryFacet's `getBlockRects(blockId)`, which walks