diff --git a/.changeset/accessibility-bindings-fixes.md b/.changeset/accessibility-bindings-fixes.md deleted file mode 100644 index ea7f377b5..000000000 --- a/.changeset/accessibility-bindings-fixes.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@input/pen-dom": patch -"@input/pen-react": patch -"@input/pen-vue": patch ---- - -Accessibility, iframe and binding fixes: - -- **Reduced motion (AX6):** each root has one shared reduced-motion signal. The new `getRootReducedMotion`, `AX6_MOTION_MAPPING` and `REDUCED_MOTION_ATTR` are in pen-dom, and `useReducedMotion()` is in pen-react. The AI suggestion underline no longer animates under reduced motion. -- **Announcements (AX2):** live-region announcements are written in the scheduler's write phase. -- **Focus return for custom chrome (AX3):** - - New `captureFocusReturn` / `restoreFocusReturn` implement the focus-return rule. - - The slash, suggestion and table column menus dismiss on an outside `pointerdown`. -- **Iframe-mounted editors:** - - Every node and event check uses realm-safe guards, exported from the new `@input/pen-dom/utils/domNodes` subpath (`isDomNode`, `isDomElement`, `isDomEvent` and others). - - Nodes are created in the field's own document, and chrome resolves its document through `resolveEditorOwnerDocument`. -- **`asChild`:** `asChild` composes the child's handlers, class names and styles with the primitive's instead of replacing them. - -Breaking: no diff --git a/.changeset/ai-tool-calls-breaking.md b/.changeset/ai-tool-calls-breaking.md deleted file mode 100644 index 94c8e92dd..000000000 --- a/.changeset/ai-tool-calls-breaking.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -"@input/pen-types": minor -"@input/pen-tools": minor -"@input/pen-ai": minor -"@input/pen-transport": minor ---- - -AI tool calls are attributed, classified and undone per call (AIB3, AIB4). - -- `openAIToolCall()` returns the call's own `context`. `executeAITool()` and both transports run handlers with it. Every write path books a write to the call that issued it, and refuses a read-only call's write. -- The user's typing while a call is open is no call's write. -- `StreamingTarget.disableActiveWriter()` is replaced by the `activeWriter` getter. -- `ToolDefinition.destructive` may be a resolver `(input, { staged }) => boolean` (`ToolDestructiveResolver`, `ToolAuthorityContext`). -- `edit_document` consults `confirm` only for direct calls that remove or replace content: direct replaces, deletes of non-empty blocks, and content-kind changes. `authorizeAIToolCall` and `isDestructiveAITool` take the call's context. -- New `unconfirmedDestructive: "refuse"` (on `aiExtension`, `AIToolGrant`, `AIToolTurnOptions` and `AgenticLoopOptions`) blocks destructive calls when no resolver is installed. Use it in production if you expose `delete_block` or `write_document`. -- A host that used `confirm` as an "every AI edit" hook moves to `onBeforeApply` or the commit event. -- `UndoManager.syncExplicitUndoGroup` is replaced by `withCapture(origin, groupId, run)`. `CRDTUndoManager` gains an optional `setCaptureKey` (`CRDTUndoCaptureKey`). - -Host migration: - -- Tool handlers that compared `ctx.editor === editor` compare `ctx.editor.internals.adapter` (or the document) instead, because `ctx.editor` is now a per-call view. -- Code that called `StreamingTarget.disableActiveWriter()` reads `activeWriter`. -- Code that reads `ToolDefinition.destructive` as a boolean must handle a function. -- Hosts that implement `UndoManager` replace `syncExplicitUndoGroup` with `withCapture`. - -Breaking: yes — tool handlers compare `ctx.editor.internals.adapter` instead of `ctx.editor === editor`, read `activeWriter` instead of `disableActiveWriter()`, handle a function-valued `ToolDefinition.destructive`, and `UndoManager` implementations replace `syncExplicitUndoGroup` with `withCapture` diff --git a/.changeset/ai-undo-fixes.md b/.changeset/ai-undo-fixes.md deleted file mode 100644 index 6b2448ec7..000000000 --- a/.changeset/ai-undo-fixes.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@input/pen-types": patch -"@input/pen-core": patch -"@input/pen-yjs": patch -"@input/pen-undo": patch -"@input/pen-ai": patch -"@input/pen-transport": patch -"@input/pen-react": patch ---- - -AI write attribution, undo grouping and review preview fixes: - -- **Undo grouping (AIB4)** - - Undo capture is keyed. One AI action stays one undo step even when the user types during it, and the user's typing stays its own steps. - - `directTransport` and `createSSEHandler` give each request one undo group. -- **Overlapping calls (AIB3)** - - Overlapping tool calls and generations keep their own write guard and staged-write binding. A call closing out of order no longer drops another call's guard or unstages its writes. - - `directTransport`, `createSSEHandler` and `processStream` apply the editor's `confirm` / `unconfirmedDestructive` policy and accept their own overrides. They allow destructive calls by default when no `confirm` is set, so a server reachable by clients should pass `"refuse"` or a `confirm`. -- **Review preview (RS6):** the `edit_document` streaming preview matches what accept will do: multi-block and nested replaces and deletes, insert placement, empty replacements, and moves and formatting. `EditDocumentPreviewUpdate` gains `blockIds`, `placement` and `complete`. `AIStreamingReviewPreviewInput` gains `complete`, `deletesBlocks` and `replacesBlocks`. -- **Undo drift anchors:** undo reuses the selection authority's anchors through `editor.internals.selectionAnchors()` and `selectionAnchorRepair()` (AN14). This stops spurious `anchor-target-missing` for ranges that end on dividers or tables, and halves `anchor-budget` churn. -- **Anchor repair (AN2, AN14):** `repairAnchor` reads an anchor's position from before the commit even when something resolved it after the commit landed, so a split no longer moves an anchor to the start of the new block. An anchor that resolved to `null` before a commit stays dead instead of being revived from an older position, and a merge carries an `assoc: -1` anchor at the start of the merged-away block into the target instead of losing it. -- **React AI chrome:** the AI suggestions popover is scoped to its own editor. `Pen.AI.ContextualPromptComposer` no longer throws a hook-order error when a session opens. - -Breaking: no diff --git a/.changeset/crdt-collaboration-breaking.md b/.changeset/crdt-collaboration-breaking.md deleted file mode 100644 index 34c79d9fc..000000000 --- a/.changeset/crdt-collaboration-breaking.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -"@input/pen-core": minor -"@input/pen-yjs": minor -"@input/pen-multiplayer": minor ---- - -Structural commits report and revision only the blocks they touched (SCALE2). Awareness moves to its own subpath (API2), and peer colours pass a closed grammar (COL2). - -- `CRDTEvent.affectedBlocks` on remote and undo transactions that touch `blockOrder` lists only the blocks inserted, removed or moved. -- `editor.getBlockRevision()` advances only for blocks a commit changed. -- An `insert-block` / `move-block` into a `{ parent }` that does not exist is dropped with `PEN_APPLY_003` (PR5). -- Awareness lives at `@input/pen-yjs/awareness`, and `y-protocols` becomes an optional peer. `yjsAdapter()` creates no awareness: the multiplayer extension creates its own. A second yjs copy writing to a Pen document emits `YJS_SINGLETON_MISMATCH`. -- `normalizeMultiplayerColor` accepts only hex, named, `rgb`/`rgba` and `hsl`/`hsla` colours with plain numeric arguments. A colour such as `rgb(0,0,0) url(…)` could make every viewer fetch a URL. - -Host migration: - -- Code that read `CRDTEvent.affectedBlocks` on a remote or undo structural transaction as "every block" treats it as the touched blocks only. -- Hosts that used a revision bump on every block as a "something structural happened" signal listen to `commit` events. -- Hosts that inserted or moved blocks into a missing parent handle the `PEN_APPLY_003` drop. -- Import `createYjsAwareness`, `getYjsAwareness`, `encodeYjsAwarenessUpdate` and `applyYjsAwarenessUpdate` from `@input/pen-yjs/awareness`. Hosts that wire a provider without the multiplayer extension pass `yjsAdapter({ awareness: createYjsAwareness })`. -- A host that set `user.color` to `var(--…)`, `inherit`, `color-mix(…)`, `oklch(…)` or another colour function passes a hex, named, `rgb` or `hsl` colour instead. Other values fall back to the palette colour. - -Breaking: yes — hosts treat `affectedBlocks`/`getBlockRevision` as touched-blocks-only, handle `PEN_APPLY_003` for missing parents, import awareness helpers from `@input/pen-yjs/awareness` (or pass `yjsAdapter({ awareness: createYjsAwareness })`), and use hex/named/`rgb`/`hsl` peer colours diff --git a/.changeset/crdt-integrity-fixes.md b/.changeset/crdt-integrity-fixes.md deleted file mode 100644 index 27580ffce..000000000 --- a/.changeset/crdt-integrity-fixes.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@input/pen-types": patch -"@input/pen-core": patch -"@input/pen-yjs": patch -"@input/pen-multiplayer": patch -"@input/pen-ai": patch -"@input/pen-search": patch ---- - -Document integrity fixes for concurrent, out-of-order and malformed edits: - -- **Encodable ops (OPB1):** `editor.apply` drops an op whose payload the CRDT cannot encode, with `PEN_APPLY_004`. Examples are a `set-meta` without a namespace, cyclic or non-plain prop, meta and app values, and non-finite mark values. Before, such an op could stop the document from syncing or persisting, or leave a partial write. -- **Missing parents:** a `parentId` that names no live block, and an insert or move into a parent deleted earlier in the same batch, are dropped with `PEN_APPLY_003` (PR5). -- **Dangling entries (COL4)** - - Normalization removes `blockOrder` / `children` entries whose block was deleted (`dangling-block-reference`). Entries whose block map has not arrived yet are kept (new optional `CRDTAdapter.isBlockDeleted`). - - Iteration APIs skip dangling entries instead of throwing `Block not found`. -- **Structural repairs on the next commit (COL4):** after a remote or undo commit, the next local commit repairs cycles, duplicates and cross-array membership. Orphans are re-homed with `orphan-block-rehomed`. -- **One nesting route (RI6):** deleting a container deletes its `children` descendants. A conflicting `parentId` is cleared with `nesting-route-conflict`. -- **Change summaries:** summaries now report the right `block-inserted` / `block-removed` / `block-moved` for concurrent delete-and-move, duplicate entries, replaced `children` arrays and container maps, re-entered containers, and deleted subtrees. Renderers, search and AI indexes no longer keep dead blocks or miss live ones (OB1, COL4). This adds `RawCommitDelta.absentBlockIds` and `arrivedChildArrays`. A block that loses one of two order entries while its map is deleted or not stored is reported `block-removed`, and a block whose map a commit replaced whole reports `block-props-changed` (`"type"` when it changed, plus the arrived map's prop and meta keys). A write a commit listener makes while a remote delivery or undo is being committed now has a summary naming every insert and delete it made; before, a block it moved could be reported removed and some deletes went missing. -- **Loading (DUR2):** load repair no longer copies nested children into the root order, and no longer removes an entry whose map is in flight. -- **System-origin writes:** normalization outside an apply runs in one `"system"`-origin transaction, so its repairs leave the undo stack. `setDocumentProfile()` no longer emits `ORIGIN_UNKNOWN`. -- **Awareness:** `DocumentSession.ensureAwareness(scopeId, factory)` lets the multiplayer extension create its own awareness. The extension now depends on `@input/pen-yjs` and peers on `yjs` and `y-protocols`. - -Breaking: no diff --git a/.changeset/ime-composition-fixes.md b/.changeset/ime-composition-fixes.md deleted file mode 100644 index ed79d4c8e..000000000 --- a/.changeset/ime-composition-fixes.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@input/pen-dom": patch ---- - -IME composition no longer loses, duplicates or misplaces text: - -- EditContext compositions have one lifecycle from `compositionstart` to `compositionend` (C4). Multi-update compositions (pinyin, Korean, Japanese) no longer delete text after the caret. Typing no longer duplicates edits in the buffer. -- Collaborator, AI, extension and history edits that arrive mid-composition are deferred. The composition is rebased over them with Yjs placement on both backends (C2, COL1). -- A cancelled or empty composition returns the caret to its start and renders deferred decorations (C1). -- A composition over a cross-block selection no longer crashes React in Firefox. It opens the IME window in the expanded host. -- An Android `keyCode` 229 keydown no longer deletes a cross-block range by itself (FE2). - -Breaking: no diff --git a/.changeset/interop-paste.md b/.changeset/interop-paste.md deleted file mode 100644 index eec76799e..000000000 --- a/.changeset/interop-paste.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@input/pen-interop": patch -"@input/pen-dom": patch ---- - -HTML import no longer drops pasted text. - -- Nested lists from Slack, Apple Notes and Google Docs keep every item at the right indent. -- Block content wrapped in an inline element (Google Docs' outer ``) stays as separate blocks. -- Table captions and text outside `` in a `
` are kept.
-- A conversion that would still lose text falls back to plain paragraphs or the literal clipboard text.
-- The sanitizer's `isomorphic-dompurify` moves from `~2.36.0` to `~4.3.0`. Its allowlist and output are unchanged (SEC7).
-
-Breaking: no
diff --git a/.changeset/large-document-performance.md b/.changeset/large-document-performance.md
deleted file mode 100644
index 27c17b567..000000000
--- a/.changeset/large-document-performance.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-"@input/pen-types": patch
-"@input/pen-core": patch
-"@input/pen-yjs": patch
-"@input/pen-dom": patch
-"@input/pen-react": patch
-"@input/pen-vue": patch
-"@input/pen-ai": patch
-"@input/pen-search": patch
-"@input/pen-multiplayer": patch
----
-
-Keystrokes, caret moves and structural edits cost in proportion to what they touch, not to document size (SCALE2, SCALE6).
-
-- **Indexes:**
-  - `DocumentState` gains `preorderIndexOf`, `preorderBlockIds`, `rootBlockIds` and `rootBlockIndexOf`.
-  - Root and `children` edits advance the position, parent, child and preorder indexes in place instead of rebuilding them.
-  - The change-summary index and the normalization pass index advance by each commit's delta.
-  - Measured at 50,000 blocks: an insert inside a container drops from about 41 ms to 0.15 ms, and a delete from about 11 ms to 0.3 ms.
-- **Selection reads:** selection validation, anchor resolution, `buildLazyNormalPositionSnapshot` and vertical caret motion no longer read every block.
-- **Scoped decorations:** new `scopedDecorationSource({ interest, decorate })` recomputes only the blocks a commit names. `decorationsChange` passes `(generation, changedBlockIds)`. `requestDecorationUpdate({ source?, blockIds })` targets sources, and with no argument it recomputes only function-form and static sources. AI suggestions and search use scoped sources, so typing with either installed no longer reads the whole document.
-- **Block notifier:** new `fieldEditor.blockNotifier` fans editor events out per block. React (`EditorBlock`, `InlineContent`), Vue (`PenBlock`, `PenInlineContent`) and the vanilla `mountEditor` tree re-render only the blocks whose state changed. Each block acknowledges its own mount. `blocks-merged` carries `sourceParentId` / `sourceIndex`. Its list slices are rebuilt after a commit that moves the document index without a structural change, such as a duplicate-entry repair.
-- **Multiplayer:** remote selection ranges look up positions in O(1).
-- **Normalization:** the structural rules treat the lowest parent id as the container of a block listed in several arrays. Normalized documents are unchanged.
-
-Breaking: no
diff --git a/.changeset/list-semantics-focus-breaking.md b/.changeset/list-semantics-focus-breaking.md
deleted file mode 100644
index cc688fc5d..000000000
--- a/.changeset/list-semantics-focus-breaking.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-"@input/pen-core": minor
-"@input/pen-dom": minor
-"@input/pen-react": minor
-"@input/pen-vue": minor
----
-
-List items are announced as lists (AX1), numbering follows the announced list, and focus returns to its invoker by primitive class (AX3).
-
-- Each run of list items renders inside a `div[data-pen-list-group][role="list"]`. Each item's host carries `role="listitem"`, `aria-level`, `aria-posinset` and `aria-setsize`. This applies on React, Vue and `mountEditor`.
-- New core helpers `getListSegments`, `getListItemSemantics` and `isListItemType` describe these lists.
-- `getNumberedListItemValue` and the notifier's `list.ordinal` count numbered items over the item's sibling list instead of the root `blockOrder`. Markdown export's list `start` follows the same count.
-- Toolbar buttons and toggles keep focus in the field: they prevent the primary-button `mousedown` default. Escape returns focus to the editor.
-- The selection toolbar, block handle menu, table column menu, AI command menu and AI contextual prompt return focus to their invoker synchronously.
-
-Host migration:
-
-- Hosts that walk the blocks host's direct children, or style list items with `[data-pen-editor-block] + [data-pen-editor-block]`, must also look inside the `[data-pen-list-group]` wrappers. Items moving between groups are remounted by React and Vue.
-- A numbered item that follows a container with numbered `parentId` children now starts at 1, and items in a `children` array number 1, 2, 3. Hosts or snapshots that expect the old numbering update it. There is no API change.
-- Hosts whose toolbar `onClick` handlers relied on the button holding focus (for example by reading `document.activeElement`) read the editor selection instead.
-- Hosts that refocused the editor themselves after the AI command menu or contextual prompt closed remove that refocus.
-
-Breaking: yes — hosts look inside `[data-pen-list-group]` wrappers when walking or styling list items, update expected numbered-list values after containers, read the editor selection instead of toolbar focus, and drop their own refocus after AI menus close
diff --git a/.changeset/node-engine-floor.md b/.changeset/node-engine-floor.md
deleted file mode 100644
index 7644ef48e..000000000
--- a/.changeset/node-engine-floor.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-"@input/pen": minor
-"@input/pen-ai": minor
-"@input/pen-assets": minor
-"@input/pen-autoformat": minor
-"@input/pen-bench": minor
-"@input/pen-core": minor
-"@input/pen-dom": minor
-"@input/pen-ingest": minor
-"@input/pen-interop": minor
-"@input/pen-markdown": minor
-"@input/pen-multiplayer": minor
-"@input/pen-react": minor
-"@input/pen-schema": minor
-"@input/pen-search": minor
-"@input/pen-shortcuts": minor
-"@input/pen-snapshots": minor
-"@input/pen-test": minor
-"@input/pen-tools": minor
-"@input/pen-transport": minor
-"@input/pen-types": minor
-"@input/pen-undo": minor
-"@input/pen-vue": minor
-"@input/pen-yjs": minor
----
-
-Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
-
-Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
diff --git a/.changeset/overlay-caret-breaking.md b/.changeset/overlay-caret-breaking.md
deleted file mode 100644
index bdb538294..000000000
--- a/.changeset/overlay-caret-breaking.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-"@input/pen-dom": minor
-"@input/pen-react": minor
-"@input/pen-vue": minor
----
-
-Pen draws carets, remote carets and block outlines into one overlay layer per editor root, which the DOM scheduler paints. React's caret overlays are now bindings over it, and Vue gets the overlay with no component.
-
-- Each editor root holds one `data-pen-overlay-layer` element as its last child. `getRootOverlay(root)` exposes the paint plan and the contributor API.
-- The overlay caret is on by default on every surface. It draws carets beside mentions and inline apps, in empty blocks, and at range endpoints. Block and grid-cell selections get a library outline.
-- New tokens: `--pen-overlay-z-index`, `--pen-editor-endpoint-caret-color`, `--pen-block-selection-outline`, `--pen-block-selection-background`, `--pen-block-selection-radius`, `--pen-selection-range-background` and `--pen-selection-range-opacity`.
-- The editor root is the layer's containing block (OV2). The chrome stylesheet gives it `position: relative` at zero specificity, and a static root gets it inline while attached.
-- `Pen.Editor.CaretOverlay` switches the root to `customCaret` mode, and pen-dom paints the caret. The blink restarts on each edit and caret move.
-- `Pen.Multiplayer.CaretOverlay` registers the remote-caret contributor. Remote carets stay on their text inside transformed ancestors, and the binding drops its `MutationObserver`, rAF loop and scroll listeners.
-- `EditorSelectionRect` draws only the region-selection marquee.
-
-Host migration:
-
-- Host CSS that targets the editor root's `:last-child` accounts for the `data-pen-overlay-layer` element.
-- Hosts that already style `[data-selected]` set `--pen-block-selection-outline: none` on the editor root (or drop their rule) to avoid a double outline.
-- Set caret and outline tokens (`--pen-editor-caret-*` included) on the editor root or above, not on a wrapper inside it.
-- Scope `[data-block-id]` selectors to `[data-pen-editor-block]` if they must not match overlay items.
-- Absolutely positioned host elements inside a static editor root now position against the root. Give the root its own non-static `position` to keep another containing block.
-- `renderCaret` (editor and multiplayer) and `renderLabel` position from the `transform` in `caretStyle` / `labelStyle`, not `left` / `top`. Their nodes are portaled into the overlay layer, and `renderCaret` receives `affinity`.
-- Hosts that style `[data-pen-editor-caret-overlay][data-caret-visible]` target `[data-pen-overlay-layer][data-caret-visible]`.
-- Remote carets move from the `[data-pen-multiplayer-caret-overlay]` host into the layer. Restyle them under `[data-pen-overlay-layer] [data-pen-multiplayer-caret]`. They change from `position: fixed` with `left` / `top` to `position: absolute` with a `transform`. The default label is a child of its caret and no longer carries `data-pen-multiplayer-caret` or the user attributes.
-- Hosts that relied on `EditorSelectionRect` to draw a committed block selection style `--pen-block-selection-outline` / `--pen-block-selection-background` on the editor root instead.
-
-Breaking: yes — hosts update `:last-child`, `[data-selected]`, caret-overlay and remote-caret CSS for the root's `data-pen-overlay-layer`, position custom `renderCaret`/`renderLabel` output from `transform`, set tokens on the root, and give the root its own `position` if it must not be the containing block
diff --git a/.changeset/overlay-caret-fixes.md b/.changeset/overlay-caret-fixes.md
deleted file mode 100644
index d02344eae..000000000
--- a/.changeset/overlay-caret-fixes.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-"@input/pen-core": patch
-"@input/pen-dom": patch
-"@input/pen-vue": patch
-"@input/pen-multiplayer": patch
----
-
-Additive overlay APIs and remote-caret safety and contrast fixes:
-
-- `RootOverlay.holdCaretMode`, `overlayItemStyle` and `overlayLabelStyle` are exported for bindings.
-- `attachRemoteCarets(overlay, source)` and `getRemoteCaretSource(editor)` paint collaborators' carets. Vue gains `PenMultiplayerCaretOverlay`.
-- `FieldEditorImpl.setReadOnly` carries the renderer `readonly` prop.
-- New `isSafeCssColor` in `@input/pen-core`. Every `--pen-peer-color` write is re-validated, and carets paint through `background-color` so a colour cannot become an image fetch (COL2).
-- The default peer palette (`MULTIPLAYER_COLORS`) uses darker shades so labels meet 4.5:1 contrast. Hosts that pass `user.color` are unaffected.
-- A local keystroke that leaves every peer in place no longer notifies multiplayer subscribers or repaints remote carets.
-
-Breaking: no
diff --git a/.changeset/selection-focus-fixes.md b/.changeset/selection-focus-fixes.md
deleted file mode 100644
index 63413b1ef..000000000
--- a/.changeset/selection-focus-fixes.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-"@input/pen-types": patch
-"@input/pen-core": patch
-"@input/pen-dom": patch
-"@input/pen-react": patch
-"@input/pen-vue": patch
----
-
-Selection, focus and pointer fixes across React, Vue and vanilla `mountEditor`:
-
-- **Reading and writing the selection**
-  - One `selectionchange` reader per root and one projector are the only code that reads or writes the DOM selection (S1).
-  - Input edits the editor selection instead of re-reading the DOM.
-  - `focus()` on a field with no caret commits one to the record.
-  - Every projection reads the selection back. A mismatch emits `selection-projection-mismatch` once and is not rewritten (W3.R1).
-  - The backend selection stamps (`FieldEditorSelectionSource`) are gone.
-- **Focus and projection timing**
-  - Projections are withheld while an IME composes, while editor chrome holds focus, and while a host control holds focus (HOST9).
-  - Undo/redo rebuilds and passive attaches no longer write the native range or steal focus.
-- **New APIs**
-  - The selection convenience setters take an optional `SelectionWriteOptions` with `origin` (S3).
-  - `FieldEditorImpl.scrollIntoView` and `FieldEditorImpl.setMountRequester` are new. A projection parked on an unmounted block asks the mount requester to mount it. `selection-target-unmounted` is emitted once per park, and `BlockScrollAlign` is added.
-  - `FieldEditorImpl.connect()` keeps the field editor live under React Strict Mode.
-  - `focusTextSelection` and `useFocusController` run in the calling turn (S4).
-- **Undo, redo and scrolling:** undo and redo scroll the restored selection into view even when the caret did not move. Scroll deltas round to whole pixels.
-- **Ranges over more than 50 blocks (S2):** these ranges, and ranges the engine confines to one field, show a substitute state with the focus sink revealed (`getSubstituteState()`, `pen.a11y.textRangeSelected`). Typing, deleting, IME input and clipboard actions work there.
-- **Fuzzer findings:** S2 cross-block projections now handle code blocks, tables, unit-block gaps, unmounted endpoints and WebKit drags.
-- **Gesture windows:**
-  - Gesture windows survive a field switch.
-  - `pointercancel` ends a pointer gesture, and drag and context-menu windows close properly (R1).
-  - Touch selection handles keep their range (`native-range` window).
-  - An EditContext keystroke after `editor.selectText` lands at the new caret (FE9).
-- **Clicks and arrows around atoms and blocks:**
-  - A shift-click into another block extends to the offset under the pointer on every binding, including from a block or cell selection and over dividers.
-  - Clicking an inline atom puts the caret on the clicked side, following the atom's bidi run (O1, T5).
-  - `pen.caretUp` / `pen.caretDown` into a table select its edge cell (T5).
-  - A click in a table cell during a text range selects the cell.
-- **Atom layout:** atoms share the text line, and a caret beside a chip takes the line's height. Compositions beside atoms no longer insert U+FFFC.
-- **Table cells:**
-  - `CellSelection.text` is in `@input/pen-types`, and an edited cell keeps DOM focus (W3.R18).
-  - `Mod-b`/`i`/`u` in a cell fail closed on keydown in every browser (FE6).
-  - A table props/meta change no longer resets an edited cell.
-  - Backspace and Delete in an edited cell delete the range or one character instead of clearing the cell. `editor.deleteSelection()` on a `CellSelection` with `text` deletes only that range (A1, T6).
-- **Positioned chrome:** the selection toolbar, AI prompt and slash/suggestion menus position from the editor selection, not the live DOM range. `DomScheduler.measureNow` drops geometry stale since the last commit (SCH2).
-- **Focus and activation:**
-  - Shift+Tab out of the editor works in Firefox.
-  - Overlay focus follows window blur (`bindEditorRootFocus`).
-  - The toggle empty state, block-type select and slash-menu table insert activate in the same turn.
-
-Breaking: no
diff --git a/.changeset/selection-model-breaking.md b/.changeset/selection-model-breaking.md
deleted file mode 100644
index 9bb6a69db..000000000
--- a/.changeset/selection-model-breaking.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-"@input/pen-core": minor
-"@input/pen-dom": minor
-"@input/pen-undo": minor
----
-
-The editor selection record is now the single source of truth for every caret, range, cell, focus and pointer path. The renderers read and write the DOM selection only through it (S1), and every selection write carries its real origin (S3).
-
-- Arrow keys beside inline atoms go through the keymap. In right-to-left blocks they follow the visual direction (M2).
-- `BlockHandle.length()` counts each inline embed as one offset, so a block that holds only atoms has length 1 (N1).
-- An edited table cell's caret is `CellSelection.text` (`{ anchor, focus }` in the cell's offsets). It is validated, anchored and mapped through remote edits. `selectionChange` now fires for in-cell caret moves, and Shift+Arrow inside a cell extends the range. `setCellCaretFocus`, `getCellCaretFocus`, `CellCaretFocus` and `CellCaretWrite` are removed.
-- Pointer gestures, drops and pointer pastes write origin `pointer`. Keymap commands, text input and shortcut pastes write `keyboard`, or `ime` while composing. Undo and redo write `restore`. Keyboard, IME and history moves scroll into view.
-- Focus follows the selection record. Block and cell selections focus the focus sink. App and `null` selections focus the root, but only while the editor already owns focus. Escape writes the selection and lets its projection place focus (W3.R16).
-- `programmatic`, `mapped` and `gc` writes no longer take focus the editor does not own (HOST9). Projection and focus work in editors mounted in an iframe. Tabbing into a multi-block range projects it.
-- A reconcile no longer saves and restores the native selection. Rebuilt blocks are projected through `fieldEditor.projectAfterRebuild(blockIds)` (P3).
-- A parked projection resolves on the mount ack of its own block (P4). `DomScheduler` no longer projects selections.
-- Pointer drags and block clicks are resolved through core's transitions (`resolvePointerSelectionIntent`). A pointer selection that moves to another block ends the current undo step.
-- The contenteditable, EditContext and expanded backends share one base class (CS5).
-- `FieldEditorImpl` and `FieldEditorSession` drop methods that only forwarded to pen-dom internals. pen-dom reports gestures itself.
-
-Host migration:
-
-- Hosts that relied on ArrowLeft always meaning "previous atom" in right-to-left blocks now get the visual direction. There is no API change.
-- Hosts that read `length() === 0` as "this block has no text" check `textContent() === ""` instead when an atom-only block must count as empty.
-- Hosts that drove in-cell caret motion through `setCellCaretFocus` write a `CellSelection` with `text` instead: `editor.setSelection({ type: "cell", blockId, anchor: cell, head: cell, text: { anchor: caretOffset, focus: caretOffset } })`. Hosts that treated every `selectionChange` to a `cell` selection as a grid move check `text` first.
-- Hosts that call `applyDocumentTextSelection` or `applyDomTextSelection` pass the gesture's origin, for example `"pointer"`.
-- Hosts that read `selectionChange` origins see `restore` for history restores and drop any `programmatic` check they used to detect them.
-- Hosts that call `handleEscapeSelectionTransition` drop `root`. Hosts that relied on Escape or `deactivate()` focusing the block element focus the sink or root instead.
-- Hosts that called `editor.setSelection` (or another programmatic write) while focus was outside the editor, and relied on it focusing the editor, now focus it explicitly. In React use `useFocusController().text(…)` / `.range(…)`; elsewhere use the field editor's `focus()`.
-- Hosts that called `saveSelection` / `restoreSelection` or passed `preserveSelection` to `fullReconcileToDOM` / `fullReconcileDeltasToDOM` drop them and call `fieldEditor.projectAfterRebuild([blockId])` after rebuilding a block.
-- Hosts that called `editorSelectionToDOM` call `editor.setSelection` instead and let the field editor project it. `findDOMPoint` is now exported from `@input/pen-dom/field-editor/selectionBridge`.
-- Hosts that passed `onProjectSelection`, called `setProjector` or read `projectedThisFlush` on a `DomScheduler` drop those calls, and the `SelectionProjector` type is gone. Selection projection runs in the field editor.
-- Hosts that call `resolvePointerDragSelection` drop the `getBoundaryPoint` option. Hosts that build a `PointerSelectionGesture` by hand use `createPointerSelectionGesture`, or set `startSelectionVersion` and `committed: false`.
-- Hosts that call `attachContentGestures` drop `skipNextClick` from `state`.
-- Hosts that subclass a backend and use a removed protected member move to the new members:
-  - `ContentEditableBackend`: `fullReconcileActiveField` becomes `rebuildField(inlineDecorations?, blockId?)`, and `applyTextDiffAsOps` drops its `deferredRemoteDeltas` argument.
-  - `EditContextBackend`: `resolveEditorSelectionRange`, `resolveCollapsedEditorSelectionRange`, `getAuthoritativeTextInputSelection` and `resolveKeyDownRange` become `authorityRangeIn(blockId)` / `trustedCaretIn(blockId)`.
-- Hosts drop calls to the removed field-editor forwarders: `beginPointerSelection`, `endPointerSelection`, `notifyGestureEvent`, `isAdmissibleGestureRead`, `getGestureWindows`, `requestDivergenceProjection`, `shouldProjectSelectionAfterReconcile`, `requestActivation` and `resolveInsertMarks`. Focus, pending marks and post-rebuild projection keep their session methods.
-
-Breaking: yes — hosts apply the migration list above (pass origins, write `CellSelection.text` instead of `setCellCaretFocus`, replace `saveSelection`/`restoreSelection`/`editorSelectionToDOM` with `editor.setSelection` plus `projectAfterRebuild`, drop removed scheduler/gesture/forwarder members, focus the editor explicitly after programmatic writes)
diff --git a/.changeset/tooling-bench-test.md b/.changeset/tooling-bench-test.md
deleted file mode 100644
index 0a7e037c2..000000000
--- a/.changeset/tooling-bench-test.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-"@input/pen-bench": patch
-"@input/pen-test": patch
----
-
-Scale measurement and multi-peer test tooling.
-
-- `@input/pen-test` adds:
-  - `createPeerHarness(n)`, with seeded delivery schedules (`PEER_SCHEDULES`, `runPeerSchedules`), `quiesce()` and an awareness relay. `createTwoPeerHarness()` is now its two-peer form and also destroys each `Y.Doc`.
-  - The COL4 structural oracle (`findStructuralViolations`, `assertStructuralInvariants`).
-  - `createScanProbe(editor)` for counting document reads.
-  - The mixed 1k–50k block fixture (`generateMixedBlockSpecs`, `mixedFixtureOps` and related helpers).
-- `@input/pen-bench` adds:
-  - A realistic SCALE3 variant with AI and search installed.
-  - A synced-peer SCALE3 axis.
-  - Structural-commit read gates.
-  - Renderer rows in `ENVELOPE.md`, and a measured concurrent-peer row.
-  - The envelope re-recorded on a quiet machine.
-- `bench:caches` fails on any diagnostic except `anchor-budget`.
-
-Breaking: no
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
index fff6e894d..fc36ea1c1 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -1,5 +1,195 @@
 # @input/pen-core
 
+## 0.3.0
+
+### Minor Changes
+
+- 56b8dfc: Structural commits report and revision only the blocks they touched (SCALE2). Awareness moves to its own subpath (API2), and peer colours pass a closed grammar (COL2).
+
+  - `CRDTEvent.affectedBlocks` on remote and undo transactions that touch `blockOrder` lists only the blocks inserted, removed or moved.
+  - `editor.getBlockRevision()` advances only for blocks a commit changed.
+  - An `insert-block` / `move-block` into a `{ parent }` that does not exist is dropped with `PEN_APPLY_003` (PR5).
+  - Awareness lives at `@input/pen-yjs/awareness`, and `y-protocols` becomes an optional peer. `yjsAdapter()` creates no awareness: the multiplayer extension creates its own. A second yjs copy writing to a Pen document emits `YJS_SINGLETON_MISMATCH`.
+  - `normalizeMultiplayerColor` accepts only hex, named, `rgb`/`rgba` and `hsl`/`hsla` colours with plain numeric arguments. A colour such as `rgb(0,0,0) url(…)` could make every viewer fetch a URL.
+
+  Host migration:
+
+  - Code that read `CRDTEvent.affectedBlocks` on a remote or undo structural transaction as "every block" treats it as the touched blocks only.
+  - Hosts that used a revision bump on every block as a "something structural happened" signal listen to `commit` events.
+  - Hosts that inserted or moved blocks into a missing parent handle the `PEN_APPLY_003` drop.
+  - Import `createYjsAwareness`, `getYjsAwareness`, `encodeYjsAwarenessUpdate` and `applyYjsAwarenessUpdate` from `@input/pen-yjs/awareness`. Hosts that wire a provider without the multiplayer extension pass `yjsAdapter({ awareness: createYjsAwareness })`.
+  - A host that set `user.color` to `var(--…)`, `inherit`, `color-mix(…)`, `oklch(…)` or another colour function passes a hex, named, `rgb` or `hsl` colour instead. Other values fall back to the palette colour.
+
+  Breaking: yes — hosts treat `affectedBlocks`/`getBlockRevision` as touched-blocks-only, handle `PEN_APPLY_003` for missing parents, import awareness helpers from `@input/pen-yjs/awareness` (or pass `yjsAdapter({ awareness: createYjsAwareness })`), and use hex/named/`rgb`/`hsl` peer colours
+
+- 56b8dfc: List items are announced as lists (AX1), numbering follows the announced list, and focus returns to its invoker by primitive class (AX3).
+
+  - Each run of list items renders inside a `div[data-pen-list-group][role="list"]`. Each item's host carries `role="listitem"`, `aria-level`, `aria-posinset` and `aria-setsize`. This applies on React, Vue and `mountEditor`.
+  - New core helpers `getListSegments`, `getListItemSemantics` and `isListItemType` describe these lists.
+  - `getNumberedListItemValue` and the notifier's `list.ordinal` count numbered items over the item's sibling list instead of the root `blockOrder`. Markdown export's list `start` follows the same count.
+  - Toolbar buttons and toggles keep focus in the field: they prevent the primary-button `mousedown` default. Escape returns focus to the editor.
+  - The selection toolbar, block handle menu, table column menu, AI command menu and AI contextual prompt return focus to their invoker synchronously.
+
+  Host migration:
+
+  - Hosts that walk the blocks host's direct children, or style list items with `[data-pen-editor-block] + [data-pen-editor-block]`, must also look inside the `[data-pen-list-group]` wrappers. Items moving between groups are remounted by React and Vue.
+  - A numbered item that follows a container with numbered `parentId` children now starts at 1, and items in a `children` array number 1, 2, 3. Hosts or snapshots that expect the old numbering update it. There is no API change.
+  - Hosts whose toolbar `onClick` handlers relied on the button holding focus (for example by reading `document.activeElement`) read the editor selection instead.
+  - Hosts that refocused the editor themselves after the AI command menu or contextual prompt closed remove that refocus.
+
+  Breaking: yes — hosts look inside `[data-pen-list-group]` wrappers when walking or styling list items, update expected numbered-list values after containers, read the editor selection instead of toolbar focus, and drop their own refocus after AI menus close
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+- 56b8dfc: The editor selection record is now the single source of truth for every caret, range, cell, focus and pointer path. The renderers read and write the DOM selection only through it (S1), and every selection write carries its real origin (S3).
+
+  - Arrow keys beside inline atoms go through the keymap. In right-to-left blocks they follow the visual direction (M2).
+  - `BlockHandle.length()` counts each inline embed as one offset, so a block that holds only atoms has length 1 (N1).
+  - An edited table cell's caret is `CellSelection.text` (`{ anchor, focus }` in the cell's offsets). It is validated, anchored and mapped through remote edits. `selectionChange` now fires for in-cell caret moves, and Shift+Arrow inside a cell extends the range. `setCellCaretFocus`, `getCellCaretFocus`, `CellCaretFocus` and `CellCaretWrite` are removed.
+  - Pointer gestures, drops and pointer pastes write origin `pointer`. Keymap commands, text input and shortcut pastes write `keyboard`, or `ime` while composing. Undo and redo write `restore`. Keyboard, IME and history moves scroll into view.
+  - Focus follows the selection record. Block and cell selections focus the focus sink. App and `null` selections focus the root, but only while the editor already owns focus. Escape writes the selection and lets its projection place focus (W3.R16).
+  - `programmatic`, `mapped` and `gc` writes no longer take focus the editor does not own (HOST9). Projection and focus work in editors mounted in an iframe. Tabbing into a multi-block range projects it.
+  - A reconcile no longer saves and restores the native selection. Rebuilt blocks are projected through `fieldEditor.projectAfterRebuild(blockIds)` (P3).
+  - A parked projection resolves on the mount ack of its own block (P4). `DomScheduler` no longer projects selections.
+  - Pointer drags and block clicks are resolved through core's transitions (`resolvePointerSelectionIntent`). A pointer selection that moves to another block ends the current undo step.
+  - The contenteditable, EditContext and expanded backends share one base class (CS5).
+  - `FieldEditorImpl` and `FieldEditorSession` drop methods that only forwarded to pen-dom internals. pen-dom reports gestures itself.
+
+  Host migration:
+
+  - Hosts that relied on ArrowLeft always meaning "previous atom" in right-to-left blocks now get the visual direction. There is no API change.
+  - Hosts that read `length() === 0` as "this block has no text" check `textContent() === ""` instead when an atom-only block must count as empty.
+  - Hosts that drove in-cell caret motion through `setCellCaretFocus` write a `CellSelection` with `text` instead: `editor.setSelection({ type: "cell", blockId, anchor: cell, head: cell, text: { anchor: caretOffset, focus: caretOffset } })`. Hosts that treated every `selectionChange` to a `cell` selection as a grid move check `text` first.
+  - Hosts that call `applyDocumentTextSelection` or `applyDomTextSelection` pass the gesture's origin, for example `"pointer"`.
+  - Hosts that read `selectionChange` origins see `restore` for history restores and drop any `programmatic` check they used to detect them.
+  - Hosts that call `handleEscapeSelectionTransition` drop `root`. Hosts that relied on Escape or `deactivate()` focusing the block element focus the sink or root instead.
+  - Hosts that called `editor.setSelection` (or another programmatic write) while focus was outside the editor, and relied on it focusing the editor, now focus it explicitly. In React use `useFocusController().text(…)` / `.range(…)`; elsewhere use the field editor's `focus()`.
+  - Hosts that called `saveSelection` / `restoreSelection` or passed `preserveSelection` to `fullReconcileToDOM` / `fullReconcileDeltasToDOM` drop them and call `fieldEditor.projectAfterRebuild([blockId])` after rebuilding a block.
+  - Hosts that called `editorSelectionToDOM` call `editor.setSelection` instead and let the field editor project it. `findDOMPoint` is now exported from `@input/pen-dom/field-editor/selectionBridge`.
+  - Hosts that passed `onProjectSelection`, called `setProjector` or read `projectedThisFlush` on a `DomScheduler` drop those calls, and the `SelectionProjector` type is gone. Selection projection runs in the field editor.
+  - Hosts that call `resolvePointerDragSelection` drop the `getBoundaryPoint` option. Hosts that build a `PointerSelectionGesture` by hand use `createPointerSelectionGesture`, or set `startSelectionVersion` and `committed: false`.
+  - Hosts that call `attachContentGestures` drop `skipNextClick` from `state`.
+  - Hosts that subclass a backend and use a removed protected member move to the new members:
+    - `ContentEditableBackend`: `fullReconcileActiveField` becomes `rebuildField(inlineDecorations?, blockId?)`, and `applyTextDiffAsOps` drops its `deferredRemoteDeltas` argument.
+    - `EditContextBackend`: `resolveEditorSelectionRange`, `resolveCollapsedEditorSelectionRange`, `getAuthoritativeTextInputSelection` and `resolveKeyDownRange` become `authorityRangeIn(blockId)` / `trustedCaretIn(blockId)`.
+  - Hosts drop calls to the removed field-editor forwarders: `beginPointerSelection`, `endPointerSelection`, `notifyGestureEvent`, `isAdmissibleGestureRead`, `getGestureWindows`, `requestDivergenceProjection`, `shouldProjectSelectionAfterReconcile`, `requestActivation` and `resolveInsertMarks`. Focus, pending marks and post-rebuild projection keep their session methods.
+
+  Breaking: yes — hosts apply the migration list above (pass origins, write `CellSelection.text` instead of `setCellCaretFocus`, replace `saveSelection`/`restoreSelection`/`editorSelectionToDOM` with `editor.setSelection` plus `projectAfterRebuild`, drop removed scheduler/gesture/forwarder members, focus the editor explicitly after programmatic writes)
+
+### Patch Changes
+
+- 56b8dfc: AI write attribution, undo grouping and review preview fixes:
+
+  - **Undo grouping (AIB4)**
+    - Undo capture is keyed. One AI action stays one undo step even when the user types during it, and the user's typing stays its own steps.
+    - `directTransport` and `createSSEHandler` give each request one undo group.
+  - **Overlapping calls (AIB3)**
+    - Overlapping tool calls and generations keep their own write guard and staged-write binding. A call closing out of order no longer drops another call's guard or unstages its writes.
+    - `directTransport`, `createSSEHandler` and `processStream` apply the editor's `confirm` / `unconfirmedDestructive` policy and accept their own overrides. They allow destructive calls by default when no `confirm` is set, so a server reachable by clients should pass `"refuse"` or a `confirm`.
+  - **Review preview (RS6):** the `edit_document` streaming preview matches what accept will do: multi-block and nested replaces and deletes, insert placement, empty replacements, and moves and formatting. `EditDocumentPreviewUpdate` gains `blockIds`, `placement` and `complete`. `AIStreamingReviewPreviewInput` gains `complete`, `deletesBlocks` and `replacesBlocks`.
+  - **Undo drift anchors:** undo reuses the selection authority's anchors through `editor.internals.selectionAnchors()` and `selectionAnchorRepair()` (AN14). This stops spurious `anchor-target-missing` for ranges that end on dividers or tables, and halves `anchor-budget` churn.
+  - **Anchor repair (AN2, AN14):** `repairAnchor` reads an anchor's position from before the commit even when something resolved it after the commit landed, so a split no longer moves an anchor to the start of the new block. An anchor that resolved to `null` before a commit stays dead instead of being revived from an older position, and a merge carries an `assoc: -1` anchor at the start of the merged-away block into the target instead of losing it.
+  - **React AI chrome:** the AI suggestions popover is scoped to its own editor. `Pen.AI.ContextualPromptComposer` no longer throws a hook-order error when a session opens.
+
+  Breaking: no
+
+- 56b8dfc: Document integrity fixes for concurrent, out-of-order and malformed edits:
+
+  - **Encodable ops (OPB1):** `editor.apply` drops an op whose payload the CRDT cannot encode, with `PEN_APPLY_004`. Examples are a `set-meta` without a namespace, cyclic or non-plain prop, meta and app values, and non-finite mark values. Before, such an op could stop the document from syncing or persisting, or leave a partial write.
+  - **Missing parents:** a `parentId` that names no live block, and an insert or move into a parent deleted earlier in the same batch, are dropped with `PEN_APPLY_003` (PR5).
+  - **Dangling entries (COL4)**
+    - Normalization removes `blockOrder` / `children` entries whose block was deleted (`dangling-block-reference`). Entries whose block map has not arrived yet are kept (new optional `CRDTAdapter.isBlockDeleted`).
+    - Iteration APIs skip dangling entries instead of throwing `Block not found`.
+  - **Structural repairs on the next commit (COL4):** after a remote or undo commit, the next local commit repairs cycles, duplicates and cross-array membership. Orphans are re-homed with `orphan-block-rehomed`.
+  - **One nesting route (RI6):** deleting a container deletes its `children` descendants. A conflicting `parentId` is cleared with `nesting-route-conflict`.
+  - **Change summaries:** summaries now report the right `block-inserted` / `block-removed` / `block-moved` for concurrent delete-and-move, duplicate entries, replaced `children` arrays and container maps, re-entered containers, and deleted subtrees. Renderers, search and AI indexes no longer keep dead blocks or miss live ones (OB1, COL4). This adds `RawCommitDelta.absentBlockIds` and `arrivedChildArrays`. A block that loses one of two order entries while its map is deleted or not stored is reported `block-removed`, and a block whose map a commit replaced whole reports `block-props-changed` (`"type"` when it changed, plus the arrived map's prop and meta keys). A write a commit listener makes while a remote delivery or undo is being committed now has a summary naming every insert and delete it made; before, a block it moved could be reported removed and some deletes went missing.
+  - **Loading (DUR2):** load repair no longer copies nested children into the root order, and no longer removes an entry whose map is in flight.
+  - **System-origin writes:** normalization outside an apply runs in one `"system"`-origin transaction, so its repairs leave the undo stack. `setDocumentProfile()` no longer emits `ORIGIN_UNKNOWN`.
+  - **Awareness:** `DocumentSession.ensureAwareness(scopeId, factory)` lets the multiplayer extension create its own awareness. The extension now depends on `@input/pen-yjs` and peers on `yjs` and `y-protocols`.
+
+  Breaking: no
+
+- 56b8dfc: Keystrokes, caret moves and structural edits cost in proportion to what they touch, not to document size (SCALE2, SCALE6).
+
+  - **Indexes:**
+    - `DocumentState` gains `preorderIndexOf`, `preorderBlockIds`, `rootBlockIds` and `rootBlockIndexOf`.
+    - Root and `children` edits advance the position, parent, child and preorder indexes in place instead of rebuilding them.
+    - The change-summary index and the normalization pass index advance by each commit's delta.
+    - Measured at 50,000 blocks: an insert inside a container drops from about 41 ms to 0.15 ms, and a delete from about 11 ms to 0.3 ms.
+  - **Selection reads:** selection validation, anchor resolution, `buildLazyNormalPositionSnapshot` and vertical caret motion no longer read every block.
+  - **Scoped decorations:** new `scopedDecorationSource({ interest, decorate })` recomputes only the blocks a commit names. `decorationsChange` passes `(generation, changedBlockIds)`. `requestDecorationUpdate({ source?, blockIds })` targets sources, and with no argument it recomputes only function-form and static sources. AI suggestions and search use scoped sources, so typing with either installed no longer reads the whole document.
+  - **Block notifier:** new `fieldEditor.blockNotifier` fans editor events out per block. React (`EditorBlock`, `InlineContent`), Vue (`PenBlock`, `PenInlineContent`) and the vanilla `mountEditor` tree re-render only the blocks whose state changed. Each block acknowledges its own mount. `blocks-merged` carries `sourceParentId` / `sourceIndex`. Its list slices are rebuilt after a commit that moves the document index without a structural change, such as a duplicate-entry repair.
+  - **Multiplayer:** remote selection ranges look up positions in O(1).
+  - **Normalization:** the structural rules treat the lowest parent id as the container of a block listed in several arrays. Normalized documents are unchanged.
+
+  Breaking: no
+
+- 56b8dfc: Additive overlay APIs and remote-caret safety and contrast fixes:
+
+  - `RootOverlay.holdCaretMode`, `overlayItemStyle` and `overlayLabelStyle` are exported for bindings.
+  - `attachRemoteCarets(overlay, source)` and `getRemoteCaretSource(editor)` paint collaborators' carets. Vue gains `PenMultiplayerCaretOverlay`.
+  - `FieldEditorImpl.setReadOnly` carries the renderer `readonly` prop.
+  - New `isSafeCssColor` in `@input/pen-core`. Every `--pen-peer-color` write is re-validated, and carets paint through `background-color` so a colour cannot become an image fetch (COL2).
+  - The default peer palette (`MULTIPLAYER_COLORS`) uses darker shades so labels meet 4.5:1 contrast. Hosts that pass `user.color` are unaffected.
+  - A local keystroke that leaves every peer in place no longer notifies multiplayer subscribers or repaints remote carets.
+
+  Breaking: no
+
+- 56b8dfc: Selection, focus and pointer fixes across React, Vue and vanilla `mountEditor`:
+
+  - **Reading and writing the selection**
+    - One `selectionchange` reader per root and one projector are the only code that reads or writes the DOM selection (S1).
+    - Input edits the editor selection instead of re-reading the DOM.
+    - `focus()` on a field with no caret commits one to the record.
+    - Every projection reads the selection back. A mismatch emits `selection-projection-mismatch` once and is not rewritten (W3.R1).
+    - The backend selection stamps (`FieldEditorSelectionSource`) are gone.
+  - **Focus and projection timing**
+    - Projections are withheld while an IME composes, while editor chrome holds focus, and while a host control holds focus (HOST9).
+    - Undo/redo rebuilds and passive attaches no longer write the native range or steal focus.
+  - **New APIs**
+    - The selection convenience setters take an optional `SelectionWriteOptions` with `origin` (S3).
+    - `FieldEditorImpl.scrollIntoView` and `FieldEditorImpl.setMountRequester` are new. A projection parked on an unmounted block asks the mount requester to mount it. `selection-target-unmounted` is emitted once per park, and `BlockScrollAlign` is added.
+    - `FieldEditorImpl.connect()` keeps the field editor live under React Strict Mode.
+    - `focusTextSelection` and `useFocusController` run in the calling turn (S4).
+  - **Undo, redo and scrolling:** undo and redo scroll the restored selection into view even when the caret did not move. Scroll deltas round to whole pixels.
+  - **Ranges over more than 50 blocks (S2):** these ranges, and ranges the engine confines to one field, show a substitute state with the focus sink revealed (`getSubstituteState()`, `pen.a11y.textRangeSelected`). Typing, deleting, IME input and clipboard actions work there.
+  - **Fuzzer findings:** S2 cross-block projections now handle code blocks, tables, unit-block gaps, unmounted endpoints and WebKit drags.
+  - **Gesture windows:**
+    - Gesture windows survive a field switch.
+    - `pointercancel` ends a pointer gesture, and drag and context-menu windows close properly (R1).
+    - Touch selection handles keep their range (`native-range` window).
+    - An EditContext keystroke after `editor.selectText` lands at the new caret (FE9).
+  - **Clicks and arrows around atoms and blocks:**
+    - A shift-click into another block extends to the offset under the pointer on every binding, including from a block or cell selection and over dividers.
+    - Clicking an inline atom puts the caret on the clicked side, following the atom's bidi run (O1, T5).
+    - `pen.caretUp` / `pen.caretDown` into a table select its edge cell (T5).
+    - A click in a table cell during a text range selects the cell.
+  - **Atom layout:** atoms share the text line, and a caret beside a chip takes the line's height. Compositions beside atoms no longer insert U+FFFC.
+  - **Table cells:**
+    - `CellSelection.text` is in `@input/pen-types`, and an edited cell keeps DOM focus (W3.R18).
+    - `Mod-b`/`i`/`u` in a cell fail closed on keydown in every browser (FE6).
+    - A table props/meta change no longer resets an edited cell.
+    - Backspace and Delete in an edited cell delete the range or one character instead of clearing the cell. `editor.deleteSelection()` on a `CellSelection` with `text` deletes only that range (A1, T6).
+  - **Positioned chrome:** the selection toolbar, AI prompt and slash/suggestion menus position from the editor selection, not the live DOM range. `DomScheduler.measureNow` drops geometry stale since the last commit (SCH2).
+  - **Focus and activation:**
+    - Shift+Tab out of the editor works in Firefox.
+    - Overlay focus follows window blur (`bindEditorRootFocus`).
+    - The toggle empty state, block-type select and slash-menu table insert activate in the same turn.
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-yjs@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/core/package.json b/packages/core/package.json
index 063b242a5..1f446962f 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-core",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Headless, extension-first editor engine for human-AI co-authoring",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/crdt/yjs/CHANGELOG.md b/packages/crdt/yjs/CHANGELOG.md
index b93dbc3da..2cbdc2c39 100644
--- a/packages/crdt/yjs/CHANGELOG.md
+++ b/packages/crdt/yjs/CHANGELOG.md
@@ -1,5 +1,87 @@
 # @input/pen-yjs
 
+## 0.3.0
+
+### Minor Changes
+
+- 56b8dfc: Structural commits report and revision only the blocks they touched (SCALE2). Awareness moves to its own subpath (API2), and peer colours pass a closed grammar (COL2).
+
+  - `CRDTEvent.affectedBlocks` on remote and undo transactions that touch `blockOrder` lists only the blocks inserted, removed or moved.
+  - `editor.getBlockRevision()` advances only for blocks a commit changed.
+  - An `insert-block` / `move-block` into a `{ parent }` that does not exist is dropped with `PEN_APPLY_003` (PR5).
+  - Awareness lives at `@input/pen-yjs/awareness`, and `y-protocols` becomes an optional peer. `yjsAdapter()` creates no awareness: the multiplayer extension creates its own. A second yjs copy writing to a Pen document emits `YJS_SINGLETON_MISMATCH`.
+  - `normalizeMultiplayerColor` accepts only hex, named, `rgb`/`rgba` and `hsl`/`hsla` colours with plain numeric arguments. A colour such as `rgb(0,0,0) url(…)` could make every viewer fetch a URL.
+
+  Host migration:
+
+  - Code that read `CRDTEvent.affectedBlocks` on a remote or undo structural transaction as "every block" treats it as the touched blocks only.
+  - Hosts that used a revision bump on every block as a "something structural happened" signal listen to `commit` events.
+  - Hosts that inserted or moved blocks into a missing parent handle the `PEN_APPLY_003` drop.
+  - Import `createYjsAwareness`, `getYjsAwareness`, `encodeYjsAwarenessUpdate` and `applyYjsAwarenessUpdate` from `@input/pen-yjs/awareness`. Hosts that wire a provider without the multiplayer extension pass `yjsAdapter({ awareness: createYjsAwareness })`.
+  - A host that set `user.color` to `var(--…)`, `inherit`, `color-mix(…)`, `oklch(…)` or another colour function passes a hex, named, `rgb` or `hsl` colour instead. Other values fall back to the palette colour.
+
+  Breaking: yes — hosts treat `affectedBlocks`/`getBlockRevision` as touched-blocks-only, handle `PEN_APPLY_003` for missing parents, import awareness helpers from `@input/pen-yjs/awareness` (or pass `yjsAdapter({ awareness: createYjsAwareness })`), and use hex/named/`rgb`/`hsl` peer colours
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- 56b8dfc: AI write attribution, undo grouping and review preview fixes:
+
+  - **Undo grouping (AIB4)**
+    - Undo capture is keyed. One AI action stays one undo step even when the user types during it, and the user's typing stays its own steps.
+    - `directTransport` and `createSSEHandler` give each request one undo group.
+  - **Overlapping calls (AIB3)**
+    - Overlapping tool calls and generations keep their own write guard and staged-write binding. A call closing out of order no longer drops another call's guard or unstages its writes.
+    - `directTransport`, `createSSEHandler` and `processStream` apply the editor's `confirm` / `unconfirmedDestructive` policy and accept their own overrides. They allow destructive calls by default when no `confirm` is set, so a server reachable by clients should pass `"refuse"` or a `confirm`.
+  - **Review preview (RS6):** the `edit_document` streaming preview matches what accept will do: multi-block and nested replaces and deletes, insert placement, empty replacements, and moves and formatting. `EditDocumentPreviewUpdate` gains `blockIds`, `placement` and `complete`. `AIStreamingReviewPreviewInput` gains `complete`, `deletesBlocks` and `replacesBlocks`.
+  - **Undo drift anchors:** undo reuses the selection authority's anchors through `editor.internals.selectionAnchors()` and `selectionAnchorRepair()` (AN14). This stops spurious `anchor-target-missing` for ranges that end on dividers or tables, and halves `anchor-budget` churn.
+  - **Anchor repair (AN2, AN14):** `repairAnchor` reads an anchor's position from before the commit even when something resolved it after the commit landed, so a split no longer moves an anchor to the start of the new block. An anchor that resolved to `null` before a commit stays dead instead of being revived from an older position, and a merge carries an `assoc: -1` anchor at the start of the merged-away block into the target instead of losing it.
+  - **React AI chrome:** the AI suggestions popover is scoped to its own editor. `Pen.AI.ContextualPromptComposer` no longer throws a hook-order error when a session opens.
+
+  Breaking: no
+
+- 56b8dfc: Document integrity fixes for concurrent, out-of-order and malformed edits:
+
+  - **Encodable ops (OPB1):** `editor.apply` drops an op whose payload the CRDT cannot encode, with `PEN_APPLY_004`. Examples are a `set-meta` without a namespace, cyclic or non-plain prop, meta and app values, and non-finite mark values. Before, such an op could stop the document from syncing or persisting, or leave a partial write.
+  - **Missing parents:** a `parentId` that names no live block, and an insert or move into a parent deleted earlier in the same batch, are dropped with `PEN_APPLY_003` (PR5).
+  - **Dangling entries (COL4)**
+    - Normalization removes `blockOrder` / `children` entries whose block was deleted (`dangling-block-reference`). Entries whose block map has not arrived yet are kept (new optional `CRDTAdapter.isBlockDeleted`).
+    - Iteration APIs skip dangling entries instead of throwing `Block not found`.
+  - **Structural repairs on the next commit (COL4):** after a remote or undo commit, the next local commit repairs cycles, duplicates and cross-array membership. Orphans are re-homed with `orphan-block-rehomed`.
+  - **One nesting route (RI6):** deleting a container deletes its `children` descendants. A conflicting `parentId` is cleared with `nesting-route-conflict`.
+  - **Change summaries:** summaries now report the right `block-inserted` / `block-removed` / `block-moved` for concurrent delete-and-move, duplicate entries, replaced `children` arrays and container maps, re-entered containers, and deleted subtrees. Renderers, search and AI indexes no longer keep dead blocks or miss live ones (OB1, COL4). This adds `RawCommitDelta.absentBlockIds` and `arrivedChildArrays`. A block that loses one of two order entries while its map is deleted or not stored is reported `block-removed`, and a block whose map a commit replaced whole reports `block-props-changed` (`"type"` when it changed, plus the arrived map's prop and meta keys). A write a commit listener makes while a remote delivery or undo is being committed now has a summary naming every insert and delete it made; before, a block it moved could be reported removed and some deletes went missing.
+  - **Loading (DUR2):** load repair no longer copies nested children into the root order, and no longer removes an entry whose map is in flight.
+  - **System-origin writes:** normalization outside an apply runs in one `"system"`-origin transaction, so its repairs leave the undo stack. `setDocumentProfile()` no longer emits `ORIGIN_UNKNOWN`.
+  - **Awareness:** `DocumentSession.ensureAwareness(scopeId, factory)` lets the multiplayer extension create its own awareness. The extension now depends on `@input/pen-yjs` and peers on `yjs` and `y-protocols`.
+
+  Breaking: no
+
+- 56b8dfc: Keystrokes, caret moves and structural edits cost in proportion to what they touch, not to document size (SCALE2, SCALE6).
+
+  - **Indexes:**
+    - `DocumentState` gains `preorderIndexOf`, `preorderBlockIds`, `rootBlockIds` and `rootBlockIndexOf`.
+    - Root and `children` edits advance the position, parent, child and preorder indexes in place instead of rebuilding them.
+    - The change-summary index and the normalization pass index advance by each commit's delta.
+    - Measured at 50,000 blocks: an insert inside a container drops from about 41 ms to 0.15 ms, and a delete from about 11 ms to 0.3 ms.
+  - **Selection reads:** selection validation, anchor resolution, `buildLazyNormalPositionSnapshot` and vertical caret motion no longer read every block.
+  - **Scoped decorations:** new `scopedDecorationSource({ interest, decorate })` recomputes only the blocks a commit names. `decorationsChange` passes `(generation, changedBlockIds)`. `requestDecorationUpdate({ source?, blockIds })` targets sources, and with no argument it recomputes only function-form and static sources. AI suggestions and search use scoped sources, so typing with either installed no longer reads the whole document.
+  - **Block notifier:** new `fieldEditor.blockNotifier` fans editor events out per block. React (`EditorBlock`, `InlineContent`), Vue (`PenBlock`, `PenInlineContent`) and the vanilla `mountEditor` tree re-render only the blocks whose state changed. Each block acknowledges its own mount. `blocks-merged` carries `sourceParentId` / `sourceIndex`. Its list slices are rebuilt after a commit that moves the document index without a structural change, such as a duplicate-entry repair.
+  - **Multiplayer:** remote selection ranges look up positions in O(1).
+  - **Normalization:** the structural rules treat the lowest parent id as the container of a block listed in several arrays. Normalized documents are unchanged.
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/crdt/yjs/package.json b/packages/crdt/yjs/package.json
index 3569d1dc8..0f6a41a2d 100644
--- a/packages/crdt/yjs/package.json
+++ b/packages/crdt/yjs/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-yjs",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Yjs CRDT adapter for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/extensions/ai/CHANGELOG.md b/packages/extensions/ai/CHANGELOG.md
index cc1aa22b4..2b56e4061 100644
--- a/packages/extensions/ai/CHANGELOG.md
+++ b/packages/extensions/ai/CHANGELOG.md
@@ -1,5 +1,96 @@
 # @input/pen-ai
 
+## 0.3.0
+
+### Minor Changes
+
+- 56b8dfc: AI tool calls are attributed, classified and undone per call (AIB3, AIB4).
+
+  - `openAIToolCall()` returns the call's own `context`. `executeAITool()` and both transports run handlers with it. Every write path books a write to the call that issued it, and refuses a read-only call's write.
+  - The user's typing while a call is open is no call's write.
+  - `StreamingTarget.disableActiveWriter()` is replaced by the `activeWriter` getter.
+  - `ToolDefinition.destructive` may be a resolver `(input, { staged }) => boolean` (`ToolDestructiveResolver`, `ToolAuthorityContext`).
+  - `edit_document` consults `confirm` only for direct calls that remove or replace content: direct replaces, deletes of non-empty blocks, and content-kind changes. `authorizeAIToolCall` and `isDestructiveAITool` take the call's context.
+  - New `unconfirmedDestructive: "refuse"` (on `aiExtension`, `AIToolGrant`, `AIToolTurnOptions` and `AgenticLoopOptions`) blocks destructive calls when no resolver is installed. Use it in production if you expose `delete_block` or `write_document`.
+  - A host that used `confirm` as an "every AI edit" hook moves to `onBeforeApply` or the commit event.
+  - `UndoManager.syncExplicitUndoGroup` is replaced by `withCapture(origin, groupId, run)`. `CRDTUndoManager` gains an optional `setCaptureKey` (`CRDTUndoCaptureKey`).
+
+  Host migration:
+
+  - Tool handlers that compared `ctx.editor === editor` compare `ctx.editor.internals.adapter` (or the document) instead, because `ctx.editor` is now a per-call view.
+  - Code that called `StreamingTarget.disableActiveWriter()` reads `activeWriter`.
+  - Code that reads `ToolDefinition.destructive` as a boolean must handle a function.
+  - Hosts that implement `UndoManager` replace `syncExplicitUndoGroup` with `withCapture`.
+
+  Breaking: yes — tool handlers compare `ctx.editor.internals.adapter` instead of `ctx.editor === editor`, read `activeWriter` instead of `disableActiveWriter()`, handle a function-valued `ToolDefinition.destructive`, and `UndoManager` implementations replace `syncExplicitUndoGroup` with `withCapture`
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- 56b8dfc: AI write attribution, undo grouping and review preview fixes:
+
+  - **Undo grouping (AIB4)**
+    - Undo capture is keyed. One AI action stays one undo step even when the user types during it, and the user's typing stays its own steps.
+    - `directTransport` and `createSSEHandler` give each request one undo group.
+  - **Overlapping calls (AIB3)**
+    - Overlapping tool calls and generations keep their own write guard and staged-write binding. A call closing out of order no longer drops another call's guard or unstages its writes.
+    - `directTransport`, `createSSEHandler` and `processStream` apply the editor's `confirm` / `unconfirmedDestructive` policy and accept their own overrides. They allow destructive calls by default when no `confirm` is set, so a server reachable by clients should pass `"refuse"` or a `confirm`.
+  - **Review preview (RS6):** the `edit_document` streaming preview matches what accept will do: multi-block and nested replaces and deletes, insert placement, empty replacements, and moves and formatting. `EditDocumentPreviewUpdate` gains `blockIds`, `placement` and `complete`. `AIStreamingReviewPreviewInput` gains `complete`, `deletesBlocks` and `replacesBlocks`.
+  - **Undo drift anchors:** undo reuses the selection authority's anchors through `editor.internals.selectionAnchors()` and `selectionAnchorRepair()` (AN14). This stops spurious `anchor-target-missing` for ranges that end on dividers or tables, and halves `anchor-budget` churn.
+  - **Anchor repair (AN2, AN14):** `repairAnchor` reads an anchor's position from before the commit even when something resolved it after the commit landed, so a split no longer moves an anchor to the start of the new block. An anchor that resolved to `null` before a commit stays dead instead of being revived from an older position, and a merge carries an `assoc: -1` anchor at the start of the merged-away block into the target instead of losing it.
+  - **React AI chrome:** the AI suggestions popover is scoped to its own editor. `Pen.AI.ContextualPromptComposer` no longer throws a hook-order error when a session opens.
+
+  Breaking: no
+
+- 56b8dfc: Document integrity fixes for concurrent, out-of-order and malformed edits:
+
+  - **Encodable ops (OPB1):** `editor.apply` drops an op whose payload the CRDT cannot encode, with `PEN_APPLY_004`. Examples are a `set-meta` without a namespace, cyclic or non-plain prop, meta and app values, and non-finite mark values. Before, such an op could stop the document from syncing or persisting, or leave a partial write.
+  - **Missing parents:** a `parentId` that names no live block, and an insert or move into a parent deleted earlier in the same batch, are dropped with `PEN_APPLY_003` (PR5).
+  - **Dangling entries (COL4)**
+    - Normalization removes `blockOrder` / `children` entries whose block was deleted (`dangling-block-reference`). Entries whose block map has not arrived yet are kept (new optional `CRDTAdapter.isBlockDeleted`).
+    - Iteration APIs skip dangling entries instead of throwing `Block not found`.
+  - **Structural repairs on the next commit (COL4):** after a remote or undo commit, the next local commit repairs cycles, duplicates and cross-array membership. Orphans are re-homed with `orphan-block-rehomed`.
+  - **One nesting route (RI6):** deleting a container deletes its `children` descendants. A conflicting `parentId` is cleared with `nesting-route-conflict`.
+  - **Change summaries:** summaries now report the right `block-inserted` / `block-removed` / `block-moved` for concurrent delete-and-move, duplicate entries, replaced `children` arrays and container maps, re-entered containers, and deleted subtrees. Renderers, search and AI indexes no longer keep dead blocks or miss live ones (OB1, COL4). This adds `RawCommitDelta.absentBlockIds` and `arrivedChildArrays`. A block that loses one of two order entries while its map is deleted or not stored is reported `block-removed`, and a block whose map a commit replaced whole reports `block-props-changed` (`"type"` when it changed, plus the arrived map's prop and meta keys). A write a commit listener makes while a remote delivery or undo is being committed now has a summary naming every insert and delete it made; before, a block it moved could be reported removed and some deletes went missing.
+  - **Loading (DUR2):** load repair no longer copies nested children into the root order, and no longer removes an entry whose map is in flight.
+  - **System-origin writes:** normalization outside an apply runs in one `"system"`-origin transaction, so its repairs leave the undo stack. `setDocumentProfile()` no longer emits `ORIGIN_UNKNOWN`.
+  - **Awareness:** `DocumentSession.ensureAwareness(scopeId, factory)` lets the multiplayer extension create its own awareness. The extension now depends on `@input/pen-yjs` and peers on `yjs` and `y-protocols`.
+
+  Breaking: no
+
+- 56b8dfc: Keystrokes, caret moves and structural edits cost in proportion to what they touch, not to document size (SCALE2, SCALE6).
+
+  - **Indexes:**
+    - `DocumentState` gains `preorderIndexOf`, `preorderBlockIds`, `rootBlockIds` and `rootBlockIndexOf`.
+    - Root and `children` edits advance the position, parent, child and preorder indexes in place instead of rebuilding them.
+    - The change-summary index and the normalization pass index advance by each commit's delta.
+    - Measured at 50,000 blocks: an insert inside a container drops from about 41 ms to 0.15 ms, and a delete from about 11 ms to 0.3 ms.
+  - **Selection reads:** selection validation, anchor resolution, `buildLazyNormalPositionSnapshot` and vertical caret motion no longer read every block.
+  - **Scoped decorations:** new `scopedDecorationSource({ interest, decorate })` recomputes only the blocks a commit names. `decorationsChange` passes `(generation, changedBlockIds)`. `requestDecorationUpdate({ source?, blockIds })` targets sources, and with no argument it recomputes only function-form and static sources. AI suggestions and search use scoped sources, so typing with either installed no longer reads the whole document.
+  - **Block notifier:** new `fieldEditor.blockNotifier` fans editor events out per block. React (`EditorBlock`, `InlineContent`), Vue (`PenBlock`, `PenInlineContent`) and the vanilla `mountEditor` tree re-render only the blocks whose state changed. Each block acknowledges its own mount. `blocks-merged` carries `sourceParentId` / `sourceIndex`. Its list slices are rebuilt after a commit that moves the document index without a structural change, such as a duplicate-entry repair.
+  - **Multiplayer:** remote selection ranges look up positions in O(1).
+  - **Normalization:** the structural rules treat the lowest parent id as the container of a block listed in several arrays. Normalized documents are unchanged.
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-tools@0.3.0
+  - @input/pen-core@0.3.0
+  - @input/pen-ingest@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/extensions/ai/package.json b/packages/extensions/ai/package.json
index f2d885222..e26879d2a 100644
--- a/packages/extensions/ai/package.json
+++ b/packages/extensions/ai/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-ai",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "AI extension, suggestions, autocomplete, skills, tools, and streaming for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/extensions/autoformat/CHANGELOG.md b/packages/extensions/autoformat/CHANGELOG.md
index fd39c6b4f..ddca311cb 100644
--- a/packages/extensions/autoformat/CHANGELOG.md
+++ b/packages/extensions/autoformat/CHANGELOG.md
@@ -1,5 +1,28 @@
 # @input/pen-autoformat
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/extensions/autoformat/package.json b/packages/extensions/autoformat/package.json
index 9ad9dc60d..7a6de7af3 100644
--- a/packages/extensions/autoformat/package.json
+++ b/packages/extensions/autoformat/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-autoformat",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Markdown shortcut input rules for Pen — auto-format as you type",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/extensions/interop/CHANGELOG.md b/packages/extensions/interop/CHANGELOG.md
index aa9af85f3..7e32df0a0 100644
--- a/packages/extensions/interop/CHANGELOG.md
+++ b/packages/extensions/interop/CHANGELOG.md
@@ -1,5 +1,40 @@
 # @input/pen-interop
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- 56b8dfc: HTML import no longer drops pasted text.
+
+  - Nested lists from Slack, Apple Notes and Google Docs keep every item at the right indent.
+  - Block content wrapped in an inline element (Google Docs' outer ``) stays as separate blocks.
+  - Table captions and text outside `` in a `
` are kept.
+  - A conversion that would still lose text falls back to plain paragraphs or the literal clipboard text.
+  - The sanitizer's `isomorphic-dompurify` moves from `~2.36.0` to `~4.3.0`. Its allowlist and output are unchanged (SEC7).
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+  - @input/pen-ingest@0.3.0
+  - @input/pen-markdown@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/extensions/interop/package.json b/packages/extensions/interop/package.json
index 38a32312c..629825517 100644
--- a/packages/extensions/interop/package.json
+++ b/packages/extensions/interop/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-interop",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "HTML, Markdown, JSON, and XML import/export for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/extensions/multiplayer/CHANGELOG.md b/packages/extensions/multiplayer/CHANGELOG.md
index c19dd6553..4ba9714a3 100644
--- a/packages/extensions/multiplayer/CHANGELOG.md
+++ b/packages/extensions/multiplayer/CHANGELOG.md
@@ -1,5 +1,89 @@
 # @input/pen-multiplayer
 
+## 0.3.0
+
+### Minor Changes
+
+- 56b8dfc: Structural commits report and revision only the blocks they touched (SCALE2). Awareness moves to its own subpath (API2), and peer colours pass a closed grammar (COL2).
+
+  - `CRDTEvent.affectedBlocks` on remote and undo transactions that touch `blockOrder` lists only the blocks inserted, removed or moved.
+  - `editor.getBlockRevision()` advances only for blocks a commit changed.
+  - An `insert-block` / `move-block` into a `{ parent }` that does not exist is dropped with `PEN_APPLY_003` (PR5).
+  - Awareness lives at `@input/pen-yjs/awareness`, and `y-protocols` becomes an optional peer. `yjsAdapter()` creates no awareness: the multiplayer extension creates its own. A second yjs copy writing to a Pen document emits `YJS_SINGLETON_MISMATCH`.
+  - `normalizeMultiplayerColor` accepts only hex, named, `rgb`/`rgba` and `hsl`/`hsla` colours with plain numeric arguments. A colour such as `rgb(0,0,0) url(…)` could make every viewer fetch a URL.
+
+  Host migration:
+
+  - Code that read `CRDTEvent.affectedBlocks` on a remote or undo structural transaction as "every block" treats it as the touched blocks only.
+  - Hosts that used a revision bump on every block as a "something structural happened" signal listen to `commit` events.
+  - Hosts that inserted or moved blocks into a missing parent handle the `PEN_APPLY_003` drop.
+  - Import `createYjsAwareness`, `getYjsAwareness`, `encodeYjsAwarenessUpdate` and `applyYjsAwarenessUpdate` from `@input/pen-yjs/awareness`. Hosts that wire a provider without the multiplayer extension pass `yjsAdapter({ awareness: createYjsAwareness })`.
+  - A host that set `user.color` to `var(--…)`, `inherit`, `color-mix(…)`, `oklch(…)` or another colour function passes a hex, named, `rgb` or `hsl` colour instead. Other values fall back to the palette colour.
+
+  Breaking: yes — hosts treat `affectedBlocks`/`getBlockRevision` as touched-blocks-only, handle `PEN_APPLY_003` for missing parents, import awareness helpers from `@input/pen-yjs/awareness` (or pass `yjsAdapter({ awareness: createYjsAwareness })`), and use hex/named/`rgb`/`hsl` peer colours
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- 56b8dfc: Document integrity fixes for concurrent, out-of-order and malformed edits:
+
+  - **Encodable ops (OPB1):** `editor.apply` drops an op whose payload the CRDT cannot encode, with `PEN_APPLY_004`. Examples are a `set-meta` without a namespace, cyclic or non-plain prop, meta and app values, and non-finite mark values. Before, such an op could stop the document from syncing or persisting, or leave a partial write.
+  - **Missing parents:** a `parentId` that names no live block, and an insert or move into a parent deleted earlier in the same batch, are dropped with `PEN_APPLY_003` (PR5).
+  - **Dangling entries (COL4)**
+    - Normalization removes `blockOrder` / `children` entries whose block was deleted (`dangling-block-reference`). Entries whose block map has not arrived yet are kept (new optional `CRDTAdapter.isBlockDeleted`).
+    - Iteration APIs skip dangling entries instead of throwing `Block not found`.
+  - **Structural repairs on the next commit (COL4):** after a remote or undo commit, the next local commit repairs cycles, duplicates and cross-array membership. Orphans are re-homed with `orphan-block-rehomed`.
+  - **One nesting route (RI6):** deleting a container deletes its `children` descendants. A conflicting `parentId` is cleared with `nesting-route-conflict`.
+  - **Change summaries:** summaries now report the right `block-inserted` / `block-removed` / `block-moved` for concurrent delete-and-move, duplicate entries, replaced `children` arrays and container maps, re-entered containers, and deleted subtrees. Renderers, search and AI indexes no longer keep dead blocks or miss live ones (OB1, COL4). This adds `RawCommitDelta.absentBlockIds` and `arrivedChildArrays`. A block that loses one of two order entries while its map is deleted or not stored is reported `block-removed`, and a block whose map a commit replaced whole reports `block-props-changed` (`"type"` when it changed, plus the arrived map's prop and meta keys). A write a commit listener makes while a remote delivery or undo is being committed now has a summary naming every insert and delete it made; before, a block it moved could be reported removed and some deletes went missing.
+  - **Loading (DUR2):** load repair no longer copies nested children into the root order, and no longer removes an entry whose map is in flight.
+  - **System-origin writes:** normalization outside an apply runs in one `"system"`-origin transaction, so its repairs leave the undo stack. `setDocumentProfile()` no longer emits `ORIGIN_UNKNOWN`.
+  - **Awareness:** `DocumentSession.ensureAwareness(scopeId, factory)` lets the multiplayer extension create its own awareness. The extension now depends on `@input/pen-yjs` and peers on `yjs` and `y-protocols`.
+
+  Breaking: no
+
+- 56b8dfc: Keystrokes, caret moves and structural edits cost in proportion to what they touch, not to document size (SCALE2, SCALE6).
+
+  - **Indexes:**
+    - `DocumentState` gains `preorderIndexOf`, `preorderBlockIds`, `rootBlockIds` and `rootBlockIndexOf`.
+    - Root and `children` edits advance the position, parent, child and preorder indexes in place instead of rebuilding them.
+    - The change-summary index and the normalization pass index advance by each commit's delta.
+    - Measured at 50,000 blocks: an insert inside a container drops from about 41 ms to 0.15 ms, and a delete from about 11 ms to 0.3 ms.
+  - **Selection reads:** selection validation, anchor resolution, `buildLazyNormalPositionSnapshot` and vertical caret motion no longer read every block.
+  - **Scoped decorations:** new `scopedDecorationSource({ interest, decorate })` recomputes only the blocks a commit names. `decorationsChange` passes `(generation, changedBlockIds)`. `requestDecorationUpdate({ source?, blockIds })` targets sources, and with no argument it recomputes only function-form and static sources. AI suggestions and search use scoped sources, so typing with either installed no longer reads the whole document.
+  - **Block notifier:** new `fieldEditor.blockNotifier` fans editor events out per block. React (`EditorBlock`, `InlineContent`), Vue (`PenBlock`, `PenInlineContent`) and the vanilla `mountEditor` tree re-render only the blocks whose state changed. Each block acknowledges its own mount. `blocks-merged` carries `sourceParentId` / `sourceIndex`. Its list slices are rebuilt after a commit that moves the document index without a structural change, such as a duplicate-entry repair.
+  - **Multiplayer:** remote selection ranges look up positions in O(1).
+  - **Normalization:** the structural rules treat the lowest parent id as the container of a block listed in several arrays. Normalized documents are unchanged.
+
+  Breaking: no
+
+- 56b8dfc: Additive overlay APIs and remote-caret safety and contrast fixes:
+
+  - `RootOverlay.holdCaretMode`, `overlayItemStyle` and `overlayLabelStyle` are exported for bindings.
+  - `attachRemoteCarets(overlay, source)` and `getRemoteCaretSource(editor)` paint collaborators' carets. Vue gains `PenMultiplayerCaretOverlay`.
+  - `FieldEditorImpl.setReadOnly` carries the renderer `readonly` prop.
+  - New `isSafeCssColor` in `@input/pen-core`. Every `--pen-peer-color` write is re-validated, and carets paint through `background-color` so a colour cannot become an image fetch (COL2).
+  - The default peer palette (`MULTIPLAYER_COLORS`) uses darker shades so labels meet 4.5:1 contrast. Hosts that pass `user.color` are unaffected.
+  - A local keystroke that leaves every peer in place no longer notifies multiplayer subscribers or repaints remote carets.
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+  - @input/pen-yjs@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/extensions/multiplayer/package.json b/packages/extensions/multiplayer/package.json
index ae66c484b..2b9320efa 100644
--- a/packages/extensions/multiplayer/package.json
+++ b/packages/extensions/multiplayer/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-multiplayer",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Multiplayer presence and sync primitives for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/extensions/search/CHANGELOG.md b/packages/extensions/search/CHANGELOG.md
index 37cc2f6f5..00f29c470 100644
--- a/packages/extensions/search/CHANGELOG.md
+++ b/packages/extensions/search/CHANGELOG.md
@@ -1,5 +1,59 @@
 # @input/pen-search
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- 56b8dfc: Document integrity fixes for concurrent, out-of-order and malformed edits:
+
+  - **Encodable ops (OPB1):** `editor.apply` drops an op whose payload the CRDT cannot encode, with `PEN_APPLY_004`. Examples are a `set-meta` without a namespace, cyclic or non-plain prop, meta and app values, and non-finite mark values. Before, such an op could stop the document from syncing or persisting, or leave a partial write.
+  - **Missing parents:** a `parentId` that names no live block, and an insert or move into a parent deleted earlier in the same batch, are dropped with `PEN_APPLY_003` (PR5).
+  - **Dangling entries (COL4)**
+    - Normalization removes `blockOrder` / `children` entries whose block was deleted (`dangling-block-reference`). Entries whose block map has not arrived yet are kept (new optional `CRDTAdapter.isBlockDeleted`).
+    - Iteration APIs skip dangling entries instead of throwing `Block not found`.
+  - **Structural repairs on the next commit (COL4):** after a remote or undo commit, the next local commit repairs cycles, duplicates and cross-array membership. Orphans are re-homed with `orphan-block-rehomed`.
+  - **One nesting route (RI6):** deleting a container deletes its `children` descendants. A conflicting `parentId` is cleared with `nesting-route-conflict`.
+  - **Change summaries:** summaries now report the right `block-inserted` / `block-removed` / `block-moved` for concurrent delete-and-move, duplicate entries, replaced `children` arrays and container maps, re-entered containers, and deleted subtrees. Renderers, search and AI indexes no longer keep dead blocks or miss live ones (OB1, COL4). This adds `RawCommitDelta.absentBlockIds` and `arrivedChildArrays`. A block that loses one of two order entries while its map is deleted or not stored is reported `block-removed`, and a block whose map a commit replaced whole reports `block-props-changed` (`"type"` when it changed, plus the arrived map's prop and meta keys). A write a commit listener makes while a remote delivery or undo is being committed now has a summary naming every insert and delete it made; before, a block it moved could be reported removed and some deletes went missing.
+  - **Loading (DUR2):** load repair no longer copies nested children into the root order, and no longer removes an entry whose map is in flight.
+  - **System-origin writes:** normalization outside an apply runs in one `"system"`-origin transaction, so its repairs leave the undo stack. `setDocumentProfile()` no longer emits `ORIGIN_UNKNOWN`.
+  - **Awareness:** `DocumentSession.ensureAwareness(scopeId, factory)` lets the multiplayer extension create its own awareness. The extension now depends on `@input/pen-yjs` and peers on `yjs` and `y-protocols`.
+
+  Breaking: no
+
+- 56b8dfc: Keystrokes, caret moves and structural edits cost in proportion to what they touch, not to document size (SCALE2, SCALE6).
+
+  - **Indexes:**
+    - `DocumentState` gains `preorderIndexOf`, `preorderBlockIds`, `rootBlockIds` and `rootBlockIndexOf`.
+    - Root and `children` edits advance the position, parent, child and preorder indexes in place instead of rebuilding them.
+    - The change-summary index and the normalization pass index advance by each commit's delta.
+    - Measured at 50,000 blocks: an insert inside a container drops from about 41 ms to 0.15 ms, and a delete from about 11 ms to 0.3 ms.
+  - **Selection reads:** selection validation, anchor resolution, `buildLazyNormalPositionSnapshot` and vertical caret motion no longer read every block.
+  - **Scoped decorations:** new `scopedDecorationSource({ interest, decorate })` recomputes only the blocks a commit names. `decorationsChange` passes `(generation, changedBlockIds)`. `requestDecorationUpdate({ source?, blockIds })` targets sources, and with no argument it recomputes only function-form and static sources. AI suggestions and search use scoped sources, so typing with either installed no longer reads the whole document.
+  - **Block notifier:** new `fieldEditor.blockNotifier` fans editor events out per block. React (`EditorBlock`, `InlineContent`), Vue (`PenBlock`, `PenInlineContent`) and the vanilla `mountEditor` tree re-render only the blocks whose state changed. Each block acknowledges its own mount. `blocks-merged` carries `sourceParentId` / `sourceIndex`. Its list slices are rebuilt after a commit that moves the document index without a structural change, such as a duplicate-entry repair.
+  - **Multiplayer:** remote selection ranges look up positions in O(1).
+  - **Normalization:** the structural rules treat the lowest parent id as the container of a block listed in several arrays. Normalized documents are unchanged.
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/extensions/search/package.json b/packages/extensions/search/package.json
index 6817f4180..5fbd9c76b 100644
--- a/packages/extensions/search/package.json
+++ b/packages/extensions/search/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-search",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Document search and replacement primitives for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/extensions/shortcuts/CHANGELOG.md b/packages/extensions/shortcuts/CHANGELOG.md
index 6095e19d3..cb599d119 100644
--- a/packages/extensions/shortcuts/CHANGELOG.md
+++ b/packages/extensions/shortcuts/CHANGELOG.md
@@ -1,5 +1,28 @@
 # @input/pen-shortcuts
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/extensions/shortcuts/package.json b/packages/extensions/shortcuts/package.json
index 9df79b168..c195a930e 100644
--- a/packages/extensions/shortcuts/package.json
+++ b/packages/extensions/shortcuts/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-shortcuts",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Headless keyboard shortcut extension for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/extensions/snapshots/CHANGELOG.md b/packages/extensions/snapshots/CHANGELOG.md
index 7f4630a2c..8db479327 100644
--- a/packages/extensions/snapshots/CHANGELOG.md
+++ b/packages/extensions/snapshots/CHANGELOG.md
@@ -1,5 +1,28 @@
 # @input/pen-snapshots
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/extensions/snapshots/package.json b/packages/extensions/snapshots/package.json
index b47a30fbf..756e706d4 100644
--- a/packages/extensions/snapshots/package.json
+++ b/packages/extensions/snapshots/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-snapshots",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Snapshot history and attribution primitives for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/extensions/tools/CHANGELOG.md b/packages/extensions/tools/CHANGELOG.md
index ac7ff78fb..7f499ba39 100644
--- a/packages/extensions/tools/CHANGELOG.md
+++ b/packages/extensions/tools/CHANGELOG.md
@@ -1,5 +1,50 @@
 # @input/pen-tools
 
+## 0.3.0
+
+### Minor Changes
+
+- 56b8dfc: AI tool calls are attributed, classified and undone per call (AIB3, AIB4).
+
+  - `openAIToolCall()` returns the call's own `context`. `executeAITool()` and both transports run handlers with it. Every write path books a write to the call that issued it, and refuses a read-only call's write.
+  - The user's typing while a call is open is no call's write.
+  - `StreamingTarget.disableActiveWriter()` is replaced by the `activeWriter` getter.
+  - `ToolDefinition.destructive` may be a resolver `(input, { staged }) => boolean` (`ToolDestructiveResolver`, `ToolAuthorityContext`).
+  - `edit_document` consults `confirm` only for direct calls that remove or replace content: direct replaces, deletes of non-empty blocks, and content-kind changes. `authorizeAIToolCall` and `isDestructiveAITool` take the call's context.
+  - New `unconfirmedDestructive: "refuse"` (on `aiExtension`, `AIToolGrant`, `AIToolTurnOptions` and `AgenticLoopOptions`) blocks destructive calls when no resolver is installed. Use it in production if you expose `delete_block` or `write_document`.
+  - A host that used `confirm` as an "every AI edit" hook moves to `onBeforeApply` or the commit event.
+  - `UndoManager.syncExplicitUndoGroup` is replaced by `withCapture(origin, groupId, run)`. `CRDTUndoManager` gains an optional `setCaptureKey` (`CRDTUndoCaptureKey`).
+
+  Host migration:
+
+  - Tool handlers that compared `ctx.editor === editor` compare `ctx.editor.internals.adapter` (or the document) instead, because `ctx.editor` is now a per-call view.
+  - Code that called `StreamingTarget.disableActiveWriter()` reads `activeWriter`.
+  - Code that reads `ToolDefinition.destructive` as a boolean must handle a function.
+  - Hosts that implement `UndoManager` replace `syncExplicitUndoGroup` with `withCapture`.
+
+  Breaking: yes — tool handlers compare `ctx.editor.internals.adapter` instead of `ctx.editor === editor`, read `activeWriter` instead of `disableActiveWriter()`, handle a function-valued `ToolDefinition.destructive`, and `UndoManager` implementations replace `syncExplicitUndoGroup` with `withCapture`
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+  - @input/pen-ingest@0.3.0
+  - @input/pen-markdown@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/extensions/tools/package.json b/packages/extensions/tools/package.json
index ef39a7c96..1835915c9 100644
--- a/packages/extensions/tools/package.json
+++ b/packages/extensions/tools/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-tools",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Block CRUD and generation zone extension for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/extensions/undo/CHANGELOG.md b/packages/extensions/undo/CHANGELOG.md
index eabff137a..056090843 100644
--- a/packages/extensions/undo/CHANGELOG.md
+++ b/packages/extensions/undo/CHANGELOG.md
@@ -1,5 +1,78 @@
 # @input/pen-undo
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+- 56b8dfc: The editor selection record is now the single source of truth for every caret, range, cell, focus and pointer path. The renderers read and write the DOM selection only through it (S1), and every selection write carries its real origin (S3).
+
+  - Arrow keys beside inline atoms go through the keymap. In right-to-left blocks they follow the visual direction (M2).
+  - `BlockHandle.length()` counts each inline embed as one offset, so a block that holds only atoms has length 1 (N1).
+  - An edited table cell's caret is `CellSelection.text` (`{ anchor, focus }` in the cell's offsets). It is validated, anchored and mapped through remote edits. `selectionChange` now fires for in-cell caret moves, and Shift+Arrow inside a cell extends the range. `setCellCaretFocus`, `getCellCaretFocus`, `CellCaretFocus` and `CellCaretWrite` are removed.
+  - Pointer gestures, drops and pointer pastes write origin `pointer`. Keymap commands, text input and shortcut pastes write `keyboard`, or `ime` while composing. Undo and redo write `restore`. Keyboard, IME and history moves scroll into view.
+  - Focus follows the selection record. Block and cell selections focus the focus sink. App and `null` selections focus the root, but only while the editor already owns focus. Escape writes the selection and lets its projection place focus (W3.R16).
+  - `programmatic`, `mapped` and `gc` writes no longer take focus the editor does not own (HOST9). Projection and focus work in editors mounted in an iframe. Tabbing into a multi-block range projects it.
+  - A reconcile no longer saves and restores the native selection. Rebuilt blocks are projected through `fieldEditor.projectAfterRebuild(blockIds)` (P3).
+  - A parked projection resolves on the mount ack of its own block (P4). `DomScheduler` no longer projects selections.
+  - Pointer drags and block clicks are resolved through core's transitions (`resolvePointerSelectionIntent`). A pointer selection that moves to another block ends the current undo step.
+  - The contenteditable, EditContext and expanded backends share one base class (CS5).
+  - `FieldEditorImpl` and `FieldEditorSession` drop methods that only forwarded to pen-dom internals. pen-dom reports gestures itself.
+
+  Host migration:
+
+  - Hosts that relied on ArrowLeft always meaning "previous atom" in right-to-left blocks now get the visual direction. There is no API change.
+  - Hosts that read `length() === 0` as "this block has no text" check `textContent() === ""` instead when an atom-only block must count as empty.
+  - Hosts that drove in-cell caret motion through `setCellCaretFocus` write a `CellSelection` with `text` instead: `editor.setSelection({ type: "cell", blockId, anchor: cell, head: cell, text: { anchor: caretOffset, focus: caretOffset } })`. Hosts that treated every `selectionChange` to a `cell` selection as a grid move check `text` first.
+  - Hosts that call `applyDocumentTextSelection` or `applyDomTextSelection` pass the gesture's origin, for example `"pointer"`.
+  - Hosts that read `selectionChange` origins see `restore` for history restores and drop any `programmatic` check they used to detect them.
+  - Hosts that call `handleEscapeSelectionTransition` drop `root`. Hosts that relied on Escape or `deactivate()` focusing the block element focus the sink or root instead.
+  - Hosts that called `editor.setSelection` (or another programmatic write) while focus was outside the editor, and relied on it focusing the editor, now focus it explicitly. In React use `useFocusController().text(…)` / `.range(…)`; elsewhere use the field editor's `focus()`.
+  - Hosts that called `saveSelection` / `restoreSelection` or passed `preserveSelection` to `fullReconcileToDOM` / `fullReconcileDeltasToDOM` drop them and call `fieldEditor.projectAfterRebuild([blockId])` after rebuilding a block.
+  - Hosts that called `editorSelectionToDOM` call `editor.setSelection` instead and let the field editor project it. `findDOMPoint` is now exported from `@input/pen-dom/field-editor/selectionBridge`.
+  - Hosts that passed `onProjectSelection`, called `setProjector` or read `projectedThisFlush` on a `DomScheduler` drop those calls, and the `SelectionProjector` type is gone. Selection projection runs in the field editor.
+  - Hosts that call `resolvePointerDragSelection` drop the `getBoundaryPoint` option. Hosts that build a `PointerSelectionGesture` by hand use `createPointerSelectionGesture`, or set `startSelectionVersion` and `committed: false`.
+  - Hosts that call `attachContentGestures` drop `skipNextClick` from `state`.
+  - Hosts that subclass a backend and use a removed protected member move to the new members:
+    - `ContentEditableBackend`: `fullReconcileActiveField` becomes `rebuildField(inlineDecorations?, blockId?)`, and `applyTextDiffAsOps` drops its `deferredRemoteDeltas` argument.
+    - `EditContextBackend`: `resolveEditorSelectionRange`, `resolveCollapsedEditorSelectionRange`, `getAuthoritativeTextInputSelection` and `resolveKeyDownRange` become `authorityRangeIn(blockId)` / `trustedCaretIn(blockId)`.
+  - Hosts drop calls to the removed field-editor forwarders: `beginPointerSelection`, `endPointerSelection`, `notifyGestureEvent`, `isAdmissibleGestureRead`, `getGestureWindows`, `requestDivergenceProjection`, `shouldProjectSelectionAfterReconcile`, `requestActivation` and `resolveInsertMarks`. Focus, pending marks and post-rebuild projection keep their session methods.
+
+  Breaking: yes — hosts apply the migration list above (pass origins, write `CellSelection.text` instead of `setCellCaretFocus`, replace `saveSelection`/`restoreSelection`/`editorSelectionToDOM` with `editor.setSelection` plus `projectAfterRebuild`, drop removed scheduler/gesture/forwarder members, focus the editor explicitly after programmatic writes)
+
+### Patch Changes
+
+- 56b8dfc: AI write attribution, undo grouping and review preview fixes:
+
+  - **Undo grouping (AIB4)**
+    - Undo capture is keyed. One AI action stays one undo step even when the user types during it, and the user's typing stays its own steps.
+    - `directTransport` and `createSSEHandler` give each request one undo group.
+  - **Overlapping calls (AIB3)**
+    - Overlapping tool calls and generations keep their own write guard and staged-write binding. A call closing out of order no longer drops another call's guard or unstages its writes.
+    - `directTransport`, `createSSEHandler` and `processStream` apply the editor's `confirm` / `unconfirmedDestructive` policy and accept their own overrides. They allow destructive calls by default when no `confirm` is set, so a server reachable by clients should pass `"refuse"` or a `confirm`.
+  - **Review preview (RS6):** the `edit_document` streaming preview matches what accept will do: multi-block and nested replaces and deletes, insert placement, empty replacements, and moves and formatting. `EditDocumentPreviewUpdate` gains `blockIds`, `placement` and `complete`. `AIStreamingReviewPreviewInput` gains `complete`, `deletesBlocks` and `replacesBlocks`.
+  - **Undo drift anchors:** undo reuses the selection authority's anchors through `editor.internals.selectionAnchors()` and `selectionAnchorRepair()` (AN14). This stops spurious `anchor-target-missing` for ranges that end on dividers or tables, and halves `anchor-budget` churn.
+  - **Anchor repair (AN2, AN14):** `repairAnchor` reads an anchor's position from before the commit even when something resolved it after the commit landed, so a split no longer moves an anchor to the start of the new block. An anchor that resolved to `null` before a commit stays dead instead of being revived from an older position, and a merge carries an `assoc: -1` anchor at the start of the merged-away block into the target instead of losing it.
+  - **React AI chrome:** the AI suggestions popover is scoped to its own editor. `Pen.AI.ContextualPromptComposer` no longer throws a hook-order error when a session opens.
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/extensions/undo/package.json b/packages/extensions/undo/package.json
index 8906518dd..b55e856fe 100644
--- a/packages/extensions/undo/package.json
+++ b/packages/extensions/undo/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-undo",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Undo/redo extension with origin tagging for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/pen/CHANGELOG.md b/packages/pen/CHANGELOG.md
index 9375ac0f2..a61adad8c 100644
--- a/packages/pen/CHANGELOG.md
+++ b/packages/pen/CHANGELOG.md
@@ -1,5 +1,35 @@
 # @input/pen
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-tools@0.3.0
+  - @input/pen-ai@0.3.0
+  - @input/pen-core@0.3.0
+  - @input/pen-undo@0.3.0
+  - @input/pen-interop@0.3.0
+  - @input/pen-schema@0.3.0
+  - @input/pen-shortcuts@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/pen/package.json b/packages/pen/package.json
index ed4d7c4f6..d79404902 100644
--- a/packages/pen/package.json
+++ b/packages/pen/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Default batteries-included editor preset for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/rendering/dom/CHANGELOG.md b/packages/rendering/dom/CHANGELOG.md
index 4f169e3a3..8b68f0278 100644
--- a/packages/rendering/dom/CHANGELOG.md
+++ b/packages/rendering/dom/CHANGELOG.md
@@ -1,5 +1,208 @@
 # @input/pen-dom
 
+## 0.3.0
+
+### Minor Changes
+
+- 56b8dfc: List items are announced as lists (AX1), numbering follows the announced list, and focus returns to its invoker by primitive class (AX3).
+
+  - Each run of list items renders inside a `div[data-pen-list-group][role="list"]`. Each item's host carries `role="listitem"`, `aria-level`, `aria-posinset` and `aria-setsize`. This applies on React, Vue and `mountEditor`.
+  - New core helpers `getListSegments`, `getListItemSemantics` and `isListItemType` describe these lists.
+  - `getNumberedListItemValue` and the notifier's `list.ordinal` count numbered items over the item's sibling list instead of the root `blockOrder`. Markdown export's list `start` follows the same count.
+  - Toolbar buttons and toggles keep focus in the field: they prevent the primary-button `mousedown` default. Escape returns focus to the editor.
+  - The selection toolbar, block handle menu, table column menu, AI command menu and AI contextual prompt return focus to their invoker synchronously.
+
+  Host migration:
+
+  - Hosts that walk the blocks host's direct children, or style list items with `[data-pen-editor-block] + [data-pen-editor-block]`, must also look inside the `[data-pen-list-group]` wrappers. Items moving between groups are remounted by React and Vue.
+  - A numbered item that follows a container with numbered `parentId` children now starts at 1, and items in a `children` array number 1, 2, 3. Hosts or snapshots that expect the old numbering update it. There is no API change.
+  - Hosts whose toolbar `onClick` handlers relied on the button holding focus (for example by reading `document.activeElement`) read the editor selection instead.
+  - Hosts that refocused the editor themselves after the AI command menu or contextual prompt closed remove that refocus.
+
+  Breaking: yes — hosts look inside `[data-pen-list-group]` wrappers when walking or styling list items, update expected numbered-list values after containers, read the editor selection instead of toolbar focus, and drop their own refocus after AI menus close
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+- 56b8dfc: Pen draws carets, remote carets and block outlines into one overlay layer per editor root, which the DOM scheduler paints. React's caret overlays are now bindings over it, and Vue gets the overlay with no component.
+
+  - Each editor root holds one `data-pen-overlay-layer` element as its last child. `getRootOverlay(root)` exposes the paint plan and the contributor API.
+  - The overlay caret is on by default on every surface. It draws carets beside mentions and inline apps, in empty blocks, and at range endpoints. Block and grid-cell selections get a library outline.
+  - New tokens: `--pen-overlay-z-index`, `--pen-editor-endpoint-caret-color`, `--pen-block-selection-outline`, `--pen-block-selection-background`, `--pen-block-selection-radius`, `--pen-selection-range-background` and `--pen-selection-range-opacity`.
+  - The editor root is the layer's containing block (OV2). The chrome stylesheet gives it `position: relative` at zero specificity, and a static root gets it inline while attached.
+  - `Pen.Editor.CaretOverlay` switches the root to `customCaret` mode, and pen-dom paints the caret. The blink restarts on each edit and caret move.
+  - `Pen.Multiplayer.CaretOverlay` registers the remote-caret contributor. Remote carets stay on their text inside transformed ancestors, and the binding drops its `MutationObserver`, rAF loop and scroll listeners.
+  - `EditorSelectionRect` draws only the region-selection marquee.
+
+  Host migration:
+
+  - Host CSS that targets the editor root's `:last-child` accounts for the `data-pen-overlay-layer` element.
+  - Hosts that already style `[data-selected]` set `--pen-block-selection-outline: none` on the editor root (or drop their rule) to avoid a double outline.
+  - Set caret and outline tokens (`--pen-editor-caret-*` included) on the editor root or above, not on a wrapper inside it.
+  - Scope `[data-block-id]` selectors to `[data-pen-editor-block]` if they must not match overlay items.
+  - Absolutely positioned host elements inside a static editor root now position against the root. Give the root its own non-static `position` to keep another containing block.
+  - `renderCaret` (editor and multiplayer) and `renderLabel` position from the `transform` in `caretStyle` / `labelStyle`, not `left` / `top`. Their nodes are portaled into the overlay layer, and `renderCaret` receives `affinity`.
+  - Hosts that style `[data-pen-editor-caret-overlay][data-caret-visible]` target `[data-pen-overlay-layer][data-caret-visible]`.
+  - Remote carets move from the `[data-pen-multiplayer-caret-overlay]` host into the layer. Restyle them under `[data-pen-overlay-layer] [data-pen-multiplayer-caret]`. They change from `position: fixed` with `left` / `top` to `position: absolute` with a `transform`. The default label is a child of its caret and no longer carries `data-pen-multiplayer-caret` or the user attributes.
+  - Hosts that relied on `EditorSelectionRect` to draw a committed block selection style `--pen-block-selection-outline` / `--pen-block-selection-background` on the editor root instead.
+
+  Breaking: yes — hosts update `:last-child`, `[data-selected]`, caret-overlay and remote-caret CSS for the root's `data-pen-overlay-layer`, position custom `renderCaret`/`renderLabel` output from `transform`, set tokens on the root, and give the root its own `position` if it must not be the containing block
+
+- 56b8dfc: The editor selection record is now the single source of truth for every caret, range, cell, focus and pointer path. The renderers read and write the DOM selection only through it (S1), and every selection write carries its real origin (S3).
+
+  - Arrow keys beside inline atoms go through the keymap. In right-to-left blocks they follow the visual direction (M2).
+  - `BlockHandle.length()` counts each inline embed as one offset, so a block that holds only atoms has length 1 (N1).
+  - An edited table cell's caret is `CellSelection.text` (`{ anchor, focus }` in the cell's offsets). It is validated, anchored and mapped through remote edits. `selectionChange` now fires for in-cell caret moves, and Shift+Arrow inside a cell extends the range. `setCellCaretFocus`, `getCellCaretFocus`, `CellCaretFocus` and `CellCaretWrite` are removed.
+  - Pointer gestures, drops and pointer pastes write origin `pointer`. Keymap commands, text input and shortcut pastes write `keyboard`, or `ime` while composing. Undo and redo write `restore`. Keyboard, IME and history moves scroll into view.
+  - Focus follows the selection record. Block and cell selections focus the focus sink. App and `null` selections focus the root, but only while the editor already owns focus. Escape writes the selection and lets its projection place focus (W3.R16).
+  - `programmatic`, `mapped` and `gc` writes no longer take focus the editor does not own (HOST9). Projection and focus work in editors mounted in an iframe. Tabbing into a multi-block range projects it.
+  - A reconcile no longer saves and restores the native selection. Rebuilt blocks are projected through `fieldEditor.projectAfterRebuild(blockIds)` (P3).
+  - A parked projection resolves on the mount ack of its own block (P4). `DomScheduler` no longer projects selections.
+  - Pointer drags and block clicks are resolved through core's transitions (`resolvePointerSelectionIntent`). A pointer selection that moves to another block ends the current undo step.
+  - The contenteditable, EditContext and expanded backends share one base class (CS5).
+  - `FieldEditorImpl` and `FieldEditorSession` drop methods that only forwarded to pen-dom internals. pen-dom reports gestures itself.
+
+  Host migration:
+
+  - Hosts that relied on ArrowLeft always meaning "previous atom" in right-to-left blocks now get the visual direction. There is no API change.
+  - Hosts that read `length() === 0` as "this block has no text" check `textContent() === ""` instead when an atom-only block must count as empty.
+  - Hosts that drove in-cell caret motion through `setCellCaretFocus` write a `CellSelection` with `text` instead: `editor.setSelection({ type: "cell", blockId, anchor: cell, head: cell, text: { anchor: caretOffset, focus: caretOffset } })`. Hosts that treated every `selectionChange` to a `cell` selection as a grid move check `text` first.
+  - Hosts that call `applyDocumentTextSelection` or `applyDomTextSelection` pass the gesture's origin, for example `"pointer"`.
+  - Hosts that read `selectionChange` origins see `restore` for history restores and drop any `programmatic` check they used to detect them.
+  - Hosts that call `handleEscapeSelectionTransition` drop `root`. Hosts that relied on Escape or `deactivate()` focusing the block element focus the sink or root instead.
+  - Hosts that called `editor.setSelection` (or another programmatic write) while focus was outside the editor, and relied on it focusing the editor, now focus it explicitly. In React use `useFocusController().text(…)` / `.range(…)`; elsewhere use the field editor's `focus()`.
+  - Hosts that called `saveSelection` / `restoreSelection` or passed `preserveSelection` to `fullReconcileToDOM` / `fullReconcileDeltasToDOM` drop them and call `fieldEditor.projectAfterRebuild([blockId])` after rebuilding a block.
+  - Hosts that called `editorSelectionToDOM` call `editor.setSelection` instead and let the field editor project it. `findDOMPoint` is now exported from `@input/pen-dom/field-editor/selectionBridge`.
+  - Hosts that passed `onProjectSelection`, called `setProjector` or read `projectedThisFlush` on a `DomScheduler` drop those calls, and the `SelectionProjector` type is gone. Selection projection runs in the field editor.
+  - Hosts that call `resolvePointerDragSelection` drop the `getBoundaryPoint` option. Hosts that build a `PointerSelectionGesture` by hand use `createPointerSelectionGesture`, or set `startSelectionVersion` and `committed: false`.
+  - Hosts that call `attachContentGestures` drop `skipNextClick` from `state`.
+  - Hosts that subclass a backend and use a removed protected member move to the new members:
+    - `ContentEditableBackend`: `fullReconcileActiveField` becomes `rebuildField(inlineDecorations?, blockId?)`, and `applyTextDiffAsOps` drops its `deferredRemoteDeltas` argument.
+    - `EditContextBackend`: `resolveEditorSelectionRange`, `resolveCollapsedEditorSelectionRange`, `getAuthoritativeTextInputSelection` and `resolveKeyDownRange` become `authorityRangeIn(blockId)` / `trustedCaretIn(blockId)`.
+  - Hosts drop calls to the removed field-editor forwarders: `beginPointerSelection`, `endPointerSelection`, `notifyGestureEvent`, `isAdmissibleGestureRead`, `getGestureWindows`, `requestDivergenceProjection`, `shouldProjectSelectionAfterReconcile`, `requestActivation` and `resolveInsertMarks`. Focus, pending marks and post-rebuild projection keep their session methods.
+
+  Breaking: yes — hosts apply the migration list above (pass origins, write `CellSelection.text` instead of `setCellCaretFocus`, replace `saveSelection`/`restoreSelection`/`editorSelectionToDOM` with `editor.setSelection` plus `projectAfterRebuild`, drop removed scheduler/gesture/forwarder members, focus the editor explicitly after programmatic writes)
+
+### Patch Changes
+
+- 56b8dfc: Accessibility, iframe and binding fixes:
+
+  - **Reduced motion (AX6):** each root has one shared reduced-motion signal. The new `getRootReducedMotion`, `AX6_MOTION_MAPPING` and `REDUCED_MOTION_ATTR` are in pen-dom, and `useReducedMotion()` is in pen-react. The AI suggestion underline no longer animates under reduced motion.
+  - **Announcements (AX2):** live-region announcements are written in the scheduler's write phase.
+  - **Focus return for custom chrome (AX3):**
+    - New `captureFocusReturn` / `restoreFocusReturn` implement the focus-return rule.
+    - The slash, suggestion and table column menus dismiss on an outside `pointerdown`.
+  - **Iframe-mounted editors:**
+    - Every node and event check uses realm-safe guards, exported from the new `@input/pen-dom/utils/domNodes` subpath (`isDomNode`, `isDomElement`, `isDomEvent` and others).
+    - Nodes are created in the field's own document, and chrome resolves its document through `resolveEditorOwnerDocument`.
+  - **`asChild`:** `asChild` composes the child's handlers, class names and styles with the primitive's instead of replacing them.
+
+  Breaking: no
+
+- 56b8dfc: IME composition no longer loses, duplicates or misplaces text:
+
+  - EditContext compositions have one lifecycle from `compositionstart` to `compositionend` (C4). Multi-update compositions (pinyin, Korean, Japanese) no longer delete text after the caret. Typing no longer duplicates edits in the buffer.
+  - Collaborator, AI, extension and history edits that arrive mid-composition are deferred. The composition is rebased over them with Yjs placement on both backends (C2, COL1).
+  - A cancelled or empty composition returns the caret to its start and renders deferred decorations (C1).
+  - A composition over a cross-block selection no longer crashes React in Firefox. It opens the IME window in the expanded host.
+  - An Android `keyCode` 229 keydown no longer deletes a cross-block range by itself (FE2).
+
+  Breaking: no
+
+- 56b8dfc: HTML import no longer drops pasted text.
+
+  - Nested lists from Slack, Apple Notes and Google Docs keep every item at the right indent.
+  - Block content wrapped in an inline element (Google Docs' outer ``) stays as separate blocks.
+  - Table captions and text outside `` in a `
` are kept.
+  - A conversion that would still lose text falls back to plain paragraphs or the literal clipboard text.
+  - The sanitizer's `isomorphic-dompurify` moves from `~2.36.0` to `~4.3.0`. Its allowlist and output are unchanged (SEC7).
+
+  Breaking: no
+
+- 56b8dfc: Keystrokes, caret moves and structural edits cost in proportion to what they touch, not to document size (SCALE2, SCALE6).
+
+  - **Indexes:**
+    - `DocumentState` gains `preorderIndexOf`, `preorderBlockIds`, `rootBlockIds` and `rootBlockIndexOf`.
+    - Root and `children` edits advance the position, parent, child and preorder indexes in place instead of rebuilding them.
+    - The change-summary index and the normalization pass index advance by each commit's delta.
+    - Measured at 50,000 blocks: an insert inside a container drops from about 41 ms to 0.15 ms, and a delete from about 11 ms to 0.3 ms.
+  - **Selection reads:** selection validation, anchor resolution, `buildLazyNormalPositionSnapshot` and vertical caret motion no longer read every block.
+  - **Scoped decorations:** new `scopedDecorationSource({ interest, decorate })` recomputes only the blocks a commit names. `decorationsChange` passes `(generation, changedBlockIds)`. `requestDecorationUpdate({ source?, blockIds })` targets sources, and with no argument it recomputes only function-form and static sources. AI suggestions and search use scoped sources, so typing with either installed no longer reads the whole document.
+  - **Block notifier:** new `fieldEditor.blockNotifier` fans editor events out per block. React (`EditorBlock`, `InlineContent`), Vue (`PenBlock`, `PenInlineContent`) and the vanilla `mountEditor` tree re-render only the blocks whose state changed. Each block acknowledges its own mount. `blocks-merged` carries `sourceParentId` / `sourceIndex`. Its list slices are rebuilt after a commit that moves the document index without a structural change, such as a duplicate-entry repair.
+  - **Multiplayer:** remote selection ranges look up positions in O(1).
+  - **Normalization:** the structural rules treat the lowest parent id as the container of a block listed in several arrays. Normalized documents are unchanged.
+
+  Breaking: no
+
+- 56b8dfc: Additive overlay APIs and remote-caret safety and contrast fixes:
+
+  - `RootOverlay.holdCaretMode`, `overlayItemStyle` and `overlayLabelStyle` are exported for bindings.
+  - `attachRemoteCarets(overlay, source)` and `getRemoteCaretSource(editor)` paint collaborators' carets. Vue gains `PenMultiplayerCaretOverlay`.
+  - `FieldEditorImpl.setReadOnly` carries the renderer `readonly` prop.
+  - New `isSafeCssColor` in `@input/pen-core`. Every `--pen-peer-color` write is re-validated, and carets paint through `background-color` so a colour cannot become an image fetch (COL2).
+  - The default peer palette (`MULTIPLAYER_COLORS`) uses darker shades so labels meet 4.5:1 contrast. Hosts that pass `user.color` are unaffected.
+  - A local keystroke that leaves every peer in place no longer notifies multiplayer subscribers or repaints remote carets.
+
+  Breaking: no
+
+- 56b8dfc: Selection, focus and pointer fixes across React, Vue and vanilla `mountEditor`:
+
+  - **Reading and writing the selection**
+    - One `selectionchange` reader per root and one projector are the only code that reads or writes the DOM selection (S1).
+    - Input edits the editor selection instead of re-reading the DOM.
+    - `focus()` on a field with no caret commits one to the record.
+    - Every projection reads the selection back. A mismatch emits `selection-projection-mismatch` once and is not rewritten (W3.R1).
+    - The backend selection stamps (`FieldEditorSelectionSource`) are gone.
+  - **Focus and projection timing**
+    - Projections are withheld while an IME composes, while editor chrome holds focus, and while a host control holds focus (HOST9).
+    - Undo/redo rebuilds and passive attaches no longer write the native range or steal focus.
+  - **New APIs**
+    - The selection convenience setters take an optional `SelectionWriteOptions` with `origin` (S3).
+    - `FieldEditorImpl.scrollIntoView` and `FieldEditorImpl.setMountRequester` are new. A projection parked on an unmounted block asks the mount requester to mount it. `selection-target-unmounted` is emitted once per park, and `BlockScrollAlign` is added.
+    - `FieldEditorImpl.connect()` keeps the field editor live under React Strict Mode.
+    - `focusTextSelection` and `useFocusController` run in the calling turn (S4).
+  - **Undo, redo and scrolling:** undo and redo scroll the restored selection into view even when the caret did not move. Scroll deltas round to whole pixels.
+  - **Ranges over more than 50 blocks (S2):** these ranges, and ranges the engine confines to one field, show a substitute state with the focus sink revealed (`getSubstituteState()`, `pen.a11y.textRangeSelected`). Typing, deleting, IME input and clipboard actions work there.
+  - **Fuzzer findings:** S2 cross-block projections now handle code blocks, tables, unit-block gaps, unmounted endpoints and WebKit drags.
+  - **Gesture windows:**
+    - Gesture windows survive a field switch.
+    - `pointercancel` ends a pointer gesture, and drag and context-menu windows close properly (R1).
+    - Touch selection handles keep their range (`native-range` window).
+    - An EditContext keystroke after `editor.selectText` lands at the new caret (FE9).
+  - **Clicks and arrows around atoms and blocks:**
+    - A shift-click into another block extends to the offset under the pointer on every binding, including from a block or cell selection and over dividers.
+    - Clicking an inline atom puts the caret on the clicked side, following the atom's bidi run (O1, T5).
+    - `pen.caretUp` / `pen.caretDown` into a table select its edge cell (T5).
+    - A click in a table cell during a text range selects the cell.
+  - **Atom layout:** atoms share the text line, and a caret beside a chip takes the line's height. Compositions beside atoms no longer insert U+FFFC.
+  - **Table cells:**
+    - `CellSelection.text` is in `@input/pen-types`, and an edited cell keeps DOM focus (W3.R18).
+    - `Mod-b`/`i`/`u` in a cell fail closed on keydown in every browser (FE6).
+    - A table props/meta change no longer resets an edited cell.
+    - Backspace and Delete in an edited cell delete the range or one character instead of clearing the cell. `editor.deleteSelection()` on a `CellSelection` with `text` deletes only that range (A1, T6).
+  - **Positioned chrome:** the selection toolbar, AI prompt and slash/suggestion menus position from the editor selection, not the live DOM range. `DomScheduler.measureNow` drops geometry stale since the last commit (SCH2).
+  - **Focus and activation:**
+    - Shift+Tab out of the editor works in Firefox.
+    - Overlay focus follows window blur (`bindEditorRootFocus`).
+    - The toggle empty state, block-type select and slash-menu table insert activate in the same turn.
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+  - @input/pen-shortcuts@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/rendering/dom/package.json b/packages/rendering/dom/package.json
index 7cc212ab1..29a46a1f4 100644
--- a/packages/rendering/dom/package.json
+++ b/packages/rendering/dom/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-dom",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "DOM field-editor engine and helpers for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/rendering/react/CHANGELOG.md b/packages/rendering/react/CHANGELOG.md
index cbbe0a848..2de919ee1 100644
--- a/packages/rendering/react/CHANGELOG.md
+++ b/packages/rendering/react/CHANGELOG.md
@@ -1,5 +1,168 @@
 # @input/pen-react
 
+## 0.3.0
+
+### Minor Changes
+
+- 56b8dfc: List items are announced as lists (AX1), numbering follows the announced list, and focus returns to its invoker by primitive class (AX3).
+
+  - Each run of list items renders inside a `div[data-pen-list-group][role="list"]`. Each item's host carries `role="listitem"`, `aria-level`, `aria-posinset` and `aria-setsize`. This applies on React, Vue and `mountEditor`.
+  - New core helpers `getListSegments`, `getListItemSemantics` and `isListItemType` describe these lists.
+  - `getNumberedListItemValue` and the notifier's `list.ordinal` count numbered items over the item's sibling list instead of the root `blockOrder`. Markdown export's list `start` follows the same count.
+  - Toolbar buttons and toggles keep focus in the field: they prevent the primary-button `mousedown` default. Escape returns focus to the editor.
+  - The selection toolbar, block handle menu, table column menu, AI command menu and AI contextual prompt return focus to their invoker synchronously.
+
+  Host migration:
+
+  - Hosts that walk the blocks host's direct children, or style list items with `[data-pen-editor-block] + [data-pen-editor-block]`, must also look inside the `[data-pen-list-group]` wrappers. Items moving between groups are remounted by React and Vue.
+  - A numbered item that follows a container with numbered `parentId` children now starts at 1, and items in a `children` array number 1, 2, 3. Hosts or snapshots that expect the old numbering update it. There is no API change.
+  - Hosts whose toolbar `onClick` handlers relied on the button holding focus (for example by reading `document.activeElement`) read the editor selection instead.
+  - Hosts that refocused the editor themselves after the AI command menu or contextual prompt closed remove that refocus.
+
+  Breaking: yes — hosts look inside `[data-pen-list-group]` wrappers when walking or styling list items, update expected numbered-list values after containers, read the editor selection instead of toolbar focus, and drop their own refocus after AI menus close
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+- 56b8dfc: Pen draws carets, remote carets and block outlines into one overlay layer per editor root, which the DOM scheduler paints. React's caret overlays are now bindings over it, and Vue gets the overlay with no component.
+
+  - Each editor root holds one `data-pen-overlay-layer` element as its last child. `getRootOverlay(root)` exposes the paint plan and the contributor API.
+  - The overlay caret is on by default on every surface. It draws carets beside mentions and inline apps, in empty blocks, and at range endpoints. Block and grid-cell selections get a library outline.
+  - New tokens: `--pen-overlay-z-index`, `--pen-editor-endpoint-caret-color`, `--pen-block-selection-outline`, `--pen-block-selection-background`, `--pen-block-selection-radius`, `--pen-selection-range-background` and `--pen-selection-range-opacity`.
+  - The editor root is the layer's containing block (OV2). The chrome stylesheet gives it `position: relative` at zero specificity, and a static root gets it inline while attached.
+  - `Pen.Editor.CaretOverlay` switches the root to `customCaret` mode, and pen-dom paints the caret. The blink restarts on each edit and caret move.
+  - `Pen.Multiplayer.CaretOverlay` registers the remote-caret contributor. Remote carets stay on their text inside transformed ancestors, and the binding drops its `MutationObserver`, rAF loop and scroll listeners.
+  - `EditorSelectionRect` draws only the region-selection marquee.
+
+  Host migration:
+
+  - Host CSS that targets the editor root's `:last-child` accounts for the `data-pen-overlay-layer` element.
+  - Hosts that already style `[data-selected]` set `--pen-block-selection-outline: none` on the editor root (or drop their rule) to avoid a double outline.
+  - Set caret and outline tokens (`--pen-editor-caret-*` included) on the editor root or above, not on a wrapper inside it.
+  - Scope `[data-block-id]` selectors to `[data-pen-editor-block]` if they must not match overlay items.
+  - Absolutely positioned host elements inside a static editor root now position against the root. Give the root its own non-static `position` to keep another containing block.
+  - `renderCaret` (editor and multiplayer) and `renderLabel` position from the `transform` in `caretStyle` / `labelStyle`, not `left` / `top`. Their nodes are portaled into the overlay layer, and `renderCaret` receives `affinity`.
+  - Hosts that style `[data-pen-editor-caret-overlay][data-caret-visible]` target `[data-pen-overlay-layer][data-caret-visible]`.
+  - Remote carets move from the `[data-pen-multiplayer-caret-overlay]` host into the layer. Restyle them under `[data-pen-overlay-layer] [data-pen-multiplayer-caret]`. They change from `position: fixed` with `left` / `top` to `position: absolute` with a `transform`. The default label is a child of its caret and no longer carries `data-pen-multiplayer-caret` or the user attributes.
+  - Hosts that relied on `EditorSelectionRect` to draw a committed block selection style `--pen-block-selection-outline` / `--pen-block-selection-background` on the editor root instead.
+
+  Breaking: yes — hosts update `:last-child`, `[data-selected]`, caret-overlay and remote-caret CSS for the root's `data-pen-overlay-layer`, position custom `renderCaret`/`renderLabel` output from `transform`, set tokens on the root, and give the root its own `position` if it must not be the containing block
+
+### Patch Changes
+
+- 56b8dfc: Accessibility, iframe and binding fixes:
+
+  - **Reduced motion (AX6):** each root has one shared reduced-motion signal. The new `getRootReducedMotion`, `AX6_MOTION_MAPPING` and `REDUCED_MOTION_ATTR` are in pen-dom, and `useReducedMotion()` is in pen-react. The AI suggestion underline no longer animates under reduced motion.
+  - **Announcements (AX2):** live-region announcements are written in the scheduler's write phase.
+  - **Focus return for custom chrome (AX3):**
+    - New `captureFocusReturn` / `restoreFocusReturn` implement the focus-return rule.
+    - The slash, suggestion and table column menus dismiss on an outside `pointerdown`.
+  - **Iframe-mounted editors:**
+    - Every node and event check uses realm-safe guards, exported from the new `@input/pen-dom/utils/domNodes` subpath (`isDomNode`, `isDomElement`, `isDomEvent` and others).
+    - Nodes are created in the field's own document, and chrome resolves its document through `resolveEditorOwnerDocument`.
+  - **`asChild`:** `asChild` composes the child's handlers, class names and styles with the primitive's instead of replacing them.
+
+  Breaking: no
+
+- 56b8dfc: AI write attribution, undo grouping and review preview fixes:
+
+  - **Undo grouping (AIB4)**
+    - Undo capture is keyed. One AI action stays one undo step even when the user types during it, and the user's typing stays its own steps.
+    - `directTransport` and `createSSEHandler` give each request one undo group.
+  - **Overlapping calls (AIB3)**
+    - Overlapping tool calls and generations keep their own write guard and staged-write binding. A call closing out of order no longer drops another call's guard or unstages its writes.
+    - `directTransport`, `createSSEHandler` and `processStream` apply the editor's `confirm` / `unconfirmedDestructive` policy and accept their own overrides. They allow destructive calls by default when no `confirm` is set, so a server reachable by clients should pass `"refuse"` or a `confirm`.
+  - **Review preview (RS6):** the `edit_document` streaming preview matches what accept will do: multi-block and nested replaces and deletes, insert placement, empty replacements, and moves and formatting. `EditDocumentPreviewUpdate` gains `blockIds`, `placement` and `complete`. `AIStreamingReviewPreviewInput` gains `complete`, `deletesBlocks` and `replacesBlocks`.
+  - **Undo drift anchors:** undo reuses the selection authority's anchors through `editor.internals.selectionAnchors()` and `selectionAnchorRepair()` (AN14). This stops spurious `anchor-target-missing` for ranges that end on dividers or tables, and halves `anchor-budget` churn.
+  - **Anchor repair (AN2, AN14):** `repairAnchor` reads an anchor's position from before the commit even when something resolved it after the commit landed, so a split no longer moves an anchor to the start of the new block. An anchor that resolved to `null` before a commit stays dead instead of being revived from an older position, and a merge carries an `assoc: -1` anchor at the start of the merged-away block into the target instead of losing it.
+  - **React AI chrome:** the AI suggestions popover is scoped to its own editor. `Pen.AI.ContextualPromptComposer` no longer throws a hook-order error when a session opens.
+
+  Breaking: no
+
+- 56b8dfc: Keystrokes, caret moves and structural edits cost in proportion to what they touch, not to document size (SCALE2, SCALE6).
+
+  - **Indexes:**
+    - `DocumentState` gains `preorderIndexOf`, `preorderBlockIds`, `rootBlockIds` and `rootBlockIndexOf`.
+    - Root and `children` edits advance the position, parent, child and preorder indexes in place instead of rebuilding them.
+    - The change-summary index and the normalization pass index advance by each commit's delta.
+    - Measured at 50,000 blocks: an insert inside a container drops from about 41 ms to 0.15 ms, and a delete from about 11 ms to 0.3 ms.
+  - **Selection reads:** selection validation, anchor resolution, `buildLazyNormalPositionSnapshot` and vertical caret motion no longer read every block.
+  - **Scoped decorations:** new `scopedDecorationSource({ interest, decorate })` recomputes only the blocks a commit names. `decorationsChange` passes `(generation, changedBlockIds)`. `requestDecorationUpdate({ source?, blockIds })` targets sources, and with no argument it recomputes only function-form and static sources. AI suggestions and search use scoped sources, so typing with either installed no longer reads the whole document.
+  - **Block notifier:** new `fieldEditor.blockNotifier` fans editor events out per block. React (`EditorBlock`, `InlineContent`), Vue (`PenBlock`, `PenInlineContent`) and the vanilla `mountEditor` tree re-render only the blocks whose state changed. Each block acknowledges its own mount. `blocks-merged` carries `sourceParentId` / `sourceIndex`. Its list slices are rebuilt after a commit that moves the document index without a structural change, such as a duplicate-entry repair.
+  - **Multiplayer:** remote selection ranges look up positions in O(1).
+  - **Normalization:** the structural rules treat the lowest parent id as the container of a block listed in several arrays. Normalized documents are unchanged.
+
+  Breaking: no
+
+- 56b8dfc: Selection, focus and pointer fixes across React, Vue and vanilla `mountEditor`:
+
+  - **Reading and writing the selection**
+    - One `selectionchange` reader per root and one projector are the only code that reads or writes the DOM selection (S1).
+    - Input edits the editor selection instead of re-reading the DOM.
+    - `focus()` on a field with no caret commits one to the record.
+    - Every projection reads the selection back. A mismatch emits `selection-projection-mismatch` once and is not rewritten (W3.R1).
+    - The backend selection stamps (`FieldEditorSelectionSource`) are gone.
+  - **Focus and projection timing**
+    - Projections are withheld while an IME composes, while editor chrome holds focus, and while a host control holds focus (HOST9).
+    - Undo/redo rebuilds and passive attaches no longer write the native range or steal focus.
+  - **New APIs**
+    - The selection convenience setters take an optional `SelectionWriteOptions` with `origin` (S3).
+    - `FieldEditorImpl.scrollIntoView` and `FieldEditorImpl.setMountRequester` are new. A projection parked on an unmounted block asks the mount requester to mount it. `selection-target-unmounted` is emitted once per park, and `BlockScrollAlign` is added.
+    - `FieldEditorImpl.connect()` keeps the field editor live under React Strict Mode.
+    - `focusTextSelection` and `useFocusController` run in the calling turn (S4).
+  - **Undo, redo and scrolling:** undo and redo scroll the restored selection into view even when the caret did not move. Scroll deltas round to whole pixels.
+  - **Ranges over more than 50 blocks (S2):** these ranges, and ranges the engine confines to one field, show a substitute state with the focus sink revealed (`getSubstituteState()`, `pen.a11y.textRangeSelected`). Typing, deleting, IME input and clipboard actions work there.
+  - **Fuzzer findings:** S2 cross-block projections now handle code blocks, tables, unit-block gaps, unmounted endpoints and WebKit drags.
+  - **Gesture windows:**
+    - Gesture windows survive a field switch.
+    - `pointercancel` ends a pointer gesture, and drag and context-menu windows close properly (R1).
+    - Touch selection handles keep their range (`native-range` window).
+    - An EditContext keystroke after `editor.selectText` lands at the new caret (FE9).
+  - **Clicks and arrows around atoms and blocks:**
+    - A shift-click into another block extends to the offset under the pointer on every binding, including from a block or cell selection and over dividers.
+    - Clicking an inline atom puts the caret on the clicked side, following the atom's bidi run (O1, T5).
+    - `pen.caretUp` / `pen.caretDown` into a table select its edge cell (T5).
+    - A click in a table cell during a text range selects the cell.
+  - **Atom layout:** atoms share the text line, and a caret beside a chip takes the line's height. Compositions beside atoms no longer insert U+FFFC.
+  - **Table cells:**
+    - `CellSelection.text` is in `@input/pen-types`, and an edited cell keeps DOM focus (W3.R18).
+    - `Mod-b`/`i`/`u` in a cell fail closed on keydown in every browser (FE6).
+    - A table props/meta change no longer resets an edited cell.
+    - Backspace and Delete in an edited cell delete the range or one character instead of clearing the cell. `editor.deleteSelection()` on a `CellSelection` with `text` deletes only that range (A1, T6).
+  - **Positioned chrome:** the selection toolbar, AI prompt and slash/suggestion menus position from the editor selection, not the live DOM range. `DomScheduler.measureNow` drops geometry stale since the last commit (SCH2).
+  - **Focus and activation:**
+    - Shift+Tab out of the editor works in Firefox.
+    - Overlay focus follows window blur (`bindEditorRootFocus`).
+    - The toggle empty state, block-type select and slash-menu table insert activate in the same turn.
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-dom@0.3.0
+  - @input/pen-types@0.3.0
+  - @input/pen-ai@0.3.0
+  - @input/pen-core@0.3.0
+  - @input/pen-multiplayer@0.3.0
+  - @input/pen-search@0.3.0
+  - @input/pen-interop@0.3.0
+  - @input/pen-schema@0.3.0
+  - @input/pen-shortcuts@0.3.0
+  - @input/pen-snapshots@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/rendering/react/package.json b/packages/rendering/react/package.json
index 2b2b2c5c7..69cdf1b62 100644
--- a/packages/rendering/react/package.json
+++ b/packages/rendering/react/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-react",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "React primitives, hooks, and renderers for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/rendering/vue/CHANGELOG.md b/packages/rendering/vue/CHANGELOG.md
index f0650b99a..f7efdd0e8 100644
--- a/packages/rendering/vue/CHANGELOG.md
+++ b/packages/rendering/vue/CHANGELOG.md
@@ -1,5 +1,159 @@
 # @input/pen-vue
 
+## 0.3.0
+
+### Minor Changes
+
+- 56b8dfc: List items are announced as lists (AX1), numbering follows the announced list, and focus returns to its invoker by primitive class (AX3).
+
+  - Each run of list items renders inside a `div[data-pen-list-group][role="list"]`. Each item's host carries `role="listitem"`, `aria-level`, `aria-posinset` and `aria-setsize`. This applies on React, Vue and `mountEditor`.
+  - New core helpers `getListSegments`, `getListItemSemantics` and `isListItemType` describe these lists.
+  - `getNumberedListItemValue` and the notifier's `list.ordinal` count numbered items over the item's sibling list instead of the root `blockOrder`. Markdown export's list `start` follows the same count.
+  - Toolbar buttons and toggles keep focus in the field: they prevent the primary-button `mousedown` default. Escape returns focus to the editor.
+  - The selection toolbar, block handle menu, table column menu, AI command menu and AI contextual prompt return focus to their invoker synchronously.
+
+  Host migration:
+
+  - Hosts that walk the blocks host's direct children, or style list items with `[data-pen-editor-block] + [data-pen-editor-block]`, must also look inside the `[data-pen-list-group]` wrappers. Items moving between groups are remounted by React and Vue.
+  - A numbered item that follows a container with numbered `parentId` children now starts at 1, and items in a `children` array number 1, 2, 3. Hosts or snapshots that expect the old numbering update it. There is no API change.
+  - Hosts whose toolbar `onClick` handlers relied on the button holding focus (for example by reading `document.activeElement`) read the editor selection instead.
+  - Hosts that refocused the editor themselves after the AI command menu or contextual prompt closed remove that refocus.
+
+  Breaking: yes — hosts look inside `[data-pen-list-group]` wrappers when walking or styling list items, update expected numbered-list values after containers, read the editor selection instead of toolbar focus, and drop their own refocus after AI menus close
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+- 56b8dfc: Pen draws carets, remote carets and block outlines into one overlay layer per editor root, which the DOM scheduler paints. React's caret overlays are now bindings over it, and Vue gets the overlay with no component.
+
+  - Each editor root holds one `data-pen-overlay-layer` element as its last child. `getRootOverlay(root)` exposes the paint plan and the contributor API.
+  - The overlay caret is on by default on every surface. It draws carets beside mentions and inline apps, in empty blocks, and at range endpoints. Block and grid-cell selections get a library outline.
+  - New tokens: `--pen-overlay-z-index`, `--pen-editor-endpoint-caret-color`, `--pen-block-selection-outline`, `--pen-block-selection-background`, `--pen-block-selection-radius`, `--pen-selection-range-background` and `--pen-selection-range-opacity`.
+  - The editor root is the layer's containing block (OV2). The chrome stylesheet gives it `position: relative` at zero specificity, and a static root gets it inline while attached.
+  - `Pen.Editor.CaretOverlay` switches the root to `customCaret` mode, and pen-dom paints the caret. The blink restarts on each edit and caret move.
+  - `Pen.Multiplayer.CaretOverlay` registers the remote-caret contributor. Remote carets stay on their text inside transformed ancestors, and the binding drops its `MutationObserver`, rAF loop and scroll listeners.
+  - `EditorSelectionRect` draws only the region-selection marquee.
+
+  Host migration:
+
+  - Host CSS that targets the editor root's `:last-child` accounts for the `data-pen-overlay-layer` element.
+  - Hosts that already style `[data-selected]` set `--pen-block-selection-outline: none` on the editor root (or drop their rule) to avoid a double outline.
+  - Set caret and outline tokens (`--pen-editor-caret-*` included) on the editor root or above, not on a wrapper inside it.
+  - Scope `[data-block-id]` selectors to `[data-pen-editor-block]` if they must not match overlay items.
+  - Absolutely positioned host elements inside a static editor root now position against the root. Give the root its own non-static `position` to keep another containing block.
+  - `renderCaret` (editor and multiplayer) and `renderLabel` position from the `transform` in `caretStyle` / `labelStyle`, not `left` / `top`. Their nodes are portaled into the overlay layer, and `renderCaret` receives `affinity`.
+  - Hosts that style `[data-pen-editor-caret-overlay][data-caret-visible]` target `[data-pen-overlay-layer][data-caret-visible]`.
+  - Remote carets move from the `[data-pen-multiplayer-caret-overlay]` host into the layer. Restyle them under `[data-pen-overlay-layer] [data-pen-multiplayer-caret]`. They change from `position: fixed` with `left` / `top` to `position: absolute` with a `transform`. The default label is a child of its caret and no longer carries `data-pen-multiplayer-caret` or the user attributes.
+  - Hosts that relied on `EditorSelectionRect` to draw a committed block selection style `--pen-block-selection-outline` / `--pen-block-selection-background` on the editor root instead.
+
+  Breaking: yes — hosts update `:last-child`, `[data-selected]`, caret-overlay and remote-caret CSS for the root's `data-pen-overlay-layer`, position custom `renderCaret`/`renderLabel` output from `transform`, set tokens on the root, and give the root its own `position` if it must not be the containing block
+
+### Patch Changes
+
+- 56b8dfc: Accessibility, iframe and binding fixes:
+
+  - **Reduced motion (AX6):** each root has one shared reduced-motion signal. The new `getRootReducedMotion`, `AX6_MOTION_MAPPING` and `REDUCED_MOTION_ATTR` are in pen-dom, and `useReducedMotion()` is in pen-react. The AI suggestion underline no longer animates under reduced motion.
+  - **Announcements (AX2):** live-region announcements are written in the scheduler's write phase.
+  - **Focus return for custom chrome (AX3):**
+    - New `captureFocusReturn` / `restoreFocusReturn` implement the focus-return rule.
+    - The slash, suggestion and table column menus dismiss on an outside `pointerdown`.
+  - **Iframe-mounted editors:**
+    - Every node and event check uses realm-safe guards, exported from the new `@input/pen-dom/utils/domNodes` subpath (`isDomNode`, `isDomElement`, `isDomEvent` and others).
+    - Nodes are created in the field's own document, and chrome resolves its document through `resolveEditorOwnerDocument`.
+  - **`asChild`:** `asChild` composes the child's handlers, class names and styles with the primitive's instead of replacing them.
+
+  Breaking: no
+
+- 56b8dfc: Keystrokes, caret moves and structural edits cost in proportion to what they touch, not to document size (SCALE2, SCALE6).
+
+  - **Indexes:**
+    - `DocumentState` gains `preorderIndexOf`, `preorderBlockIds`, `rootBlockIds` and `rootBlockIndexOf`.
+    - Root and `children` edits advance the position, parent, child and preorder indexes in place instead of rebuilding them.
+    - The change-summary index and the normalization pass index advance by each commit's delta.
+    - Measured at 50,000 blocks: an insert inside a container drops from about 41 ms to 0.15 ms, and a delete from about 11 ms to 0.3 ms.
+  - **Selection reads:** selection validation, anchor resolution, `buildLazyNormalPositionSnapshot` and vertical caret motion no longer read every block.
+  - **Scoped decorations:** new `scopedDecorationSource({ interest, decorate })` recomputes only the blocks a commit names. `decorationsChange` passes `(generation, changedBlockIds)`. `requestDecorationUpdate({ source?, blockIds })` targets sources, and with no argument it recomputes only function-form and static sources. AI suggestions and search use scoped sources, so typing with either installed no longer reads the whole document.
+  - **Block notifier:** new `fieldEditor.blockNotifier` fans editor events out per block. React (`EditorBlock`, `InlineContent`), Vue (`PenBlock`, `PenInlineContent`) and the vanilla `mountEditor` tree re-render only the blocks whose state changed. Each block acknowledges its own mount. `blocks-merged` carries `sourceParentId` / `sourceIndex`. Its list slices are rebuilt after a commit that moves the document index without a structural change, such as a duplicate-entry repair.
+  - **Multiplayer:** remote selection ranges look up positions in O(1).
+  - **Normalization:** the structural rules treat the lowest parent id as the container of a block listed in several arrays. Normalized documents are unchanged.
+
+  Breaking: no
+
+- 56b8dfc: Additive overlay APIs and remote-caret safety and contrast fixes:
+
+  - `RootOverlay.holdCaretMode`, `overlayItemStyle` and `overlayLabelStyle` are exported for bindings.
+  - `attachRemoteCarets(overlay, source)` and `getRemoteCaretSource(editor)` paint collaborators' carets. Vue gains `PenMultiplayerCaretOverlay`.
+  - `FieldEditorImpl.setReadOnly` carries the renderer `readonly` prop.
+  - New `isSafeCssColor` in `@input/pen-core`. Every `--pen-peer-color` write is re-validated, and carets paint through `background-color` so a colour cannot become an image fetch (COL2).
+  - The default peer palette (`MULTIPLAYER_COLORS`) uses darker shades so labels meet 4.5:1 contrast. Hosts that pass `user.color` are unaffected.
+  - A local keystroke that leaves every peer in place no longer notifies multiplayer subscribers or repaints remote carets.
+
+  Breaking: no
+
+- 56b8dfc: Selection, focus and pointer fixes across React, Vue and vanilla `mountEditor`:
+
+  - **Reading and writing the selection**
+    - One `selectionchange` reader per root and one projector are the only code that reads or writes the DOM selection (S1).
+    - Input edits the editor selection instead of re-reading the DOM.
+    - `focus()` on a field with no caret commits one to the record.
+    - Every projection reads the selection back. A mismatch emits `selection-projection-mismatch` once and is not rewritten (W3.R1).
+    - The backend selection stamps (`FieldEditorSelectionSource`) are gone.
+  - **Focus and projection timing**
+    - Projections are withheld while an IME composes, while editor chrome holds focus, and while a host control holds focus (HOST9).
+    - Undo/redo rebuilds and passive attaches no longer write the native range or steal focus.
+  - **New APIs**
+    - The selection convenience setters take an optional `SelectionWriteOptions` with `origin` (S3).
+    - `FieldEditorImpl.scrollIntoView` and `FieldEditorImpl.setMountRequester` are new. A projection parked on an unmounted block asks the mount requester to mount it. `selection-target-unmounted` is emitted once per park, and `BlockScrollAlign` is added.
+    - `FieldEditorImpl.connect()` keeps the field editor live under React Strict Mode.
+    - `focusTextSelection` and `useFocusController` run in the calling turn (S4).
+  - **Undo, redo and scrolling:** undo and redo scroll the restored selection into view even when the caret did not move. Scroll deltas round to whole pixels.
+  - **Ranges over more than 50 blocks (S2):** these ranges, and ranges the engine confines to one field, show a substitute state with the focus sink revealed (`getSubstituteState()`, `pen.a11y.textRangeSelected`). Typing, deleting, IME input and clipboard actions work there.
+  - **Fuzzer findings:** S2 cross-block projections now handle code blocks, tables, unit-block gaps, unmounted endpoints and WebKit drags.
+  - **Gesture windows:**
+    - Gesture windows survive a field switch.
+    - `pointercancel` ends a pointer gesture, and drag and context-menu windows close properly (R1).
+    - Touch selection handles keep their range (`native-range` window).
+    - An EditContext keystroke after `editor.selectText` lands at the new caret (FE9).
+  - **Clicks and arrows around atoms and blocks:**
+    - A shift-click into another block extends to the offset under the pointer on every binding, including from a block or cell selection and over dividers.
+    - Clicking an inline atom puts the caret on the clicked side, following the atom's bidi run (O1, T5).
+    - `pen.caretUp` / `pen.caretDown` into a table select its edge cell (T5).
+    - A click in a table cell during a text range selects the cell.
+  - **Atom layout:** atoms share the text line, and a caret beside a chip takes the line's height. Compositions beside atoms no longer insert U+FFFC.
+  - **Table cells:**
+    - `CellSelection.text` is in `@input/pen-types`, and an edited cell keeps DOM focus (W3.R18).
+    - `Mod-b`/`i`/`u` in a cell fail closed on keydown in every browser (FE6).
+    - A table props/meta change no longer resets an edited cell.
+    - Backspace and Delete in an edited cell delete the range or one character instead of clearing the cell. `editor.deleteSelection()` on a `CellSelection` with `text` deletes only that range (A1, T6).
+  - **Positioned chrome:** the selection toolbar, AI prompt and slash/suggestion menus position from the editor selection, not the live DOM range. `DomScheduler.measureNow` drops geometry stale since the last commit (SCH2).
+  - **Focus and activation:**
+    - Shift+Tab out of the editor works in Firefox.
+    - Overlay focus follows window blur (`bindEditorRootFocus`).
+    - The toggle empty state, block-type select and slash-menu table insert activate in the same turn.
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-dom@0.3.0
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+  - @input/pen-interop@0.3.0
+  - @input/pen-schema@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/rendering/vue/package.json b/packages/rendering/vue/package.json
index 5a9452c5e..2b32ffccf 100644
--- a/packages/rendering/vue/package.json
+++ b/packages/rendering/vue/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-vue",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Vue rendering primitives for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/schema/CHANGELOG.md b/packages/schema/CHANGELOG.md
index 6d044aeef..593ea07bb 100644
--- a/packages/schema/CHANGELOG.md
+++ b/packages/schema/CHANGELOG.md
@@ -1,5 +1,28 @@
 # @input/pen-schema
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/schema/package.json b/packages/schema/package.json
index 741beca44..92153c8c0 100644
--- a/packages/schema/package.json
+++ b/packages/schema/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-schema",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Default block and inline schemas for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/shared/ingest/CHANGELOG.md b/packages/shared/ingest/CHANGELOG.md
index 917ab982e..efdbdd057 100644
--- a/packages/shared/ingest/CHANGELOG.md
+++ b/packages/shared/ingest/CHANGELOG.md
@@ -1,5 +1,28 @@
 # @input/pen-ingest
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/shared/ingest/package.json b/packages/shared/ingest/package.json
index dbd7cd9e8..cbf935673 100644
--- a/packages/shared/ingest/package.json
+++ b/packages/shared/ingest/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-ingest",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Shared content parsing and write ops for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/shared/markdown/CHANGELOG.md b/packages/shared/markdown/CHANGELOG.md
index c902710a7..c3e390d10 100644
--- a/packages/shared/markdown/CHANGELOG.md
+++ b/packages/shared/markdown/CHANGELOG.md
@@ -1,5 +1,28 @@
 # @input/pen-markdown
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/shared/markdown/package.json b/packages/shared/markdown/package.json
index 42dbfd741..ad525877f 100644
--- a/packages/shared/markdown/package.json
+++ b/packages/shared/markdown/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-markdown",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Shared markdown serialization helpers for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/tooling/assets/CHANGELOG.md b/packages/tooling/assets/CHANGELOG.md
index 2df24d083..2c53b62e9 100644
--- a/packages/tooling/assets/CHANGELOG.md
+++ b/packages/tooling/assets/CHANGELOG.md
@@ -1,5 +1,23 @@
 # @input/pen-assets
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/tooling/assets/package.json b/packages/tooling/assets/package.json
index 83951d95f..d7be10ca0 100644
--- a/packages/tooling/assets/package.json
+++ b/packages/tooling/assets/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-assets",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "In-memory asset provider for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/tooling/bench/CHANGELOG.md b/packages/tooling/bench/CHANGELOG.md
index 0905564b3..f98fe569f 100644
--- a/packages/tooling/bench/CHANGELOG.md
+++ b/packages/tooling/bench/CHANGELOG.md
@@ -1,5 +1,32 @@
 # @input/pen-bench
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- 56b8dfc: Scale measurement and multi-peer test tooling.
+
+  - `@input/pen-test` adds:
+    - `createPeerHarness(n)`, with seeded delivery schedules (`PEER_SCHEDULES`, `runPeerSchedules`), `quiesce()` and an awareness relay. `createTwoPeerHarness()` is now its two-peer form and also destroys each `Y.Doc`.
+    - The COL4 structural oracle (`findStructuralViolations`, `assertStructuralInvariants`).
+    - `createScanProbe(editor)` for counting document reads.
+    - The mixed 1k–50k block fixture (`generateMixedBlockSpecs`, `mixedFixtureOps` and related helpers).
+  - `@input/pen-bench` adds:
+    - A realistic SCALE3 variant with AI and search installed.
+    - A synced-peer SCALE3 axis.
+    - Structural-commit read gates.
+    - Renderer rows in `ENVELOPE.md`, and a measured concurrent-peer row.
+    - The envelope re-recorded on a quiet machine.
+  - `bench:caches` fails on any diagnostic except `anchor-budget`.
+
+  Breaking: no
+
 ## 0.2.14
 
 ## 0.2.13
diff --git a/packages/tooling/bench/package.json b/packages/tooling/bench/package.json
index 66d8062f6..9466a23d4 100644
--- a/packages/tooling/bench/package.json
+++ b/packages/tooling/bench/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-bench",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Performance benchmarks for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/tooling/test/CHANGELOG.md b/packages/tooling/test/CHANGELOG.md
index 94bd73c79..a0f459f6b 100644
--- a/packages/tooling/test/CHANGELOG.md
+++ b/packages/tooling/test/CHANGELOG.md
@@ -1,5 +1,49 @@
 # @input/pen-test
 
+## 0.3.0
+
+### Minor Changes
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- 56b8dfc: Scale measurement and multi-peer test tooling.
+
+  - `@input/pen-test` adds:
+    - `createPeerHarness(n)`, with seeded delivery schedules (`PEER_SCHEDULES`, `runPeerSchedules`), `quiesce()` and an awareness relay. `createTwoPeerHarness()` is now its two-peer form and also destroys each `Y.Doc`.
+    - The COL4 structural oracle (`findStructuralViolations`, `assertStructuralInvariants`).
+    - `createScanProbe(editor)` for counting document reads.
+    - The mixed 1k–50k block fixture (`generateMixedBlockSpecs`, `mixedFixtureOps` and related helpers).
+  - `@input/pen-bench` adds:
+    - A realistic SCALE3 variant with AI and search installed.
+    - A synced-peer SCALE3 axis.
+    - Structural-commit read gates.
+    - Renderer rows in `ENVELOPE.md`, and a measured concurrent-peer row.
+    - The envelope re-recorded on a quiet machine.
+  - `bench:caches` fails on any diagnostic except `anchor-budget`.
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-core@0.3.0
+  - @input/pen-yjs@0.3.0
+  - @input/pen-interop@0.3.0
+  - @input/pen-schema@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/tooling/test/package.json b/packages/tooling/test/package.json
index b61b33d96..63bc3a79c 100644
--- a/packages/tooling/test/package.json
+++ b/packages/tooling/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-test",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Headless testing utilities for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/transport/CHANGELOG.md b/packages/transport/CHANGELOG.md
index 37f8b96ec..cdf44977f 100644
--- a/packages/transport/CHANGELOG.md
+++ b/packages/transport/CHANGELOG.md
@@ -1,5 +1,64 @@
 # @input/pen-transport
 
+## 0.3.0
+
+### Minor Changes
+
+- 56b8dfc: AI tool calls are attributed, classified and undone per call (AIB3, AIB4).
+
+  - `openAIToolCall()` returns the call's own `context`. `executeAITool()` and both transports run handlers with it. Every write path books a write to the call that issued it, and refuses a read-only call's write.
+  - The user's typing while a call is open is no call's write.
+  - `StreamingTarget.disableActiveWriter()` is replaced by the `activeWriter` getter.
+  - `ToolDefinition.destructive` may be a resolver `(input, { staged }) => boolean` (`ToolDestructiveResolver`, `ToolAuthorityContext`).
+  - `edit_document` consults `confirm` only for direct calls that remove or replace content: direct replaces, deletes of non-empty blocks, and content-kind changes. `authorizeAIToolCall` and `isDestructiveAITool` take the call's context.
+  - New `unconfirmedDestructive: "refuse"` (on `aiExtension`, `AIToolGrant`, `AIToolTurnOptions` and `AgenticLoopOptions`) blocks destructive calls when no resolver is installed. Use it in production if you expose `delete_block` or `write_document`.
+  - A host that used `confirm` as an "every AI edit" hook moves to `onBeforeApply` or the commit event.
+  - `UndoManager.syncExplicitUndoGroup` is replaced by `withCapture(origin, groupId, run)`. `CRDTUndoManager` gains an optional `setCaptureKey` (`CRDTUndoCaptureKey`).
+
+  Host migration:
+
+  - Tool handlers that compared `ctx.editor === editor` compare `ctx.editor.internals.adapter` (or the document) instead, because `ctx.editor` is now a per-call view.
+  - Code that called `StreamingTarget.disableActiveWriter()` reads `activeWriter`.
+  - Code that reads `ToolDefinition.destructive` as a boolean must handle a function.
+  - Hosts that implement `UndoManager` replace `syncExplicitUndoGroup` with `withCapture`.
+
+  Breaking: yes — tool handlers compare `ctx.editor.internals.adapter` instead of `ctx.editor === editor`, read `activeWriter` instead of `disableActiveWriter()`, handle a function-valued `ToolDefinition.destructive`, and `UndoManager` implementations replace `syncExplicitUndoGroup` with `withCapture`
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- 56b8dfc: AI write attribution, undo grouping and review preview fixes:
+
+  - **Undo grouping (AIB4)**
+    - Undo capture is keyed. One AI action stays one undo step even when the user types during it, and the user's typing stays its own steps.
+    - `directTransport` and `createSSEHandler` give each request one undo group.
+  - **Overlapping calls (AIB3)**
+    - Overlapping tool calls and generations keep their own write guard and staged-write binding. A call closing out of order no longer drops another call's guard or unstages its writes.
+    - `directTransport`, `createSSEHandler` and `processStream` apply the editor's `confirm` / `unconfirmedDestructive` policy and accept their own overrides. They allow destructive calls by default when no `confirm` is set, so a server reachable by clients should pass `"refuse"` or a `confirm`.
+  - **Review preview (RS6):** the `edit_document` streaming preview matches what accept will do: multi-block and nested replaces and deletes, insert placement, empty replacements, and moves and formatting. `EditDocumentPreviewUpdate` gains `blockIds`, `placement` and `complete`. `AIStreamingReviewPreviewInput` gains `complete`, `deletesBlocks` and `replacesBlocks`.
+  - **Undo drift anchors:** undo reuses the selection authority's anchors through `editor.internals.selectionAnchors()` and `selectionAnchorRepair()` (AN14). This stops spurious `anchor-target-missing` for ranges that end on dividers or tables, and halves `anchor-budget` churn.
+  - **Anchor repair (AN2, AN14):** `repairAnchor` reads an anchor's position from before the commit even when something resolved it after the commit landed, so a split no longer moves an anchor to the start of the new block. An anchor that resolved to `null` before a commit stays dead instead of being revived from an older position, and a merge carries an `assoc: -1` anchor at the start of the merged-away block into the target instead of losing it.
+  - **React AI chrome:** the AI suggestions popover is scoped to its own editor. `Pen.AI.ContextualPromptComposer` no longer throws a hook-order error when a session opens.
+
+  Breaking: no
+
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [c921845]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+- Updated dependencies [56b8dfc]
+  - @input/pen-types@0.3.0
+  - @input/pen-ai@0.3.0
+  - @input/pen-core@0.3.0
+
 ## 0.2.14
 
 ### Patch Changes
diff --git a/packages/transport/package.json b/packages/transport/package.json
index 9acdbdba1..e0983b8cf 100644
--- a/packages/transport/package.json
+++ b/packages/transport/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-transport",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Transports for Pen: in-process direct and SSE",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md
index b7477af8a..3a6aac800 100644
--- a/packages/types/CHANGELOG.md
+++ b/packages/types/CHANGELOG.md
@@ -1,5 +1,124 @@
 # @input/pen-types
 
+## 0.3.0
+
+### Minor Changes
+
+- 56b8dfc: AI tool calls are attributed, classified and undone per call (AIB3, AIB4).
+
+  - `openAIToolCall()` returns the call's own `context`. `executeAITool()` and both transports run handlers with it. Every write path books a write to the call that issued it, and refuses a read-only call's write.
+  - The user's typing while a call is open is no call's write.
+  - `StreamingTarget.disableActiveWriter()` is replaced by the `activeWriter` getter.
+  - `ToolDefinition.destructive` may be a resolver `(input, { staged }) => boolean` (`ToolDestructiveResolver`, `ToolAuthorityContext`).
+  - `edit_document` consults `confirm` only for direct calls that remove or replace content: direct replaces, deletes of non-empty blocks, and content-kind changes. `authorizeAIToolCall` and `isDestructiveAITool` take the call's context.
+  - New `unconfirmedDestructive: "refuse"` (on `aiExtension`, `AIToolGrant`, `AIToolTurnOptions` and `AgenticLoopOptions`) blocks destructive calls when no resolver is installed. Use it in production if you expose `delete_block` or `write_document`.
+  - A host that used `confirm` as an "every AI edit" hook moves to `onBeforeApply` or the commit event.
+  - `UndoManager.syncExplicitUndoGroup` is replaced by `withCapture(origin, groupId, run)`. `CRDTUndoManager` gains an optional `setCaptureKey` (`CRDTUndoCaptureKey`).
+
+  Host migration:
+
+  - Tool handlers that compared `ctx.editor === editor` compare `ctx.editor.internals.adapter` (or the document) instead, because `ctx.editor` is now a per-call view.
+  - Code that called `StreamingTarget.disableActiveWriter()` reads `activeWriter`.
+  - Code that reads `ToolDefinition.destructive` as a boolean must handle a function.
+  - Hosts that implement `UndoManager` replace `syncExplicitUndoGroup` with `withCapture`.
+
+  Breaking: yes — tool handlers compare `ctx.editor.internals.adapter` instead of `ctx.editor === editor`, read `activeWriter` instead of `disableActiveWriter()`, handle a function-valued `ToolDefinition.destructive`, and `UndoManager` implementations replace `syncExplicitUndoGroup` with `withCapture`
+
+- c921845: Raise `engines.node` from `>=22` to `^22.22.2 || ^24.15.0 || >=26.0.0` (HOST3). `@input/pen-interop` sanitizes HTML through `isomorphic-dompurify` 4.3, which builds its Node window with jsdom 30, and jsdom 30 declares that range; `@input/pen`, `@input/pen-react`, and `@input/pen-vue` depend on interop, so the workspace-wide floor moves with it. Node 22 releases before 22.22.2, Node 24 releases before 24.15.0, and Node 23 and 25 are no longer supported.
+
+  Breaking: yes — hosts running Node below 22.22.2, 24.0–24.14, or 23/25 must move to a supported Node line (^22.22.2, ^24.15.0 or >=26.0.0)
+
+### Patch Changes
+
+- 56b8dfc: AI write attribution, undo grouping and review preview fixes:
+
+  - **Undo grouping (AIB4)**
+    - Undo capture is keyed. One AI action stays one undo step even when the user types during it, and the user's typing stays its own steps.
+    - `directTransport` and `createSSEHandler` give each request one undo group.
+  - **Overlapping calls (AIB3)**
+    - Overlapping tool calls and generations keep their own write guard and staged-write binding. A call closing out of order no longer drops another call's guard or unstages its writes.
+    - `directTransport`, `createSSEHandler` and `processStream` apply the editor's `confirm` / `unconfirmedDestructive` policy and accept their own overrides. They allow destructive calls by default when no `confirm` is set, so a server reachable by clients should pass `"refuse"` or a `confirm`.
+  - **Review preview (RS6):** the `edit_document` streaming preview matches what accept will do: multi-block and nested replaces and deletes, insert placement, empty replacements, and moves and formatting. `EditDocumentPreviewUpdate` gains `blockIds`, `placement` and `complete`. `AIStreamingReviewPreviewInput` gains `complete`, `deletesBlocks` and `replacesBlocks`.
+  - **Undo drift anchors:** undo reuses the selection authority's anchors through `editor.internals.selectionAnchors()` and `selectionAnchorRepair()` (AN14). This stops spurious `anchor-target-missing` for ranges that end on dividers or tables, and halves `anchor-budget` churn.
+  - **Anchor repair (AN2, AN14):** `repairAnchor` reads an anchor's position from before the commit even when something resolved it after the commit landed, so a split no longer moves an anchor to the start of the new block. An anchor that resolved to `null` before a commit stays dead instead of being revived from an older position, and a merge carries an `assoc: -1` anchor at the start of the merged-away block into the target instead of losing it.
+  - **React AI chrome:** the AI suggestions popover is scoped to its own editor. `Pen.AI.ContextualPromptComposer` no longer throws a hook-order error when a session opens.
+
+  Breaking: no
+
+- 56b8dfc: Document integrity fixes for concurrent, out-of-order and malformed edits:
+
+  - **Encodable ops (OPB1):** `editor.apply` drops an op whose payload the CRDT cannot encode, with `PEN_APPLY_004`. Examples are a `set-meta` without a namespace, cyclic or non-plain prop, meta and app values, and non-finite mark values. Before, such an op could stop the document from syncing or persisting, or leave a partial write.
+  - **Missing parents:** a `parentId` that names no live block, and an insert or move into a parent deleted earlier in the same batch, are dropped with `PEN_APPLY_003` (PR5).
+  - **Dangling entries (COL4)**
+    - Normalization removes `blockOrder` / `children` entries whose block was deleted (`dangling-block-reference`). Entries whose block map has not arrived yet are kept (new optional `CRDTAdapter.isBlockDeleted`).
+    - Iteration APIs skip dangling entries instead of throwing `Block not found`.
+  - **Structural repairs on the next commit (COL4):** after a remote or undo commit, the next local commit repairs cycles, duplicates and cross-array membership. Orphans are re-homed with `orphan-block-rehomed`.
+  - **One nesting route (RI6):** deleting a container deletes its `children` descendants. A conflicting `parentId` is cleared with `nesting-route-conflict`.
+  - **Change summaries:** summaries now report the right `block-inserted` / `block-removed` / `block-moved` for concurrent delete-and-move, duplicate entries, replaced `children` arrays and container maps, re-entered containers, and deleted subtrees. Renderers, search and AI indexes no longer keep dead blocks or miss live ones (OB1, COL4). This adds `RawCommitDelta.absentBlockIds` and `arrivedChildArrays`. A block that loses one of two order entries while its map is deleted or not stored is reported `block-removed`, and a block whose map a commit replaced whole reports `block-props-changed` (`"type"` when it changed, plus the arrived map's prop and meta keys). A write a commit listener makes while a remote delivery or undo is being committed now has a summary naming every insert and delete it made; before, a block it moved could be reported removed and some deletes went missing.
+  - **Loading (DUR2):** load repair no longer copies nested children into the root order, and no longer removes an entry whose map is in flight.
+  - **System-origin writes:** normalization outside an apply runs in one `"system"`-origin transaction, so its repairs leave the undo stack. `setDocumentProfile()` no longer emits `ORIGIN_UNKNOWN`.
+  - **Awareness:** `DocumentSession.ensureAwareness(scopeId, factory)` lets the multiplayer extension create its own awareness. The extension now depends on `@input/pen-yjs` and peers on `yjs` and `y-protocols`.
+
+  Breaking: no
+
+- 56b8dfc: Keystrokes, caret moves and structural edits cost in proportion to what they touch, not to document size (SCALE2, SCALE6).
+
+  - **Indexes:**
+    - `DocumentState` gains `preorderIndexOf`, `preorderBlockIds`, `rootBlockIds` and `rootBlockIndexOf`.
+    - Root and `children` edits advance the position, parent, child and preorder indexes in place instead of rebuilding them.
+    - The change-summary index and the normalization pass index advance by each commit's delta.
+    - Measured at 50,000 blocks: an insert inside a container drops from about 41 ms to 0.15 ms, and a delete from about 11 ms to 0.3 ms.
+  - **Selection reads:** selection validation, anchor resolution, `buildLazyNormalPositionSnapshot` and vertical caret motion no longer read every block.
+  - **Scoped decorations:** new `scopedDecorationSource({ interest, decorate })` recomputes only the blocks a commit names. `decorationsChange` passes `(generation, changedBlockIds)`. `requestDecorationUpdate({ source?, blockIds })` targets sources, and with no argument it recomputes only function-form and static sources. AI suggestions and search use scoped sources, so typing with either installed no longer reads the whole document.
+  - **Block notifier:** new `fieldEditor.blockNotifier` fans editor events out per block. React (`EditorBlock`, `InlineContent`), Vue (`PenBlock`, `PenInlineContent`) and the vanilla `mountEditor` tree re-render only the blocks whose state changed. Each block acknowledges its own mount. `blocks-merged` carries `sourceParentId` / `sourceIndex`. Its list slices are rebuilt after a commit that moves the document index without a structural change, such as a duplicate-entry repair.
+  - **Multiplayer:** remote selection ranges look up positions in O(1).
+  - **Normalization:** the structural rules treat the lowest parent id as the container of a block listed in several arrays. Normalized documents are unchanged.
+
+  Breaking: no
+
+- 56b8dfc: Selection, focus and pointer fixes across React, Vue and vanilla `mountEditor`:
+
+  - **Reading and writing the selection**
+    - One `selectionchange` reader per root and one projector are the only code that reads or writes the DOM selection (S1).
+    - Input edits the editor selection instead of re-reading the DOM.
+    - `focus()` on a field with no caret commits one to the record.
+    - Every projection reads the selection back. A mismatch emits `selection-projection-mismatch` once and is not rewritten (W3.R1).
+    - The backend selection stamps (`FieldEditorSelectionSource`) are gone.
+  - **Focus and projection timing**
+    - Projections are withheld while an IME composes, while editor chrome holds focus, and while a host control holds focus (HOST9).
+    - Undo/redo rebuilds and passive attaches no longer write the native range or steal focus.
+  - **New APIs**
+    - The selection convenience setters take an optional `SelectionWriteOptions` with `origin` (S3).
+    - `FieldEditorImpl.scrollIntoView` and `FieldEditorImpl.setMountRequester` are new. A projection parked on an unmounted block asks the mount requester to mount it. `selection-target-unmounted` is emitted once per park, and `BlockScrollAlign` is added.
+    - `FieldEditorImpl.connect()` keeps the field editor live under React Strict Mode.
+    - `focusTextSelection` and `useFocusController` run in the calling turn (S4).
+  - **Undo, redo and scrolling:** undo and redo scroll the restored selection into view even when the caret did not move. Scroll deltas round to whole pixels.
+  - **Ranges over more than 50 blocks (S2):** these ranges, and ranges the engine confines to one field, show a substitute state with the focus sink revealed (`getSubstituteState()`, `pen.a11y.textRangeSelected`). Typing, deleting, IME input and clipboard actions work there.
+  - **Fuzzer findings:** S2 cross-block projections now handle code blocks, tables, unit-block gaps, unmounted endpoints and WebKit drags.
+  - **Gesture windows:**
+    - Gesture windows survive a field switch.
+    - `pointercancel` ends a pointer gesture, and drag and context-menu windows close properly (R1).
+    - Touch selection handles keep their range (`native-range` window).
+    - An EditContext keystroke after `editor.selectText` lands at the new caret (FE9).
+  - **Clicks and arrows around atoms and blocks:**
+    - A shift-click into another block extends to the offset under the pointer on every binding, including from a block or cell selection and over dividers.
+    - Clicking an inline atom puts the caret on the clicked side, following the atom's bidi run (O1, T5).
+    - `pen.caretUp` / `pen.caretDown` into a table select its edge cell (T5).
+    - A click in a table cell during a text range selects the cell.
+  - **Atom layout:** atoms share the text line, and a caret beside a chip takes the line's height. Compositions beside atoms no longer insert U+FFFC.
+  - **Table cells:**
+    - `CellSelection.text` is in `@input/pen-types`, and an edited cell keeps DOM focus (W3.R18).
+    - `Mod-b`/`i`/`u` in a cell fail closed on keydown in every browser (FE6).
+    - A table props/meta change no longer resets an edited cell.
+    - Backspace and Delete in an edited cell delete the range or one character instead of clearing the cell. `editor.deleteSelection()` on a `CellSelection` with `text` deletes only that range (A1, T6).
+  - **Positioned chrome:** the selection toolbar, AI prompt and slash/suggestion menus position from the editor selection, not the live DOM range. `DomScheduler.measureNow` drops geometry stale since the last commit (SCH2).
+  - **Focus and activation:**
+    - Shift+Tab out of the editor works in Firefox.
+    - Overlay focus follows window blur (`bindEditorRootFocus`).
+    - The toggle empty state, block-type select and slash-menu table insert activate in the same turn.
+
+  Breaking: no
+
 ## 0.2.14
 
 ## 0.2.13
diff --git a/packages/types/package.json b/packages/types/package.json
index 46a03ae5e..0cd5e7d0d 100644
--- a/packages/types/package.json
+++ b/packages/types/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@input/pen-types",
-  "version": "0.2.14",
+  "version": "0.3.0",
   "description": "Type definitions and lightweight runtime helpers for Pen",
   "license": "MIT",
   "homepage": "https://github.com/input-systems/pen#readme",
diff --git a/spec/packages/core.md b/spec/packages/core.md
index d4e58f49c..cfebad4bf 100644
--- a/spec/packages/core.md
+++ b/spec/packages/core.md
@@ -100,7 +100,7 @@ Headless editors default to the core apply pipeline only, same as bare `createEd
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. In practice, this is still the package that defines the architecture for the rest of the repo, so churn here has repo-wide impact.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. In practice, this is still the package that defines the architecture for the rest of the repo, so churn here has repo-wide impact.
 
 ## Non-goals
 
diff --git a/spec/packages/crdt/yjs.md b/spec/packages/crdt/yjs.md
index 04611f0cc..4b4e876f8 100644
--- a/spec/packages/crdt/yjs.md
+++ b/spec/packages/crdt/yjs.md
@@ -64,7 +64,7 @@ A write made while another transaction is being observed — a commit listener a
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving.
 
 ## Non-goals
 
diff --git a/spec/packages/extensions/ai.md b/spec/packages/extensions/ai.md
index 294930534..729f0d86e 100644
--- a/spec/packages/extensions/ai.md
+++ b/spec/packages/extensions/ai.md
@@ -216,7 +216,7 @@ Streaming protocol and processing pipeline. Optional runtime that turns a `PenSt
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. This is one of the most ambitious packages in the workspace and should be treated as a large extension surface rather than a minimal helper package.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. This is one of the most ambitious packages in the workspace and should be treated as a large extension surface rather than a minimal helper package.
 
 ## Non-goals
 
diff --git a/spec/packages/extensions/autoformat.md b/spec/packages/extensions/autoformat.md
index 635315b3e..d4f87334f 100644
--- a/spec/packages/extensions/autoformat.md
+++ b/spec/packages/extensions/autoformat.md
@@ -33,7 +33,7 @@ Extension package packages in Pen should stay package-first and explicit about o
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving.
 
 ## Non-goals
 
diff --git a/spec/packages/extensions/interop.md b/spec/packages/extensions/interop.md
index a7302661f..e7ca93a3d 100644
--- a/spec/packages/extensions/interop.md
+++ b/spec/packages/extensions/interop.md
@@ -193,7 +193,7 @@ flowchart TD
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. HTML ingest is security-sensitive. JSON already defines Pen's explicit structured document contract.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. HTML ingest is security-sensitive. JSON already defines Pen's explicit structured document contract.
 
 ## Non-goals
 
diff --git a/spec/packages/extensions/multiplayer.md b/spec/packages/extensions/multiplayer.md
index b19cb0994..446d9a560 100644
--- a/spec/packages/extensions/multiplayer.md
+++ b/spec/packages/extensions/multiplayer.md
@@ -68,7 +68,7 @@ Important rules:
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. It is already an important architectural layer because it defines how collaboration presence becomes editor-visible state without collapsing transport and rendering into one package.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. It is already an important architectural layer because it defines how collaboration presence becomes editor-visible state without collapsing transport and rendering into one package.
 
 ## Non-goals
 
diff --git a/spec/packages/extensions/search.md b/spec/packages/extensions/search.md
index a667f0079..57e751d71 100644
--- a/spec/packages/extensions/search.md
+++ b/spec/packages/extensions/search.md
@@ -66,7 +66,7 @@ Important rules:
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. The package is small in surface area relative to `@input/pen-ai`, but it is important because it establishes the correct pattern for headless feature packages with renderer-agnostic UI.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. The package is small in surface area relative to `@input/pen-ai`, but it is important because it establishes the correct pattern for headless feature packages with renderer-agnostic UI.
 
 ## Non-goals
 
diff --git a/spec/packages/extensions/shortcuts.md b/spec/packages/extensions/shortcuts.md
index 6ab2ad208..27f551d99 100644
--- a/spec/packages/extensions/shortcuts.md
+++ b/spec/packages/extensions/shortcuts.md
@@ -32,7 +32,7 @@ Add optional runtime behavior on top of the editor core without changing the can
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving.
 
 ## Non-goals
 
diff --git a/spec/packages/extensions/snapshots.md b/spec/packages/extensions/snapshots.md
index a5ea8861e..ec1b1f1ce 100644
--- a/spec/packages/extensions/snapshots.md
+++ b/spec/packages/extensions/snapshots.md
@@ -62,7 +62,7 @@ Important rules:
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. It is an important architectural package because it defines how Pen treats durable history across single-editor and multi-editor document scopes.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. It is an important architectural package because it defines how Pen treats durable history across single-editor and multi-editor document scopes.
 
 ## Non-goals
 
diff --git a/spec/packages/extensions/tools.md b/spec/packages/extensions/tools.md
index cb62b2004..7a293ddc1 100644
--- a/spec/packages/extensions/tools.md
+++ b/spec/packages/extensions/tools.md
@@ -66,7 +66,7 @@ Important rules:
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. It already has an outsized architectural role because it defines how non-human actors interact with Pen documents without bypassing editor boundaries.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. It already has an outsized architectural role because it defines how non-human actors interact with Pen documents without bypassing editor boundaries.
 
 ## Non-goals
 
diff --git a/spec/packages/extensions/undo.md b/spec/packages/extensions/undo.md
index 5b849538e..7ac1631e2 100644
--- a/spec/packages/extensions/undo.md
+++ b/spec/packages/extensions/undo.md
@@ -58,7 +58,7 @@ Important rules:
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. Even so, it is already a subtle package because grouping and restore behavior shape how editing feels across typing, paste, AI operations, and collaboration-aware origins.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. Even so, it is already a subtle package because grouping and restore behavior shape how editing feels across typing, paste, AI operations, and collaboration-aware origins.
 
 ## Non-goals
 
diff --git a/spec/packages/pen.md b/spec/packages/pen.md
index 840c00563..2a88e8213 100644
--- a/spec/packages/pen.md
+++ b/spec/packages/pen.md
@@ -35,7 +35,7 @@ The preset's `resolve()` returns `createDefaultSchema()` plus, unless turned off
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving.
 
 ## Non-goals
 
diff --git a/spec/packages/rendering/dom.md b/spec/packages/rendering/dom.md
index cf4ac47ba..2d8f169c4 100644
--- a/spec/packages/rendering/dom.md
+++ b/spec/packages/rendering/dom.md
@@ -94,7 +94,7 @@ Important rules:
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. It is now a key architectural package because it proves the shared editing engine can outlive any single framework renderer.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. It is now a key architectural package because it proves the shared editing engine can outlive any single framework renderer.
 
 ## Non-goals
 
diff --git a/spec/packages/rendering/react.md b/spec/packages/rendering/react.md
index edcd4b00e..3d8a7e620 100644
--- a/spec/packages/rendering/react.md
+++ b/spec/packages/rendering/react.md
@@ -89,7 +89,7 @@ Important responsibilities:
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. This is still the main renderer the repo documents and validates most thoroughly.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. This is still the main renderer the repo documents and validates most thoroughly.
 
 ## Non-goals
 
diff --git a/spec/packages/rendering/vue.md b/spec/packages/rendering/vue.md
index 7c3e5b65b..aa7926039 100644
--- a/spec/packages/rendering/vue.md
+++ b/spec/packages/rendering/vue.md
@@ -69,7 +69,7 @@ Important responsibilities:
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. The package is intentionally lean, but it is now important enough that regressions here should be treated as architectural regressions, not just renderer-specific bugs.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. The package is intentionally lean, but it is now important enough that regressions here should be treated as architectural regressions, not just renderer-specific bugs.
 
 ## Non-goals
 
diff --git a/spec/packages/schema.md b/spec/packages/schema.md
index 78dfa1676..410283859 100644
--- a/spec/packages/schema.md
+++ b/spec/packages/schema.md
@@ -36,7 +36,7 @@ Schema surface packages in Pen should stay package-first and explicit about owne
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving.
 
 ## Non-goals
 
diff --git a/spec/packages/shared/ingest.md b/spec/packages/shared/ingest.md
index d9f59bbea..6270c1b2c 100644
--- a/spec/packages/shared/ingest.md
+++ b/spec/packages/shared/ingest.md
@@ -60,7 +60,7 @@ Important rules:
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. It is already a high-leverage package because many higher-level features depend on its normalization and write-op rules staying stable.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. It is already a high-leverage package because many higher-level features depend on its normalization and write-op rules staying stable.
 
 ## Non-goals
 
diff --git a/spec/packages/shared/markdown.md b/spec/packages/shared/markdown.md
index 4a8ca325a..e8892939f 100644
--- a/spec/packages/shared/markdown.md
+++ b/spec/packages/shared/markdown.md
@@ -32,7 +32,7 @@ This package still owns markdown serialization (`exportMarkdownForBlocks()`, `ex
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving.
 
 ## Non-goals
 
diff --git a/spec/packages/tooling/assets.md b/spec/packages/tooling/assets.md
index d96f6b41c..e3ef120b2 100644
--- a/spec/packages/tooling/assets.md
+++ b/spec/packages/tooling/assets.md
@@ -35,7 +35,7 @@ Tooling package packages in Pen should stay package-first and explicit about own
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving.
 
 ## Non-goals
 
diff --git a/spec/packages/tooling/bench.md b/spec/packages/tooling/bench.md
index 69d19ef95..159bc9599 100644
--- a/spec/packages/tooling/bench.md
+++ b/spec/packages/tooling/bench.md
@@ -41,7 +41,7 @@ The SCALE1 envelope is recorded with `pnpm --filter @input/pen-bench run bench:e
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving.
 
 ## Non-goals
 
diff --git a/spec/packages/tooling/test.md b/spec/packages/tooling/test.md
index bebf51299..b8730c0c7 100644
--- a/spec/packages/tooling/test.md
+++ b/spec/packages/tooling/test.md
@@ -43,7 +43,7 @@ Tooling packages in Pen should stay package-first and explicit about ownership.
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving.
 
 ## Non-goals
 
diff --git a/spec/packages/transport.md b/spec/packages/transport.md
index defd09f97..99b5f8668 100644
--- a/spec/packages/transport.md
+++ b/spec/packages/transport.md
@@ -40,7 +40,7 @@ In both variants each `toolCalls` entry is authorized with `openAIToolCall()` be
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving.
 
 ## Non-goals
 
diff --git a/spec/packages/types.md b/spec/packages/types.md
index dd0996b66..9dcb5ccd3 100644
--- a/spec/packages/types.md
+++ b/spec/packages/types.md
@@ -136,7 +136,7 @@ The Yjs adapter implements `createRelativePosition` and `resolveRelativePosition
 
 ## Current Maturity / Intended Usage
 
-Workspace package at version `0.2.14`; intended usage is current-state but still evolving. In practice it is one of the most sensitive packages in the repo because seemingly small contract changes can cascade through most of the workspace.
+Workspace package at version `0.3.0`; intended usage is current-state but still evolving. In practice it is one of the most sensitive packages in the repo because seemingly small contract changes can cascade through most of the workspace.
 
 ## Non-goals