@beyondwork/docx-react-component 1.0.104 → 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.
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.104",
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,
@@ -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
 
@@ -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
  };
@@ -127,6 +127,21 @@ export interface UiController {
127
127
  readonly subscribeOverlays?: (
128
128
  listener: UiListener<OverlayAnchorQuery>,
129
129
  ) => UiUnsubscribe;
130
+ /**
131
+ * PE2 overlay-lane snapshot resolver. This is the mounted-surface lane
132
+ * contract for selection/caret/redline/scope/comment/table/object/search
133
+ * and presence overlays. The bind-side supplies already-projected lane
134
+ * entries; L10 owns the observable shape, not layout or geometry caches.
135
+ */
136
+ readonly getOverlayLane?: (kind: UiOverlayLaneKind) => UiOverlayLaneSnapshot;
137
+ /**
138
+ * PE2 overlay-lane subscription. Must fan out lane changes without
139
+ * dispatching PM document transactions or invalidating L04 layout.
140
+ */
141
+ readonly subscribeOverlayLane?: (
142
+ kind: UiOverlayLaneKind,
143
+ listener: UiListener<UiOverlayLaneSnapshot>,
144
+ ) => UiUnsubscribe;
130
145
  /**
131
146
  * Overlay anchor resolver. Expected to be a direct projection of the
132
147
  * geometry facet (U4 — overlays derive from geometry, not DOM). Returns
@@ -267,6 +282,60 @@ export type OverlayAnchorQuery =
267
282
  | { kind: "page"; value: number }
268
283
  | { kind: "selection" };
269
284
 
285
+ /* ================================================================== */
286
+ /* PE2 overlay lanes */
287
+ /* ================================================================== */
288
+
289
+ export type UiOverlayLaneKind =
290
+ | "selection"
291
+ | "caret"
292
+ | "redlines"
293
+ | "field-scopes"
294
+ | "broad-scopes"
295
+ | "comments"
296
+ | "issues"
297
+ | "tables"
298
+ | "objects"
299
+ | "search"
300
+ | "presence";
301
+
302
+ export type UiOverlayLaneStatus =
303
+ | "resolved"
304
+ | "requires-rehydration"
305
+ | "unavailable";
306
+
307
+ export type UiOverlayLaneSource =
308
+ | "geometry"
309
+ | "workflow"
310
+ | "search"
311
+ | "awareness"
312
+ | "controller"
313
+ | "unavailable";
314
+
315
+ export interface UiOverlayLaneEntry {
316
+ readonly id: string;
317
+ readonly status: UiOverlayLaneStatus;
318
+ readonly anchor?: OverlayAnchorQuery;
319
+ readonly rects?: readonly GeometryRect[];
320
+ readonly reason?: string;
321
+ /**
322
+ * Lane-specific plain payload. Examples: peer identity for presence,
323
+ * issue severity, search rank, table/object classification. Kept plain
324
+ * so non-React consumers and debug runners can serialize snapshots.
325
+ */
326
+ readonly data?: Readonly<Record<string, unknown>>;
327
+ }
328
+
329
+ export interface UiOverlayLaneSnapshot {
330
+ readonly __mock?: true;
331
+ readonly kind: UiOverlayLaneKind;
332
+ readonly status: UiOverlayLaneStatus;
333
+ readonly entries: readonly UiOverlayLaneEntry[];
334
+ readonly revision: number;
335
+ readonly source: UiOverlayLaneSource;
336
+ readonly reason?: string;
337
+ }
338
+
270
339
  /* ================================================================== */
271
340
  /* Overlay visibility — U9 (state-classes X3) */
272
341
  /* ================================================================== */
@@ -453,6 +522,23 @@ export interface ApiV3UiOverlays {
453
522
  query: OverlayAnchorQuery,
454
523
  listener: UiListener<GeometryRect | null>,
455
524
  ): UiUnsubscribe;
525
+ /**
526
+ * PE2 overlay-lane read. Returns a plain snapshot for a mounted lane
527
+ * such as selection, caret, redlines, comments, tables, objects,
528
+ * search, or presence. When the host has not wired a lane yet, returns
529
+ * an explicit `unavailable` snapshot instead of measuring DOM or
530
+ * forcing geometry rehydration.
531
+ */
532
+ getLane(kind: UiOverlayLaneKind): UiOverlayLaneSnapshot;
533
+ /**
534
+ * PE2 overlay-lane subscription. The listener receives lane snapshots,
535
+ * never PM transactions. Presence/awareness updates must travel through
536
+ * this channel without invalidating L04 layout.
537
+ */
538
+ subscribeLane(
539
+ kind: UiOverlayLaneKind,
540
+ listener: UiListener<UiOverlayLaneSnapshot>,
541
+ ): UiUnsubscribe;
456
542
 
457
543
  /**
458
544
  * U9 · Composed overlay visibility. Merges the class-A policy from
@@ -43,6 +43,11 @@ export type {
43
43
  DebugSession,
44
44
  DebugAttachment,
45
45
  GeometryRect,
46
+ UiOverlayLaneEntry,
47
+ UiOverlayLaneKind,
48
+ UiOverlayLaneSnapshot,
49
+ UiOverlayLaneSource,
50
+ UiOverlayLaneStatus,
46
51
  } from "./_types.ts";
47
52
 
48
53
  // Chrome composition types (U5.b) — relocated to layer 10 in Slice 8.
@@ -42,6 +42,8 @@ import type {
42
42
  GeometryRect,
43
43
  OverlayAnchorQuery,
44
44
  OverlayVisibility,
45
+ UiOverlayLaneKind,
46
+ UiOverlayLaneSnapshot,
45
47
  UiListener,
46
48
  UiUnsubscribe,
47
49
  } from "./_types.ts";
@@ -195,6 +197,57 @@ export const subscribeQueryMetadata: ApiV3FnMetadata = {
195
197
  rwdReference: "§UI API § ui.overlays.subscribeQuery (KI-006 close). Per-query coalesced variant of `subscribe`. Registers a shared coarse `controller.subscribeOverlays` tick on the first query subscription; re-resolves each attached query on every tick via `getAnchor(query)` (memoized); fires per-query listeners only when the rect actually changes. Torn down on last unsubscribe. Listener receives the new `GeometryRect | null` value. Callers that need the full invalidation stream keep using `subscribe(listener)`.",
196
198
  };
197
199
 
200
+ // ----- PE2 overlay-lane contract -------------------------------------------
201
+
202
+ export const getLaneMetadata: ApiV3FnMetadata = {
203
+ name: "ui.overlays.getLane",
204
+ status: "live-with-adapter",
205
+ sourceLayer: "presentation",
206
+ liveEvidence: {
207
+ runnerTest: "test/api/v3/ui/overlays-lanes.test.ts",
208
+ commit: "refactor-10-pe2-overlay-lanes",
209
+ },
210
+ mockShape: {
211
+ deterministic: true,
212
+ seededFrom: "fixed",
213
+ shapeDescription:
214
+ "Unavailable UiOverlayLaneSnapshot with entries:[] and revision:0 when the mounted controller has not wired the requested PE2 overlay lane yet.",
215
+ carriesMockFlag: true,
216
+ },
217
+ uxIntent: { uiVisible: false },
218
+ agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "ui-overlays-lane-read" },
219
+ stateClass: "C-local",
220
+ persistsTo: "none",
221
+ rwdReference:
222
+ "§UI API § PE2 overlay lanes. Reads the mounted controller's lane snapshot for selection/caret/redlines/field-scopes/broad-scopes/comments/issues/tables/objects/search/presence. Returns explicit status:'unavailable' when the lane is not wired; never measures DOM or wakes layout.",
223
+ };
224
+
225
+ export const subscribeLaneMetadata: ApiV3FnMetadata = {
226
+ name: "ui.overlays.subscribeLane",
227
+ status: "live-with-adapter",
228
+ sourceLayer: "presentation",
229
+ liveEvidence: {
230
+ runnerTest: "test/api/v3/ui/overlays-lanes.test.ts",
231
+ commit: "refactor-10-pe2-overlay-lanes",
232
+ },
233
+ uxIntent: {
234
+ uiVisible: true,
235
+ expectsUxResponse: "surface-refresh",
236
+ expectedDelta: "overlay-lane subscriber attached; future lane snapshots propagate through the listener without PM document transactions",
237
+ },
238
+ agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "ui-overlays-lane-subscribe" },
239
+ stateClass: "C-local",
240
+ persistsTo: "none",
241
+ bidirectional: true,
242
+ subscriptionShape: {
243
+ eventType: "ui.overlays.lane_changed",
244
+ payloadType: "UiOverlayLaneSnapshot",
245
+ coalescing: "raf",
246
+ },
247
+ rwdReference:
248
+ "§UI API § PE2 overlay lanes. Adapter delegates to UiController.subscribeOverlayLane(kind, listener). Presence/awareness, selection, search, and review lane updates are observable UI state only; this method does not dispatch PM document transactions or invalidate L04 layout.",
249
+ };
250
+
198
251
  // ----- U9 overlay-visibility metadata (state-classes X3) -----
199
252
 
200
253
  export const getVisibilityMetadata: ApiV3FnMetadata = {
@@ -354,6 +407,21 @@ export function createOverlaysFamily(ctx: UiApiContext) {
354
407
  const queryChannels = new Map<string, QueryChannel>();
355
408
  let queryCoarseUnsubscribe: (() => void) | null = null;
356
409
 
410
+ function unavailableLane(
411
+ kind: UiOverlayLaneKind,
412
+ reason = "overlay lane is not wired on the active controller",
413
+ ): UiOverlayLaneSnapshot {
414
+ return {
415
+ __mock: true,
416
+ kind,
417
+ status: "unavailable",
418
+ entries: Object.freeze([]),
419
+ revision: 0,
420
+ source: "unavailable",
421
+ reason,
422
+ };
423
+ }
424
+
357
425
  function queryKey(q: OverlayAnchorQuery): string {
358
426
  // `selection` is a singleton kind with no `value` — key by kind alone.
359
427
  return q.kind === "selection" ? "selection" : `${q.kind}:${q.value}`;
@@ -560,6 +628,42 @@ export function createOverlaysFamily(ctx: UiApiContext) {
560
628
  };
561
629
  },
562
630
 
631
+ // ----- PE2 overlay lanes -----
632
+
633
+ getLane(kind: UiOverlayLaneKind): UiOverlayLaneSnapshot {
634
+ const hook = ctx.binding?.controller.getOverlayLane;
635
+ return hook
636
+ ? hook(kind)
637
+ : unavailableLane(kind);
638
+ },
639
+
640
+ subscribeLane(
641
+ kind: UiOverlayLaneKind,
642
+ listener: UiListener<UiOverlayLaneSnapshot>,
643
+ ): UiUnsubscribe {
644
+ const controller = ctx.binding?.controller;
645
+ if (!controller) {
646
+ throw new Error(
647
+ "ui.overlays.subscribeLane: no controller bound — call ui.session.bind(controller) first",
648
+ );
649
+ }
650
+ if (!controller.subscribeOverlayLane) {
651
+ throw new Error(
652
+ `ui.overlays.subscribeLane: controller of kind "${controller.kind}" did not provide a subscribeOverlayLane hook`,
653
+ );
654
+ }
655
+ const unsubscribe = controller.subscribeOverlayLane(kind, listener);
656
+ emitUxResponse(ctx.handle, {
657
+ apiFn: subscribeLaneMetadata.name,
658
+ intent: subscribeLaneMetadata.uxIntent.expectedDelta ?? "",
659
+ mockOrLive: "live-with-adapter",
660
+ uiVisible: true,
661
+ expectedDelta: subscribeLaneMetadata.uxIntent.expectedDelta,
662
+ actualDelta: { kind: "surface-refresh", payload: { subscribed: "ui.overlays.lane", lane: kind } },
663
+ });
664
+ return unsubscribe;
665
+ },
666
+
563
667
  // ----- U9 overlay-visibility (state-classes X3) -----
564
668
 
565
669
  getVisibility(kind: OverlayKind): OverlayVisibility {
@@ -65,6 +65,9 @@ export type {
65
65
  RuntimeLayoutDivergenceKind,
66
66
  RuntimeLayoutDivergence,
67
67
  RuntimePageFrame,
68
+ RuntimePageLocalStoryInstance,
69
+ RuntimeResolvedStoryField,
70
+ RuntimeStoryAnchoredObject,
68
71
  RuntimeBlockFragment,
69
72
  RuntimeLineBox,
70
73
  RuntimeNoteAllocation,