@beyondwork/docx-react-component 1.0.96 → 1.0.98

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 +47 -19
  3. package/src/api/v3/ui/_types.ts +11 -21
  4. package/src/api/v3/ui/chrome.ts +8 -9
  5. package/src/api/v3/ui/debug.ts +15 -77
  6. package/src/api/v3/ui/overlays-visibility.ts +9 -10
  7. package/src/api/v3/ui/overlays.ts +8 -75
  8. package/src/io/ooxml/parse-main-document.ts +30 -0
  9. package/src/io/ooxml/parse-picture.ts +14 -0
  10. package/src/io/ooxml/parse-shapes.ts +41 -1
  11. package/src/io/ooxml/payload-signature.ts +101 -0
  12. package/src/model/canonical-document.ts +17 -0
  13. package/src/runtime/layout/layout-engine-version.ts +14 -1
  14. package/src/runtime/layout/page-story-resolver.ts +1 -0
  15. package/src/runtime/layout/paginated-layout-engine.ts +26 -10
  16. package/src/runtime/surface-projection.ts +114 -12
  17. package/src/session/export/stateful-export-pipeline.ts +18 -2
  18. package/src/session/export/stateful-export.ts +21 -1
  19. package/src/ui/WordReviewEditor.tsx +6 -10
  20. package/src/ui/editor-command-bag.ts +2 -0
  21. package/src/ui/ui-controller-factory.ts +2 -2
  22. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +11 -25
  23. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +2 -2
  24. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +1 -1
  25. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +22 -220
  26. package/src/ui-tailwind/debug/README.md +12 -50
  27. package/src/ui-tailwind/debug/tw-debug-overlay.tsx +6 -6
  28. package/src/ui-tailwind/debug/tw-debug-presentation.tsx +9 -20
  29. package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +5 -6
  30. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +1 -4
  31. package/src/ui-tailwind/editor-surface/picture-effects.ts +96 -0
  32. package/src/ui-tailwind/editor-surface/pm-schema.ts +89 -62
  33. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +205 -0
  34. package/src/ui-tailwind/editor-surface/preserve-position.ts +12 -2
  35. package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +190 -0
  36. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +67 -56
  37. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +83 -20
  38. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +114 -4
  39. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +5 -0
  40. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +3 -0
  41. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +3 -0
  42. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
  43. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +26 -0
  44. package/src/ui-tailwind/theme/editor-theme.css +18 -11
  45. package/src/ui-tailwind/tw-review-workspace.tsx +15 -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.96",
4
+ "version": "1.0.98",
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": [
@@ -81,6 +81,15 @@ export type { CanonicalDocumentEnvelope };
81
81
  export type * from "../model/chart-types.ts";
82
82
  export type { ResolvedTheme } from "../model/canonical-document.ts";
83
83
 
84
+ /**
85
+ * Type-only re-export of the canonical `BlockNode` union. Exposes the
86
+ * shape to presentation-layer consumers that need it for identity caching
87
+ * (refactor/11b Slice A) without forcing them to reach into
88
+ * `src/model/**` directly — which is flagged by the layer-11 boundary
89
+ * guard. This is a TYPE-ONLY re-export: no runtime dependency is added.
90
+ */
91
+ export type { BlockNode } from "../model/canonical-document.ts";
92
+
84
93
  /**
85
94
  * Workflow-rail types re-exported for the Layer-11 scope-rail + scope-
86
95
  * card overlays (refactor/11 Slice 6 retirement). `WorkflowFacet` +
@@ -1198,6 +1207,8 @@ export interface SurfaceDrawingAnchor {
1198
1207
  * degree (OOXML `a:xfrm a:rot`).
1199
1208
  */
1200
1209
  export interface SurfacePictureEffects {
1210
+ /** DrawingML a:lum brightness/contrast adjustments. Values are OOXML fixed percentages. */
1211
+ lum?: { bright?: number; contrast?: number };
1201
1212
  srcRect?: { top: number; bottom: number; left: number; right: number };
1202
1213
  rotation?: number;
1203
1214
  flipH?: boolean;
@@ -1390,10 +1401,32 @@ export type SurfaceInlineSegment =
1390
1401
  };
1391
1402
  line?: { color?: string; widthEmu?: number; noLine?: boolean };
1392
1403
  isTextBox?: boolean;
1404
+ /** Text box body layout from `wps:bodyPr` / `a:bodyPr`. */
1405
+ textBoxBody?: SurfaceTextBoxBodyProperties;
1393
1406
  /** First-paragraph plain-text preview when `isTextBox` is true. */
1394
1407
  txbxText?: string;
1408
+ /** Run-level marks from the first text-box text run, when available. */
1409
+ txbxMarks?: SurfaceTextMark[];
1410
+ /** Run-level visual attrs from the first text-box text run, when available. */
1411
+ txbxMarkAttrs?: {
1412
+ backgroundColor?: string;
1413
+ charSpacing?: number;
1414
+ kerning?: number;
1415
+ textFill?: string;
1416
+ fontFamily?: string;
1417
+ fontSize?: number;
1418
+ textColor?: string;
1419
+ };
1395
1420
  };
1396
1421
 
1422
+ export interface SurfaceTextBoxBodyProperties {
1423
+ anchor?: "t" | "ctr" | "b";
1424
+ insetLeftEmu?: number;
1425
+ insetTopEmu?: number;
1426
+ insetRightEmu?: number;
1427
+ insetBottomEmu?: number;
1428
+ }
1429
+
1397
1430
  export interface SurfaceTableCellSnapshot {
1398
1431
  gridSpan: number;
1399
1432
  verticalMerge: "restart" | "continue" | null;
@@ -2607,6 +2640,20 @@ export interface AddScopeResult {
2607
2640
  export interface ExportDocxOptions {
2608
2641
  fileName?: string;
2609
2642
  reason?: string;
2643
+ /**
2644
+ * Optional signer for the workflow-payload `<bw:signature>` block
2645
+ * (coord-06 cleanup §3). When present, `buildWorkflowPayloadParts`
2646
+ * computes the signature over the canonicalized payload XML (bw-
2647
+ * canon/1; `<bw:signature>` excluded from hashing) and injects a
2648
+ * `<bw:signature>` element inside `<bw:workflowPayload>`. Reload
2649
+ * passes the extracted signature to `collabSession.attach({ payload
2650
+ * })` where the tamper-gate transitions to `verified` / `tampered`.
2651
+ *
2652
+ * Hosts that don't pass a signer get the trust-on-first-use
2653
+ * (unsigned) behaviour that shipped before — the payload part is
2654
+ * written without a `<bw:signature>` element.
2655
+ */
2656
+ signer?: import("../io/ooxml/payload-signature.ts").PayloadSigner;
2610
2657
  /**
2611
2658
  * Controls the browser download fallback used by the mounted
2612
2659
  * `WordReviewEditor` ref when no host/datastore `saveExport` adapter is
@@ -5758,27 +5805,8 @@ export interface WordReviewEditorProps {
5758
5805
  * unless this prop suppresses it).
5759
5806
  */
5760
5807
  commandPaletteDisabled?: boolean;
5761
- /**
5762
- * Phase Q runtime-embedded debug UX (refactor/11 Slice 7 + refactor/10
5763
- * Slice 5). Default is `"off"` — the editor mounts no debug chrome
5764
- * and silently ignores the `ui.debug.attach()` hook. Flip to
5765
- * `"top-bar"` to mount a slim status bar showing the active session
5766
- * + scope telemetry, or to `"full"` to mount the tabbed overlay with
5767
- * inspector / events / REPL panes.
5768
- *
5769
- * **CLAUDE.md Protected Invariant** — the default MUST be `"off"`.
5770
- * Predecessor debug-preview props (`showUnsupportedObjectPreviews`,
5771
- * `unsupportedPreviewsPolicy`) have regressed their safe defaults
5772
- * multiple times; keep this one strict.
5773
- */
5774
- debugMode?: DebugMode;
5775
5808
  }
5776
5809
 
5777
- /**
5778
- * Phase Q debug-UX visibility mode. Consumed by
5779
- * `WordReviewEditorProps.debugMode` + `src/ui-tailwind/debug/**`.
5780
- */
5781
- export type DebugMode = "off" | "top-bar" | "full";
5782
5810
 
5783
5811
  /**
5784
5812
  * Selection context handed to host-delegated shortcut callbacks
@@ -18,7 +18,8 @@
18
18
  * U3 — selection + viewport are the only bidirectional channels.
19
19
  * U4 — overlays derive anchors from geometry, never DOM.
20
20
  * U5 — chrome posture composed once per read.
21
- * U6 — debug attachment opt-in; gated by `debugMode` prop, default "off".
21
+ * U6 — runtime debug attachment is production-disabled; local diagnostics
22
+ * use the mounted keyboard shortcut.
22
23
  * U7 — subscribers fire on runtime state changes + coalesced rAF.
23
24
  * U8 — no access to internal controllers.
24
25
  */
@@ -147,14 +148,13 @@ export interface UiController {
147
148
  readonly getOverlayRects?: (query: OverlayAnchorQuery) => readonly GeometryRect[];
148
149
  /**
149
150
  * Host-provided chrome posture slice — the fields that live outside the
150
- * runtime (host props like reviewMode / markupDisplay / debugMode /
151
- * chromePreset). `ui.chrome.getPosture` composes this with the runtime's
151
+ * runtime (host props like reviewMode / markupDisplay / chromePreset).
152
+ * `ui.chrome.getPosture` composes this with the runtime's
152
153
  * guard snapshot + session/render state into the final ChromePosture.
153
154
  *
154
155
  * Default values when the hook is missing or returns undefined:
155
156
  * reviewMode — "observer"
156
157
  * markupDisplay — "final"
157
- * debugMode — "off" (gated per U6; default MUST remain "off")
158
158
  * chromePreset — undefined
159
159
  */
160
160
  readonly getHostPosture?: () => ChromeHostPosture | undefined;
@@ -172,14 +172,6 @@ export interface UiController {
172
172
  readonly subscribeChrome?: (
173
173
  listener: UiListener<ChromePosture>,
174
174
  ) => UiUnsubscribe;
175
- /**
176
- * Debug attachment. The bind-side wires the runtime's telemetry bus and
177
- * projector to the mounted debug surface (Phase Q — see
178
- * `src/ui-tailwind/debug/`). Returns a cleanup function. Omitting the
179
- * hook causes `ui.debug.attach` to throw rather than silently ignoring
180
- * the request.
181
- */
182
- readonly attachDebug?: (session: DebugSession) => () => void;
183
175
  }
184
176
 
185
177
  /**
@@ -317,6 +309,7 @@ export interface ChromePosture {
317
309
  readonly readOnly: boolean;
318
310
  readonly reviewMode: ChromeReviewMode;
319
311
  readonly markupDisplay: ChromeMarkupDisplay;
312
+ /** Always "off" in production; runtime debug chrome is not API-enabled. */
320
313
  readonly debugMode: ChromeDebugMode;
321
314
  readonly chromePreset?: string;
322
315
  readonly blockedReasons: readonly string[];
@@ -346,8 +339,6 @@ export interface ChromeSurface {
346
339
  export interface ChromeHostPosture {
347
340
  readonly reviewMode?: ChromeReviewMode;
348
341
  readonly markupDisplay?: ChromeMarkupDisplay;
349
- /** MUST default to "off" at the caller level (U6); see CLAUDE.md Protected Invariants. */
350
- readonly debugMode?: ChromeDebugMode;
351
342
  readonly chromePreset?: string;
352
343
  }
353
344
 
@@ -466,9 +457,8 @@ export interface ApiV3UiOverlays {
466
457
  /**
467
458
  * U9 · Composed overlay visibility. Merges the class-A policy from
468
459
  * `handle.getVisibilityPolicy(kind)` with the class-C local preference
469
- * stored per UI API instance. `"debug-panel"` has an extra gate:
470
- * local preference is ignored when `ChromePosture.debugMode === "off"`
471
- * (CLAUDE.md Protected Invariant).
460
+ * stored per UI API instance. `"debug-panel"` is production-forced off;
461
+ * local preference and legacy host posture values cannot unlock it.
472
462
  */
473
463
  getVisibility(kind: OverlayKind): OverlayVisibility;
474
464
  /** Set the per-session local preference (class C). */
@@ -478,9 +468,9 @@ export interface ApiV3UiOverlays {
478
468
  /**
479
469
  * Subscribe to visibility changes for a specific `OverlayKind`. Fires
480
470
  * when the local preference for that kind changes (via
481
- * `setLocalPreference` / `resetLocalPreference`). Policy changes and
482
- * `debugMode` changes are not fanned out in this slice consumers
483
- * that need those should re-read on their own cadence. Returns an
471
+ * `setLocalPreference` / `resetLocalPreference`). Policy changes are
472
+ * also fanned out through the handle when the runtime exposes the policy
473
+ * subscription hook. Returns an
484
474
  * unsubscribe function.
485
475
  */
486
476
  subscribeVisibility(
@@ -493,7 +483,7 @@ export interface ApiV3UiChrome {
493
483
  /**
494
484
  * Runtime-sourced chrome posture (U5.a). Narrow slice — effectiveMode,
495
485
  * documentMode, readOnly, blocked reasons, plus the host-provided
496
- * reviewMode / markupDisplay / debugMode / chromePreset.
486
+ * reviewMode / markupDisplay / chromePreset. debugMode is always "off".
497
487
  */
498
488
  getPosture(): ChromePosture;
499
489
  /**
@@ -7,10 +7,10 @@
7
7
  * `handle.getInteractionGuardSnapshot()` and the
8
8
  * mode/readOnly from `handle.getRenderSnapshot()`;
9
9
  * overlays the host-provided slice (reviewMode,
10
- * markupDisplay, debugMode, chromePreset) via
10
+ * markupDisplay, chromePreset) via
11
11
  * `controller.getHostPosture()`. Host defaults
12
- * applied at this layer debugMode defaults to
13
- * "off" per U6 + CLAUDE.md Protected Invariants.
12
+ * applied at this layer; debugMode is forced
13
+ * "off" in production.
14
14
  * - getPinnedSurfaces — live-with-adapter. Delegates to
15
15
  * `controller.getPinnedSurfaces()`; returns [] when
16
16
  * unbound or the hook is absent. The existing
@@ -59,7 +59,7 @@ export const getPostureMetadata: ApiV3FnMetadata = {
59
59
  stateClass: "C-local",
60
60
  persistsTo: "none",
61
61
  rwdReference:
62
- "§UI API § ui.chrome.getPosture. Composes handle.getInteractionGuardSnapshot() + handle.getRenderSnapshot() with controller.getHostPosture() at a single site. debugMode defaults 'off' per U6.",
62
+ "§UI API § ui.chrome.getPosture. Composes handle.getInteractionGuardSnapshot() + handle.getRenderSnapshot() with controller.getHostPosture() at a single site. debugMode is production-forced 'off'.",
63
63
  };
64
64
 
65
65
  export const getCompositionMetadata: ApiV3FnMetadata = {
@@ -103,7 +103,7 @@ export const subscribeMetadata: ApiV3FnMetadata = {
103
103
  commit: "refactor-07-slice-2",
104
104
  },
105
105
  // Stream-form bidirectional channel (U5 + U7). Posture changes
106
- // (guard-snapshot updates, host posture flips like debugMode toggle)
106
+ // (guard-snapshot updates, host posture flips like reviewMode)
107
107
  // fan out through this listener. Coalescing is microtask — posture
108
108
  // composes at most once per render tick, no rAF batching needed.
109
109
  uxIntent: {
@@ -328,13 +328,12 @@ export function createChromeFamily(ctx: UiApiContext) {
328
328
  : "edit";
329
329
  const readOnly = render?.readOnly === true;
330
330
 
331
- // Host-provided slice — reviewMode / markupDisplay / debugMode /
332
- // chromePreset. Defaults applied here; U6 debugMode MUST default
333
- // to "off" (CLAUDE.md Protected Invariants).
331
+ // Host-provided slice — reviewMode / markupDisplay / chromePreset.
332
+ // Runtime debug chrome is not host-controllable in production.
334
333
  const host = ctx.binding?.controller.getHostPosture?.() ?? undefined;
335
334
  const reviewMode = host?.reviewMode ?? "observer";
336
335
  const markupDisplay = host?.markupDisplay ?? "final";
337
- const debugMode = host?.debugMode ?? "off";
336
+ const debugMode = "off";
338
337
  const chromePreset = host?.chromePreset;
339
338
 
340
339
  return {
@@ -1,28 +1,14 @@
1
1
  /**
2
2
  * @endStateApi v3 — `ui.debug` family (layer 10).
3
3
  *
4
- * Slice 5 wiring:
5
- * - attach — live-with-adapter. Delegates to the bound controller's
6
- * `attachDebug` hook, which on the bind-side wires
7
- * `runtime.debug.bus` + `runtime.debug.getSnapshot` to the
8
- * mounted debug surface. Returns a DebugAttachment whose
9
- * `detach` invokes the controller-provided cleanup and
10
- * clears the UI API's attachment record.
11
- * - detach — live-with-adapter. Ends the current attachment (if any)
12
- * by invoking its controller-provided cleanup. Idempotent
13
- * — calling detach without an attachment is a no-op.
4
+ * Production hardening:
5
+ * - attach — intentionally disabled. Runtime debug chrome is no longer
6
+ * activatable through the public UI API.
7
+ * - detach — retained as an idempotent no-op for callers that defensively
8
+ * clean up a previously attempted attachment.
14
9
  *
15
- * Phase Q (runtime-embedded debug UX at `src/ui-tailwind/debug/**`) is the
16
- * presentation consumer of this surface. Per refactor plan 10 risk register
17
- * #3 and §9 finishing-development-branch discipline, Slice 5 ships the
18
- * substrate; the Phase Q React components + `debugMode` public prop +
19
- * `test/ui/debug-mode-visibility-invariant.test.ts` land in a follow-up.
20
- * `src/ui-tailwind/debug/README.md` documents the reserved scope.
21
- *
22
- * Contract U6 — debug attachment is opt-in and scoped. Default state:
23
- * no debug UX. The `debugMode` prop gate (to-be-added) defaults to
24
- * `"off"`. CLAUDE.md Protected Invariants flag this has regressed
25
- * multiple times.
10
+ * The only supported local diagnostic surface in the mounted editor is the
11
+ * runtime REPL keyboard shortcut.
26
12
  */
27
13
 
28
14
  import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
@@ -42,7 +28,7 @@ export const attachMetadata: ApiV3FnMetadata = {
42
28
  stateClass: "C-local",
43
29
  persistsTo: "none",
44
30
  rwdReference:
45
- "§UI API § ui.debug.attach. Adapter delegates to UiController.attachDebug which on the bind-side wires runtime.debug.bus + getSnapshot to the mounted debug surface. Throws when no controller is bound or the hook is absent.",
31
+ "§UI API § ui.debug.attach. Production-disabled hard stop: always throws and never routes to a controller hook. Use the mounted runtime REPL keyboard shortcut for local diagnostics.",
46
32
  };
47
33
 
48
34
  export const detachMetadata: ApiV3FnMetadata = {
@@ -58,67 +44,19 @@ export const detachMetadata: ApiV3FnMetadata = {
58
44
  stateClass: "C-local",
59
45
  persistsTo: "none",
60
46
  rwdReference:
61
- "§UI API § ui.debug.detach. Idempotent invokes the controller-provided cleanup from the active attachment (if any) and clears the record.",
47
+ "§UI API § ui.debug.detach. Production-disabled companion no-op retained for defensive cleanup compatibility.",
62
48
  };
63
49
 
64
- interface DebugAttachmentState {
65
- session: DebugSession;
66
- cleanup: () => void;
67
- }
68
-
69
- export function createDebugFamily(ctx: UiApiContext) {
70
- // Per-instance attachment state. One UI API holds at most one active
71
- // debug attachment; re-attaching tears down the prior attachment first.
72
- let current: DebugAttachmentState | null = null;
73
-
74
- function detachInternal(): void {
75
- if (current) {
76
- try {
77
- current.cleanup();
78
- } finally {
79
- current = null;
80
- }
81
- }
82
- }
50
+ const DEBUG_ATTACH_DISABLED_MESSAGE =
51
+ "ui.debug.attach is disabled in production; use the runtime REPL keyboard shortcut instead.";
83
52
 
53
+ export function createDebugFamily(_ctx: UiApiContext) {
84
54
  return {
85
- attach(session: DebugSession): DebugAttachment {
86
- const controller = ctx.binding?.controller;
87
- if (!controller) {
88
- throw new Error(
89
- "ui.debug.attach: no controller bound — call ui.session.bind(controller) first",
90
- );
91
- }
92
- if (!controller.attachDebug) {
93
- throw new Error(
94
- `ui.debug.attach: controller of kind "${controller.kind}" did not provide an attachDebug hook`,
95
- );
96
- }
97
- // Tear down any prior attachment so re-attaching does not leak
98
- // subscriptions or telemetry-bus listeners.
99
- detachInternal();
100
- const cleanup = controller.attachDebug(session);
101
- current = { session, cleanup };
102
- // NOTE: `DebugAttachment.detach` is aliased to `detachInternal`,
103
- // which detaches **whatever is currently attached** — not
104
- // specifically the session this attachment was created with. At
105
- // most one attachment is live per UI API instance, so this is
106
- // semantically correct under the "single attachment at a time"
107
- // model. Callers that hold onto an older `attachment` ref and
108
- // call its `.detach()` after a re-attach will tear down the
109
- // NEWER attachment. Typical hosts create + detach in lockstep
110
- // (attach on mount, detach on unmount), so this is fine; the
111
- // debug service's RPC driver re-attaches on every session, which
112
- // matches this model.
113
- return {
114
- session,
115
- detach: detachInternal,
116
- };
55
+ attach(_session: DebugSession): DebugAttachment {
56
+ throw new Error(DEBUG_ATTACH_DISABLED_MESSAGE);
117
57
  },
118
58
  detach(): void {
119
- // Idempotent no-op when nothing is attached. See U6; the default
120
- // state is "no debug UX", so repeated detach calls must stay safe.
121
- detachInternal();
59
+ // Idempotent compatibility no-op.
122
60
  },
123
61
  };
124
62
  }
@@ -20,8 +20,8 @@
20
20
  * policy = handle.getVisibilityPolicy(kind) // may be null
21
21
  * pref = localPrefs.get(kind) // may be undefined
22
22
  *
23
- * # debug-panel special case: debugMode gate wins over everything.
24
- * if kind === "debug-panel" and debugMode === "off":
23
+ * # debug-panel special case: production debug gate wins over everything.
24
+ * if kind === "debug-panel":
25
25
  * → { state: "forced-off", reason: "policy-enforcement" }
26
26
  *
27
27
  * # Policy enforcement is a hard override.
@@ -39,7 +39,7 @@
39
39
  * → defaultOn ? visible : hidden, reason: "policy-default"
40
40
  *
41
41
  * Subscribe fan-out is local-preference-only in this slice. Policy
42
- * changes and debugMode changes do not wake listeners yet — consumers
42
+ * changes do not wake listeners yet — consumers
43
43
  * that depend on either re-read visibility on their own cadence.
44
44
  * Follow-up slice will subscribe to the L06 policy telemetry bus and
45
45
  * fan out on `workflow.visibility_policy_changed`.
@@ -85,13 +85,12 @@ export function createOverlaysVisibilityState(): OverlaysVisibilityState {
85
85
  }
86
86
 
87
87
  /**
88
- * Read the current effective debugMode from the bound controller's host
89
- * posture slice. Mirrors the default handling in `chrome.ts::getPosture`
90
- * — unbound / no hook / no debugMode field all resolve to `"off"` per
91
- * U6 + CLAUDE.md Protected Invariants.
88
+ * Runtime debug chrome is production-disabled. Legacy host posture values
89
+ * must not unlock the debug panel.
92
90
  */
93
91
  function readDebugMode(ctx: UiApiContext): "off" | "top-bar" | "full" {
94
- return ctx.binding?.controller.getHostPosture?.()?.debugMode ?? "off";
92
+ void ctx;
93
+ return "off";
95
94
  }
96
95
 
97
96
  function readPolicy(
@@ -115,8 +114,8 @@ export function composeOverlayVisibility(
115
114
  state: OverlaysVisibilityState,
116
115
  kind: OverlayKind,
117
116
  ): OverlayVisibility {
118
- // debug-panel: when debugMode is "off" the panel is forced off
119
- // regardless of policy or preference (CLAUDE.md Protected Invariant).
117
+ // debug-panel: runtime debug chrome is forced off in production
118
+ // regardless of policy, preference, or legacy host posture.
120
119
  if (kind === "debug-panel" && readDebugMode(ctx) === "off") {
121
120
  return { state: "forced-off", reason: "policy-enforcement" };
122
121
  }
@@ -39,7 +39,6 @@
39
39
 
40
40
  import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
41
41
  import type {
42
- ChromePosture,
43
42
  GeometryRect,
44
43
  OverlayAnchorQuery,
45
44
  OverlayVisibility,
@@ -210,7 +209,7 @@ export const getVisibilityMetadata: ApiV3FnMetadata = {
210
209
  agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "ui-overlays-read" },
211
210
  stateClass: "C-local",
212
211
  persistsTo: "none",
213
- rwdReference: "§UI API § ui.overlays.getVisibility (U9). Single composition site merging class-A policy (handle.getVisibilityPolicy) with class-C local preference. debug-panel is additionally gated on ChromePosture.debugMode per CLAUDE.md Protected Invariant.",
212
+ rwdReference: "§UI API § ui.overlays.getVisibility (U9). Single composition site merging class-A policy (handle.getVisibilityPolicy) with class-C local preference. debug-panel is production-forced off.",
214
213
  };
215
214
 
216
215
  export const setLocalPreferenceMetadata: ApiV3FnMetadata = {
@@ -273,7 +272,7 @@ export const subscribeVisibilityMetadata: ApiV3FnMetadata = {
273
272
  payloadType: "OverlayVisibility",
274
273
  coalescing: "microtask",
275
274
  },
276
- rwdReference: "§UI API § ui.overlays.subscribeVisibility (U9). Fires on THREE channels: (a) local-preference mutation, (b) class-A policy-change (chained to L06's `handle.subscribeVisibilityPolicy`, shipped 2026-04-22), (c) `ChromePosture.debugMode` transitions on the `debug-panel` kind (chained to the bound controller's `subscribeChrome`, shipped 2026-04-24 closing KI-007). Chrome fan-out is lazy registered on the first `debug-panel` subscriber, torn down on the last. debugMode-comparison-based firing avoids false fan-out on unrelated chrome ticks (reviewMode, markupDisplay).",
275
+ rwdReference: "§UI API § ui.overlays.subscribeVisibility (U9). Fires on local-preference mutation and class-A policy-change (chained to L06's `handle.subscribeVisibilityPolicy`, shipped 2026-04-22). debug-panel remains production-forced off and does not subscribe to chrome debugMode changes.",
277
276
  };
278
277
 
279
278
  export function createOverlaysFamily(ctx: UiApiContext) {
@@ -313,59 +312,13 @@ export function createOverlaysFamily(ctx: UiApiContext) {
313
312
  }
314
313
  });
315
314
 
316
- // KI-007 close — chrome-change fan-out for `debug-panel` visibility.
317
- //
318
- // The composed `debug-panel` visibility depends on three inputs:
319
- // (a) class-A policy, (b) class-C local preference, and (c)
320
- // ChromePosture.debugMode. Fan-out for (a) + (b) was already wired;
321
- // (c) was the missing channel. Now registered lazily on the first
322
- // `subscribeVisibility("debug-panel", …)` call and torn down when the
323
- // last such subscriber unsubscribes, so the chrome-listener cost only
324
- // applies when someone is actually listening.
325
- //
326
- // Comparison-based firing: we compare `ChromePosture.debugMode` to
327
- // the previous value on each chrome tick and only re-notify when it
328
- // changes. This avoids firing debug-panel visibility subscribers on
329
- // unrelated chrome changes (reviewMode flip, markup-display change,
330
- // etc.) — cost-efficient + semantically correct.
331
- //
332
- // Note on re-bind: if the host calls `ui.session.release()` then
333
- // `bind(newController)`, the chrome-listener's underlying hook is
334
- // torn down with the old controller. Consumers already subscribed to
335
- // debug-panel visibility continue holding their listeners but stop
336
- // receiving chrome-tick fan-out until they unsubscribe and re-
337
- // subscribe. Release+rebind is a lifecycle event hosts handle
338
- // explicitly; documented here rather than auto-re-registering
339
- // because a full bind-cycle integration would also require reading
340
- // the initial posture to seed `lastKnownDebugMode`, which is out of
341
- // scope for this targeted fix.
342
- let chromeUnsubscribe: (() => void) | null = null;
343
- let lastKnownDebugMode: ChromePosture["debugMode"] | undefined;
344
-
345
315
  function ensureChromeFanout(): void {
346
- if (chromeUnsubscribe) return;
347
- const controller = ctx.binding?.controller;
348
- if (!controller?.subscribeChrome) return;
349
- // Seed last-known from the current posture so the first real change
350
- // doesn't false-fire against undefined.
351
- const posture = readPostureForDebugModeSeed(ctx);
352
- lastKnownDebugMode = posture?.debugMode;
353
- chromeUnsubscribe = controller.subscribeChrome((next) => {
354
- const nextMode = next?.debugMode;
355
- if (nextMode === lastKnownDebugMode) return;
356
- lastKnownDebugMode = nextMode;
357
- notifyVisibilitySubscribers(ctx, visibilityState, "debug-panel");
358
- });
316
+ // Runtime debug chrome is production-disabled, so debug-panel
317
+ // visibility is constant forced-off and never depends on chrome ticks.
359
318
  }
360
319
 
361
320
  function releaseChromeFanoutIfEmpty(): void {
362
- const debugPanelSubs = visibilityState.subscribers.get("debug-panel");
363
- if (debugPanelSubs && debugPanelSubs.size > 0) return;
364
- if (chromeUnsubscribe) {
365
- chromeUnsubscribe();
366
- chromeUnsubscribe = null;
367
- lastKnownDebugMode = undefined;
368
- }
321
+ // Paired no-op for the disabled debug-panel chrome fan-out.
369
322
  }
370
323
 
371
324
  // KI-006 close — per-query coalesced subscribe channel.
@@ -653,9 +606,8 @@ export function createOverlaysFamily(ctx: UiApiContext) {
653
606
  visibilityState.subscribers.set(kind, listeners);
654
607
  }
655
608
  listeners.add(listener);
656
- // KI-007 lazy-register the chrome-change fan-out when the first
657
- // debug-panel subscriber lands. No-op for other kinds + idempotent
658
- // when a chrome listener is already active.
609
+ // Runtime debug chrome is production-disabled; this remains a no-op
610
+ // for debug-panel subscribers and other kinds.
659
611
  if (kind === "debug-panel") {
660
612
  ensureChromeFanout();
661
613
  }
@@ -672,8 +624,7 @@ export function createOverlaysFamily(ctx: UiApiContext) {
672
624
  if (!current) return;
673
625
  current.delete(listener);
674
626
  if (current.size === 0) visibilityState.subscribers.delete(kind);
675
- // KI-007 tear down the chrome-change fan-out when the last
676
- // debug-panel subscriber unsubscribes.
627
+ // Paired no-op for the disabled debug-panel chrome fan-out.
677
628
  if (kind === "debug-panel") {
678
629
  releaseChromeFanoutIfEmpty();
679
630
  }
@@ -681,21 +632,3 @@ export function createOverlaysFamily(ctx: UiApiContext) {
681
632
  },
682
633
  };
683
634
  }
684
-
685
- /**
686
- * KI-007 helper — read the current `ChromePosture.debugMode` value so
687
- * the chrome-change fan-out can seed its `lastKnownDebugMode` tracker
688
- * without false-firing on the first tick. Mirrors the logic in
689
- * `chrome.ts::getPosture` but only extracts `debugMode` + returns
690
- * `undefined` on any missing piece (rather than composing a full
691
- * posture). Keeps the debug-panel fan-out path cheap.
692
- */
693
- function readPostureForDebugModeSeed(
694
- ctx: UiApiContext,
695
- ): { debugMode: ChromePosture["debugMode"] } | null {
696
- const hook = ctx.binding?.controller.getHostPosture;
697
- if (!hook) return null;
698
- const host = hook();
699
- if (!host) return null;
700
- return { debugMode: host.debugMode ?? "off" };
701
- }
@@ -2749,6 +2749,36 @@ function parseRun(
2749
2749
  );
2750
2750
  break;
2751
2751
  }
2752
+ case "AlternateContent": {
2753
+ const altXml = sourceXml.slice(child.start, child.end);
2754
+ try {
2755
+ const frame = parseDrawingFrame(altXml, {
2756
+ relationships,
2757
+ mediaParts,
2758
+ sourcePartPath,
2759
+ chartPartLookup: activeChartPartLookup,
2760
+ blockParser: (xml) =>
2761
+ parseBlockStreamFromXml(xml, {
2762
+ relationships,
2763
+ mediaParts,
2764
+ sourcePartPath,
2765
+ depth: activeTxbxBlockStreamDepth + 1,
2766
+ }) as unknown as ReadonlyArray<{ type: string; [key: string]: unknown }>,
2767
+ });
2768
+ if (frame) {
2769
+ result.push(frame);
2770
+ break;
2771
+ }
2772
+ } catch {
2773
+ // Fall through to opaque preservation below.
2774
+ }
2775
+ encounteredUnsupportedChild = true;
2776
+ result.push({
2777
+ type: "opaque_inline",
2778
+ rawXml: altXml,
2779
+ });
2780
+ break;
2781
+ }
2752
2782
  case "pict": {
2753
2783
  const pictXml = sourceXml.slice(child.start, child.end);
2754
2784
  try {
@@ -49,6 +49,13 @@ export function parsePicture(graphicDataEl: XmlElementNode): PictureContent | nu
49
49
  const blipRef = blipEmbed ?? blipLink ?? "";
50
50
  if (!blipRef) return null;
51
51
  const isLinked = !blipEmbed && !!blipLink;
52
+ const lumEl = findFirstChild(blip, "lum");
53
+ const lum = lumEl
54
+ ? {
55
+ bright: readIntAttr(lumEl, "bright"),
56
+ contrast: readIntAttr(lumEl, "contrast"),
57
+ }
58
+ : undefined;
52
59
 
53
60
  // srcRect (percentage crop: 0..100000 = 0..100%)
54
61
  const srcRectEl = findFirstChild(blipFill, "srcRect");
@@ -95,6 +102,9 @@ export function parsePicture(graphicDataEl: XmlElementNode): PictureContent | nu
95
102
 
96
103
  const result: PictureContent = { type: "picture", blipRef };
97
104
  if (isLinked) result.isLinked = true;
105
+ if (lum && (lum.bright !== undefined || lum.contrast !== undefined)) {
106
+ result.lum = lum;
107
+ }
98
108
  if (srcRect) result.srcRect = srcRect;
99
109
  if (stretch !== undefined) result.stretch = stretch;
100
110
  if (tile !== undefined) result.tile = tile;
@@ -109,6 +119,10 @@ export function parsePicture(graphicDataEl: XmlElementNode): PictureContent | nu
109
119
  }
110
120
 
111
121
  function readEmuAttr(el: XmlElementNode, name: string): number | undefined {
122
+ return readIntAttr(el, name);
123
+ }
124
+
125
+ function readIntAttr(el: XmlElementNode, name: string): number | undefined {
112
126
  const v = el.attributes[name];
113
127
  if (v === undefined) return undefined;
114
128
  const n = parseInt(v, 10);