@beyondwork/docx-react-component 1.0.104 → 1.0.106

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +3 -0
  3. package/src/api/v3/_create.ts +9 -2
  4. package/src/api/v3/ai/_audit-reference.ts +28 -0
  5. package/src/api/v3/ai/_pe2-evidence.ts +419 -0
  6. package/src/api/v3/ai/attach.ts +22 -2
  7. package/src/api/v3/ai/bundle.ts +18 -6
  8. package/src/api/v3/ai/inspect.ts +12 -2
  9. package/src/api/v3/ai/replacement.ts +124 -0
  10. package/src/api/v3/index.ts +7 -0
  11. package/src/api/v3/ui/_types.ts +139 -0
  12. package/src/api/v3/ui/index.ts +9 -0
  13. package/src/api/v3/ui/overlays.ts +104 -0
  14. package/src/api/v3/ui/viewport.ts +97 -0
  15. package/src/model/layout/index.ts +3 -0
  16. package/src/model/layout/page-graph-types.ts +118 -0
  17. package/src/model/layout/runtime-page-graph-types.ts +13 -0
  18. package/src/runtime/document-runtime.ts +39 -18
  19. package/src/runtime/event-refresh-hints.ts +33 -6
  20. package/src/runtime/geometry/geometry-facet.ts +9 -1
  21. package/src/runtime/geometry/geometry-index.ts +461 -10
  22. package/src/runtime/geometry/geometry-types.ts +6 -0
  23. package/src/runtime/geometry/object-handles.ts +7 -4
  24. package/src/runtime/layout/layout-engine-instance.ts +2 -0
  25. package/src/runtime/layout/layout-engine-version.ts +36 -1
  26. package/src/runtime/layout/page-graph.ts +697 -10
  27. package/src/runtime/layout/paginated-layout-engine.ts +10 -0
  28. package/src/runtime/layout/project-block-fragments.ts +187 -8
  29. package/src/runtime/layout/public-facet.ts +236 -0
  30. package/src/runtime/prerender/graph-canonicalize.ts +14 -0
  31. package/src/runtime/workflow/index.ts +1 -0
  32. package/src/runtime/workflow/overlay-lanes.ts +228 -0
  33. package/src/ui/presence-overlay-lane.ts +131 -0
  34. package/src/ui/ui-controller-factory.ts +21 -0
@@ -16,6 +16,11 @@ 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 AiPe2EvidenceOptions,
22
+ type AiPe2DocumentEvidence,
23
+ } from "./_pe2-evidence.ts";
19
24
 
20
25
  export interface InspectDocumentResult {
21
26
  readonly documentId: string;
@@ -23,6 +28,7 @@ export interface InspectDocumentResult {
23
28
  readonly pageCount?: number;
24
29
  readonly semanticSummary: string;
25
30
  readonly kindDistribution: Readonly<Partial<Record<SemanticScopeKind, number>>>;
31
+ readonly pe2Evidence: AiPe2DocumentEvidence;
26
32
  }
27
33
 
28
34
  export const inspectDocumentMetadata: ApiV3FnMetadata = {
@@ -40,7 +46,7 @@ export const inspectDocumentMetadata: ApiV3FnMetadata = {
40
46
  boundedScope: "document",
41
47
  auditCategory: "document-inspect",
42
48
  contextPromptShape:
43
- "Summarize document by scope count + page count + semantic kind distribution.",
49
+ "Summarize document by scope count + page count + semantic kind distribution + PE2 geometry coverage evidence.",
44
50
  },
45
51
  stateClass: "A-canonical",
46
52
  persistsTo: "canonical",
@@ -79,7 +85,10 @@ function buildKindDistribution(
79
85
  return out;
80
86
  }
81
87
 
82
- export function createInspectFamily(runtime: RuntimeApiHandle) {
88
+ export function createInspectFamily(
89
+ runtime: RuntimeApiHandle,
90
+ pe2Evidence?: AiPe2EvidenceOptions,
91
+ ) {
83
92
  const compiler = createScopeCompilerService(runtime);
84
93
  return {
85
94
  inspectDocument(): InspectDocumentResult {
@@ -97,6 +106,7 @@ export function createInspectFamily(runtime: RuntimeApiHandle) {
97
106
  scopeCount: scopes.length,
98
107
  semanticSummary: summary,
99
108
  kindDistribution,
109
+ pe2Evidence: projectDocumentPe2Evidence(runtime, pe2Evidence),
100
110
  };
101
111
  },
102
112
 
@@ -33,6 +33,10 @@ import type {
33
33
  } from "../../../runtime/scopes/index.ts";
34
34
  import type { AIAction } from "../../../runtime/workflow/ai-action-policy.ts";
35
35
  import type { TextFormattingDirective } from "../../public-types.ts";
36
+ import {
37
+ projectAuditReference,
38
+ type AiActionAuditReference,
39
+ } from "./_audit-reference.ts";
36
40
 
37
41
  export interface ReplacementProposalInput {
38
42
  readonly targetScopeId: string;
@@ -123,7 +127,9 @@ export interface ApplyResult {
123
127
  readonly applied: boolean;
124
128
  readonly reason?: string;
125
129
  readonly blockers?: readonly string[];
130
+ readonly blockerDetails?: readonly ActionBlockerDetail[];
126
131
  readonly auditHint?: string;
132
+ readonly auditReference?: AiActionAuditReference;
127
133
  /**
128
134
  * Gap A (post-Slice-7 integration) — revision IDs authored during
129
135
  * the apply. Populated for suggest-mode (tracked insert + delete);
@@ -133,6 +139,19 @@ export interface ApplyResult {
133
139
  readonly authoredRevisionIds: readonly string[];
134
140
  }
135
141
 
142
+ export interface ActionBlockerDetail {
143
+ readonly code: string;
144
+ readonly category:
145
+ | "unsupported-scope-kind"
146
+ | "unsupported-operation"
147
+ | "unresolved-scope"
148
+ | "policy-or-guard";
149
+ readonly message: string;
150
+ readonly nextStep: string;
151
+ readonly scopeKind?: string;
152
+ readonly operation?: string;
153
+ }
154
+
136
155
  export interface ApplyReplacementScopeInput {
137
156
  readonly targetScopeId: string;
138
157
  readonly operation?: ReplacementOperationKind;
@@ -263,6 +282,97 @@ function projectValidationResult(
263
282
  };
264
283
  }
265
284
 
285
+ function blockerDetailFor(code: string): ActionBlockerDetail | null {
286
+ if (code.startsWith("scope-not-resolvable:")) {
287
+ return {
288
+ code,
289
+ category: "unresolved-scope",
290
+ message: "The target scope no longer resolves in the current document.",
291
+ nextStep:
292
+ "Call ai.resolveReference or ai.queryScopeAtPosition again, then retry with the returned handle's scopeId.",
293
+ };
294
+ }
295
+
296
+ if (!code.startsWith("compile-refused:")) return null;
297
+
298
+ const [, scopeKind = "unknown", ...rest] = code.split(":");
299
+ const suffix = rest.join(":");
300
+ if (suffix.startsWith("operation-not-implemented:")) {
301
+ const operation = suffix.slice("operation-not-implemented:".length);
302
+ return {
303
+ code,
304
+ category: "unsupported-operation",
305
+ scopeKind,
306
+ operation,
307
+ message: `The ${operation} operation is not implemented for ${scopeKind} scopes.`,
308
+ nextStep:
309
+ "Retry with operation:\"replace\" when that is acceptable, or attach an explanation/issue instead of mutating.",
310
+ };
311
+ }
312
+
313
+ if (scopeKind === "scope" && suffix === "multi-paragraph-replace-not-implemented") {
314
+ return {
315
+ code,
316
+ category: "unsupported-scope-kind",
317
+ scopeKind,
318
+ message: "Multi-paragraph scope replacement is not implemented.",
319
+ nextStep:
320
+ "Split the request into paragraph-scoped replacements, or attach an explanation/issue until the multi-paragraph planner ships.",
321
+ };
322
+ }
323
+
324
+ if (scopeKind === "table" || scopeKind === "table-row" || scopeKind === "table-cell") {
325
+ return {
326
+ code,
327
+ category: "unsupported-scope-kind",
328
+ scopeKind,
329
+ message: `Flat text replacement is not implemented for ${scopeKind} scopes because it can break table structure.`,
330
+ nextStep:
331
+ "Use ai.attachExplanation or ai.createIssue for now, or wait for the Layer 08 table-family replacement planner.",
332
+ };
333
+ }
334
+
335
+ if (scopeKind === "field") {
336
+ return {
337
+ code,
338
+ category: "unsupported-scope-kind",
339
+ scopeKind,
340
+ message: "Field result replacement is preserve-only because field text is computed from instructions.",
341
+ nextStep:
342
+ "Attach an explanation/issue, or use a future field-aware edit path that updates field instructions safely.",
343
+ };
344
+ }
345
+
346
+ if (scopeKind === "image" || scopeKind === "note") {
347
+ return {
348
+ code,
349
+ category: "unsupported-scope-kind",
350
+ scopeKind,
351
+ message: `Replacement is not implemented for ${scopeKind} scopes.`,
352
+ nextStep:
353
+ "Attach an explanation/issue rather than mutating until a scope-specific planner exists.",
354
+ };
355
+ }
356
+
357
+ return {
358
+ code,
359
+ category: "unsupported-scope-kind",
360
+ scopeKind,
361
+ message: `Replacement is not implemented for ${scopeKind} scopes.`,
362
+ nextStep:
363
+ "Use a supported paragraph-like scope, or attach an explanation/issue and route the unsupported planner to the owning layer.",
364
+ };
365
+ }
366
+
367
+ function projectBlockerDetails(
368
+ blockers: readonly string[],
369
+ ): readonly ActionBlockerDetail[] | undefined {
370
+ const details = blockers
371
+ .map((code) => blockerDetailFor(code))
372
+ .filter((detail): detail is ActionBlockerDetail => detail !== null);
373
+ return details.length > 0 ? Object.freeze(details) : undefined;
374
+ }
375
+
266
376
  export function createReplacementFamily(runtime: RuntimeApiHandle) {
267
377
  const compiler = createScopeCompilerService(runtime);
268
378
  return {
@@ -371,6 +481,9 @@ export function createReplacementFamily(runtime: RuntimeApiHandle) {
371
481
  expectedDelta: applyReplacementScopeMetadata.uxIntent.expectedDelta,
372
482
  });
373
483
 
484
+ const blockerDetails = projectBlockerDetails(
485
+ result.validation.blockedReasons,
486
+ );
374
487
  return {
375
488
  proposalId,
376
489
  applied: result.applied,
@@ -378,7 +491,11 @@ export function createReplacementFamily(runtime: RuntimeApiHandle) {
378
491
  ...(result.validation.blockedReasons.length > 0
379
492
  ? { blockers: Object.freeze([...result.validation.blockedReasons]) }
380
493
  : {}),
494
+ ...(blockerDetails ? { blockerDetails } : {}),
381
495
  ...(result.audit ? { auditHint: result.audit.actionId } : {}),
496
+ ...(result.audit
497
+ ? { auditReference: projectAuditReference(result.audit) }
498
+ : {}),
382
499
  authoredRevisionIds: result.authoredRevisionIds,
383
500
  };
384
501
  },
@@ -412,6 +529,9 @@ export function createReplacementFamily(runtime: RuntimeApiHandle) {
412
529
  expectedDelta: applyScopeActionMetadata.uxIntent.expectedDelta,
413
530
  });
414
531
 
532
+ const blockerDetails = projectBlockerDetails(
533
+ result.validation.blockedReasons,
534
+ );
415
535
  return {
416
536
  proposalId,
417
537
  applied: result.applied,
@@ -419,7 +539,11 @@ export function createReplacementFamily(runtime: RuntimeApiHandle) {
419
539
  ...(result.validation.blockedReasons.length > 0
420
540
  ? { blockers: Object.freeze([...result.validation.blockedReasons]) }
421
541
  : {}),
542
+ ...(blockerDetails ? { blockerDetails } : {}),
422
543
  ...(result.audit ? { auditHint: result.audit.actionId } : {}),
544
+ ...(result.audit
545
+ ? { auditReference: projectAuditReference(result.audit) }
546
+ : {}),
423
547
  authoredRevisionIds: result.authoredRevisionIds,
424
548
  };
425
549
  },
@@ -14,6 +14,13 @@
14
14
 
15
15
  export { createApiV3 } from "./_create.ts";
16
16
  export type { ApiV3, CreateApiV3Opts } from "./_create.ts";
17
+ export type {
18
+ AiPe2EvidenceOptions,
19
+ AiPe2OracleEvidence,
20
+ AiPe2OracleEvidenceProvider,
21
+ AiPe2OracleEvidenceProviderInput,
22
+ AiPe2OracleVerdict,
23
+ } from "./ai/_pe2-evidence.ts";
17
24
 
18
25
  export type {
19
26
  ApiStatus,
@@ -118,6 +118,21 @@ export interface UiController {
118
118
  * error rather than silently no-op.
119
119
  */
120
120
  readonly subscribeViewport?: (listener: UiListener<ViewportState>) => UiUnsubscribe;
121
+ /**
122
+ * PE2 page-residency policy read. L10 owns the observable residency
123
+ * shape; L11 owns realized DOM/PM attachment and L05 owns geometry
124
+ * caches. Omitting the hook makes `ui.viewport.getPageResidency`
125
+ * return an explicit unavailable snapshot.
126
+ */
127
+ readonly getPageResidency?: (pageIndex: number) => PageResidencySnapshot;
128
+ /**
129
+ * Page-residency subscription. Fires when the mounted surface changes
130
+ * whether a page is realized, cold, or evicted.
131
+ */
132
+ readonly subscribePageResidency?: (
133
+ pageIndex: number,
134
+ listener: UiListener<PageResidencySnapshot>,
135
+ ) => UiUnsubscribe;
121
136
  /**
122
137
  * Overlay invalidation subscription. Fires when geometry invalidation
123
138
  * ranges overlap attached overlay queries (U7). `ui.overlays.subscribe`
@@ -127,6 +142,21 @@ export interface UiController {
127
142
  readonly subscribeOverlays?: (
128
143
  listener: UiListener<OverlayAnchorQuery>,
129
144
  ) => UiUnsubscribe;
145
+ /**
146
+ * PE2 overlay-lane snapshot resolver. This is the mounted-surface lane
147
+ * contract for selection/caret/redline/scope/comment/table/object/search
148
+ * and presence overlays. The bind-side supplies already-projected lane
149
+ * entries; L10 owns the observable shape, not layout or geometry caches.
150
+ */
151
+ readonly getOverlayLane?: (kind: UiOverlayLaneKind) => UiOverlayLaneSnapshot;
152
+ /**
153
+ * PE2 overlay-lane subscription. Must fan out lane changes without
154
+ * dispatching PM document transactions or invalidating L04 layout.
155
+ */
156
+ readonly subscribeOverlayLane?: (
157
+ kind: UiOverlayLaneKind,
158
+ listener: UiListener<UiOverlayLaneSnapshot>,
159
+ ) => UiUnsubscribe;
130
160
  /**
131
161
  * Overlay anchor resolver. Expected to be a direct projection of the
132
162
  * geometry facet (U4 — overlays derive from geometry, not DOM). Returns
@@ -219,6 +249,29 @@ export interface ViewportState {
219
249
  readonly devicePixelRatio: number;
220
250
  }
221
251
 
252
+ export type PageResidency = "realized" | "cold" | "evicted";
253
+
254
+ export type RehydrationStatus =
255
+ | "available"
256
+ | "requires-rehydration"
257
+ | "unavailable";
258
+
259
+ export type PageResidencySource =
260
+ | "controller"
261
+ | "unavailable";
262
+
263
+ export interface PageResidencySnapshot {
264
+ readonly __mock?: true;
265
+ /** 0-based page index, matching GeometryFacet.getPage(index). */
266
+ readonly pageIndex: number;
267
+ readonly pageId?: string;
268
+ readonly residency: PageResidency;
269
+ readonly status: RehydrationStatus;
270
+ readonly revision: number;
271
+ readonly source: PageResidencySource;
272
+ readonly reason?: string;
273
+ }
274
+
222
275
  export type ScrollTargetBehavior = "auto" | "smooth" | "instant";
223
276
 
224
277
  export type ScrollTarget =
@@ -267,6 +320,60 @@ export type OverlayAnchorQuery =
267
320
  | { kind: "page"; value: number }
268
321
  | { kind: "selection" };
269
322
 
323
+ /* ================================================================== */
324
+ /* PE2 overlay lanes */
325
+ /* ================================================================== */
326
+
327
+ export type UiOverlayLaneKind =
328
+ | "selection"
329
+ | "caret"
330
+ | "redlines"
331
+ | "field-scopes"
332
+ | "broad-scopes"
333
+ | "comments"
334
+ | "issues"
335
+ | "tables"
336
+ | "objects"
337
+ | "search"
338
+ | "presence";
339
+
340
+ export type UiOverlayLaneStatus =
341
+ | "resolved"
342
+ | "requires-rehydration"
343
+ | "unavailable";
344
+
345
+ export type UiOverlayLaneSource =
346
+ | "geometry"
347
+ | "workflow"
348
+ | "search"
349
+ | "awareness"
350
+ | "controller"
351
+ | "unavailable";
352
+
353
+ export interface UiOverlayLaneEntry {
354
+ readonly id: string;
355
+ readonly status: UiOverlayLaneStatus;
356
+ readonly anchor?: OverlayAnchorQuery;
357
+ readonly rects?: readonly GeometryRect[];
358
+ readonly reason?: string;
359
+ /**
360
+ * Lane-specific plain payload. Examples: peer identity for presence,
361
+ * issue severity, search rank, table/object classification. Kept plain
362
+ * so non-React consumers and debug runners can serialize snapshots.
363
+ */
364
+ readonly data?: Readonly<Record<string, unknown>>;
365
+ }
366
+
367
+ export interface UiOverlayLaneSnapshot {
368
+ readonly __mock?: true;
369
+ readonly kind: UiOverlayLaneKind;
370
+ readonly status: UiOverlayLaneStatus;
371
+ readonly entries: readonly UiOverlayLaneEntry[];
372
+ readonly revision: number;
373
+ readonly source: UiOverlayLaneSource;
374
+ readonly reason?: string;
375
+ }
376
+
270
377
  /* ================================================================== */
271
378
  /* Overlay visibility — U9 (state-classes X3) */
272
379
  /* ================================================================== */
@@ -383,6 +490,21 @@ export interface ApiV3UiSurface {
383
490
  export interface ApiV3UiViewport {
384
491
  get(): ViewportState;
385
492
  subscribe(listener: UiListener<ViewportState>): UiUnsubscribe;
493
+ /**
494
+ * Read L10's page-residency policy for a 0-based page index. When no
495
+ * mounted controller has supplied a residency policy yet, returns an
496
+ * explicit `unavailable` mock snapshot instead of probing geometry or
497
+ * realizing DOM.
498
+ */
499
+ getPageResidency(pageIndex: number): PageResidencySnapshot;
500
+ /**
501
+ * Subscribe to residency changes for a 0-based page index. The listener
502
+ * receives plain snapshots; realization/teardown stays owned by L11.
503
+ */
504
+ subscribePageResidency(
505
+ pageIndex: number,
506
+ listener: UiListener<PageResidencySnapshot>,
507
+ ): UiUnsubscribe;
386
508
 
387
509
  /**
388
510
  * Scroll the mounted surface to a specific 1-based page number.
@@ -453,6 +575,23 @@ export interface ApiV3UiOverlays {
453
575
  query: OverlayAnchorQuery,
454
576
  listener: UiListener<GeometryRect | null>,
455
577
  ): UiUnsubscribe;
578
+ /**
579
+ * PE2 overlay-lane read. Returns a plain snapshot for a mounted lane
580
+ * such as selection, caret, redlines, comments, tables, objects,
581
+ * search, or presence. When the host has not wired a lane yet, returns
582
+ * an explicit `unavailable` snapshot instead of measuring DOM or
583
+ * forcing geometry rehydration.
584
+ */
585
+ getLane(kind: UiOverlayLaneKind): UiOverlayLaneSnapshot;
586
+ /**
587
+ * PE2 overlay-lane subscription. The listener receives lane snapshots,
588
+ * never PM transactions. Presence/awareness updates must travel through
589
+ * this channel without invalidating L04 layout.
590
+ */
591
+ subscribeLane(
592
+ kind: UiOverlayLaneKind,
593
+ listener: UiListener<UiOverlayLaneSnapshot>,
594
+ ): UiUnsubscribe;
456
595
 
457
596
  /**
458
597
  * U9 · Composed overlay visibility. Merges the class-A policy from
@@ -24,6 +24,10 @@ export type {
24
24
  UiScopeListFilter,
25
25
  UiScopeRailOptions,
26
26
  ViewportState,
27
+ PageResidency,
28
+ PageResidencySnapshot,
29
+ PageResidencySource,
30
+ RehydrationStatus,
27
31
  ScrollTarget,
28
32
  ScrollTargetBehavior,
29
33
  ScrollToPageResult,
@@ -43,6 +47,11 @@ export type {
43
47
  DebugSession,
44
48
  DebugAttachment,
45
49
  GeometryRect,
50
+ UiOverlayLaneEntry,
51
+ UiOverlayLaneKind,
52
+ UiOverlayLaneSnapshot,
53
+ UiOverlayLaneSource,
54
+ UiOverlayLaneStatus,
46
55
  } from "./_types.ts";
47
56
 
48
57
  // 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 {