docs/plans/2026-05-03-slate-v2-core-editor-method-hard-cut-ralplan.md
Status: done.
Verdict: hard cut the public Editor.* method namespace. Keep the small editor
instance lifecycle, keep pure data namespaces, move normal reads to state.*,
move normal writes to tx.*, and internalize runtime-policy leftovers.
The live source already has the right substrate:
BaseEditor is small: read, subscribe, update, extend
(.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:480-490).EditorCoreStateView and EditorCoreUpdateTransaction already expose grouped
state/tx APIs (.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:445-475).EditorStaticApi still exposes 99 methods and mixes reads, writes, runtime
internals, extension registration, and legacy helper policy
(.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:1113-1704).Blunt take: keeping all 99 public static methods is architectural debt. It keeps
old Slate familiar, but it also preserves the exact object-shaped junk drawer
that read/update + state/tx was supposed to replace.
Intent:
elementReadOnly and
shouldMergeNodesRemovePrevNode from public API thinking.Desired outcome:
EditorStaticApi method has a keep / move / internalize / cut
decision.editor.read((state) => ...) and
editor.update((tx) => ...).In scope:
packages/slate/src/interfaces/editor.ts core method surfaces.BaseEditor, EditorStateView, EditorUpdateTransaction,
EditorTransformApi, and EditorStaticApi.isVoid, isInline, isSelectable,
isElementReadOnly.Non-goals:
Decision boundaries:
ralph execution owns code edits.Unresolved user-decision points:
Resolved staging decisions:
Editor value in the same batch as the public static method
cut. Keeping a public empty/zombie Editor namespace would be worse DX than
a clean break. The internal Editor value remains under slate/internal.editor.extend(...) as a public instance lifecycle method. If
construction-time extension registration lands later, docs can teach that as
the default path, but runtime extension install is still a valid Slate-core
capability.state.nodes.void(...). The scoped namespace makes the legacy name
readable enough, and voidElement is verbosity without a stronger payoff.Principles:
Top drivers:
read/update lifecycle discipline and typed lifecycle tags.Viable options:
Editor.* as compatibility surface while teaching state/tx.
Pro: easier migration. Con: two public ways to do every core thing, worse
autocomplete, weak architecture signal.Editor.*; keep instance editor.read/update/subscribe/extend;
expose named pure helpers and state/tx groups. Pro: clean architecture.
Con: bigger migration.editor.commands / editor.chain() as main API. Pro:
approachable toolbar DX. Con: too product-shaped for raw Slate.Chosen option:
Rejected alternatives:
editor.update should be the write
lifecycle; command catalogs belong above core.$ helper style and class nodes: rejected; Slate keeps plain data and
direct callback parameters.Consequences:
Editor.foo(editor, ...) to
state/tx groups or named exports.Follow-ups:
EditorStaticApi leaks publicly.state/tx first, not as an advanced alternative.Current score: 0.93.
| Dimension | Score | Evidence |
|---|---|---|
| React 19.2 runtime performance | 0.86 | Method census shows docs/site source are already on editor.read/update; hot render paths should not depend on public static methods. Generated bundles were excluded from the census. |
| Slate-close unopinionated DX | 0.91 | Live BaseEditor is only read, subscribe, update, extend; docs/examples already teach state/tx; state.nodes.void stays Slate-familiar. |
| Plate and slate-yjs migration backbone | 0.87 | Live extension namespaces, tx.operations.replay, commit metadata, bookmarks, runtime ids, and slate/internal imports cover the backbone without a public command namespace. |
| Regression-proof testing strategy | 0.85 | Existing public-surface, state/tx, write-boundary, extension, bookmark, command/internal, and migration contracts are identified; final implementation still needs red contract edits per batch. |
| Research evidence completeness | 0.90 | Live source/test census now backs the Lexical read/update, ProseMirror transaction ownership, and Tiptap-extension-DX comparison. |
| shadcn-style composability/minimalism | 0.88 | Public API becomes small instance lifecycle plus grouped state/tx APIs; product commands stay above raw Slate. |
Completion gates met:
.tmp/slate-v2state.schema.isElementReadOnly is renamed to state.schema.isReadOnlyEditor.* remains behind internal entrypoints onlybun check is green in .tmp/slate-v2Status: complete.
Live source read:
.tmp/slate-v2/packages/slate/src/interfaces/editor.ts still contains
EditorStaticApi, InternalEditor, and export { InternalEditor as Editor }..tmp/slate-v2/packages/slate/src/internal/index.ts exports internal
Editor..tmp/slate-v2/packages/slate/test/public-surface-contract.ts already asserts
the primary public package surface does not expose Editor, transform
namespaces, command registry helpers, or broad editor instance methods..tmp/slate-v2/packages/slate/test/state-tx-public-api-contract.ts already
proves grouped state and tx reads/writes..tmp/slate-v2/packages/slate/test/write-boundary-contract.ts already proves
normal writes go through editor.update and tx.Census command shape:
rg --files packages site docs
exclude: site/out, site/.next, CHANGELOG.md, docs/general/changelog.md
count: /\bEditor\.([A-Za-z_$][\w$]*)\b/
Result:
Editor.* occurrences.Editor.* method names.Editor.* strings, neither teaching current
authoring API.Editor.getSnapshot occurrence.Top current Editor.* pressure:
| Method | Count | Meaning |
|---|---|---|
replace | 488 | Mostly test fixture seeding; belongs behind internal/test helpers and tx.value.replace. |
getSnapshot | 396 | Mostly tests/snapshot contracts; public docs/examples should keep using state.runtime.snapshot only when a full snapshot is intentional. |
getChildren | 84 | Mostly tests/internal; public read path is state.value.get(). |
after | 70 | query tests/internal; public read path is state.points.after. |
string | 65 | query tests/internal; public read path is state.text.string. |
isBlock | 58 | query tests/internal; public read path is state.schema.isBlock. |
registerCommand | 18 | tests only; confirms command helpers are internal contract/test substrate, not public authoring API. |
pathRef / pointRef / rangeRef | 36 combined | runtime/browser internals plus ref tests; supports internalizing ref sets while keeping bookmark/runtime-id public anchor story. |
Sensitive-method census:
| Method/family | Count | Public docs/site | Runtime | Tests | Decision |
|---|---|---|---|---|---|
elementReadOnly / isElementReadOnly | 6 | 0 | 6 | 0 | cut/rename out of public API; runtime becomes isReadOnly policy. |
shouldMergeNodesRemovePrevNode | 1 | 0 | 1 | 0 | internal merge policy only. |
| ref creation/sets | 44 | 0 | 37 | 7 | internal runtime/test surface; public durable anchors are bookmarks/runtime ids. |
| normalizing toggles | 7 | 0 | 7 | 0 | internal only; public control is tx.withoutNormalizing. |
| command registration/definition | 19 | 0 | 0 | 19 | internal/test only; no public command catalog in raw Slate. |
| extension registry/capability/normalizer/commit listeners | 22 | 0 | 1 | 21 | internal extension runtime only. |
replace / reset | 489 | 0 | 3 | 486 | replace remains internal/test; reset dies. |
static read / update | 2 | 0 | 0 | 2 | cut static wrappers; instance methods stay. |
Conclusion:
editor.read/update.slate/internal or test helpers, not preserve Editor.* for app authors.Editor value staging question: public Editor
dies with the method cut.Status: complete.
Live source changes:
.tmp/slate-v2/packages/slate/src/index.ts no longer exports ./core,
./editor, ./transforms-node, ./transforms-selection, or
./transforms-text..tmp/slate-v2/packages/slate/src/index.ts now explicitly exports the intended
public root: createEditor, defineEditorExtension, elementProperty,
isEditor, pure data namespaces, public editor lifecycle/state/tx types, and
transform option types..tmp/slate-v2/packages/slate/src/index.ts no longer wildcard-exports
./interfaces, so EditorStaticApi and EditorElementReadOnlyOptions do
not leak through the primary package..tmp/slate-v2/packages/slate/src/interfaces/editor.ts,
.tmp/slate-v2/packages/slate/src/create-editor.ts, and
.tmp/slate-v2/packages/slate/src/core/public-state.ts expose the public
read-only schema predicate as isReadOnly..tmp/slate-v2/site/examples/ts/dom-coverage-boundaries.tsx no longer imports
internal Editor for a public example snapshot read..tmp/slate-v2/scripts/benchmarks/core/current/*.mjs import internal Editor
from the internal entrypoint instead of the primary package.Regression coverage:
.tmp/slate-v2/packages/slate/test/public-surface-contract.ts now asserts the
intended small public root, rejects raw editor/core/transform helper exports,
and rejects wildcard-exporting the internal editor type table..tmp/slate-v2/packages/slate/test/schema-contract.ts now proves
state.schema.isReadOnly(...).Verification:
bun test ./packages/slate/test/public-surface-contract.ts ./packages/slate/test/schema-contract.ts ./packages/slate/test/state-tx-public-api-contract.ts ./packages/slate/test/write-boundary-contract.ts
285 pass, 0 fail
bun check
biome check green
package/site/root typecheck green
bun test: 1007 pass, 95 skip, 0 fail
slate-react vitest: 20 files passed, 146 tests passed
Decision:
EditorStaticApi remains as an internal implementation type because the
internal runtime/test owner still needs a table-shaped dispatch object.elementReadOnly remains internal runtime policy only; the public schema name
is isReadOnly.shouldMergeNodesRemovePrevNode remains internal merge policy only.Target public shape:
editor.read((state) => {
state.selection.get();
state.nodes.above();
state.schema.isVoid(element);
});
editor.update((tx) => {
tx.nodes.set({ type: "heading" });
tx.selection.collapse({ edge: "end" });
tx.value.replace({ children, selection: null });
});
Keep:
editor.readeditor.updateeditor.subscribeeditor.extendNode, Path, Point, Range, Element, TextisEditor(value) if public runtime checking is neededdefineEditorExtension(...)Cut from public author API:
Editor.* static method namespacetxstate/txCurrent source:
BaseEditor.read, subscribe, update, extend
(.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:480-490).Decision:
editor.read.editor.update.editor.subscribe.editor.extend, but review whether construction-time extension
registration should become the default docs path.Editor.read, Editor.update, Editor.subscribe,
Editor.extend.Editor.*Methods:
isEditordefineEditorExtensionDecision:
Editor.isEditor or Editor.defineEditorExtension.Reason:
state.*Methods:
above, first, getChildren, hasBlocks, hasInlines, hasPath,
hasTexts, isEmpty, last, leaf, levels, next, parent, path,
positions, previous, voidafter, before, edges, point, isEdge, isEnd, isStartbookmark, range, projectRange, unhangRangefragment, getFragmentstringgetOperations, getLastCommit, getPathByRuntimeId, getRuntimeId,
getSelection, getSnapshotisBlock, isElementReadOnly, isInline, isSelectable, isVoidTarget shape:
editor.read((state) => {
state.nodes.above();
state.points.after(at);
state.ranges.get(at);
state.fragment.get({ at });
state.text.string(at);
state.value.operations();
state.value.lastCommit();
state.runtime.idAt(path);
state.runtime.pathOf(runtimeId);
state.runtime.snapshot();
state.schema.isVoid(element);
});
Resolved revisions:
state.schema.isElementReadOnly(element) with
state.schema.isReadOnly(element).state.schema; keep structural predicates under
state.nodes.state.nodes.void(). The namespace makes it clear enough, and the name
stays close to Slate vocabulary.tx.*Methods:
addMark, removeMark, toggleMarkdelete, deleteBackward, deleteForward,
deleteFragment, insertText, insertFragment, insertBreak,
insertSoftBreakcollapse, deselect, move, select, setPoint,
setSelectioninsertNode, insertNodes, liftNodes, mergeNodes, moveNodes,
removeNodes, setNodes, splitNodes, unsetNodes, unwrapNodes,
wrapNodesreplace, resetnormalize, withoutNormalizingTarget shape:
editor.update((tx) => {
tx.marks.add("bold", true);
tx.text.insert("hello");
tx.fragment.insert(fragment);
tx.break.insert();
tx.selection.set(target);
tx.nodes.insert(node);
tx.value.replace({ children, marks: null, selection: null });
tx.normalize();
tx.withoutNormalizing(() => {});
});
Hard cuts:
insertNode / insertNodes split. Use one
tx.nodes.insert(nodes, options).tx.nodes.insertMany unless a later proof shows it has a distinct
semantic. Current live insert and insertMany both accept T | T[]
(.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:213-220), so keeping
both is just API noise.reset; use tx.value.replace.withoutNormalizing(editor, fn); keep only inside tx.Methods:
getOperationDirtinessgetDirtyPathsgetExtensionRegistrypathRef, pathRefspointRef, pointRefsrangeRef, rangeRefsdefineCommandregisterCommandregisterCapabilityregisterNormalizerregisterCommitListenersubscribeSourceisNormalizingsetNormalizingshouldMergeNodesRemovePrevNodeDecision:
Reason:
Replacement shapes:
defineEditorExtension + editor.extend.tx.withoutNormalizing only.Methods:
elementReadOnlyDecision:
Replacement:
editor.read((state) => {
state.nodes.above({
match: (node) => Element.isElement(node) && state.schema.isReadOnly(node),
});
});
Reason:
nodes.above plus schema behavior.EditorStaticApi method coverageEvery current static method is accounted for below.
| Decision | Methods |
|---|---|
| keep instance, cut static wrapper | read, update, subscribe, extend |
| keep named export, cut static wrapper | isEditor, defineEditorExtension |
move to state.nodes | above, first, getChildren, hasBlocks, hasInlines, hasPath, hasTexts, isEmpty, last, leaf, levels, next, parent, path, positions, previous, void |
move to state.points | after, before, point, isEdge, isEnd, isStart |
move to state.ranges | bookmark, edges, range, projectRange, unhangRange |
move to state.fragment | fragment, getFragment |
move to state.text | string |
move to state.value | getOperations, getLastCommit |
move to state.runtime | getPathByRuntimeId, getRuntimeId, getSelection, getSnapshot |
move to state.schema | isBlock, isElementReadOnly -> isReadOnly, isInline, isSelectable, isVoid |
move to tx.marks | addMark, removeMark, toggleMark |
move to tx.text | delete, deleteBackward, deleteForward, insertText |
move to tx.fragment | deleteFragment, insertFragment |
move to tx.break | insertBreak, insertSoftBreak |
move to tx.selection | collapse, deselect, move, select, setPoint, setSelection |
move to tx.nodes | insertNode, insertNodes, liftNodes, mergeNodes, moveNodes, removeNodes, setNodes, splitNodes, unsetNodes, unwrapNodes, wrapNodes |
move to tx.value | replace, reset |
move to tx control | normalize, withoutNormalizing |
| internal runtime | getOperationDirtiness, getDirtyPaths, getExtensionRegistry, pathRef, pathRefs, pointRef, pointRefs, rangeRef, rangeRefs, defineCommand, registerCommand, registerCapability, registerNormalizer, registerCommitListener, subscribeSource, isNormalizing, setNormalizing, shouldMergeNodesRemovePrevNode |
| cut | elementReadOnly |
Keep EditorTransformApi internal. Its comment already says normal public
writes go through editor.update((tx) => ...)
(.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:493-497).
Hard rule:
EditorTransformApi is runtime-owned implementation, not public architecture.
Target:
This plan is core API first, but React fallout matters.
Target:
EditorCommit dirtiness, not broad Editor.*
snapshot helpers.editor.read / editor.update.Performance lens:
editor.update, not direct static wrappersPlate should migrate to:
editor.extend(...) or construction-time extension registrationstate.<plugin> read namespacestx.<plugin> write namespacesPlate should not migrate by wrapping every removed static method with a Plate compatibility shim. That would recreate the junk drawer one layer up.
Collaboration needs:
tx.operations.replayThe plan must prove:
Editor.applyPathRef/PointRef
sets| Risk | Required proof |
|---|---|
| static API leaks after cut | public export contract rejects EditorStaticApi value exports |
| docs keep teaching old API | docs grep for Editor. write helpers is empty outside migration/internal notes |
| examples regress | site examples compile and browser smoke through editor.read/update |
| transforms lose behavior | unit tests for mark/text/fragment/selection/node transforms through tx.* |
| replace/reset break fixtures | public fixtures use tx.value.replace; internal fixtures use test helper |
| schema predicates drift | tests for schema.isVoid, schema.isInline, schema.isSelectable, schema.isReadOnly |
| merge policy hidden break | merge-nodes tests cover previous-node removal without public helper |
| refs cut breaks anchors | bookmark/runtime-id tests cover durable anchors through operations |
| collab replay breaks | replay operations through tx.operations.replay with commit metadata |
| extension migration fails | extension namespace tests for state.<name> and tx.<name> |
This hard cut is mostly core API, but browser proof still matters because selection and DOM import/export call core methods.
Required browser families:
tx.text.insert| Lens | Status | Finding | Plan delta |
|---|---|---|---|
vercel-react-best-practices | applied | Avoid broad render-time reads and repeated subscriptions. | React fallout section requires selector-backed reads. |
performance-oracle | applied | Public APIs should not force O(n) or full-snapshot paths into hot render loops. | Internalize dirty/runtime helpers; use commit metadata. |
performance | applied | Repeated-unit budgets and INP rows are needed before closure. | Score stays pending; proof matrix names repeated-unit risk. |
tdd | applied | Behavior proof should go through public API, not removed helper existence tests. | Proof matrix uses tx/state behavior contracts. |
build-web-apps:shadcn | skipped | No UI/component API is being designed in this pass. | No change. |
react-useeffect | skipped | No effect implementation shape is being changed yet. | Revisit during React selector implementation. |
Trigger:
Blast radius:
packages/slatepackages/slate-reactpackages/slate-domFailure scenarios:
Proof expansion:
Verdict:
Hard cuts:
Editor.* value methodselementReadOnlyshouldMergeNodesRemovePrevNodesetNormalizingpathRefs / pointRefs / rangeRefsresetRejected:
Editor.*Editor.nodes(editor, ...) style is familiar and
easy to grep.Editor.foo(editor, ...) maps to state.* or tx.*.keep.Editor value entirelyEditor from slate.Editor as a familiar namespace could make the
migration feel less abrupt.Editor
namespace is a zombie API. It keeps autocomplete noise and invites static
helper growth again.'Editor' in Slate
is false; the remaining Editor.* use is internal/test-heavy, not public
authoring-heavy.Editor.isEditor and a few blessed statics.
Rejected because it preserves namespace ambiguity.createEditor, instance read/update, and
named pure helpers. Runtime/tests import Editor from slate/internal.Editor; internal docs/tests
explicitly use slate/internal.tx.operations.replay and commit metadata.keep.elementReadOnlynodes.above plus schema
predicate, and the name is awful.readOnlyElement. Rejected because the
helper still duplicates query composition.state.nodes.above with state.schema.isReadOnly.keep.shouldMergeNodesRemovePrevNodekeep.state.ranges.bookmark; current runtime
ref use is in transforms, DOM/browser handlers, Android input, selection
reconciler, and internal tests.keep.editor.update; command catalogs are app/product ergonomics. Making commands
core would recreate a Tiptap-shaped surface.Editor.registerCommand and Editor.defineCommand hits are
tests only; slate/internal already exports command registry helpers.editor.commands or Editor.registerCommand.
Rejected because it makes commands a second mutation lifecycle.editor.update; raw extension runtime can keep internal middleware.editor.read and
editor.update, not registry mutation.keep.isElementReadOnly.isReadOnly loses the word Element.state.schema; every predicate there is about element behavior. Repeating
Element is noise.isElementReadOnly; current
elementReadOnly helper is only runtime use.isElementReadOnly. Rejected as legacy wording.state.schema.isElementReadOnly(element) becomes
state.schema.isReadOnly(element).state.nodes.above.readOnly; collab treats it as
schema behavior, not operation data.keep.| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| Current-state read and initial score | complete | live .tmp/slate-v2 editor interfaces, public-state runtime, extension runtime; compiled research; local Lexical/ProseMirror/Tiptap greps | initial hard-cut matrix for all 99 methods | need import census and detailed ledger closure | slate-ralplan |
| Intent/boundary and decision brief | complete | user request + live source mismatch | intent/non-goals/decision boundaries added | none after method-census pass | slate-ralplan |
| Research/live-source refresh | complete for pass 1 | research pages and local source greps | Lexical/PM/Tiptap evidence recorded | no research page update needed yet | slate-ralplan |
| Performance/DX/migration/regression pressure | complete for planning | 1590-file source/docs/example census; 2284 Editor.* hits bucketed by docs/site/runtime/test | confirms public teaching surface is already state/tx; static namespace is internal/test debt | implementation still needs batch red contracts | slate-ralplan |
| Maintainer objection ledger | complete for planning | public Editor value, command helpers, ref sets, read-only predicate rows closed | no row remains revise | closure score still below threshold | slate-ralplan |
| High-risk deliberate pass | complete for planning | pre-mortem plus concrete batch gates | implementation must stay batched | closure pass must decide readiness | slate-ralplan |
| Method census and objection closure | complete | live counts for Editor.*, sensitive method families, existing public-surface/state-tx/write-boundary tests | public Editor value cut decided; state.nodes.void kept; editor.extend kept | no user decision open | slate-ralplan |
| Closure score and implementation readiness | pending | none | none | score below threshold; final handoff not written | slate-ralplan |
Added:
EditorStaticApi methodselementReadOnlyshouldMergeNodesRemovePrevNodeEditor value hard-cut decisionisElementReadOnly -> isReadOnlyDropped:
Strengthened:
Editor.* as internal/test debt, not public
migration debtOpen:
0.92 or identify the
exact implementation proof needed before code cuts.Decision would change if:
state/txtx.operations.replayStatus: complete for planning.
ralph executionEditor.*Status: next.
EditorStaticApi value methods are not public final APIEditor value does not leak from slateisEditor and defineEditorExtensionstate/txelementReadOnlyshouldMergeNodesRemovePrevNodedone only after these passrg -n "Editor\\." .tmp/slate-v2/docs .tmp/slate-v2/site .tmp/slate-v2/packages
must only show internal/test-allowed rows after docs migration.bun check in .tmp/slate-v2 before closure.EditorStaticApi leaks again.When ready, final handoff must list:
statetxThis plan is not done until:
>= 0.920.85ralphactive goal state points to the next review/implementation passEditor.replace-like APIsStatus: done.
Trigger:
Editor.replace should be hard-cut because Lexical does
not expose an Editor.* namespace, then clarified that the pass must cover
every similar API, not only replacement.Fresh live-source read:
slate root exports createEditor, top-level isEditor, type-only
Editor, and type-only state/tx groups; it does not export a public
Editor value (.tmp/slate-v2/packages/slate/src/index.ts:1-90).BaseEditor is already the right small instance spine:
read, subscribe, update, extend
(.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:480-490).EditorCoreStateView and EditorCoreUpdateTransaction are already the
public read/write grouping target
(.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:445-475).EditorTransformApi is documented as internal runtime transform API; normal
writes belong in editor.update((tx) => ...)
(.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:493-608).EditorStaticApi still contains the old mixed namespace for internal/tests:
reads, writes, runtime metadata, refs, extension plumbing, lifecycle wrappers,
replace, reset, elementReadOnly, and
shouldMergeNodesRemovePrevNode
(.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:1113-1704).InternalEditor implements those wrappers and still exports them as
Editor from interfaces/editor; this remains internal/friend surface, not
public root API (.tmp/slate-v2/packages/slate/src/interfaces/editor.ts:1720-2190).Editor
value, transform namespaces, instance replace / reset, direct instance
read aliases, and public docs/examples teaching internal Editor snapshot/ref
helpers (.tmp/slate-v2/packages/slate/test/public-surface-contract.ts:70-420).DOMEditor / ReactEditor values are not normal public root APIs:
slate-dom exports DOMEditor as a type and exposes withDOM; slate-react
exports ReactEditor as a type and withReact
(.tmp/slate-v2/packages/slate-dom/src/index.ts:1-7,
.tmp/slate-v2/packages/slate-react/src/index.ts:106-108).editor.dom.*, but its capability is still implemented by delegating through
the internal static DOMEditor.* table
(.tmp/slate-v2/packages/slate-dom/src/plugin/dom-editor.ts:57-119,
.tmp/slate-v2/packages/slate-dom/src/plugin/dom-editor.ts:1398-1464).LexicalEditor.update as the only safe mutation callback and
setEditorState as an explicit whole-state setter; it does not expose a
Slate-style Editor.replace(editor, ...) namespace
(../lexical/packages/lexical/src/LexicalEditor.ts:1301-1340,
../lexical/packages/lexical/src/LexicalEditor.ts:1386-1390,
../lexical/packages/lexical/src/index.ts:140-176).Decision:
Node, Path, Point, Range, Operation,
Element, Text, and Scrubber.editor.read, editor.update,
editor.subscribe, editor.extend.createEditor, defineEditorExtension, isEditor,
elementProperty.tx.value.replace(...) as the only public whole-document replacement.Editor.replace and Editor.reset; keep only an internal
replaceSnapshot(editor, input) primitive and a test helper for seeding.Editor.read, Editor.update,
Editor.subscribe, Editor.extend; the instance methods are the API.addMark, removeMark, toggleMark, insertText, insertFragment,
insertNode(s), delete*, move*, mergeNodes, splitNodes, setNodes,
unsetNodes, wrapNodes, unwrapNodes, select, collapse, deselect,
setSelection, setPoint, normalize, withoutNormalizing.
Target: editor.update((tx) => tx.<group>...).above, after, before, edges, first, last, leaf, levels,
next, previous, parent, path, point, range, positions,
string, fragment, unhangRange, projectRange.
Target: editor.read((state) => state.<group>...).getChildren, getSelection, getFragment, getSnapshot,
getOperations, getLastCommit, getRuntimeId, getPathByRuntimeId,
getDirtyPaths, getOperationDirtiness.
Target: state.value, state.selection, state.fragment,
state.runtime, or internal runtime only.pathRef, pointRef, rangeRef, pathRefs, pointRefs, rangeRefs.
Target: internal runtime refs; public durable range story is bookmarks and
runtime ids, not live mutable handles.defineCommand, registerCommand, registerCapability,
registerNormalizer, registerCommitListener, getExtensionRegistry.
Target: internal/friend runtime plus defineEditorExtension and
editor.extend.elementReadOnly, isElementReadOnly, setNormalizing, isNormalizing,
and shouldMergeNodesRemovePrevNode.
Target: state.schema.isReadOnly, tx.withoutNormalizing, internal
normalizer/merge policy.editor.dom.* after withDOM / withReact;
DOMEditor.* and ReactEditor.* remain package-internal implementation
tables until migrated or hidden behind internal entrypoints.Hard rule:
If a helper needs an editor instance and can observe or mutate editor state,
it does not belong in a public static namespace.
Allowed exceptions:
Path.next(path), Range.edges(range), Node.string(node), etc.isEditor(value) stays public because it is a pure guard and avoids
keeping a public Editor value alive.Lexical consistency verdict:
editor.update(...);
full-state replacement is explicit instance API (setEditorState) rather than
a broad static namespace.$ helpers, or command bus as
public authoring API.Implementation owner for a later ralph run:
slate-dom and slate-react so
public root APIs do not export DOMEditor / ReactEditor values.replaceEditorValue(editor, input) implemented through
editor.update((tx) => tx.value.replace(input)).Editor.replace; keep direct internal
primitive tests only for replaceSnapshot / internal Editor behavior.Editor.replace shortcuts with
editor.update((tx) => tx.value.replace(...)) where not already inside a
transaction.Editor.* reads/writes to direct runtime modules,
state/tx groups, or editor.dom.* capabilities by family.Node / Path / Point / Range / Operation namespaces
untouched.Proof gates for execution:
packages/slate/test/public-surface-contract.ts remains green.DOMEditor / ReactEditor values.state-tx-public-api-contract.ts proves every public read/write family has a
state/tx route.tx.value.replace.bun check passes in .tmp/slate-v2.Decision status:
Editor.replace is not special. It is one member
of a broader class of stateful static editor helpers that should not be public
API.Status: done.
Scope executed:
slate-dom public surface coverage so the package root rejects a
runtime DOMEditor value while keeping withDOM.slate-react surface coverage so the package root rejects runtime
ReactEditor / DOMEditor values while keeping withReact.Editor.replace(...) shortcuts:
initialValue through
editor.update((tx) => tx.value.replace(...)).replaceSnapshot(...) primitive instead of the static
Editor.replace(...) wrapper.replaceEditorValue(editor, input) test support, implemented through
editor.update((tx) => tx.value.replace(input)).Editor.replace(...) seed boilerplate.Editor.elementReadOnly(...),
Editor.shouldMergeNodesRemovePrevNode(...),
Editor.isNormalizing(...), and Editor.setNormalizing(...).Editor.isEditor(...), Editor.subscribe(...),
Editor.getOperations(...).Files changed:
.tmp/slate-v2/packages/slate-dom/test/public-surface-contract.ts.tmp/slate-v2/packages/slate-dom/test/public-surface-contract.test.ts.tmp/slate-v2/packages/slate-react/test/surface-contract.tsx.tmp/slate-v2/packages/slate-react/src/components/slate.tsx.tmp/slate-v2/packages/slate-react/src/editable/browser-handle.ts.tmp/slate-v2/packages/slate-react/src/editable/editing-kernel.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-kernel-trace.ts.tmp/slate-v2/packages/slate-react/src/editable/runtime-selection-engine.ts.tmp/slate-v2/packages/slate/src/transforms-text/insert-text.ts.tmp/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts.tmp/slate-v2/packages/slate/src/transforms-node/merge-nodes.ts.tmp/slate-v2/packages/slate/src/editor/insert-text.ts.tmp/slate-v2/packages/slate/src/editor/normalize.ts.tmp/slate-v2/packages/slate/src/editor/without-normalizing.ts.tmp/slate-v2/packages/slate/test/support/snapshot.ts.tmp/slate-v2/packages/slate/test/test-helper-boundary-contract.ts.tmp/slate-v2/packages/slate/test/state-tx-public-api-contract.ts.tmp/slate-v2/packages/slate/test/read-update-contract.tsSource inventory after execution:
rg "\\bEditor\\.(read|update|subscribe|extend|replace|reset|getOperations|elementReadOnly|shouldMergeNodesRemovePrevNode|setNormalizing|isNormalizing)\\("
.tmp/slate-v2/packages/slate/src
.tmp/slate-v2/packages/slate-dom/src
.tmp/slate-v2/packages/slate-react/src
0 matches
Verification:
bun test ./packages/slate-dom/test/public-surface-contract.tsbun test --preload ../../config/bun-test-setup.ts test/surface-contract.test.tsxbun test ./packages/slate/test/test-helper-boundary-contract.tsbun test ./packages/slate/test/state-tx-public-api-contract.tsbun test ./packages/slate/test/read-update-contract.tsbun test ./packages/slate/test/transforms-contract.tsbun test ./packages/slate/test/normalization-contract.tsbun --filter slate typecheckbun --filter slate-dom typecheckbun --filter slate-react typecheckbun lint:fixbun checkFresh full-gate result:
bun check
biome check: pass
packages typecheck: 6 successful
site/root typecheck: pass
bun test: 1007 pass, 95 skip, 0 fail
slate-react vitest: 20 files passed, 147 tests passed
Browser proof:
http://localhost:3102/examples/mentions.data: URL safety block. No alternate browser
workaround was used.Decision: