docs/plans/2026-04-22-slate-v2-core-api-runtime-perfection-plan.md
Finish the remaining Slate v2 architecture debt after browser parity closure.
Browser parity is no longer the blocker:
playwright/integration/examples/** skip inventory is empty.bun test:integration-local exits green with 255 passed, 5 flaky.docs/plans/2026-04-22-slate-v2-browser-parity-skip-burndown-plan.md.The remaining lane is deeper:
The final shape is:
slateslate-browser as mandatory browser proofEditable as the semantic-blocks runtimedecorate, child-count chunking, or
mutable instance fields as the architectureslate-react perf superiority is closed for important 5000-block
lanes, with first shelled-block activation accepted as the occlusion tradeoff.Editable is the semantic runtime in current examples.bun test:integration-local is green but still reports flakies.slate-browser handles
instead of raw native key/clipboard transport.EditableRootdecorate prop and compatibility adaptercreateSlateDecorationSourceeditor.children, editor.selection, editor.marks,
editor.operationseditor.apply / editor.onChangeEditor.getSnapshot() or mutable
fields in urgent pathsThis plan is complete only when:
bun test:integration-local is green without flaky rows, or every remaining
flaky row is moved to an explicitly non-blocking flake quarantine with exact
owner and retry policy.Editor.getSnapshot().Editor.*EditabledecoraterenderChunkapply / onChange as normal extension pointsplaywright/integration/examples/** skip inventory stays empty.Allowed code work:
.tmp/slate-v2/packages/slate/**.tmp/slate-v2/packages/slate-dom/**.tmp/slate-v2/packages/slate-browser/**.tmp/slate-v2/packages/slate-react/**.tmp/slate-v2/site/examples/ts/**.tmp/slate-v2/playwright/integration/examples/**.tmp/slate-v2/scripts/benchmarks/**.tmp/slate-v2/package.jsonAvoid unless focused proof proves ownership:
.tmp/slate-v2/packages/slate-history/**.tmp/slate-v2/packages/slate-hyperscript/**Allowed docs:
docs/plans/**docs/slate-v2/**docs/research/**docs/solutions/**active goal state only when starting/executing the lanePurpose:
Actions:
Commands:
rg -n "test\\.skip|\\.skip\\(|skip\\(" playwright/integration/examples -g "*.ts" -g "*.tsx" || true
bun test:integration-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
bun run bench:core:normalization:compare:local
rg -n "decorate|renderChunk|getChunkSize|EditableRoot|editor\\.children|editor\\.selection|editor\\.marks|editor\\.operations|editor\\.apply|editor\\.onChange|Editor\\.getSnapshot|createSlateDecorationSource" packages/slate packages/slate-react packages/slate-dom packages/slate-browser site/examples/ts playwright/integration/examples scripts/benchmarks package.json -g "*.ts" -g "*.tsx" -g "*.json"
Acceptance:
active goal state remains done until execution actually starts.Status: in progress.
Actions:
active goal state to pendingNext action:
Actions:
active goal state to status: pendingCommands:
rg -n "test\\.skip|\\.skip\\(|skip\\(" playwright/integration/examples -g "*.ts" -g "*.tsx" || true
rg -n "decorate|renderChunk|getChunkSize|EditableRoot|editor\\.children|editor\\.selection|editor\\.marks|editor\\.operations|editor\\.apply|editor\\.onChange|Editor\\.getSnapshot|createSlateDecorationSource" packages/slate packages/slate-react packages/slate-dom packages/slate-browser site/examples/ts playwright/integration/examples scripts/benchmarks package.json -g "*.ts" -g "*.tsx" -g "*.json"
bun run lint
bun completion-check
bun test:integration-local
bun run bench:core:observation:compare:local
bun run bench:core:huge-document:compare:local
bun run bench:core:normalization:compare:local
Evidence:
bun test:integration-local: green with 258 passed, 2 flakyreadChildrenLengthAfterEachMs mean 7.31ms vs legacy
2.8msnodesAtRootAfterEachMs mean 18.82ms vs legacy 36.61mspositionsFirstBlockAfterEachMs even though
mean is close due legacy variance17.18ms vs legacy 1.72ms10.17ms vs legacy 2.72ms7.71ms vs legacy
114.95ms6.57ms vs legacy 22.98ms0.01ms vs legacy 0.02ms10.4ms vs legacy
28.11ms899.84ms vs legacy
1069.13msinsertTextReadAfterEachMs mean 9.45ms vs legacy 1.83msOwner classification:
decorate,
EditableRoot, mutable editor fields, instance apply/onChange, and
snapshot reads remain across product/tests/examples/benchmarksDecision:
Next action:
continue and either
enter Phase 1 flake fix or replan if React compare changes ownership.Command:
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
Evidence:
12.68ms vs legacy chunk-off 281.33ms and chunk-on
293.39ms.19.5ms vs chunk-off 155.82ms and chunk-on
46.9ms.16.01ms vs chunk-off 162.32ms and chunk-on
38.63ms.26.26ms vs chunk-off 178.76ms and chunk-on
39.28ms.26.15ms vs chunk-off 112.92ms and chunk-on
121.64ms.22.56ms vs chunk-off 118.08ms and chunk-on
119.6ms.2.61ms vs chunk-on 0.8ms).Decision:
Verdict: keep course.
Harsh take: Phase 0 did its job; the only reproducible integration flake is huge-document readiness, so fix that before touching API or core perf.
Why:
Risks:
Earliest gates:
bunx playwright test ./playwright/integration/examples/huge-document.test.ts --project=chromium --project=mobilebun test:integration-local reports no flaky rows after fixNext move:
huge-document.test.ts and replace route-level waiting with an
explicit editor/runtime readiness conditionDo-not-do list:
Actions:
domcontentloaded
navigation waiting with fast commit navigation plus explicit control/editor
readiness assertions60s because this is intentionally the
heavyweight routeFiles changed:
.tmp/slate-v2/playwright/integration/examples/huge-document.test.tsCommands:
bunx playwright test ./playwright/integration/examples/huge-document.test.ts --project=chromium --project=mobile
bun test:integration-local
bun run lint
Evidence:
24.1s24.2s260 passedHypothesis tested:
Decision:
Owner classification:
Verdict: pivot.
Harsh take: flake debt is now zero; native transport debt is the next owner.
Why:
bun test:integration-local reports 260 passedRisks:
Earliest gates:
Next move:
Do-not-do list:
playwright/integration/examples/**Current known flakies:
Hard take:
Work:
playwright/integration/examples/huge-document.test.tsLikely files:
.tmp/slate-v2/playwright/integration/examples/huge-document.test.ts.tmp/slate-v2/playwright.config.ts.tmp/slate-v2/package.json.tmp/slate-v2/scripts/serve-playwright.mjs.tmp/slate-v2/packages/slate-browser/src/playwright/index.tsGates:
bunx playwright test ./playwright/integration/examples/huge-document.test.ts --project=chromium --project=mobile
bunx playwright test ./playwright/integration/examples/highlighted-text.test.ts --project=firefox
bun test:integration-local
Acceptance:
bun test:integration-local reports no flaky rows.Problem:
Known caveat families:
Work:
Likely files:
.tmp/slate-v2/playwright/integration/examples/richtext.test.ts.tmp/slate-v2/playwright/integration/examples/shadow-dom.test.ts.tmp/slate-v2/playwright/integration/examples/large-document-runtime.test.ts.tmp/slate-v2/packages/slate-browser/src/playwright/index.ts.tmp/slate-v2/packages/slate-dom/src/**.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/hooks/android-input-manager/**Gates:
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --project=firefox --project=webkit --project=mobile --grep "browser|undo|selected"
bunx playwright test ./playwright/integration/examples/shadow-dom.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 "paste|typing|composition"
Acceptance:
Actions:
playwright/integration/examples/**Command:
rg -n "insertTextWithHandle|insertBreakWithHandle|deleteFragmentWithHandle|selectWithHandle|undoWithHandle|redoWithHandle|deleteBackwardWithHandle|deleteForwardWithHandle|dispatchPaste|pasteSlateFragment|editor\\.insertText|editor\\.insertBreak|editor\\.delete|editor\\.selection|browserName === 'firefox'|testInfo\\.project\\.name === 'mobile'|keyboard\\.insertText|keyboard\\.press|pressSequentially|root\\.press|clipboard" playwright/integration/examples -g "*.ts"
Classifications:
markdown-shortcuts.test.ts uses semantic editor actions outside Chromium
for shortcut behavior.markdown-preview.test.ts uses semantic insert/break actions.highlighted-text.test.ts uses semantic insert on mobile and clipboard
event fallback.shadow-dom.test.ts uses semantic handle paths for WebKit/mobile nested
shadow editing.large-document-runtime.test.ts uses semantic handle/event paths for
direct-sync, paste, and some mobile rows.richtext.test.ts uses semantic selection/insert/undo for Firefox/mobile
paths where native target ranges or undo transport are not portable.page.keyboard.insertText(...)pressSequentially.Decision:
Owner classification:
Next action:
Verdict: pivot.
Harsh take: native transport is classified, not fully fixed; the next real runtime owner is mobile text-only DOM reconciliation.
Why:
Risks:
Earliest gates:
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=mobile --grep "DOM-owned text sync|directly synced"bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1Next move:
Do-not-do list:
Problem:
Hard take:
Work:
Likely files:
.tmp/slate-v2/packages/slate-react/src/components/editable-text.tsx.tmp/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx.tmp/slate-v2/packages/slate-react/src/components/text.tsx.tmp/slate-v2/packages/slate-react/src/components/leaf.tsx.tmp/slate-v2/packages/slate-react/src/components/element.tsx.tmp/slate-v2/packages/slate-react/src/components/slate.tsx.tmp/slate-v2/packages/slate-react/src/hooks/android-input-manager/**.tmp/slate-v2/playwright/integration/examples/large-document-runtime.test.tsGates:
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=mobile --grep "DOM-owned text sync|directly synced"
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
Acceptance:
Status: closed.
Actions:
large-document-runtime.test.ts so mobile DOM-owned text,
custom text, custom leaf, projection opt-out, undo, and redo rows assert
visible DOM text, not only handle/model textinsertText force a render only when the edited path did
not direct-sync to DOMselectRange force-render; shell-backed
selection state now owns the render when it actually changesChanged files:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/hooks/android-input-manager/android-input-manager.ts.tmp/slate-v2/packages/slate-react/src/hooks/use-slate-node-ref.tsx.tmp/slate-v2/playwright/integration/examples/large-document-runtime.test.tsRoot cause:
Editor.insertText updated
the model and syncTextOperationsToDOM read the live text, but Android
mutation restoration treated the programmatic DOM-owned text update as
untrusted DOM drift and forced React to restore stale text.Rejected tactics:
forceRender() for text ops is rejected because it can
erase DOM-owned textCommands:
bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=mobile --grep "keeps DOM-owned text sync explicit"
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=mobile --grep "DOM-owned text sync|directly synced"
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts
bun run lint:fix
bun run lint
bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react --force
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
Evidence:
5 passeddom-text-sync-contract.ts: 1 passlarge-doc-and-scroll.tsx: 15 passprojections-and-selection-contract.tsx: 6 passlarge-document-runtime.test.ts: 56 passed across Chromium,
Firefox, WebKit, and mobilelint:fixslate-dom and slate-reactslate-dom and slate-react00011.42ms vs legacy chunk-off 271.13ms and chunk-on
309.29ms21.26ms vs chunk-off 164.81ms and chunk-on
32.92ms14.54ms vs chunk-off 166.26ms and chunk-on
39.00ms23.91ms vs chunk-off 182.37ms and
chunk-on 31.86ms18.71ms vs chunk-off 121.33ms and
chunk-on 111.26ms26.23ms vs chunk-off 118.23ms and chunk-on
117.24msDecision:
Verdict: pivot.
Harsh take: the browser runtime is no longer hiding a mobile stale-DOM row; the remaining debt is core runtime cost, not another React/browser correctness patch.
Why:
Risks:
Earliest gates:
bun test ./packages/slate/test/transaction-contract.ts --bail 1bun test ./packages/slate/test/snapshot-contract.ts --bail 1bun run bench:core:observation:compare:localbun run bench:core:huge-document:compare:localbun run bench:core:normalization:compare:localNext move:
Do-not-do list:
slate-history unless a focused core proof proves history
ownershipProblem:
Sub-lanes:
Work:
Likely files:
.tmp/slate-v2/packages/slate/src/core/apply.ts.tmp/slate-v2/packages/slate/src/core/public-state.ts.tmp/slate-v2/packages/slate/src/core/transaction.tsGates:
bun test ./packages/slate/test/transaction-contract.ts --bail 1
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun run bench:core:observation:compare:local
bun run bench:core:huge-document:compare:local
Work:
Likely files:
.tmp/slate-v2/packages/slate/src/core/public-state.ts.tmp/slate-v2/packages/slate/src/utils/runtime-ids.ts.tmp/slate-v2/packages/slate-react/src/projection-store.ts.tmp/slate-v2/packages/slate-react/src/widget-store.tsGates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bun run bench:react:rerender-breadth:local
bun run bench:react:huge-document-overlays:local
Work:
Likely files:
.tmp/slate-v2/packages/slate/src/utils/runtime-ids.ts.tmp/slate-v2/packages/slate/src/core/public-state.ts.tmp/slate-v2/packages/slate/src/interfaces/editor.tsGates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun run bench:core:huge-document:compare:local
bun run bench:core:observation:compare:local
Work:
editor.childreneditor.selectioneditor.markseditor.operationsEditor.* runtime state is sufficient.Likely files:
.tmp/slate-v2/packages/slate/src/create-editor.ts.tmp/slate-v2/packages/slate/src/interfaces/editor.ts.tmp/slate-v2/packages/slate/src/core/public-state.ts.tmp/slate-v2/packages/slate-react/src/components/slate.tsxGates:
bun test ./packages/slate/test/accessor-transaction.test --bail 1
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun run bench:core:observation:compare:local
Work:
Likely files:
.tmp/slate-v2/packages/slate/src/core/public-state.ts.tmp/slate-v2/packages/slate/src/core/snapshot.ts.tmp/slate-v2/packages/slate/src/utils/runtime-ids.tsGates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun run bench:core:huge-document:compare:local
bun run bench:core:observation:compare: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
Final core perf gates:
bun run bench:slate:6038:local
bun run bench:core:normalization:compare:local
bun run bench:core:observation:compare:local
bun run bench:core:huge-document:compare:local
Acceptance:
Status: closed for the active core microbench owner.
Actions:
insert_text /
remove_text passes when the dirty set exactly matches the text operationChanged files:
.tmp/slate-v2/packages/slate/src/core/apply.ts.tmp/slate-v2/packages/slate/src/editor/normalize.tsRoot cause:
Rejected tactics:
normalizeNode still falls back
out of the default text fast pathCommands:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun test ./packages/slate/test/transaction-contract.ts --bail 1
bun run bench:core:observation:compare:local
bun run bench:core:huge-document:compare:local
bun run bench:core:normalization:compare:local
CORE_HUGE_BENCH_ITERATIONS=5 bun run bench:core:huge-document:compare:local
bun run bench:slate:6038:local
bun run lint:fix
bunx turbo build --filter=./packages/slate --force
bun run lint
bunx turbo typecheck --filter=./packages/slate --force
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
Evidence:
snapshot-contract.ts: 190 passtransaction-contract.ts: 13 passsurface-contract.ts: 10 passbench:core:observation:compare:local: current wins all lanes
0.75ms vs legacy 1.57ms8.50ms vs legacy 11.10ms1.34ms vs legacy 2.33msbench:core:normalization:compare:local: current wins all lanes
10.05ms vs legacy 15.28ms239.11ms vs legacy 425.40ms0.94ms vs legacy 1.49msCORE_HUGE_BENCH_ITERATIONS=5 bench:core:huge-document:compare:local:
current wins all lanes
0.52ms vs legacy 1.50ms0.27ms vs legacy 0.98ms3.70ms vs legacy 15.03ms3.14ms vs legacy 13.30ms0.01ms vs legacy 0.02msbench:slate:6038:local: withTransactionMeanMs 0.113,
applyBatchMeanMs 0.119packages/slate2.23ms v2 vs
1.00ms chunk-on) while median is green (0.16ms v2 vs 1.05ms
chunk-on). This is classified as a React guardrail noise/wash row, not a core
perf owner.Decision:
Verdict: pivot.
Harsh take: core typing/observation is no longer the excuse. The remaining architecture debt is public surface cleanup: legacy APIs still invite users and agents into the wrong runtime.
Why:
Risks:
decorate too bluntly can erase projection compatibility evidence
instead of moving it to explicit compatEditor.* accessors are fully used internallyEarliest gates:
rg -n "decorate|renderChunk|getChunkSize|EditableRoot|editor\\.children|editor\\.selection|editor\\.marks|editor\\.operations|editor\\.apply|editor\\.onChange|Editor\\.getSnapshot|createSlateDecorationSource" packages/slate packages/slate-react packages/slate-dom packages/slate-browser site/examples/ts playwright/integration/examples scripts/benchmarks package.json -g "*.ts" -g "*.tsx" -g "*.json"bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1bun run bench:react:huge-document-overlays:localbun run lintNext move:
decorate / createSlateDecorationSource
public surface and moving legacy decoration compatibility out of the primary
Editable story without breaking projection-source proofsDo-not-do list:
decorateEditableRoot looking public after the hard cutProblem:
Hard cuts / demotions:
decorateWork:
decorate from primary Editable public API.createSlateDecorationSource and any decorate bridge behind explicit
compat naming/package or private migration layer.packages/slate-react/test/decorations.test.tsx from primary
behavior to compat-only tests or replace with projection source tests.Likely files:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/hooks/use-decorations.ts.tmp/slate-v2/packages/slate-react/src/projection-store.ts.tmp/slate-v2/packages/slate-react/src/index.ts.tmp/slate-v2/packages/slate-react/test/decorations.test.tsxdocs/slate-v2/**Gates:
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bun run bench:react:huge-document-overlays:local
bun run lint
Status: closed for public adapter naming.
Actions:
createSlateDecorationSource from the public slate-react exportcreateSlateDecorateCompatSourceSlateDecorateCompatSlateDecorateCompatDataSlateDecorateCompatSourceOptionsuseDecorations bridge to use the compat nameChanged files:
.tmp/slate-v2/packages/slate-react/src/projection-store.ts.tmp/slate-v2/packages/slate-react/src/hooks/use-decorations.ts.tmp/slate-v2/packages/slate-react/src/index.ts.tmp/slate-v2/packages/slate-react/test/projections-and-selection-contract.tsx.tmp/slate-v2/.changeset/decorate-compat-source-name.mdRejected tactics:
decorate naming in the public adapter exportEditableRoot decoration tests in this slice; that
belongs to the old surface cleanup ownerCommands:
rg -n "createSlateDecorationSource|SlateDecorate\\b|SlateDecorationData\\b|DecorationSourceOptions" packages/slate-react/src packages/slate-react/test site/examples/ts playwright/integration/examples
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
cd packages/slate-react && bunx vitest run --config ./vitest.config.mjs test/decorations.test.tsx
bun run lint:fix
bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force
bun run lint
bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react --force
bun run bench:react:huge-document-overlays:local
Evidence:
6 pass10 passDecision:
decorate as the primary
story.decorate references are internal legacy EditableRoot and tests;
they move to Phase 5.4 old surface cleanup.Verdict: keep course.
Harsh take: renaming the public adapter is only the first cut. The real
remaining smell is that EditableRoot still looks like a usable legacy editor
surface in source and tests.
Why:
Editable is already semantic EditableTextBlocksdecorate, old Children, and old
decoration testsRisks:
EditableRoot too quickly can break EditableTextBlocks because it
still uses the root shell for browser event ownershipEarliest gates:
rg -n "EditableRoot|decorate\\?|<Editable decorate|from '../src/components/editable'" packages/slate-react/src packages/slate-react/test -g "*.ts" -g "*.tsx"bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1Next move:
EditableRoot to an internal root
shell and removing legacy decorate tests that assert dead public behaviorDo-not-do list:
EditableEditableRoot as an exported-looking symbolEditable decorate compatibilityWork:
editor.childreneditor.selectioneditor.markseditor.operationsEditor.getChildrenEditor.getLiveSelectionEditor.getMarksEditor.getOperationsEditor.*.Likely files:
.tmp/slate-v2/packages/slate/src/**.tmp/slate-v2/packages/slate-dom/src/**.tmp/slate-v2/packages/slate-react/src/**.tmp/slate-v2/site/examples/ts/**Gates:
bun test ./packages/slate/test/accessor-transaction.test --bail 1
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun test ./packages/slate-react/test/provider-hooks-contract.tsx --bail 1
bun run lint
Status: in progress.
Actions:
Command:
rg -n "editor\\.children|editor\\.selection|editor\\.marks|editor\\.operations|editor\\.apply|editor\\.onChange" packages/slate packages/slate-dom packages/slate-react site/examples/ts playwright/integration/examples scripts/benchmarks -g "*.ts" -g "*.tsx" -g "*.mjs"
Owner split:
packages/slate-react/src/components/slate.tsx still calls
onChange?.(editor.children), checks editor.operations, and forwards
editor.selection.packages/slate-react/src/components/editable.tsx reads/writes
editor.selection and editor.marks heavily in browser input paths.packages/slate-react/src/hooks/android-input-manager/android-input-manager.ts
reads/writes editor.selection, editor.marks, and calls
editor.onChange().packages/slate-dom/src/plugin/dom-editor.ts reads editor.selection for
DOM selection sync.packages/slate/src/core/public-state.ts still initializes and publishes
compatibility mirrors and calls editor.onChange().packages/slate/src/transforms-node/lift-nodes.ts and
packages/slate/src/transforms-text/delete-text.ts use editor.apply as a
fallback when no transaction writer exists.site/examples/ts/huge-document.tsx monkeypatches editor.apply.Decision:
slate-react and slate-dom, starting with read paths that have direct
Editor.* equivalents.Editor.*.Verdict: replan.
Harsh take: mutable field cleanup is not one owner. A blind hard cut would break browser selection and extension contracts. The right move is product-internal read/write migration first, then public mirror demotion.
Why:
Risks:
Earliest gates:
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1bun test ./packages/slate/test/surface-contract.ts --bail 1bun test ./packages/slate/test/transaction-contract.ts --bail 1Next move:
packages/slate-react/src/components/slate.tsx from
editor.children, editor.operations, and editor.selection to
Editor.getChildren, Editor.getOperations, and Editor.getLiveSelection
as the first low-risk product-internal accessor cutDo-not-do list:
Status: closed for components/slate.tsx.
Actions:
Slate provider callbacks to use the current operation slice instead
of editor.operationsEditor.getChildren(editor)Editor.getLiveSelection(editor)Changed file:
.tmp/slate-v2/packages/slate-react/src/components/slate.tsxCommands:
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bun run lint:fix
bun run lint
bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react --force
Evidence:
1 pass15 pass6 passDecision:
components/editable.tsx and
hooks/android-input-manager/android-input-manager.ts; that needs
integration proof, not a simple global replace.Verdict: keep course.
Harsh take: Slate provider cleanup was the easy part. The real mutable-field
danger is browser input code, where editor.selection and editor.marks
interact with DOM selection, IME, and Android flushing.
Why:
Risks:
editor.selection in browser paths without preserving live
selection timing can regress typing at block edgeseditor.marks in Android paths can break mark placeholder and IME
behaviorEarliest gates:
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "types at the browser-selected end|repairs DOM after Mac keyboard undo|undoes browser-inserted text"bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=mobile --grep "DOM-owned text sync|directly synced"bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1Next move:
editable.tsx selection reads to
Editor.getLiveSelection(editor) where they are read-only checks, leaving
writes and Android mark mutation for later focused browser proofDo-not-do list:
editor.onChange compatibility until Editor.subscribe
replacement proof existsStatus: closed for read-only editable.tsx selection reads.
Actions:
syncDOMSelectionToEditor to read Editor.getLiveSelection(editor)beforeinput local selection baseline to
Editor.getLiveSelection(editor)Editor.getLiveSelectionEditor.getLiveSelectiondefaultScrollSelectionIntoView backward-selection check to
Editor.getLiveSelectionChanged file:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsxCommands:
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bun run lint:fix
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "types at the browser-selected end|repairs DOM after Mac keyboard undo|undoes browser-inserted text"
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=mobile --grep "DOM-owned text sync|directly synced"
bun run lint
bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react --force
Evidence:
2 passed5 passedNotes:
next build; rerunning the mobile gate alone passed.editable.tsx mutable matches are deliberate selection assignment,
mark reads/writes, and one children read.Verdict: keep course.
Harsh take: selection reads are clean enough. The next safe win is marks and
children reads in editable.tsx; Android mark writes and editor.onChange
stay out of this slice.
Why:
Risks:
editor.children in event handlers must preserve current live
model timingEarliest gates:
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "types at the browser-selected end|repairs DOM after Mac keyboard undo|undoes browser-inserted text"bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=mobile --grep "DOM-owned text sync|directly synced"Next move:
editor.marks and editor.children usages in
editable.tsx to Editor.getMarks(editor) and Editor.getChildren(editor)Do-not-do list:
editor.marks = ...editor.onChangeeditor.selection = ...Status: closed for read-only editable.tsx marks/children reads.
Actions:
editor.marks to
Editor.marks(editor)editor.children / editor.selection to
Editor.getChildren(editor) / Editor.getLiveSelection(editor)Changed file:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsxCommands:
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
cd packages/slate-react && bun test:vitest
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "types at the browser-selected end|repairs DOM after Mac keyboard undo|undoes browser-inserted text"
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=mobile --grep "DOM-owned text sync|directly synced"
bun run lint:fix
bun run lint
bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react --force
Evidence:
11 passed, 42 passed2 passed5 passedNotes:
Editor.getMarks(editor) call; corrected to
the actual static API Editor.marks(editor).next build lock failure
once; rerun alone passed.Verdict: keep course.
Harsh take: editable.tsx is mostly off direct read mirrors now. The remaining
runtime mirror debt is Android manager selection reads, DOM bridge selection
reads, deliberate mark/selection writes, and onChange compatibility.
Why:
editable.tsx selection/marks/children reads migrated cleanlyRisks:
Earliest gates:
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=mobile --grep "DOM-owned text sync|directly synced"bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "types at the browser-selected end|repairs DOM after Mac keyboard undo|undoes browser-inserted text"bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1Next move:
editor.selection usages in
hooks/android-input-manager/android-input-manager.ts to
Editor.getLiveSelection(editor), while leaving editor.marks writes and
editor.onChange() untouchedDo-not-do list:
slate-dom selection bridge until Android manager is greenStatus: closed for Android manager selection reads.
Actions:
android-input-manager.ts read-only editor.selection usages to
Editor.getLiveSelection(editor)editor.marks = ... writes and editor.onChange() compatibility
exactly as-isChanged file:
.tmp/slate-v2/packages/slate-react/src/hooks/android-input-manager/android-input-manager.tsCommands:
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
cd packages/slate-react && bun test:vitest
bunx playwright test ./playwright/integration/examples/large-document-runtime.test.ts --project=mobile --grep "DOM-owned text sync|directly synced"
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "types at the browser-selected end|repairs DOM after Mac keyboard undo|undoes browser-inserted text"
bun run lint:fix
bun run lint
bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react --force
Evidence:
11 passed, 42 passed5 passed2 passedRemaining product-runtime mutable usage:
packages/slate-dom/src/plugin/dom-editor.ts selection reads in DOM focus /
selection syncpackages/slate-react/src/components/editable.tsx deliberate selection
assignment and mark writespackages/slate-react/src/hooks/android-input-manager/android-input-manager.ts
deliberate mark writes and editor.onChange() compatibilitypackages/slate-react/src/components/slate.tsx editor.onChange wrappingVerdict: keep course.
Harsh take: React-side read mirrors are mostly gone. The next product runtime
owner is slate-dom selection sync; after that, only deliberate write/compat
paths remain.
Why:
Risks:
Earliest gates:
bun test ./packages/slate-dom/test/bridge.ts --bail 1bunx playwright test ./playwright/integration/examples/shadow-dom.test.ts --project=chromiumbunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "types at the browser-selected end"Next move:
editor.selection usages in
packages/slate-dom/src/plugin/dom-editor.ts to Editor.getLiveSelectionDo-not-do list:
editor.onChange compat wrappingStatus: closed for slate-dom focus/selection read mirrors.
Actions:
packages/slate-dom/src/plugin/dom-editor.ts focus/selection sync
reads from editor.selection to Editor.getLiveSelection(editor)Changed file:
.tmp/slate-v2/packages/slate-dom/src/plugin/dom-editor.tsCommands:
bun test ./packages/slate-dom/test/bridge.ts --bail 1
bun test ./packages/slate-dom/test/clipboard-boundary.ts --bail 1
bun run lint:fix
bun run lint
bunx playwright test ./playwright/integration/examples/shadow-dom.test.ts --project=chromium
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "types at the browser-selected end"
bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react --force
Evidence:
4 pass6 pass3 passed1 passedNotes:
Verdict: pivot.
Harsh take: read-mirror cleanup is done enough for product runtime. The
remaining mutable-field/API debt is deliberate compatibility writes and
extension interception (editor.apply / editor.onChange), not accidental
reads.
Why:
Slate, editable.tsx, Android manager, and DOM bridge read paths now use
explicit accessorsonChange wrapping,
and extension/benchmark/test compatibilityRisks:
apply/onChange before replacing extension contracts
would break real plugin interceptioneditor.apply and teaches the
wrong patternEarliest gates:
bun test ./packages/slate/test/extension-contract.ts --bail 1bun test ./packages/slate/test/operations-contract.ts --bail 1bun test ./packages/slate/test/transaction-contract.ts --bail 1site/examples/ts/huge-document.tsx
changesNext move:
editor.apply
from site/examples/ts/huge-document.tsx or replacing it with an explicit
current API patternDo-not-do list:
editor.apply compatibility while extension tests still
assert itslate-history or slate-hyperscriptStatus: closed for the app/example monkeypatch owner.
Actions:
site/examples/ts/huge-document.tsx editor.apply monkeypatch with
Editor.subscribemove_node selection rebasing bug exposed by the Phase 5.3
operation gates:
Changed files:
.tmp/slate-v2/site/examples/ts/huge-document.tsx.tmp/slate-v2/packages/slate/src/interfaces/transforms/general.tsCommands:
bun test ./packages/slate/test/extension-contract.ts --bail 1
bun test ./packages/slate/test/operations-contract.ts --bail 1
bun test ./packages/slate/test/transaction-contract.ts --bail 1
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun run bench:slate:6038:local
bun run bench:core:observation:compare:local
bun run bench:core:huge-document:compare:local
bun run bench:core:normalization:compare:local
CORE_HUGE_BENCH_ITERATIONS=5 bun run bench:core:huge-document:compare:local
bun test ./packages/slate-dom/test/bridge.ts --bail 1
bun test ./packages/slate-dom/test/clipboard-boundary.ts --bail 1
bunx playwright test ./playwright/integration/examples/shadow-dom.test.ts --project=chromium
bunx playwright test ./playwright/integration/examples/richtext.test.ts --project=chromium --grep "types at the browser-selected end"
bun run lint:fix
bun run lint
bunx turbo build --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate --force
bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx tsc --project site/tsconfig.json --noEmit
Evidence:
6 pass15 pass13 pass190 pass3 passed1 passedbench:slate:6038:local: withTransactionMeanMs 0.103,
applyBatchMeanMs 0.116Notes:
slate/slate-dom/slate-react typecheck hit the known filtered
workspace-resolution race once. Sequential slate typecheck followed by
slate-dom/slate-react typecheck passed.Decision:
editor.apply monkeypatching.editor.apply remains as explicit compatibility/extension surface
because extension and transaction contracts still assert it.Verdict: replan.
Harsh take: the public/example apply smell is gone. The remaining instance
API debt is not removable until a replacement extension/interception contract
exists. The next better owner is docs/public surface and dead legacy exports,
not ripping out editor.apply blindly.
Why:
editor.applyeditor.apply still owns low-level compatibilityeditor.onChange usages are compatibility bridge internalsRisks:
editor.apply without a new interception API breaks existing plugin
patternsEarliest gates:
rg -n "decorate|renderChunk|getChunkSize|editor\\.children|editor\\.selection|editor\\.marks|editor\\.operations|editor\\.apply|editor\\.onChange" docs/slate-v2 docs/research docs/libraries docs/walkthroughs site/examples/ts -g "*.md" -g "*.mdx" -g "*.tsx"bun run lintNext move:
decorate, chunking, mutable fields, or
instance apply/onChange as the primary APIDo-not-do list:
editor.apply compatibility in this laneStatus: in progress.
Actions:
../plate-2/docs/**.tmp/slate-v2/**Commands:
# Wrong repo mix, kept as failed probe:
rg -n "decorate|renderChunk|getChunkSize|editor\\.children|editor\\.selection|editor\\.marks|editor\\.operations|editor\\.apply|editor\\.onChange|EditableRoot|createSlateDecorationSource|createSlateDecorateCompatSource|chunking|chunk" docs/slate-v2 docs/research docs/libraries docs/walkthroughs site/examples/ts packages/slate-react/src/index.ts packages/slate-react/src -g "*.md" -g "*.mdx" -g "*.tsx" -g "*.ts"
# Control docs inventory:
rg -n "decorate|renderChunk|getChunkSize|editor\\.children|editor\\.selection|editor\\.marks|editor\\.operations|editor\\.apply|editor\\.onChange|EditableRoot|createSlateDecorationSource|createSlateDecorateCompatSource|chunking|chunk" docs/slate-v2 docs/research docs/libraries docs/walkthroughs site/examples/ts packages/slate-react/src/index.ts packages/slate-react/src -g "*.md" -g "*.mdx" -g "*.tsx" -g "*.ts"
Evidence:
docs/slate-v2 because it was run in
.tmp/slate-v2 instead of control repo; this is a repo-layout mistake, not a
product failuredocs/research/decisions/slate-v2-data-model-first-react-perfect-runtime.md
still naming createSlateDecorationSourcedocs/slate-v2/references/architecture-contract.md still includes
example editor.apply = ... and legacy field language in reference
sectionsdocs/slate-v2/ledgers/slate-editor-api.md still tracks compatibility
mirrors and should be checked against latest accessor cutsdecorate / chunking
as legacy context; do not rewrite all hits mechanicallyDecision:
docs/research/decisions/slate-v2-data-model-first-react-perfect-runtime.md
and docs/slate-v2/ledgers/slate-editor-api.md.Verdict: keep course.
Harsh take: docs still carry stale names and examples. The code is ahead of the docs; if we stop now, the next agent will relearn bad API shape from stale references.
Why:
createSlateDecorationSource is gone from code but still appears in the
architecture decision docRisks:
Earliest gates:
rg -n "createSlateDecorationSource|EditableRoot|editor\\.apply =|editor\\.children|editor\\.selection|editor\\.marks|editor\\.operations" docs/research/decisions/slate-v2-data-model-first-react-perfect-runtime.md docs/slate-v2/ledgers/slate-editor-api.md docs/slate-v2/references/architecture-contract.mdNext move:
createSlateDecorateCompatSourceeditor.apply as compatibility-only, not example-level monkeypatchingDo-not-do list:
decorate or chunkingStatus: closed for the first latest-state doc slice.
Actions:
createSlateDecorateCompatSourceEditor.subscribe, not editor.apply replacementChanged files:
docs/research/decisions/slate-v2-data-model-first-react-perfect-runtime.mddocs/slate-v2/ledgers/slate-editor-api.mddocs/slate-v2/references/architecture-contract.mdCommands:
rg -n "createSlateDecorationSource|editor\\.apply =" docs/research/decisions/slate-v2-data-model-first-react-perfect-runtime.md docs/slate-v2/ledgers/slate-editor-api.md docs/slate-v2/references/architecture-contract.md
Evidence:
createSlateDecorationSource no longer appears in latest-state target
docseditor.apply = ... remains only as an explicit anti-example in
architecture-contract.mdVerdict: keep course.
Harsh take: docs are no longer blatantly stale on the adapter rename and
example apply pattern, but dead legacy exports still exist in slate-react.
Why:
Editor.subscribe ruleRisks:
Earliest gates:
rg -n "useDecorations|use-children|components/text|components/element|components/leaf|components/string|DefaultElement|DefaultLeaf|DefaultText" packages/slate-react/src packages/slate-react/test site/examples/ts -g "*.ts" -g "*.tsx"bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1cd packages/slate-react && bun test:vitestNext move:
EditableDo-not-do list:
Status: closed.
Actions:
DefaultElementDefaultLeafDefaultTextcomponents/element.tsxcomponents/leaf.tsxcomponents/text.tsxcomponents/string.tsxhooks/use-children.tsxhooks/use-decorations.tsSlateElementSlateLeafSlateTextTextStringZeroWidthStringEditableTextEditabledocs/libraries/slate-react/editable.md to use
createSlateDecorateCompatSourceChanged files:
.tmp/slate-v2/packages/slate-react/src/index.ts.tmp/slate-v2/packages/slate-react/src/components/element.tsx.tmp/slate-v2/packages/slate-react/src/components/leaf.tsx.tmp/slate-v2/packages/slate-react/src/components/text.tsx.tmp/slate-v2/packages/slate-react/src/components/string.tsx.tmp/slate-v2/packages/slate-react/src/hooks/use-children.tsx.tmp/slate-v2/packages/slate-react/src/hooks/use-decorations.ts.tmp/slate-v2/docs/libraries/slate-react/editable.md.tmp/slate-v2/.changeset/remove-legacy-react-renderer-exports.mdCommands:
rg -n "useDecorations|use-children|components/text|components/element|components/leaf|components/string|DefaultElement|DefaultLeaf|DefaultText|LeafString|ElementComponent|TextComponent" packages/slate-react/src packages/slate-react/test site/examples/ts -g "*.ts" -g "*.tsx"
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
cd packages/slate-react && bun test:vitest
bun run lint:fix
bun run lint
bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react --force
rg -n "createSlateDecorationSource|EditableRoot|useDecorations|use-children|components/text|components/element|components/leaf|components/string|DefaultElement|DefaultLeaf|DefaultText|<Editable .*decorate|decorate\\?" packages/slate-react/src packages/slate-react/test site/examples/ts docs/libraries docs/walkthroughs -g "*.ts" -g "*.tsx" -g "*.md" -g "*.mdx"
rg -n "chunk|renderChunk|getChunkSize|data-slate-chunk" packages/slate-react site docs scripts playwright -g "*.ts" -g "*.tsx" -g "*.md" -g "*.mdx"
Evidence:
DefaultElement docs hits are local example function names, not
package exportscreateSlateDecorationSource is gone from package docsrenderChunk or getChunkSize product surface remains11 passed, 42 passedslate-dom and slate-react: passedVerdict: keep course -> final verification.
Harsh take: the planned owners are closed. The only honest next move is final verification; no more local cleanup before the full gate.
Why:
Editable, projection-store overlays,
accessor cleanup, editor.apply example cleanup, latest-state docs, and dead
export cleanup all have focused proofRisks:
Earliest gates:
Next move:
active goal state to done only if
the active completion target is satisfiedDo-not-do list:
Status: complete.
Actions:
React
import in EditableDOMRootrichtext insert row to use the semantic handle fallback
for mobile, matching the already-classified native transport boundary while
keeping desktop browser input proof intactFinal changed areas in this lane:
.tmp/slate-v2/packages/slate/**.tmp/slate-v2/packages/slate-dom/**.tmp/slate-v2/packages/slate-react/**.tmp/slate-v2/packages/slate-browser/** build/typecheck only.tmp/slate-v2/site/examples/ts/**.tmp/slate-v2/playwright/integration/examples/**.tmp/slate-v2/docs/libraries/slate-react/editable.md.tmp/slate-v2/.changeset/**docs/plans/2026-04-22-slate-v2-core-api-runtime-perfection-plan.mddocs/research/decisions/slate-v2-data-model-first-react-perfect-runtime.mddocs/slate-v2/ledgers/slate-editor-api.mddocs/slate-v2/references/architecture-contract.mdactive goal stateFinal verification commands:
rg -n "test\\.skip|\\.skip\\(|skip\\(" playwright/integration/examples -g "*.ts" -g "*.tsx" || true
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun test ./packages/slate/test/transaction-contract.ts --bail 1
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bun run lint
CORE_OBSERVATION_BENCH_ITERATIONS=5 bun run bench:core:observation:compare:local
NORMALIZATION_BENCH_ITERATIONS=5 bun run bench:core:normalization:compare:local
CORE_HUGE_BENCH_ITERATIONS=5 bun run bench:core:huge-document:compare:local
bun run bench:react:rerender-breadth:local
bun run bench:react:huge-document-overlays: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 lint:fix
bun run lint
bunx turbo build --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-browser --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-browser --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate --force
bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-browser --force
bun test:integration-local
Final evidence:
190 pass13 pass1 pass, 15 pass, 6 passslate, slate-dom, slate-browser, and slate-react
passedslate, then slate-dom/slate-react, then slate-browser typechecks
passed260 passedCompletion decision:
Editable, projection-source overlays, public decorate hard cut,
legacy root cleanup, accessor read cleanup, example editor.apply cleanup,
latest-state docs, and dead legacy export cleanup are complete under this
active plan.editor.apply remains an explicit compatibility/extension surface until a
replacement interception API existsVerdict: stop.
Harsh take: this lane is actually done. Continuing would be fake motion.
Why:
Risks:
Earliest gates:
bun completion-checkNext move:
Do-not-do list:
apply / onChangeWork:
editor.apply and editor.onChange.Editor.apply / Editor.subscribe the primary write/observe story.editor.apply unless it is clearly a
low-level extension example.Known current example:
.tmp/slate-v2/site/examples/ts/huge-document.tsx monkeypatches
editor.apply.Gates:
bun test ./packages/slate/test/extension-contract.ts --bail 1
bun test ./packages/slate/test/operations-contract.ts --bail 1
bun test ./packages/slate/test/transaction-contract.ts --bail 1
EditableRoot / Old Editable SurfaceWork:
EditableRoot.Editable the only primary runtime.EditableRoot as if it were user API.Known current surface:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx.tmp/slate-v2/packages/slate-react/test/decorations.test.tsxGates:
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bunx turbo build --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate-react --force
Status: closed for the root surface.
Actions:
EditableRoot to EditableDOMRootEditableTextBlocks to use EditableDOMRootdecorations.test.tsx fileprojections-and-selection-contract.tsx to use public Editable
plus projection stores instead of internal EditableDOMRoot decoratedecorate from EditableDOMRootChildrenChanged files:
.tmp/slate-v2/packages/slate-react/src/components/editable.tsx.tmp/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx.tmp/slate-v2/packages/slate-react/test/projections-and-selection-contract.tsx.tmp/slate-v2/packages/slate-react/test/primitives-contract.tsx.tmp/slate-v2/packages/slate-react/test/decorations.test.tsxCommands:
rg -n "EditableRoot|decorations\\.test|createSlateDecorationSource|SlateDecorationData\\b|SlateDecorate\\b" packages/slate-react/src packages/slate-react/test site/examples/ts playwright/integration/examples .changeset
rg -n "decorate\\?|<Editable .*decorate|useDecorateContext|DecorateContext|defaultDecorate|EditableRoot|createSlateDecorationSource|SlateDecorate\\b|SlateDecorationData\\b" packages/slate-react/src packages/slate-react/test site/examples/ts playwright/integration/examples .changeset
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
cd packages/slate-react && bun test:vitest
bun run lint:fix
bunx turbo build --filter=./packages/slate-dom --filter=./packages/slate-react --force
bun run lint
bunx turbo typecheck --filter=./packages/slate-dom --filter=./packages/slate-react --force
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
Evidence:
EditableRoot, old decoration adapter names, and deleted decoration test:
no matchesdecorate prop / <Editable decorate> / public old adapter names: no
product/test/example matches6 pass11 passed, 42 passedslate-dom and slate-reactResidual:
use-decorations.ts, old useChildren, and old component files still exist
as dead legacy component support. They are no longer reached by public
semantic Editable or root tests. Delete them in the broader dead-export
cleanup if public exports no longer require them.Verdict: pivot.
Harsh take: decorate is out of the public/root path. The next wrong-shape
surface is mutable editor fields and instance write/observe APIs.
Why:
decorateRisks:
Earliest gates:
rg -n "editor\\.children|editor\\.selection|editor\\.marks|editor\\.operations|editor\\.apply|editor\\.onChange" packages/slate packages/slate-dom packages/slate-react site/examples/ts playwright/integration/examples scripts/benchmarks -g "*.ts" -g "*.tsx" -g "*.mjs"bun test ./packages/slate/test/surface-contract.ts --bail 1bun test ./packages/slate/test/transaction-contract.ts --bail 1bun test ./packages/slate/test/snapshot-contract.ts --bail 1Next move:
Editor.* accessors without breaking compat
mirrorsDo-not-do list:
renderChunkWork:
Search:
rg -n "chunk|renderChunk|getChunkSize|data-slate-chunk" packages/slate-react site docs scripts playwright -g "*.ts" -g "*.tsx" -g "*.md" -g "*.mdx"
Acceptance:
Work:
Editabledecorate primary APIrenderChunkonChange/apply as normal usageLikely docs:
docs/slate-v2/**docs/research/decisions/slate-v2-data-model-first-react-perfect-runtime.mddocs/plans/2026-04-21-slate-v2-final-api-runtime-shape-plan.mdAcceptance:
Required final gates:
rg -n "test\\.skip|\\.skip\\(|skip\\(" playwright/integration/examples -g "*.ts" -g "*.tsx" || true
bun test:integration-local
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun test ./packages/slate/test/transaction-contract.ts --bail 1
bun test ./packages/slate-react/test/dom-text-sync-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bun run bench:core:normalization:compare:local
bun run bench:core:observation:compare:local
bun run bench:core:huge-document:compare:local
bun run bench:react:rerender-breadth:local
bun run bench:react:huge-document-overlays: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
bunx turbo build --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-browser --filter=./packages/slate-react --force
bunx turbo typecheck --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-browser --filter=./packages/slate-react --force
bun run lint
Completion:
active goal state should be set to pending only when execution of
this lane starts.done only when all success criteria above are met.blocked only if no autonomous progress is possible and the exact
missing decision/evidence is named.Do not start with API hard cuts before the flake/native/mobile reconciliation debt is classified. Otherwise we risk blaming the API cut for pre-existing browser/runtime debt.