docs/plans/2026-04-27-slate-v2-editable-event-runtime-hard-cut-plan.md
Active.
Execution started from complete-plan on 2026-04-27.
Current next owner: slate-react Editable event-runtime boundary.
The previous Editable runtime/root selector lane moved direct hot policy bodies
out of EditableDOMRoot. That was the right cut.
It is not the final architecture.
EditableDOMRoot still assembles the editor event runtime in React component
closures:
Policy bodies are mostly in runtime/strategy modules now, but the component is still the traffic controller. For React 19.2-perfect runtime, that is still too much React ownership.
React attaches the editor.
The event runtime drives the editor.
EditableDOMRoot should:
EditableDOMRoot should not:
prepareEditable*Kernel(...)applyEditable*Strategy(...)forceRender through event-family codeAdd an internal event runtime facade:
.tmp/slate-v2/packages/slate-react/src/editable/runtime-event-engine.tsPreferred hook shape:
const eventRuntime = useEditableEventRuntime({
androidInputManagerRef,
attributes,
browserHandleNextId,
browserHandleRangeRefs,
deferredOperations,
editor,
inputController,
inputRules,
isShellBackedSelection,
largeDocument,
onKeyCommand,
onUserInput,
readOnly,
rootRef,
scrollSelectionIntoView,
setExplicitShellBackedSelection,
setIsComposing,
shellBackedSelection,
});
Returned shape:
{
attachBrowserHandle(): void
handlers: EditableRootEventHandlers
repair: EditableRepairRuntime
selection: EditableSelectionRuntime
}
Exact names can change. Ownership cannot.
Do not make one new giant file. The facade should compose smaller owners:
runtime-before-input-events.tsruntime-input-events.tsruntime-clipboard-events.tsruntime-composition-events.tsruntime-focus-mouse-events.tsruntime-keyboard-events.tsruntime-drag-events.tsruntime-browser-handle-events.tsKeep existing strategy modules as workers. The event runtime orchestrates event families; it should not swallow every mutation algorithm.
App callbacks should not churn hot handler identity.
Use callback refs or a tiny internal useLatestEditableProps(...) helper for:
attributes.onBeforeInputattributes.onInputattributes.onKeyDownonKeyCommandinputRulesThe hot root handlers should be stable across ordinary app prop callback changes when editor/runtime identity is unchanged.
This follows the React 19.2 performance posture:
EditableDOMRoot Stops Importing Event WorkersAfter this lane, components/editable.tsx should not import event worker
families directly:
clipboard-input-strategycomposition-state event applicatorsediting-kernel prepare functionskeyboard-input-strategymodel-input-strategynative-input-strategyselection-reconciler workersIt may import render-only helpers, contexts, types, and the event runtime hook.
All root handlers are assembled in runtime modules:
onDOMBeforeInputonReactBeforeInputonDOMInputonInputCaptureonPasteonCopyonCutonDragStartonDragOveronDragEndonDroponCompositionStartonCompositionUpdateonCompositionEndonFocusonBluronClickonMouseDownonMouseUponKeyDownEditableDOMRoot spreads or assigns eventRuntime.handlers.
Event handlers use named runtime capabilities:
eventRuntime.selection.flushSelectionChange()eventRuntime.selection.applyKeyDownSelectionPolicy(...)eventRuntime.selection.syncDOMSelectionFromRuntime()eventRuntime.repair.request(...)eventRuntime.trace.record(...)No event handler in EditableDOMRoot calls those directly because the handler
is not in EditableDOMRoot.
Add guards that fail when EditableDOMRoot imports or calls forbidden event
workers.
Do not rely on code review memory. The next rushed patch must fail locally.
Purpose: prove the plan is moving the real current surface, not stale debt.
Actions:
EditableDOMRoot.components/editable.tsx.Acceptance:
EditableDOMRoot event imports are listed in one guard.Likely files:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/test/kernel-authority-audit-contract.ts.tmp/slate-v2/packages/slate-react/test/surface-contract.tsxDriver gate:
bun --filter slate-react test:vitest test/kernel-authority-audit-contract.test.ts test/surface-contract.test.tsxPurpose: introduce the owner without changing behavior.
Actions:
runtime-event-engine.ts.EditableRootEventHandlers and runtime input/output types.Acceptance:
EditableDOMRoot can instantiate useEditableEventRuntime(...).Likely files:
.tmp/slate-v2/packages/slate-react/src/editable/runtime-event-engine.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-selection-engine.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-repair-engine.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-kernel-trace.ts.tmp/slate-v2/packages/slate-react/src/components/editable.tsxDriver gate:
bun --filter slate-react typecheckPurpose: shrink EditableDOMRoot without touching the most timing-sensitive
input path first.
Move these families into runtime event modules:
Acceptance:
EditableDOMRoot no longer defines those handler closures.Likely files:
.tmp/slate-v2/packages/slate-react/src/editable/runtime-clipboard-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-drag-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-focus-mouse-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-event-engine.ts.tmp/slate-v2/packages/slate-react/src/components/editable.tsxDriver gates:
bun --filter slate-react test:vitest test/editing-kernel-contract.test.ts test/editing-epoch-kernel-contract.test.tsPLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/hovering-toolbar.test.ts playwright/integration/examples/richtext.test.ts --project=chromium --grep "hovering toolbar|paste|undo"Purpose: isolate platform event assembly before the native input path moves.
Actions:
runtime-composition-events.ts.runtime-composition-engine.ts.runtime-android-engine.ts.Acceptance:
EditableDOMRoot no longer assembles composition handlers.Likely files:
.tmp/slate-v2/packages/slate-react/src/editable/runtime-composition-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-composition-engine.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-android-engine.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-event-engine.tsDriver gates:
bun --filter slate-react test:vitest test/editing-epoch-kernel-contract.test.tsSTRESS_FAMILIES=selection-repair-ime PLAYWRIGHT_RETRIES=0 bun test:stressPurpose: cut the hardest React-owned path.
Actions:
beforeinput handler assembly into
runtime-before-input-events.ts.runtime-input-events.ts.Acceptance:
EditableDOMRoot no longer imports or calls beforeinput/input strategy
workers.Likely files:
.tmp/slate-v2/packages/slate-react/src/editable/runtime-before-input-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-input-events.ts.tmp/slate-v2/packages/slate-react/src/editable/model-input-strategy.ts.tmp/slate-v2/packages/slate-react/src/editable/native-input-strategy.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-event-engine.tsDriver gates:
bun --filter slate-react test:vitest test/selection-controller-contract.test.ts test/editing-kernel-contract.test.ts test/surface-contract.test.tsxPLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/richtext.test.ts playwright/integration/examples/search-highlighting.test.ts playwright/integration/examples/placeholder.test.ts --project=chromium --grep "native word-delete|search|placeholder"Purpose: finish hot keyboard ownership.
Actions:
runtime-keyboard-events.ts.prepareEditableKeyDownKernel(...)onKeyCommand, read-only behavior, shell-backed selection updates,
and large-document policy.Acceptance:
EditableDOMRoot no longer assembles keydown.0.Likely files:
.tmp/slate-v2/packages/slate-react/src/editable/runtime-keyboard-events.ts.tmp/slate-v2/packages/slate-react/src/editable/keyboard-input-strategy.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-event-engine.tsDriver gates:
bun --filter slate-react test:vitest test/selection-runtime-contract.test.ts test/selection-controller-contract.test.tsPLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/mentions.test.ts playwright/integration/examples/tables.test.ts playwright/integration/examples/images.test.ts playwright/integration/examples/large-document-runtime.test.ts --project=chromiumPurpose: make proof-only and implicit-target bridges explicit event-runtime capabilities.
Actions:
attachSlateBrowserHandle(...) setup behind
runtime-browser-handle-events.ts.writeTargetRuntime(...) setup into the event runtime facade or a
small target-runtime bridge owner.Acceptance:
EditableDOMRoot no longer attaches the browser handle directly.Likely files:
.tmp/slate-v2/packages/slate-react/src/editable/runtime-browser-handle-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-event-engine.ts.tmp/slate-v2/packages/slate-react/src/editable/browser-handle.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-mutation-state.tsDriver gate:
bun --filter slate-react test:vitest test/target-runtime-contract.test.ts test/kernel-authority-audit-contract.test.tsEditableDOMRoot And Lock The BoundaryPurpose: enforce the architecture.
Actions:
components/editable.tsx.EditableDOMRoot as a wiring/render component.components/editable.tsxEditableDOMRootAcceptance:
EditableDOMRoot imports event workers again.EditableDOMRoot calls prepareEditable*Kernel,
applyEditable*, or recordKernelEventTrace.Likely files:
.tmp/slate-v2/packages/slate-react/test/kernel-authority-audit-contract.ts.tmp/slate-v2/packages/slate-react/test/surface-contract.tsx.tmp/slate-v2/packages/slate-react/test/event-runtime-contract.test.tsxDriver gates:
bun --filter slate-react test:vitest test/kernel-authority-audit-contract.test.ts test/surface-contract.test.tsxbun --filter slate-react typecheckPurpose: prove this was not just file shuffling.
Required focused browser rows:
0Required stress families:
inline-void-boundary-navigationblock-void-navigationtable-cell-boundary-navigationexternal-decoration-refreshmouse-selection-toolbarpaste-normalize-undoselection-repair-imeFinal closure gates:
bun --filter slate-react test:vitestbun --filter slate-react typecheckbun --filter slate-react buildbun lint:fixbun test:stressbun check:fullIf bun check:full retries a row, rerun that exact row alone with retries
disabled before closure.
Reason: beforeinput/input and keydown are the highest-risk timing paths, so the event runtime should be structurally real before touching them.
runtime-event-engine.ts with every event body inside.bun check.Replan if:
EditableDOMRootforceRender()EditableDOMRootThis plan is complete only when:
EditableDOMRoot no longer assembles root event handlers.EditableDOMRoot no longer imports event worker strategy modules directly.useEditableEventRuntime(...).EditableDOMRoot.bun check:full passes before the lane is marked done.Start with Phase 0.
Do not move behavior first.
Add the inventory guard, then use it to drive the extraction.
Actions:
active goal state for the Editable event runtime lane.active goal state.Commands:
EditableDOMRoot source and existing runtime authority
contracts.Artifacts:
active goal stateactive goal stateEvidence:
EditableDOMRoot still contains 20 handle* event closures and 21
on* root handler wrapper constants.EditableDOMRoot still imports event worker families directly from
browser-handle, clipboard-input-strategy, composition-state,
editing-kernel, input-router, keyboard-input-strategy,
model-input-strategy, native-input-strategy, and
selection-reconciler.Hypothesis:
Decision:
Owner classification:
slate-react event-runtime architecture/test guard
ownership.Changed files:
active goal stateactive goal statedocs/plans/2026-04-27-slate-v2-editable-event-runtime-hard-cut-plan.mdRejected tactics:
runtime-event-engine.ts before the event import and
handler surface is guarded.Next action:
.tmp/slate-v2/packages/slate-react/test/kernel-authority-audit-contract.ts.Actions:
EditableDOMRoot event-worker import and handler-closure
inventory guard.useEditableEventRuntime(...) facade.EditableDOMRoot into runtime event family modules.Commands:
bun --filter slate-react test:vitest test/kernel-authority-audit-contract.test.ts test/surface-contract.test.tsxbun --filter slate-react test:vitest test/editing-kernel-contract.test.ts test/editing-epoch-kernel-contract.test.tsbun --filter slate-react typecheckbunx turbo build --filter=./packages/slate-browser --filter=./packages/slate-dom --filter=./packages/slate-react --forcePLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/hovering-toolbar.test.ts playwright/integration/examples/richtext.test.ts --project=chromium --grep "hovering toolbar|paste|undo"Evidence:
slate-react typecheck passed.is-hotkey external
warning from slate-dom/src/utils/hotkeys.ts, but the build succeeded.Changed files:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/editable/runtime-clipboard-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-drag-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-event-engine.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-focus-mouse-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-selection-engine.ts.tmp/slate-v2/packages/slate-react/test/kernel-authority-audit-contract.tsDecision:
Rejected tactics:
EditableDOMRoot still
assembles composition, beforeinput/input, keyboard, browser-handle, and
target-runtime wiring.Next action:
runtime-composition-events.ts while keeping composition state transitions in
runtime-composition-engine.ts / composition-state.ts.Actions:
runtime-composition-events.ts.EditableDOMRoot.Commands:
bun --filter slate-react test:vitest test/kernel-authority-audit-contract.test.ts test/surface-contract.test.tsxbun --filter slate-react typecheckbun --filter slate-react test:vitest test/editing-epoch-kernel-contract.test.tsEvidence:
slate-react typecheck passed.Changed files:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/editable/runtime-composition-events.ts.tmp/slate-v2/packages/slate-react/test/kernel-authority-audit-contract.tsDecision:
Rejected tactics:
runtime-event-engine.ts the god module the plan explicitly rejects.Next action:
runtime-before-input-events.ts and runtime-input-events.ts.Actions:
runtime-before-input-events.ts.runtime-input-events.ts.EditableDOMRoot.Commands:
bun --filter slate-react test:vitest test/kernel-authority-audit-contract.test.ts test/surface-contract.test.tsxbun --filter slate-react typecheckbun --filter slate-react test:vitest test/selection-controller-contract.test.ts test/editing-kernel-contract.test.ts test/surface-contract.test.tsxbunx turbo build --filter=./packages/slate-browser --filter=./packages/slate-dom --filter=./packages/slate-react --forcePLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/richtext.test.ts playwright/integration/examples/search-highlighting.test.ts --project=chromium --grep "inserts text through browser input|runs generated navigation and typing|runs generated destructive paste|records core command metadata|does not duplicate native input|keeps focus in search input|highlights the searched text|keeps caret editable after plain text paste"Evidence:
slate-react typecheck passed.is-hotkey external warning.Changed files:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/editable/runtime-before-input-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-input-events.ts.tmp/slate-v2/packages/slate-react/test/kernel-authority-audit-contract.tsDecision:
Rejected tactics:
Next action:
runtime-keyboard-events.ts.Actions:
runtime-keyboard-events.ts.EditableDOMRoot.Commands:
bun --filter slate-react test:vitest test/kernel-authority-audit-contract.test.ts test/surface-contract.test.tsxbun --filter slate-react typecheckbun --filter slate-react test:vitest test/selection-runtime-contract.test.ts test/selection-controller-contract.test.tsbunx turbo build --filter=./packages/slate-browser --filter=./packages/slate-dom --filter=./packages/slate-react --forcePLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/mentions.test.ts playwright/integration/examples/tables.test.ts playwright/integration/examples/images.test.ts playwright/integration/examples/large-document-runtime.test.ts --project=chromiumEvidence:
slate-react typecheck passed.is-hotkey external warning.Changed files:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/editable/runtime-keyboard-events.ts.tmp/slate-v2/packages/slate-react/test/kernel-authority-audit-contract.tsDecision:
Rejected tactics:
Next action:
Actions:
runtime-browser-handle-events.ts.runtime-target-bridge.ts.EditableDOMRoot.EditableDOMRoot has no direct
attachSlateBrowserHandle(...) or writeTargetRuntime(...) calls.Commands:
bun --filter slate-react test:vitest test/kernel-authority-audit-contract.test.ts test/surface-contract.test.tsxbun --filter slate-react typecheckbun --filter slate-react test:vitest test/target-runtime-contract.test.ts test/kernel-authority-audit-contract.test.tsbunx turbo build --filter=./packages/slate-browser --filter=./packages/slate-dom --filter=./packages/slate-react --forcePLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/mentions.test.ts playwright/integration/examples/tables.test.ts playwright/integration/examples/images.test.ts playwright/integration/examples/large-document-runtime.test.ts --project=chromiumEvidence:
slate-react typecheck passed.is-hotkey external warning.Changed files:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/editable/runtime-browser-handle-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-event-engine.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-target-bridge.ts.tmp/slate-v2/packages/slate-react/test/kernel-authority-audit-contract.tsDecision:
EditableDOMRoot; the remaining cleanup is facade consolidation and final
release proof.Rejected tactics:
EditableDOMRoot importing every event family module directly.
The event runtime facade should be the only event assembly import the root
needs.Next action:
useEditableEventRuntime(...), rerun the focused gates, then run
bun check:full.Actions:
useEditableEventRuntime(...).EditableDOMRoot to consuming the event runtime facade for root event
bindings.EditableDOMRoot.Commands:
bun --filter slate-react test:vitest test/kernel-authority-audit-contract.test.ts test/surface-contract.test.tsxbun --filter slate-react typecheckbun --filter slate-react test:vitest test/selection-controller-contract.test.ts test/selection-runtime-contract.test.ts test/editing-kernel-contract.test.ts test/editing-epoch-kernel-contract.test.ts test/target-runtime-contract.test.ts test/surface-contract.test.tsx test/kernel-authority-audit-contract.test.tsbunx turbo build --filter=./packages/slate-browser --filter=./packages/slate-dom --filter=./packages/slate-react --forcePLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/hovering-toolbar.test.ts playwright/integration/examples/richtext.test.ts playwright/integration/examples/search-highlighting.test.ts --project=chromium --grep "hovering toolbar|paste|undo|search"PLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/mentions.test.ts playwright/integration/examples/tables.test.ts playwright/integration/examples/images.test.ts playwright/integration/examples/large-document-runtime.test.ts --project=chromiumbun lint:fixbun --filter slate-react typecheckbun check:fullEvidence:
slate-react typecheck passed after fixing the runtime facade state type to
EditableInputControllerState.is-hotkey external warning.bun lint:fix passed and formatted 10 files.slate-react typecheck passed.bun check:full passed.Changed files:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/editable/runtime-before-input-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-browser-handle-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-clipboard-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-composition-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-drag-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-event-engine.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-focus-mouse-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-input-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-keyboard-events.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-selection-engine.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-target-bridge.ts.tmp/slate-v2/packages/slate-react/test/kernel-authority-audit-contract.tsactive goal stateactive goal stateDecision:
EditableDOMRoot, event family
assembly lives behind useEditableEventRuntime(...), static guards cover
drift, focused browser rows passed, generated stress passed through
bun check:full, and no runnable in-scope owner remains.Rejected tactics:
Next action:
Actions:
docs/solutions/developer-experience/2026-04-27-slate-react-runtime-owner-cuts-need-static-inventories-and-browser-proof.md
with the event runtime facade rule: EditableDOMRoot gets one
useEditableEventRuntime(...) import, while event-family workers, browser
handle setup, and target-runtime publication stay behind runtime-owned hooks.Commands:
bun run completion-checkEvidence: