docs/plans/2026-04-24-slate-v2-selection-caret-conformance-kernel-plan.md
Do not pivot to a different editor model.
Pivot harder into the stricter version of the accepted Slate v2 architecture:
Slate model + operations
Lexical-style read/update lifecycle
ProseMirror-style transaction + DOM-selection authority
Tiptap-style extension DX
React 19.2 live-read / dirty-commit runtime
Selection/Caret Conformance Kernel
Generated browser gauntlets
The previous master plan closed the current checked suite. User-visible cursor regressions mean that suite was not strong enough. The architecture direction is still right; the proof and timing discipline are not yet good enough.
Latest browser evidence reopens release confidence: fresh deterministic rows can pass while a warm, no-refresh page gets stuck after real user selection, toolbar toggles, and arrow navigation. That class is not a missing assertion. It is stale async selection/repair work surviving into the next user event.
The bug class is not "missing ArrowRight row" or "bold click edge case".
The bug class is that selection and caret truth can still leak across:
If those owners can disagree without one audited kernel deciding the sequence, cursor regressions will keep escaping.
editor.read and editor.update remain the public lifecycleeditor.updateslate-react owns DOM selection import/exportEditable.focus().chain().run() the required DX.tx.resolveTarget() to app/plugin authors.Keep the public shape:
editor.read(() => {
editor.getSelection();
editor.getChildren();
editor.getMarks();
});
editor.update(() => {
editor.unwrapNodes({ match: isList });
editor.setNodes({ type: "list-item" });
editor.wrapNodes({ type: "bulleted-list", children: [] });
});
Keep primitive editor methods flexible:
editor.selecteditor.setNodeseditor.wrapNodeseditor.unwrapNodeseditor.insertNodeseditor.removeNodeseditor.splitNodeseditor.mergeNodeseditor.moveNodeseditor.insertTexteditor.deleteeditor.insertFragmenteditor.insertBreakConvenience methods are allowed only when tiny and stable:
editor.toggleMarkeditor.toggleBlockDo not grow semantic methods for every custom node type.
Every browser editing path must reduce to:
event
-> event frame / epoch
-> input intent
-> selection source
-> target resolution
-> editor.update / transaction
-> operations
-> EditorCommit
-> DOM repair decision
-> trace
Exactly one owner decides that sequence: EditableConformanceKernel.
Current controllers become workers:
InputRouter: classify raw browser events into intents onlySelectionReconciler: import/export model and DOM selection onlyCaretEngine: compute movement and canonical caret placement onlyMutationController: execute model mutations only through editor.updateDOMRepairQueue: execute scheduled repair onlyBrowserHandle: proof-only semantic control surface, never native proofEditableConformanceKernel: owns order, authority, legality, and traceThe current plan already steals the right high-level ideas. The deeper source read adds one missing non-negotiable: editing work must be framed by a single event epoch.
Source lessons to steal:
selectionchange, beforeinput, composition, and movement
timing in one coherent control flow. Recover the ordering comments and
browser cautions, not the monolith.selectionFromDOM / selectionToDOM, disconnects or suppresses selection
observation while writing DOM selection, and uses counters like
domChangeCount to cancel stale async fallbacks.editor.update lifecycle, tags updates, owns
dirty nodes and DOM selection update during commit, and lets tags like
skip-dom-selection or composition tags decide post-commit DOM work.Slate v2 answer:
EditableEventFrame
-> selection import/export
-> editor.update transaction
-> commit
-> repair scheduling
-> trace
Every frame gets an id. Every repair, selection export, delayed retry, selectionchange import, and trace entry carries that id. Starting a new user event invalidates stale async work from older frames unless the kernel explicitly transfers ownership.
Required frame shape:
type EditableEventFrame = {
id: number;
active: boolean;
eventFamily: EditableEventFamily;
focusOwner: EditableFocusOwner;
inputIntent: InputIntent | null;
modelSelectionBefore: Selection | null;
selectionSource: EditableSelectionSource;
startedAt: number;
targetOwner: EditableTargetOwner;
};
Rules:
selectionchange records whether it is native, programmatic export,
repair-induced, or unknownselectionchange; it must not re-import as a fresh user selectionCut from primary runtime, docs, examples, and blessed plugin patterns:
editor.selectioneditor.childreneditor.markseditor.operationsTransforms.* as the primary mutation storyeditor.apply as an extension pointeditor.onChange as an extension pointReactEditor.runCommandfocus().chain().run() command ceremonyeditor.updateslate-browser proof primitivesRecover the useful parts, not the monolith:
Do not recover:
Editable as one giant event filedecorate as overlay architectureThe kernel owns a state machine, not a pile of callbacks.
States:
idlefocuseddom-selection-authoritativemodel-selection-authoritativetransaction-openrepair-pendingcompositionclipboarddragginginternal-controlshell-backedshadow-rootmobile-semanticEvents:
keydownbeforeinputinputselectionchangecompositionstartcompositionupdatecompositionendpastecutdropfocusblurmousedownmouseupclickpointerdowntoolbar-commandbrowser-handle-commandpost-commit-repairFor every transition record:
Illegal transitions fail tests immediately. Runtime throwing can remain dev/test-only until the transition map stabilizes.
Use DOM selection as target when:
Use model selection as target when:
editor.updateUse explicit target with no DOM import when:
at is providedUse no selection target when:
These are internal rules. Plugin authors should not choose policies manually.
Every commit that can affect visible caret/text must produce a repair decision:
nonesync-dom-selectionsync-dom-textforce-react-renderfocus-editorrestore-model-selectionskip-dom-syncdefer-for-compositiondefer-for-shadow-rootdefer-for-mobileRepair input:
Repair output:
DOMRepairQueue must never be a free-floating retry loop.
Required behavior:
DOMRepairQueue.beginFrame(frameId) records the newest event frameDOMRepairQueue.cancelBefore(frameId) invalidates older pending retriesframeId, commitId, reason, and target
selection bookmarkframeId before touching DOMThis is the likely owner for warm no-refresh flakes where keydown/keyup fires but visual/model selection stays stuck: an older repair or delayed export can overwrite the browser's newer caret movement.
Do not make all caret movement model-owned by default.
The kernel must choose movement ownership from a capability matrix:
Every model-owned movement must import the current DOM selection at the start of the current event frame unless the frame already declares model selection authoritative. Every native-owned movement must cancel stale model-owned repair work before the browser moves the caret.
Model selection and DOM selection are not enough.
Browser rows that claim caret correctness must assert one of:
When a browser cannot expose reliable caret geometry, the row must say so and fall back to:
No silent downgrade to model-only proof.
Purpose:
editor.read / editor.updateFiles:
.tmp/slate-v2/packages/slate/test/read-update-contract.ts.tmp/slate-v2/packages/slate/test/primitive-method-runtime-contract.ts.tmp/slate-v2/packages/slate/test/transaction-target-runtime-contract.ts.tmp/slate-v2/packages/slate/test/commit-metadata-contract.ts.tmp/slate-v2/packages/slate/test/bookmark-contract.tsPurpose:
Files:
.tmp/slate-v2/packages/slate-react/test/selection-conformance-kernel-contract.ts.tmp/slate-v2/packages/slate-react/test/caret-repair-contract.ts.tmp/slate-v2/packages/slate-react/test/target-runtime-contract.tsx.tmp/slate-v2/packages/slate-react/test/dom-text-sync-contract.ts.tmp/slate-v2/packages/slate-react/test/projections-and-selection-contract.tsx.tmp/slate-v2/packages/slate-react/test/large-doc-and-scroll.tsxPurpose:
Files:
.tmp/slate-v2/packages/slate-browser/src/playwright/**.tmp/slate-v2/playwright/integration/examples/**Every row asserts:
Warm-state rows are mandatory. A browser proof must include no-refresh repeated interaction sequences, not only fresh-page deterministic setup. At least one row per high-risk family must run the same editor instance through repeated selection, toolbar, navigation, mutation, and follow-up typing cycles.
Scenarios:
Scenarios:
Scenarios:
Scenarios:
unwrapNodes / setNodes / wrapNodesat transform never imports DOMScenarios:
Scenarios:
Scenarios:
Scenarios:
Scenarios:
Scenarios:
Build slate-browser scenario generation as a first-class test tool.
Inputs:
Output:
Shrinking:
Use tracer bullets, not horizontal rewrite.
For each slice:
Do not write a giant imagined test matrix before the first red row proves the test harness can catch the class.
This plan has already completed the first conformance lane. Do not rerun it as if nothing happened.
Completed:
slate-browser has typed kernel trace entries and reusable trace assertionsbun test:integration-local passed with 508 passedStill open:
selectionchange is not yet classified/suppressed everywhereConclusion:
EditableEventFrame plus cancellable repair/selectionchange ownershipGoal:
Actions:
--repeat-each=10 first, then raise the loop count only
if the failure needs more pressureExit criteria:
Goal:
Actions:
EditableEventFrame to the React editing runtimeframeId through kernel trace entries, selection import/export,
transactions/commits where available, and repair requestsExit criteria:
Goal:
Actions:
DOMRepairQueue.beginFrame(frameId) and
DOMRepairQueue.cancelBefore(frameId)selectionchange origin:
Exit criteria:
Goal:
Actions:
Exit criteria:
Goal:
Actions:
slate-browser scenario specsExit criteria:
Goal:
Actions:
bun test:integration-localExit criteria:
Do not mix these into the event-frame fix unless a red row proves they block it:
Transforms.*Core:
bun test ./packages/slate/test/read-update-contract.ts --bail 1
bun test ./packages/slate/test/primitive-method-runtime-contract.ts --bail 1
bun test ./packages/slate/test/transaction-target-runtime-contract.ts --bail 1
bun test ./packages/slate/test/commit-metadata-contract.ts --bail 1
bun test ./packages/slate/test/bookmark-contract.ts --bail 1
React:
bun test ./packages/slate-react/test/selection-conformance-kernel-contract.ts --bail 1
bun test ./packages/slate-react/test/caret-repair-contract.ts --bail 1
bun test ./packages/slate-react/test/target-runtime-contract.tsx --bail 1
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
Browser focused:
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "selection|caret|toolbar|bold|heading|arrow|backspace|delete" --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "warm|no-refresh|event frame|stale repair|toolbar|arrow" --workers=1 --retries=0 --repeat-each=10
bunx playwright test ./playwright/integration/examples/inlines.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/editable-voids.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/shadow-dom.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --workers=4 --retries=0
Full browser:
bun test:integration-local
Perf:
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
bun run bench:core:observation:compare:local
bun run bench:core:huge-document:compare:local
Build/type/lint:
bun run lint:fix
bun run lint
bunx turbo build --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react --filter=./packages/slate-browser --force
cd ./packages/slate && bun run build
bunx turbo typecheck --filter=./packages/slate --force
bunx turbo typecheck --filter=./packages/slate-dom --force
bunx turbo typecheck --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-browser --force
This plan is complete only when:
editor.updatebun test:integration-local is green without broad skip debtA green focused row is not stop.
A local cursor patch is not stop.
A Chromium-only proof is not stop.
Stop only when:
Start with Remaining Batch A:
active goal state pending for the reopened timing layerEditableEventFrame once the red owner is reproduced or the
automation gap is explicitly recordedStatus: complete.
Actions:
active goal state to status: pending for active
Selection/Caret Conformance Kernel executionselectDOM scenario step to slate-browserdoubleClickTextOffset scenario step to slate-browsercreateSlateBrowserToolbarMarkClickTypingGauntlet(...)Evidence:
bunx turbo build --filter=./packages/slate-browser --force
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "toolbar marking selected text" --workers=1 --retries=0
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "toolbar marking selected text" --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "native word selection toolbar mark" --workers=1 --retries=0
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "native word selection toolbar mark|toolbar marking selected text" --workers=1 --retries=0
Results:
slate-browser build: passed2 passed on ChromiumOwner classification:
Rejected tactics:
Checkpoint:
unknown[] and not a typed conformance
contractslate-browser build and focused richtext toolbar caret rowsunknown[]Status: complete.
Actions:
slate-browser Playwright kernelTrace: unknown[] with a typed
SlateBrowserKernelTraceEntry[] wire contractmatchesSlateBrowserKernelTrace(...)findSlateBrowserKernelTraceEntry(...)assertSlateBrowserKernelTraceEntry(...)assertKernelTrace scenario stepharness.assert.kernelTrace(...)getIllegalKernelTransitions(...)repair trace with repair-caretEvidence:
bunx turbo build --filter=./packages/slate-browser --force
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "native word selection toolbar mark|toolbar marking selected text" --workers=1 --retries=0
bunx turbo typecheck --filter=./packages/slate-browser --force
Results:
slate-browser build: passed2 passedslate-browser typecheck: passedOwner classification:
Rejected tactics:
slate-react types into slate-browser; the harness stays package
independent and owns a browser-proof wire contractCheckpoint:
slate-react, so
future kernel shape changes must update the harness contract intentionallyslate-browser build/typecheck and focused richtext toolbar caret
rowsStatus: complete.
Actions:
createSlateBrowserMixedEditingConformanceGauntlet(...)Failed probe:
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "mixed editing conformance" --workers=1 --retries=0
Initial result:
selectDOM alone did not model the real user
activation path before the toolbar command[1,0]@0, received [0,6]@1Resolution:
mousedown on the editor before
selectDOM(...) for DOM-selected toolbar command pathsEvidence:
bunx turbo build --filter=./packages/slate-browser --force
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "mixed editing conformance" --workers=1 --retries=0
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "mixed editing conformance" --workers=4 --retries=0
bunx turbo typecheck --filter=./packages/slate-browser --force
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "mixed editing conformance|native word selection toolbar mark|toolbar marking selected text" --workers=4 --retries=0
Results:
slate-browser build: passedslate-browser typecheck: passed12 passedOwner classification:
Rejected tactics:
Checkpoint:
Status: complete.
Actions:
deleteBackward and deleteForward scenario steps to
slate-browsercreateSlateBrowserSemanticEditingConformanceGauntlet(...)Evidence:
bunx turbo build --filter=./packages/slate-browser --force
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=mobile --grep "mobile semantic editing conformance" --workers=1 --retries=0
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "mobile semantic editing conformance|mixed editing conformance" --workers=4 --retries=0
bunx turbo typecheck --filter=./packages/slate-browser --force
Results:
slate-browser build: passed8 passedslate-browser typecheck: passedOwner classification:
Rejected tactics:
Checkpoint:
inlines.test.ts harness supportStatus: complete.
Actions:
createSlateBrowserInlineCutTypingGauntlet(...) with typed
trace assertions:
delete-fragment command traceslate-browser shortcut handling so ControlOrMeta+X uses
Playwright/native cut transport instead of a synthetic keydownapplyEditableCut(...) to return the command it actually appliedEditable cut handling to record that command in the kernel trace
after cut mutationFailed probes:
bunx playwright test ./playwright/integration/examples/inlines.test.ts --project=chromium --grep "generated inline cut" --workers=1 --retries=0
Initial results:
delete-fragment command trace but the cut
path only recorded a generic cut eventControlOrMeta+X was synthetic in slate-browser, so the test was not
actually exercising native cut transportResolution:
ControlOrMeta+XEvidence:
bunx turbo build --filter=./packages/slate-browser --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx playwright test ./playwright/integration/examples/inlines.test.ts --project=chromium --grep "generated inline cut" --workers=1 --retries=0
bunx playwright test ./playwright/integration/examples/inlines.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "generated inline cut" --workers=4 --retries=0
bun test ./packages/slate-react/test/editing-kernel-contract.ts --bail 1
bun test ./packages/slate-react/test/dom-repair-policy-contract.ts --bail 1
bun test ./packages/slate-react/test/selection-controller-contract.ts --bail 1
bunx turbo typecheck --filter=./packages/slate-browser --force
bunx turbo typecheck --filter=./packages/slate-react --force
Results:
slate-browser and slate-react typechecks: passedOwner classification:
slate-react cut handlingslate-browser native-transport lie for cutRejected tactics:
cut eventControlOrMeta+X while claiming native cut proofCheckpoint:
Status: complete.
Actions:
createSlateBrowserClipboardPasteGauntlet(...) with typed
trace assertions:
insert-data command traceapplyEditablePaste(...) to return the command it actually applies
for clipboard-backed insert-data pathsEditable paste handling to record the returned command in the
kernel traceinsertFromPaste,
insertFromDrop, and insertFromYank with DataTransfer classify as
insert-databeforeinput and pasteFailed probe:
bunx turbo build --filter=./packages/slate-browser --force && bunx playwright test ./playwright/integration/examples/paste-html.test.ts --project=chromium --grep "generated clipboard paste" --workers=1 --retries=0
Initial result:
insert-data
command traceResolution:
Evidence:
bunx turbo build --filter=./packages/slate-browser --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx playwright test ./playwright/integration/examples/paste-html.test.ts --project=chromium --grep "generated clipboard paste" --workers=1 --retries=0
bunx playwright test ./playwright/integration/examples/paste-html.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "generated clipboard paste" --workers=4 --retries=0
bun test ./packages/slate-react/test/editing-kernel-contract.ts --bail 1
bun test ./packages/slate-react/test/dom-repair-policy-contract.ts --bail 1
bun test ./packages/slate-react/test/selection-controller-contract.ts --bail 1
bunx turbo typecheck --filter=./packages/slate-browser --force
bunx turbo typecheck --filter=./packages/slate-react --force
Results:
slate-browser and slate-react typechecks: passed after fixing an
explicit null return in the shell-backed plain-text branchOwner classification:
Rejected tactics:
applyEditablePaste(...) as a mutation path with no command returnCheckpoint:
null command for now
and should be classified separately if it becomes release-criticalStatus: complete.
Actions:
createSlateBrowserCompositionGauntlet(...) with typed trace
assertions for:
compositionstartcompositionupdatecompositionendEvidence:
bunx turbo build --filter=./packages/slate-browser --force
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=chromium --grep "generated composition" --workers=1 --retries=0
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "generated composition" --workers=4 --retries=0
bunx turbo typecheck --filter=./packages/slate-browser --force
Results:
slate-browser build: passedslate-browser typecheck: passedOwner classification:
Rejected tactics:
Checkpoint:
slate-browser typecheckStatus: classified open.
Actions:
packages/slate-react/src/editable/clipboard-input-strategy.tspackages/slate-react/src/components/editable.tsxapplyEditableDrop(...) mutates through ReactEditor.insertData(...)Editable records a generic drop event trace before mutationinsert-data command authority for dropDecision:
Owner classification:
Rejected tactics:
paste coverage closes dropCheckpoint:
DataTransfer and event rangeStatus: complete.
Actions:
Evidence:
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "mixed editing conformance|mobile semantic editing conformance|native word selection toolbar mark|toolbar marking selected text" --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/inlines.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "generated inline cut" --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/paste-html.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "generated clipboard paste" --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "generated composition" --workers=4 --retries=0
Results:
Owner classification:
Checkpoint:
DataTransfer row laterStatus: complete.
Actions:
createSlateBrowserTextInsertionGauntlet(...) so semantic text
insertion rows must prove an insert-text repair trace, not just final textcreateSlateBrowserInternalControlGauntlet(...) so editable void
internal-control rows must prove:
internal-control while the nested input is activeinsert-text repair traceEvidence:
bunx turbo build --filter=./packages/slate-browser --force
bunx playwright test ./playwright/integration/examples/shadow-dom.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "generated shadow DOM typing" --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/editable-voids.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "generated internal-control" --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "selects void content" --workers=4 --retries=0
bunx turbo typecheck --filter=./packages/slate-browser --force
Results:
slate-browser build: passedslate-browser typecheck: passedOwner classification:
Rejected tactics:
Checkpoint:
DataTransfer harness supportslate-browser; broader type/lint remains for
closeoutslate-browser typecheckslate-browser without
speculative runtime changes; if not, record drop as exact deferred owner
and move to full focused browser gateStatus: complete.
Actions:
DragEvent with
DataTransfer and target coordinatesdropHtml scenario step to slate-browserdrop kernel eventinsert-data command traceapplyEditableDrop(...) to return the applied insert-data commandEditable drop handling to record that command in the kernel traceRed probe:
bunx turbo build --filter=./packages/slate-browser --force
bunx playwright test ./playwright/integration/examples/paste-html.test.ts --project=chromium --grep "generated drop data" --workers=1 --retries=0
Initial result:
insert-data command trace assertionGreen evidence:
bunx turbo build --filter=./packages/slate-browser --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx playwright test ./playwright/integration/examples/paste-html.test.ts --project=chromium --grep "generated drop data" --workers=1 --retries=0
bunx playwright test ./playwright/integration/examples/paste-html.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "generated drop data" --workers=4 --retries=0
bunx turbo typecheck --filter=./packages/slate-browser --filter=./packages/slate-react --force
Results:
slate-browser + slate-react typecheck: passed after fixing the
helper to convert fallback HTML text through the page/frame surface instead
of a locatorOwner classification:
DataTransfer drop, not OS-native
drag proofRejected tactics:
DataTransfer row proves all OS-native drag
behaviorCheckpoint:
Status: complete.
Actions:
Evidence:
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "selection|caret|toolbar|bold|heading|arrow|backspace|delete" --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/inlines.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/editable-voids.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/shadow-dom.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/paste-html.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --workers=4 --retries=0
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --workers=4 --retries=0
Results:
Owner classification:
test:integration-local, lint, build/type, and perf gates remain
outstanding before completionCheckpoint:
DataTransfer drop proofbun test:integration-localStatus: complete.
Actions:
slate-browser and slate-react typechecksEvidence:
bun test ./packages/slate-react/test/editing-kernel-contract.ts --bail 1
bun test ./packages/slate-react/test/dom-repair-policy-contract.ts --bail 1
bun test ./packages/slate-react/test/selection-controller-contract.ts --bail 1
bunx turbo build --filter=./packages/slate-browser --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-browser --filter=./packages/slate-react --force
Results:
slate-browser + slate-react typecheck: passedOwner classification:
Checkpoint:
bun test:integration-localbun test:integration-localStatus: done.
Actions:
--concurrency=1 after the first
parallel probe exposed a slate dist rebuild race in slate-domEvidence:
bun test:integration-local
bun run lint:fix
bun run lint
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
bun run bench:core:observation:compare:local
bun run bench:core:huge-document:compare:local
bunx turbo build --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react --filter=./packages/slate-browser --force
cd ./packages/slate && bun run build
bunx turbo typecheck --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react --filter=./packages/slate-browser --force --concurrency=1
Results:
selectAllMs effectively tiedpackages/slate local build: passed--concurrency=1: passedFailed probe:
bunx turbo typecheck --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react --filter=./packages/slate-browser --force
Result:
slate-dom with Cannot find module 'slate'packages/slate-dom typecheck passed--concurrency=1Decision:
Accepted/narrow limitations:
DragEvent +
DataTransfer; OS-native drag from the desktop is not claimedCompletion classification:
slate-browserCheckpoint:
bun test:integration-local--concurrency=1Status: active.
Why this addendum exists:
Source-backed findings:
Editable avoided some bugs by keeping selectionchange flush,
beforeinput flush, composition guards, keydown movement, and repair timing in
one mental modelDecision:
editor.read, editor.update,
flexible primitives, operations as collaboration truth, commits as local
runtime truthEditableEditableEventFrame / epoch layer before more local cursor patchesNext execution owner:
select word -> toolbar bold on -> toolbar bold off -> ArrowLeft/Right -> repeat without reload -> follow-up typeEarliest gates:
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "warm|no-refresh|event frame|stale repair|toolbar|arrow" --workers=1 --retries=0 --repeat-each=10
bun test ./packages/slate-react/test/dom-repair-policy-contract.ts --bail 1
bun test ./packages/slate-react/test/editing-kernel-contract.ts --bail 1
bun test ./packages/slate-react/test/selection-controller-contract.ts --bail 1
bunx turbo build --filter=./packages/slate-browser --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-browser --filter=./packages/slate-react --force
Reopened completion criteria:
Harsh take:
Progress sync:
Remaining Architecture PlanActions:
createSlateBrowserWarmToolbarArrowGauntlet in
/Users/zbeyens/git/slate-v2/packages/slate-browser/src/playwright/index.ts/Users/zbeyens/git/slate-v2/playwright/integration/examples/richtext.test.ts
for select editable -> toolbar bold on -> bold off -> ArrowRight -> ArrowLeft -> ArrowRight -> typeeditable into [0,1]EditableEventFrame storage and frame ids to kernel traces in
/Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/editing-kernel.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable.tsxDOMRepairFrameState plus beginFrame / cancelBefore on
/Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/dom-repair-queue.tsEvidence:
bun test ./packages/slate-react/test/editing-kernel-contract.ts --bail 1
# 4 pass, 0 fail
bun test ./packages/slate-react/test/dom-repair-policy-contract.ts --bail 1
# 3 pass, 0 fail
bun test ./packages/slate-react/test/editing-kernel-contract.ts ./packages/slate-react/test/dom-repair-policy-contract.ts --bail 1
# 7 pass, 0 fail
bunx turbo build --filter=./packages/slate-react --filter=./packages/slate-browser --force
# 2 successful, 2 total
bunx turbo typecheck --filter=./packages/slate-react --filter=./packages/slate-browser --force
# 4 successful, 4 total
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "keeps warm toolbar mark selection usable through arrows without reload" --workers=1 --retries=0
# 1 passed
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "keeps warm toolbar mark selection usable through arrows without reload" --workers=1 --retries=0 --repeat-each=10
# 10 passed
Owner classification:
Rejected tactics:
Checkpoint:
bun test ./packages/slate-react/test/editing-kernel-contract.ts --bail 1bun test ./packages/slate-react/test/dom-repair-policy-contract.ts --bail 1Actions:
SelectionChangeOrigin to the input/controller stateselectionChangeOrigin to editable kernel trace entries and
slate-browser trace typesrepair-inducedbrowser-handleprogrammatic-exportselectionchange traces as native-userunknown for non-selection event familiesbrowser-handle origin for semantic insert/delete/follow-up command tracesEvidence:
bun test ./packages/slate-react/test/editing-kernel-contract.ts ./packages/slate-react/test/dom-repair-policy-contract.ts ./packages/slate-react/test/selection-controller-contract.ts --bail 1
# 10 pass, 0 fail
bunx turbo build --filter=./packages/slate-react --filter=./packages/slate-browser --force
# 2 successful, 2 total
bunx turbo typecheck --filter=./packages/slate-react --filter=./packages/slate-browser --force
# 4 successful, 4 total
bun run lint:fix
# passed after replacing assignment-in-condition in the DOM text walker
bun run lint
# passed
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "keeps warm toolbar mark selection usable through arrows without reload" --workers=1 --retries=0 --repeat-each=10
# 10 passed
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=mobile --grep "runs generated mobile semantic editing conformance gauntlet" --workers=1 --retries=0
# 1 passed
Owner classification:
Rejected tactics:
editor.update shapeCheckpoint:
Actions:
createWarmTimingWaitStep: zero-delay macrotask, animation frame, then the
existing timeout[0,1]@8 instead of importing the DOM/native caret to
[0,1]@7Evidence:
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "keeps warm toolbar mark selection usable through arrows without reload" --workers=1 --retries=0
# chromium, firefox, webkit passed; mobile row returns early because native
# keyboard-arrow transport is not claimed there
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=mobile --grep "runs generated mobile semantic editing conformance gauntlet" --workers=1 --retries=0
# 1 passed
bun test ./packages/slate-react/test/editing-kernel-contract.ts ./packages/slate-react/test/dom-repair-policy-contract.ts ./packages/slate-react/test/selection-controller-contract.ts --bail 1
# 10 pass, 0 fail
bunx turbo build --filter=./packages/slate-react --filter=./packages/slate-browser --force
# 2 successful, 2 total
bunx turbo typecheck --filter=./packages/slate-react --filter=./packages/slate-browser --force
# 4 successful, 4 total
bun run lint:fix
# passed
bun run lint
# passed
Rejected tactic:
Checkpoint:
bun test:integration-localbun test:integration-local; if green, update remaining
Batch D/E/F owner list; if red, classify the first failure by frame/origin
owner before patchingActions:
EditableMovementOwnershipTrace to editable kernel tracesmodel-horizontal-inline-void-compatnative-vertical-layoutslate-browser
assertKernelTraceEvidence:
bun test ./packages/slate-react/test/editing-kernel-contract.ts --bail 1
# 7 pass, 0 fail
bunx turbo build --filter=./packages/slate-react --filter=./packages/slate-browser --force
# 2 successful, 2 total
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "keeps ArrowDown then ArrowRight|keeps warm toolbar mark selection usable" --workers=1 --retries=0
# 2 passed
bun test ./packages/slate-react/test/editing-kernel-contract.ts ./packages/slate-react/test/dom-repair-policy-contract.ts ./packages/slate-react/test/selection-controller-contract.ts --bail 1
# 12 pass, 0 fail
bunx turbo typecheck --filter=./packages/slate-react --filter=./packages/slate-browser --force
# 4 successful, 4 total
bun run lint:fix
# passed; no fixes applied
bun run lint
# passed
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "keeps warm toolbar mark selection usable through arrows without reload" --workers=1 --retries=0
# 4 passed
bun test:integration-local
# 516 passed
Failed probe:
movement field; rebuilding
slate-react and slate-browser fixed the proof. This reinforces the
build-before-browser rule for package trace shape changes.Owner classification:
Rejected tactics:
Checkpoint:
Actions:
model-word-boundary-compatmodel-line-browser-compatmodel-horizontal-inline-void-compatEvidence:
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=webkit --grep "word movement|line extension" --workers=1 --retries=0
# 4 passed
bunx playwright test ./playwright/integration/examples/inlines.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "arrow keys skip over read-only inline" --workers=1 --retries=0
# 4 passed
bun run lint:fix
# passed; no fixes applied
bun test ./packages/slate-react/test/editing-kernel-contract.ts ./packages/slate-react/test/dom-repair-policy-contract.ts ./packages/slate-react/test/selection-controller-contract.ts --bail 1
# 12 pass, 0 fail
bun run lint
# passed
bunx turbo typecheck --filter=./packages/slate-react --filter=./packages/slate-browser --force
# 4 successful, 4 total
bun test:integration-local
# 516 passed
Owner classification:
Checkpoint:
slate-browser scenario execution and convert the toolbar warm row to use
that generated loop instead of a single hand-expanded sequenceActions:
selection-controller contract for model-owned
selectionchange finalization:
selectionchange finalization in Editable to call the
shared selection-controller helper instead of always clearing origin stateselectionChangeOrigin after a model-owned handle commandslate-dom bundled declaration output by using import('slate').X
exported signature types where tsdown aliases Slate symbols to avoid DOM
globalsEvidence:
bun test ./packages/slate-react/test/selection-controller-contract.ts --bail 1
# 10 pass, 0 fail
bun test ./packages/slate-react/test/editing-kernel-contract.ts ./packages/slate-react/test/dom-repair-policy-contract.ts --bail 1
# 10 pass, 0 fail
bun test ./packages/slate/test/primitive-method-runtime-contract.ts ./packages/slate-react/test/selection-controller-contract.ts ./packages/slate-react/test/editing-kernel-contract.ts ./packages/slate-react/test/dom-repair-policy-contract.ts --bail 1
# 38 pass, 0 fail
bun run --cwd packages/slate-browser test:core
# 14 pass, 0 fail
bunx playwright test ./playwright/integration/examples/review-comments.test.ts --project=firefox --workers=1 --retries=0
# 1 passed
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "records kernel policies for browser command and repair traces|runs generated mobile semantic editing conformance gauntlet|records core command metadata for text input and delete|runs a traced slate-browser scenario" --workers=1 --retries=0
# 16 passed
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "keeps warm toolbar mark selection usable through arrows without reload" --workers=1 --retries=0
# 4 passed
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "keeps warm toolbar mark selection usable through arrows without reload" --workers=1 --retries=0 --repeat-each=10
# 10 passed
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=webkit --grep "word movement|line extension" --workers=1 --retries=0
# 4 passed
bunx playwright test ./playwright/integration/examples/inlines.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "arrow keys skip over read-only inline" --workers=1 --retries=0
# 4 passed
bun run lint:fix
# passed; fixed formatting
bun run lint
# passed
bunx turbo build --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react --filter=./packages/slate-browser --force
# 4 successful, 4 total
bun run typecheck:packages
# 12 successful, 12 total
bun test:integration-local
# 516 passed
bun run bench:react:rerender-breadth:local
# passed; selection/edit breadth stayed at zero broad/sibling rerenders in the key rows
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
# passed; v2 kept large-document wins against legacy chunk-off and chunk-on on the tracked means except middle-block typing/select-then-type vs legacy chunk-on
Failed probes:
bunx turbo typecheck --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react --filter=./packages/slate-browser --force
hit a stale/concurrent package-resolution failure where slate-dom could not
resolve slate; serial package typecheck exposed the real declaration issue.SlateBaseEditor, SlateEditor,
SlateAncestor), but tsdown did not emit those alias imports in
slate-dom/dist/index.d.ts.BaseEditor$1 / Editor$1 / Ancestor$1 in the bundle and still left
exported signatures unrewritten.import('slate').X in exported source signatures,
which survived bundling and passed serial package typecheck.Owner classification:
Checkpoint:
slate-browserslate-browser and richtext proof
rowsActions:
warmLoop and iteration metadata to generated slate-browser
scenario stepsslate-browser press() so it preserves an existing usable DOM
selection instead of always focusing and restoring model state firstisInteractiveInternalTarget(...) owns the eventEvidence:
bun run --cwd packages/slate-browser test:core --bail 1
# 15 pass, 0 fail
bunx turbo build --filter=./packages/slate-browser --force
# passed
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "keeps warm toolbar mark selection usable through arrows without reload" --workers=1 --retries=0
# 4 passed
bunx playwright test ./playwright/integration/examples/huge-document.test.ts --project=chromium --project=mobile --workers=2 --retries=0 --repeat-each=3
# failed before widening the 10k route readiness budget; passed 6/6 after the
# readiness budget change
bun test:integration-local
# failed once after DOM-selection mark-click transport exposed that press()
# reselected stale model state before keyboard transport
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "keeps browser caret valid after marking selected text then clicking elsewhere|runs generated mark typing gauntlet" --workers=1 --retries=0
# 2 passed after the press() focus-discipline fix
bunx playwright test ./playwright/integration/examples/highlighted-text.test.ts --project=chromium --project=firefox --project=webkit --grep "runs generated mark-click gauntlet" --workers=1 --retries=0
# 3 passed
bunx playwright test ./playwright/integration/examples/highlighted-text.test.ts ./playwright/integration/examples/shadow-dom.test.ts ./playwright/integration/examples/editable-voids.test.ts --project=chromium --project=firefox --project=webkit --grep "projected text movement|shadow DOM ArrowLeft movement|ArrowLeft inside editable void input" --workers=1 --retries=0
# 9 passed
bun test ./packages/slate-react/test/editing-kernel-contract.ts ./packages/slate-react/test/selection-controller-contract.ts ./packages/slate-react/test/dom-repair-policy-contract.ts --bail 1
# 20 pass, 0 fail
bun run typecheck:packages
# 12 successful, 12 total
bun run lint:fix
# passed; fixed formatting
bun run lint
# passed
bun test:integration-local
# 528 passed
bun run bench:react:rerender-breadth:local
# passed; key rows stayed at zero broad/sibling rerenders
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
# passed; v2 preserved large-document wins against legacy chunk-off/chunk-on on
# the tracked means except middle-block typing and middle-block select-then-type
# vs legacy chunk-on
bun run bench:core:observation:compare:local
# passed; current means beat legacy means on all tracked rows
bun run bench:core:huge-document:compare:local
# passed; current means beat legacy on all tracked rows except selectAllMs
# where both are near-zero and legacy is 0.02ms faster locally
Failed probes:
move-selection command for
an internal-control targetControlOrMeta+b as a mark-click fix was rejected; the owner was harness
focus discipline, not shortcut namingOwner classification:
Checkpoint: