docs/plans/2026-04-22-slate-v2-authoritative-editing-kernel-perfect-architecture-plan.md
Pivot execution strategy.
Do not pivot away from Slate's data model, operations, transactions, semantic
Editable, projection overlays, or React-perfect runtime work.
Do pivot away from patching browser bugs row by row.
The target architecture is:
browser event
-> kernel dispatch
-> target owner
-> input intent
-> command
-> selection source
-> transaction/mutation
-> DOM text/selection repair
-> trace
Exactly one owner decides that chain.
Everything else is a worker.
The current runtime is promising, but it is not the final architecture.
The files are better:
editing-kernel.tsinput-controller.tsselection-controller.tsmutation-controller.tscaret-engine.tsdom-repair-queue.tsinput-router.tsBut the kernel is still not fully authoritative.
The runtime still has too many places where timing truth can leak:
keydown can import DOM selection before a model command.beforeinput can store stale user selection and restore it after a command.selectionchange can race model-owned repair.That is why regressions keep appearing.
The problem is not missing cases. The problem is ambiguous authority.
Order matters:
React should get better runtime APIs.
React should not define the core model.
Editable.EditableInputRule.onKeyCommand.slate-browser model + DOM proof.onKeyDown.onDOMBeforeInput.Editor.getSnapshot() as urgent render/read path.decorate as primary overlay API.Recover:
Do not recover:
Editable monolith.renderChunk.decorate as the primary API.Local refs:
../lexical/packages/lexical/src/LexicalUpdates.ts../lexical/packages/lexical/src/LexicalUtils.ts../lexical/packages/lexical/src/LexicalUpdateTags.tsUseful lessons:
Slate v2 take:
Local refs:
../prosemirror/view/src/input.ts../prosemirror/view/src/selection.ts../prosemirror/view/src/domobserver.ts../prosemirror/state/src/transaction.tsUseful lessons:
Slate v2 take:
EditableEditingKernel is Slate v2's view authority.Local refs:
../edix/src/editor.ts../edix/src/commands.ts../edix/src/doc/edit.ts../edix/e2e/common.spec.tsUseful lessons:
Slate v2 take:
EditableEditingKernelThe kernel owns:
Public internal shape:
type EditableEditingKernel = {
dispatch(event: EditableKernelEvent): EditableKernelResult
}
EditableKernelEventEvery browser/input path becomes one event shape:
type EditableKernelEvent = {
family:
| 'keydown'
| 'beforeinput'
| 'input'
| 'selectionchange'
| 'compositionstart'
| 'compositionupdate'
| 'compositionend'
| 'paste'
| 'copy'
| 'cut'
| 'drop'
| 'dragstart'
| 'dragover'
| 'dragend'
| 'focus'
| 'blur'
| 'mousedown'
| 'click'
| 'repair'
nativeEvent: Event
target: EventTarget | null
}
EditableKernelResultAll handlers return a result.
No handler performs hidden policy.
type EditableKernelResult = {
handled: boolean
nativeAllowed: boolean
ownership:
| 'model-owned'
| 'native-allowed'
| 'native-denied'
| 'app-owned'
| 'deferred'
| 'no-op'
targetOwner:
| 'editor'
| 'internal-control'
| 'app-owned'
| 'shell'
| 'outside-editor'
| 'unknown'
intent: InputIntent | null
command: EditableCommand | null
selectionSource: SelectionSource
repair: EditableRepairRequest | null
transition: EditableKernelTransition
trace: EditableKernelTraceEntry
}
EditableCommandCommands are the only mutation language.
Required command families:
SelectionSourceEvery event has one selection truth source:
type SelectionSource =
| 'model-owned'
| 'dom-current'
| 'composition-owned'
| 'shell-backed'
| 'internal-control'
| 'app-owned'
| 'unknown'
Rules:
KernelModeModes are explicit:
idledom-selectionmodel-ownedcompositionapp-ownedclipboarddragginginternal-controlshell-backedrepairingIllegal examples:
nativeAllowed: trueWorkers do not decide global timing.
InputController:
SelectionController:
MutationController:
CaretEngine:
DOMRepairQueue:
InputRouter:
Editable supports:
inputRulesonKeyCommandappCommandsDo not expose:
renderChunkdecorate as primary overlay APIEvery browser-editing scenario must assert:
Add slate-browser generated scenarios for:
Each generated step records:
Phase 1:
Phase 2:
Phase 3:
Do not throw in production.
Goal:
Actions:
EditableKernelTransitioncreateEditableKernelResultGates:
bunx turbo build --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-react --force
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "kernel transitions|Arrow|navigation and mutation"
Exit:
Goal:
Actions:
slate-browser navigation scenario generatorGates:
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "navigation gauntlet"
bunx playwright test ./playwright/integration/examples/inlines.test.ts --project=chromium --project=firefox --project=webkit --project=mobile
Exit:
Goal:
Actions:
moveHorizontalmoveWordmoveLineextendLinemoveHomeEndGates:
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "Arrow|word movement|line extension|navigation gauntlet"
Exit:
Goal:
Actions:
onKeyCommandappCommands helper APIGates:
bunx playwright test ./playwright/integration/examples/code-highlighting.test.ts --project=chromium --project=firefox --project=webkit --project=mobile
bunx playwright test ./playwright/integration/examples/mentions.test.ts --project=chromium --project=firefox --project=webkit --project=mobile
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile
Exit:
Goal:
Actions:
Gates:
bunx playwright test ./playwright/integration/examples/paste-html.test.ts --project=chromium --project=firefox --project=webkit --project=mobile
bunx playwright test ./playwright/integration/examples/highlighted-text.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "copy|cut"
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "paste|fragment|shell"
Exit:
Goal:
Actions:
Gates:
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "composition|IME"
Exit:
Goal:
Actions:
Gates:
bunx playwright test ./playwright/integration/examples/editable-voids.test.ts --project=chromium --project=firefox --project=webkit --project=mobile
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "shell|activation|selection"
Exit:
Goal:
Actions:
Gates:
bun test:integration-local
Exit:
Every phase must preserve:
Perf gates:
bun run bench:react:rerender-breadth:local
REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
Run these after changes to:
This lane is not complete until:
CaretEngine owns movement onlyDOMRepairQueue repairs onlyMutationController mutates onlySelectionController imports/exports onlybun test:integration-local is green or every remaining row is explicitly
accepted/deferred with exact rationalePhase A is active.
Start with keydown and beforeinput because they caused the observed regressions.
Do not move to clipboard/composition until keydown/beforeinput have generated gauntlet proof.