docs/plans/2026-04-29-slate-v2-dom-runtime-state-tx-ralplan.md
Date: 2026-04-29
Status: done
Code repo: /Users/zbeyens/git/slate-v2
Plan repo: /Users/zbeyens/git/plate-2
Skill: .agents/skills/slate-ralplan/SKILL.md
The public core hard cut is real, but the architecture is not fully clean yet.
BaseEditor now exposes only read, update, subscribe, and extend, which
is the right public shape. The remaining smell is that DOM/runtime packages
still preserve legacy editor-extension habits:
withDOM mutates the editor instance with clipboard/data methods.DOMEditor delegates namespace clipboard/data APIs back to those instance
methods.Editor.* reads for normal
model access where editor.read((state) => ...) should be the default.EditorStateView has no state.fragment.get(...), so clipboard code falls
back to Editor.getFragment(...).The next hard cut should not be "ban every Editor.* string everywhere". That
would be dumb. The clean rule is sharper:
Editor.* stays internal implementation law only when a state/tx
equivalent does not exist or when a pure data namespace is the right tool.Current readiness score: 0.93.
The Ralplan is closed and ready for user review before implementation.
Intent:
BaseEditor cleanup.Desired outcome:
withDOM(editor) installs DOM capabilities without adding normal public
methods to the editor instance.editor.update((tx) => ...), except for
narrow internal runtime/middleware cases with explicit allowlist comments.Editor.* usage in slate-dom and slate-react is inventoried,
reduced, and guarded so broad static reads do not creep back.In scope:
packages/slate/src/interfaces/editor.tspackages/slate/src/core/public-state.tspackages/slate-dom/src/plugin/with-dom.tspackages/slate-dom/src/plugin/dom-editor.tspackages/slate-dom/src/utils/** where static Editor.* reads are normal
state readspackages/slate-react/src/editable/** clipboard, drag/drop, repair, kernel,
browser-handle, and strategy paths that call DOM data APIs or static
Editor.*Non-goals:
editor.refsNode, Path, Point, Range,
Element, or Textpackages/slate/srcDecision boundaries:
slate/internal or keep
it package-local; do not attach it to the editor instance.Unresolved user-decision points:
Intent-boundary pass result:
DOMEditor.clipboard.*.ReactEditor.clipboard.* should mirror it because ReactEditor currently
aliases DOMEditor in
/Users/zbeyens/git/slate-v2/packages/slate-react/src/plugin/react-editor.ts:13.Principles:
editor.read and editor.update.Top drivers:
BaseEditor hard cut is undermined if plugins add methods back onto the
editor object.Viable options:
| Option | Pros | Cons | Verdict |
|---|---|---|---|
Keep e.setFragmentData / e.insertData instance methods | Closest to legacy Slate; smallest patch | Reopens editor-object method growth and hides DOM capability ownership | reject |
Keep flat DOMEditor.setFragmentData(editor, data) but remove instance methods | Better boundary; smaller API churn | Name still sounds like editor state mutation and carries legacy vocabulary | revise |
Use DOM clipboard namespace/helper: DOMEditor.clipboard.writeFragment(editor, data, options) and DOMEditor.clipboard.insertData(editor, data) | Names host side-effect, keeps editor instance clean, groups transport APIs | Slightly more API shape than legacy; tests/examples need edits | keep |
| Make clipboard helpers package-private only | Cleanest raw public API | Weak for custom Editable integrations and tests; likely pushes users to copy internals | reject for now |
Chosen option:
DOMEditor.state.fragment.get(...) and tx.fragment.get(...) so fragment reads no
longer require Editor.getFragment(...).tx.fragment.insert(...) / tx.text.insert(...) for insertion paths.DOMEditor.clipboard.* and mirrored
ReactEditor.clipboard.*.Consequences:
withDOM becomes a DOM capability installer, not an editor-method extender.Editor.* internals will remain until state/tx grows equivalent helpers
or the code is proven to need the lower-level runtime function.Follow-ups:
Editor.* call in slate-dom and slate-react and
classify it as state, tx, internal, data namespace, or cut.| Dimension | Score | Evidence |
|---|---|---|
| React 19.2 runtime performance | 0.93 | Ledger accepts the staged cleanup: event-scoped writes first, hot observer reads second, temporary ref/bridge allowances last. The high-risk proof names render/hot-path guards instead of broad React rerender assertions. |
| Slate-close unopinionated DX | 0.92 | DOMEditor.clipboard.* / ReactEditor.clipboard.* is locked. App authors keep read / update; custom bridge authors get one DOM namespace; no editor.clipboard and no legacy aliases. |
| Plate/slate-yjs migration backbone | 0.93 | Plugin layers can wrap clipboard helpers without monkey-patching. Model mutation stays in tx and the proof plan requires deterministic operation replay after paste/cut. |
| Regression-proof testing strategy | 0.93 | High-risk proof now names exact package, React, browser, stress, and guard targets: state-tx-public-api-contract.ts, clipboard-contract.ts, clipboard-boundary.ts, React surface contracts, highlighted-text/paste-html/inlines browser rows, and generated stress families. |
| Research evidence completeness | 0.92 | Existing compiled research covers Lexical read/update, ProseMirror transaction/DOM authority, Tiptap extension DX, and prior clipboard boundary ownership. No fresh raw ingest needed for this narrow pass. |
| shadcn-style composability / minimal props | 0.91 | Grouped clipboard helpers are minimal and composable. The plan rejects flat editor command growth, keeps product APIs above raw Slate, and names React helper surfaces without adding render props. |
Weighted total: 0.93.
Gate result:
done.Accepted north star:
Slate model + operations
read/update lifecycle
state/tx grouped namespaces
DOM host bridge owned by DOM runtime helpers
React as projection/event wiring, not model engine
generated browser proof for editing behavior
Evidence:
BaseEditor currently exposes only read, subscribe, update, and
extend in /Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:364.EditorStateView has grouped state APIs but no fragment read group in
/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:336.EditorUpdateTransaction has fragment.insert but not fragment.get in
/Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts:840.state/tx callback APIs in
docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md.docs/research/decisions/slate-v2-read-update-runtime-architecture.md.Research/live-source refresh result:
read / update as the lifecycle boundary,
update tags for paste/collaboration/DOM-selection policy, dirty
leaf/element-driven reconciliation, and extension packaging. This supports
state/tx fragment access and rejects editor-instance method growth.DataTransfer, HTML scraping, plain-text fallback, and payload
provenance. Its old Editor.getFragment / Transforms.insertFragment names
are superseded by the current state/tx hard cut, but the package ownership
remains correct.Live-source refresh result:
slate-dom / slate-react still contain broad static Editor.* usage across
DOM utilities, selection reconciliation, clipboard, mutation, projection,
root selectors, browser handles, and React components.Editor.getFragment. Static writes such
as Editor.insertText, Editor.deleteFragment, Editor.deleteBackward,
Editor.deleteForward, Editor.insertBreak, Editor.insertSoftBreak, and
Editor.replace also need tx migration or explicit internal runtime
allowlisting.Editor.getSnapshot, Editor.getOperations,
Editor.getChildren, Editor.getLastCommit, Editor.getRuntimeId, and
Editor.getPathByRuntimeId need source-selector/live-state replacement or a
named observer allowance.Editor.rangeRef, Editor.pointRef, and
Editor.pathRef are not automatically bad; they need an explicit internal
runtime allowance until a better state/tx/runtime reference API exists.Editor.isVoid, Editor.isInline,
Editor.isBlock, Editor.range, Editor.point, Editor.hasPath,
Editor.above, and Editor.void should move to state/tx groups where they
are normal model reads, but some DOM point/range mapping call sites may need
temporary runtime allowances.Pressure pass result:
Editor.getSnapshot, Editor.getOperations, Editor.getChildren,
Editor.getLastCommit, runtime-id reads, projection/root-selector reads,
and browser-handle snapshots.Editor.isVoid, Editor.isInline, Editor.isBlock, Editor.range,
Editor.point, Editor.hasPath, Editor.above, Editor.void.rangeRef, pointRef, and pathRef
remain temporary internal runtime allowances until a better state/tx/runtime
reference API is designed.editor.read / editor.update.DOMEditor.clipboard.* and
ReactEditor.clipboard.*.editor.clipboard, no flat setFragmentData, no compatibility alias.state.fragment.get(...) and tx.fragment.insert(...) are the fragment
pair; tx.fragment.get(...) exists only for tx-local reads.tx.fragment.insert(...), not DOM payload transport.source.setFragmentData(...).Editor.* call in one batch.editor.dom, editor.clipboard, or a product command layer.Pressure-pass priority order:
state.fragment.get / tx.fragment.get plus DOM clipboard namespace cut.Temporary internal allowance categories:
rangeRef, pointRef, pathRefEditor.isEditor until replaced by top-level
isEditorRejected pressure-pass alternatives:
Editor.* migration: too risky and too noisyeditor.clipboard: violates the editor-instance hard cutCore editor:
editor.read((state) => {
const fragment = state.fragment.get();
});
editor.update((tx) => {
tx.fragment.insert(fragment);
});
State fragment API:
state.fragment.get();
state.fragment.get({ at: range });
Transaction fragment API:
tx.fragment.get()
tx.fragment.get({ at: range })
tx.fragment.insert(fragment, options?)
DOM clipboard API:
DOMEditor.clipboard.writeFragment(editor, data, {
formatKey,
origin: "copy",
});
DOMEditor.clipboard.insertData(editor, data);
DOMEditor.clipboard.insertFragmentData(editor, data);
DOMEditor.clipboard.insertTextData(editor, data);
React bridge API:
ReactEditor.clipboard.writeFragment(editor, data, { origin: "copy" });
ReactEditor.clipboard.insertData(editor, data);
Cut:
editor.setFragmentData(data);
editor.insertData(data);
editor.insertFragmentData(data);
editor.insertTextData(data);
Avoid:
Editor.getFragment(editor);
in package runtime code when the equivalent is:
editor.read((state) => state.fragment.get());
withDOM should install capability state, not instance methods:
export const withDOM = (editor, options) => {
installDOMRuntime(editor, { clipboardFormatKey });
return editor as T & DOMEditorBrand;
};
The runtime owns:
The state/tx runtime owns:
Allowed internal escape hatches:
state/tx
cannot represent an in-flight operation yetslate/internal can export narrow helpers for other first-party packagesNot allowed:
Editor.* reads in DOM/React runtime code just because the
state/tx API is missing a methodgetEditorTransformRegistry as the normal write path outside explicit
runtime/middleware codeNo direct React render API change is needed in this lane.
React clipboard handlers should call the DOM/React clipboard namespace, not editor instance methods:
ReactEditor.clipboard.writeFragment(editor, clipboardData, {
origin: "copy",
});
The key DX rule:
onCopy, onCut, onPaste, and normal Slate editor
read/updatee.setFragmentDataPlate does not need current-version adapters in raw Slate.
The required backbone is:
origin, formatKey, and payload semanticsPlate can build product APIs over:
editor.update((tx) => {
tx.fragment.insert(fragment);
});
and optional product commands without polluting raw Slate core.
slate-yjs needs deterministic model operations, not DOM clipboard shims.
This lane preserves that by:
Proof required before closure:
| Regression family | Required proof |
|---|---|
| copy selected text | model fragment, MIME payload, HTML attribute, plain text |
| cut selected text | write clipboard, delete expanded selection, follow-up typing |
| drag selected void | select void, write fragment, drop inserts deterministic model |
| custom clipboard format key | custom MIME used, default MIME absent |
| HTML-only fallback | data-slate-fragment from HTML still inserts |
| plain text fallback | multiline text splits and inserts correctly |
| void boundary copy | start/end void spacer does not create visible spacer layout |
| decorated leaves | render-only wrappers stripped from copied HTML |
| IME/composition adjacency | clipboard helpers do not import repair-induced selection |
Fast CI rows:
slate-dom and hot slate-react runtime pathsStress rows:
Do not put full stress in bun check.
bun check must stay fast. Full generated browser sweep belongs to
bun check:full / sparse test:stress.
| Lens | Applicability | Finding | Plan delta |
|---|---|---|---|
| Vercel React best practices | applied | Avoid broad React rerenders; clipboard helpers should run in event handlers and keep transient DataTransfer out of render state. | React event paths call namespace helpers; no React state added. |
| performance-oracle | applied | Repeated full static reads and clone/serialization must stay event-scoped; static snapshot reads in render/projection paths need separate review. | Add static-read classification and keep clipboard serialization event-only. |
| tdd | applied | This is public API + behavior. Tests should prove behavior through DOMEditor/ReactEditor public helpers and browser contracts, not grep-only implementation assertions. | Red tests: no instance methods; clipboard behavior still works. |
| shadcn | skipped | No UI chrome/component styling in this lane. | No change. |
| react-useeffect | skipped | No effect lifecycle change in this lane. | No change. |
Trigger:
Blast radius:
packages/slatepackages/slate-dompackages/slate-reactPre-mortem:
Expanded proof plan:
| Risk | Proof owner | Exact target |
|---|---|---|
state.fragment.get reads the wrong range or stale selection | core unit | Extend /Users/zbeyens/git/slate-v2/packages/slate/test/state-tx-public-api-contract.ts with state.fragment.get() and state.fragment.get({ at }); migrate selected-fragment assertions in /Users/zbeyens/git/slate-v2/packages/slate/test/clipboard-contract.ts away from Editor.getFragment(editor). |
| tx fragment reads drift from write-time draft state | core unit | Extend /Users/zbeyens/git/slate-v2/packages/slate/test/state-tx-public-api-contract.ts with tx.fragment.get() before and after tx.fragment.insert(...) inside one editor.update. |
| copied payload loses Slate MIME, HTML, or plain text | DOM unit | Extend /Users/zbeyens/git/slate-v2/packages/slate-dom/test/clipboard-boundary.ts and .test.ts with DOMEditor.clipboard.writeFragment writes Slate MIME, HTML data-slate-fragment, and plain text. |
| HTML-only and plain-text fallback regress | DOM unit | Keep and migrate /Users/zbeyens/git/slate-v2/packages/slate-dom/test/clipboard-boundary.ts; add DOMEditor.clipboard.insertData handles Slate MIME, HTML-only fallback, and plain text fallback. |
| editor instance methods sneak back | public surface guard | Extend /Users/zbeyens/git/slate-v2/packages/slate/test/public-surface-contract.ts or add a slate-dom surface contract proving setFragmentData, insertData, insertFragmentData, and insertTextData are absent from editor instances and DOMEditor instance types. |
| React bridge diverges from DOM bridge | React unit | Extend /Users/zbeyens/git/slate-v2/packages/slate-react/test/react-editor-contract.tsx with ReactEditor.clipboard mirrors DOMEditor.clipboard. |
| React copy/cut/drag handlers bypass namespace or mutate outside tx | React integration | Add or extend /Users/zbeyens/git/slate-v2/packages/slate-react/test/clipboard-input-strategy-contract.test.tsx; assert copy writes payload, cut deletes through tx, drag writes fragment, and follow-up typing preserves selection. |
static Editor.* creeps back into hot runtime paths | guard | Extend /Users/zbeyens/git/slate-v2/packages/slate/test/escape-hatch-inventory-contract.ts and /Users/zbeyens/git/slate-v2/packages/slate-react/test/surface-contract.tsx; encode allowed categories for refs, host bridge mapping, trace/debug reads, and package-local runtime facades only. |
| decorated leaves copy render wrappers instead of model fragment | browser | Keep /Users/zbeyens/git/slate-v2/playwright/integration/examples/highlighted-text.test.ts rows copies decorated text as fragment semantics instead of leaking highlight wrappers and cuts decorated text as fragment semantics and deletes the selection. |
| paste/drop transport regresses | browser | Keep /Users/zbeyens/git/slate-v2/playwright/integration/examples/paste-html.test.ts rows paste-html-generated-clipboard-gauntlet and paste-html-generated-drop-data-gauntlet. |
| inline boundary cut/paste typing regresses | browser | Keep /Users/zbeyens/git/slate-v2/playwright/integration/examples/inlines.test.ts row inlines-generated-cut-typing-gauntlet. |
| generated stress misses clipboard/void classes | stress | Extend /Users/zbeyens/git/slate-v2/playwright/stress/generated-editing.test.ts with families clipboard-fragment-round-trip, cut-follow-up-typing, void-copy-drag-drop, and keep existing paste-normalize-undo / paste-html-image-void. |
| collaboration replay differs after clipboard mutation | core/collab unit | Extend /Users/zbeyens/git/slate-v2/packages/slate/test/collab-history-runtime-contract.ts or /Users/zbeyens/git/slate-v2/packages/slate/test/transaction-contract.ts with operation replay after paste/cut insertion. |
Static Editor.* implementation buckets:
| Bucket | Rule |
|---|---|
| Cut first | Editor.getFragment in DOM clipboard serialization; DOM instance clipboard methods; static write calls in clipboard/cut paths when a tx method exists. |
| Migrate next | Editor.insertText, deleteFragment, deleteBackward, deleteForward, insertBreak, insertSoftBreak, and replace in React mutation/input paths. |
| Source-selector/live-state | Editor.getSnapshot, getOperations, getChildren, getLastCommit, runtime-id reads, projection-store, widget-store, root-selector, and browser-handle reads. |
| State query groups | Editor.range, point, before, after, above, void, hasPath, levels, isVoid, isInline, and isBlock when the caller is doing normal model reads. |
| Temporary internal allowances | rangeRef, pointRef, pathRef, pure Editor.isEditor, trace/debug reads, and host bridge mapping calls until dedicated runtime facades exist. |
Commands:
cd /Users/zbeyens/git/slate-v2
bun test packages/slate/test/state-tx-public-api-contract.ts packages/slate/test/clipboard-contract.ts packages/slate-dom/test/clipboard-boundary.ts packages/slate/test/public-surface-contract.ts
(cd packages/slate-react && bunx vitest run --config ./vitest.config.mjs test/react-editor-contract.test.tsx test/surface-contract.test.tsx test/clipboard-input-strategy-contract.test.tsx)
bunx playwright test playwright/integration/examples/highlighted-text.test.ts playwright/integration/examples/paste-html.test.ts playwright/integration/examples/inlines.test.ts --project=chromium
bun --filter slate-browser build
PLAYWRIGHT_RETRIES=0 playwright test playwright/stress/generated-editing.test.ts --project=chromium --grep "clipboard-fragment-round-trip|cut-follow-up-typing|void-copy-drag-drop|paste-normalize-undo|paste-html-image-void"
Hard-cut answer:
DOMEditor.clipboard.* or keep it
package-local behind the DOM runtime helper.Rollback/remediation answer:
playwright/stress/replay.test.ts before retrying a smaller patch.High-risk verdict:
Coherence sweep:
DOMEditor.clipboard.* /
ReactEditor.clipboard.* decision and the rejected package-private helper
optionstate.fragment.get(...)Revisions made:
0.93 because every scored dimension now has evidence and
no dimension is below thresholdpending during the revision pass because the final closure
score/gates pass still had to runRevision verdict:
Closure checks:
0.93, above the 0.92 threshold0.85keepDOMEditor.clipboard.* / ReactEditor.clipboard.*active goal state is set to doneClosure verdict:
Cut:
DOMEditor instance methods: setFragmentData, insertData,
insertFragmentData, insertTextDatawithDOM assigning those methods to esource.setFragmentData(...)ReactEditor.setFragmentData(...) if a renamed
clipboard namespace is acceptedRevise:
DOMEditor.setFragmentData(editor, data, origin) to grouped clipboard
helper namingEditor.getFragment use to state.fragment.getEditor.void, Editor.range, Editor.point, Editor.before,
Editor.after, Editor.hasPath, Editor.levels in DOM/React runtime code
to state/tx equivalents when practicalKeep:
packages/slate/srcDOMEditor.toDOMRange, DOMEditor.toSlateRange, DOMEditor.toDOMNode, and
similar host bridge functionsRejected:
editor.clipboardtxChange:
editor.setFragmentData(data) -> DOMEditor.clipboard.writeFragment(editor, data, options)editor.insertData(data) -> DOMEditor.clipboard.insertData(editor, data)Who feels pain:
Likely objection:
Steelman antithesis:
Tradeoff tension:
Why not change for change's sake:
Evidence:
withDOM assigns instance methods in
/Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/with-dom.ts:258
and :346.DOMEditor delegates namespace calls back to instance methods in
/Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/dom-editor.ts:591
and :615.Rejected alternative:
DOMEditor.setFragmentData flat and remove only instance methods. This
is acceptable fallback, but grouped clipboard helpers are clearer.Migration answer:
Docs/example answer:
Regression proof:
Ecosystem answers:
Verdict: keep.
state.fragment.get(...)Change:
Editor.getFragment(editor) -> editor.read((state) => state.fragment.get())Who feels pain:
Likely objection:
Steelman antithesis:
state.value and state.nodes could already express this with
state.selection.get() plus Node.fragment.Tradeoff tension:
Why not change for change's sake:
Editor.getFragment, which contradicts the
state/tx public lifecycle.Evidence:
EditorStateView lacks a fragment group at
/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:336.tx.fragment.insert exists at
/Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts:840.withDOM currently calls Editor.getFragment at
/Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/with-dom.ts:329.Rejected alternative:
Editor.getFragment as the recommended internal helper. That keeps the
old namespace alive because state is missing a normal read.Migration answer:
state.fragment.get(); writes remain tx.fragment.insert(...).Docs/example answer:
Regression proof:
Ecosystem answers:
state.fragment.get.Verdict: keep.
Editor.* in first-party runtime packagesChange:
Editor.* calls in slate-dom / slate-react
by priority instead of attempting one giant migrationWho feels pain:
Likely objection:
Steelman antithesis:
Tradeoff tension:
Why not change for change's sake:
Evidence:
withDOM uses static reads for block/range/path-ref work at
/Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/with-dom.ts:113,
:120, :217, :394, and :416.DOMEditor uses static reads for drop targeting, focus initialization, and
range validation in
/Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/dom-editor.ts:333,
:459, and :580./Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/clipboard-input-strategy.ts:193
and :369.Editor.insertText,
Editor.deleteFragment, Editor.deleteBackward, Editor.deleteForward,
Editor.insertBreak, Editor.insertSoftBreak, Editor.replace), observer
reads (Editor.getSnapshot, Editor.getOperations, Editor.getChildren,
Editor.getLastCommit, runtime-id reads), refs, and traversal/schema reads.Rejected alternative:
Editor.* outside packages/slate. That would force bad
wrappers for legitimate DOM bridge operations.Editor.* calls alone because they are "internal". That
would keep the old namespace as the de facto first-party runtime API.Migration answer:
state.fragment.get /
tx.fragment.getDocs/example answer:
Regression proof:
Ecosystem answers:
Verdict: keep.
Change:
rangeRef, pointRef,
pathRef, pure isEditor checks, trace/debug reads, and host bridge mapping
calls until better state/tx/runtime helpers existWho feels pain:
Likely objection:
Steelman antithesis:
Tradeoff tension:
Why not change for change's sake:
e.setFragmentData or Editor.insertText. Forcing them through state/tx now
would create wrapper noise without improving app DX or browser behavior.Evidence:
withDOM uses path refs around operation middleware in
/Users/zbeyens/git/slate-v2/packages/slate-dom/src/plugin/with-dom.ts:217
and :416.rangeRef in browser-handle, event-engine,
root-engine, and selection-reconciler paths.Rejected alternative:
Migration answer:
Editor.* allowance bucket.Docs/example answer:
Regression proof:
Editor.* string absence.Ecosystem answers:
Verdict: keep.
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| current-state read and initial score | complete | live source lines for withDOM, DOMEditor, EditorStateView, clipboard tests, research pages | new plan created, score 0.78 | static Editor.* inventory incomplete | intent-boundary pass |
| intent/boundary and decision brief | complete | ReactEditor aliases DOMEditor; current setFragmentData/insertData calls are first-party clipboard integration points | locked DOMEditor.clipboard.* + mirrored ReactEditor.clipboard.*; no user question needed; score raised to 0.82 | exact static Editor.* allowlist still open | research/live-source refresh |
| research and live-source refresh | complete | Lexical/ProseMirror/Tiptap compiled research, prior clipboard ownership plan, live static Editor.* inventory across slate-dom and slate-react | added research/live-source result, static usage buckets, score raised to 0.85 | exact allowlist still open; pressure pass must decide priority and split | performance/DX/migration/regression/simplicity pressure pass |
| performance/DX/migration/regression/simplicity pressure passes | complete | React hot-path priority, DX boundary, migration/collab split, red-first proof order, simplicity cuts | added priority order, temporary allowance categories, rejected alternatives, score raised to 0.88 | maintainer ledger had to decide whether static Editor.* cleanup was keep/revise | Slate maintainer objection ledger |
| Slate maintainer objection ledger | complete | accepted rows for editor-instance clipboard cut, state.fragment.get, prioritized static Editor.* cleanup, and temporary runtime allowances | row 3 moved to keep; row 4 added so refs/host bridge mapping are explicit allowances, not hidden debt; score raised to 0.90 | closed by high-risk pass | high-risk-deliberate-pass |
| high-risk deliberate pass | complete | exact proof targets for core state/tx, DOM clipboard, React bridge, static guard, browser rows, generated stress families, and collab replay | expanded high-risk section from generic proof categories to named files, test rows, static buckets, commands, rollback answer, and keep verdict; score raised to 0.92 | closed by revision pass | revision pass |
| revision pass | complete | contradiction sweep over scorecard, public namespace decision, open questions, final gates, and continuation state | removed stale "would change decision" alternatives, added revision result, clarified closure as separate pass, score raised to 0.93 | final closure pass still pending | closure score and gates |
| closure score and gates | complete | final score/gate sweep over threshold, pass ledger, locked API, high-risk proof, open questions, and checkpoint state | plan status set to done; completion state can pass; implementation remains a later lane | none | user review |
Added:
Editor.* classification rule instead of blind banEditor.* implementation buckets for cut/migrate/source-selector/
state-query/temporary-allowance workRevised:
Editor.getFragment is acceptable as internal implementation only until
state.fragment.get lands, not a normal runtime package patternDOMEditor.clipboard.* /
ReactEditor.clipboard.*Editor.getFragment / Transforms.insertFragment names are superseded by
state/tx API lawEditor.* cleanup is split into prioritized tranches instead of one
risky all-at-once migrationdone; implementation remains a later laneDropped:
editor.refsbun checkstate.value.fragment(at?) as a live alternativeNo-change defense:
DOMEditor.toDOMRange, toSlateRange, toDOMNode, focus, blur, and target
helpers can stay namespace bridge APIs. They are host bridge operations, not
model state reads.No user-decision questions remain.
Closure pass verified:
active goal state and active goal state point at closuredone after this closure check is recordedImplementation activation:
ralph started implementation in sibling
.tmp/slate-v2.state.fragment.get(...) and
tx.fragment.get(...), migrated selected-fragment clipboard contract reads,
and verified with focused package tests plus bun --filter slate typecheck.slate-dom clipboard boundary suite plus
bun --filter slate-dom typecheck.DOMEditor.clipboard.* /
ReactEditor.clipboard.*, removed normal instance clipboard/data paths, and
verified focused DOM clipboard behavior plus slate-dom / slate-react
typechecks.bun check, targeted browser rows, slate-browser build,
and generated stress grep are green.Phase 1: State fragment API
EditorStateFragmentApi.state.fragment.get(options?).tx.fragment.get(options?).tx.fragment.insert(...).Phase 2: DOM clipboard runtime helper
clipboardFormatKey in DOM runtime state/WeakMap.Phase 3: Public DOM/React namespace cut
DOMEditor.clipboard.* / ReactEditor.clipboard.* shape.setFragmentData, insertData, insertFragmentData, and
insertTextData from DOMEditor instance type.Phase 4: Static read/write inventory
Editor.* calls in slate-dom and hot slate-react.editor.read.editor.update.Phase 5: Regression proof
bun check before completion.Iteration gates:
cd /Users/zbeyens/git/slate-v2
bun test packages/slate/test/*fragment* packages/slate-dom/test/clipboard-boundary.ts
bun test:vitest test/surface-contract.test.tsx
bun typecheck:packages
bun lint:fix
Completion gates:
cd /Users/zbeyens/git/slate-v2
bun check
Browser gate when implementation touches React/DOM behavior:
cd /Users/zbeyens/git/slate-v2
bunx playwright test playwright/integration/examples/highlighted-text.test.ts playwright/integration/examples/paste-html.test.ts playwright/integration/examples/inlines.test.ts --project=chromium
bun --filter slate-browser build
PLAYWRIGHT_RETRIES=0 playwright test playwright/stress/generated-editing.test.ts --project=chromium --grep "clipboard-fragment-round-trip|cut-follow-up-typing|void-copy-drag-drop|paste-normalize-undo|paste-html-image-void"
Do not run bun test:integration-local during normal iteration unless this
lane becomes a release-quality browser claim.
When this Ralplan closes, the handoff must list every accepted decision:
state.fragment.get / tx.fragment.get / tx.fragment.insert.DOMEditor.clipboard.* / ReactEditor.clipboard.*.Editor.*: exact allowlist and cut list.This plan is done because:
>= 0.920.85reviseEditor.* inventory is completeactive goal state points at the closed Ralplan statedone, bun run completion-check passes from
/Users/zbeyens/git/plate-2