docs/plans/2026-04-29-slate-v2-absolute-architecture-review-plan.md
Date: 2026-04-29 Status: done Review mode: slate-ralplan Current pass: Pass 8 closure score and final gates complete Current score: 0.936 Execution status: complete Execution current phase: Public Editor/static namespace hard cut complete
Hard cut the public Editor value export.
The current live source already cut primitive writers from the editor instance,
but export const Editor: EditorInterface still keeps a second public editor
API alive. That value mixes editor-state reads, writes, extension registration,
snapshot replacement, setup helpers, and type plumbing in one namespace. That
is not absolute architecture. It is legacy Slate gravity with better naming.
Target:
import { createEditor, isEditor } from "slate";
import type { Editor } from "slate";
editor.read((state) => {
state.selection.get();
state.text.string([]);
});
editor.update((tx) => {
tx.nodes.set(props, { at: target });
});
Normal public API:
Editor remains a type only.editor.read((state) => ...) is the editor-state read path.editor.update((tx) => ...) is the write and tx-local read path.editor.update((tx) => tx.value.replace(input)).Node, Path, Point, Range, Element,
Text, and similarly pure helpers.isEditor(value) is top-level if users need a public predicate.defineEditorExtension(...)
and editor.extend(...).Hard cut:
export const EditorEditorInterfacegetEditorTransformRegistry / setEditorTransformRegistryEditor.* editor-state reads and writeseditor.replace / editor.reset as normal app APIsstate / tx group existsEditor.* as normal app codeInternal code can still have an implementation table, but it must live behind
an internal module boundary. A public static object named Editor should not
survive an unpublished hard-cut rewrite.
This plan is complete. The review lane closed, the implementation lane shipped
the hard cut in .tmp/slate-v2, and the completion gate is green.
Intent:
Desired outcome:
Editor type.Editor value.read / update, state / tx, and pure
data namespaces.editor.update((tx) => tx.value.replace(input)).In scope:
/Users/zbeyens/git/slate-v2/packages/slate/srcEditorInterface and Editor static value usageEditor as a valuetypeof Editor.*Non-goals:
editor.refs proposalNode, Path, Range, Point,
Element, or TextDecision boundaries:
Editor value hard cut.Unresolved user-decision points:
Editor value;
this plan can decide that without another question.Principles:
Top drivers:
Editor.string(editor, []),
editor.string([]), and editor.read((state) => state.text.string([])).Viable options:
| Option | Pros | Cons | Verdict |
|---|---|---|---|
Keep public Editor value | closest to legacy Slate; smallest migration delta | preserves a second public read/write path; keeps huge namespace; conflicts with state / tx doctrine | reject |
Split into EditorQuery / EditorTransform static namespaces | clearer than one giant Editor; partial migration story | still creates parallel static read/write APIs and more names to teach | reject |
Cut public Editor value; keep internal implementation table | one public lifecycle; type-only Editor; clean docs; internal code can migrate in phases | larger source/test migration; fixture helpers need replacement | choose |
| Cut every instance query too in the same pass | most radical cleanup | too much blast radius for the namespace pass; risks conflating lifecycle cleanup with query-surface design | defer |
Chosen option:
Editor value.Editor as a type.state and tx.tx.value.replace(input), not Editor.replace or
public editor.replace.Rejected alternatives:
Editor.* static reads as "legacy-compatible but documented advanced" is
rejected. It still teaches the wrong shape.editor.api / editor.tf is rejected for raw Slate. It is Plate-shaped and
splits read freshness inside updates.editor.commands is rejected for core Slate. It is product-DX sugar.Consequences:
typeof Editor.* need per-function or internal type
sources.editor.replace / editor.reset callsites need either
tx.value.replace(input) or non-public test seeding helpers.read and update.Follow-ups:
state / tx.| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.90 | The React evidence says React 19.2 helps projection and external-store scheduling but does not replace editor-owned dirty-node/runtime invalidation in docs/research/sources/editor-architecture/react-19-2-external-store-and-background-ui.md:57. Cutting the public Editor value avoids broad static editor access in hot React code, but this pass does not change render runtime. |
| Slate-close unopinionated DX | 0.20 | 0.87 | The accepted naming decision says public lifecycle is editor.read((state) => ...) and editor.update((tx) => ...) in docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md:27; live source still exports EditorInterface and Editor value in /Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:798 and :1345. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.86 | Extension namespaces on state and tx are the accepted migration backbone in docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md:60; the current public static value still mixes extension registration and editor-state helpers in /Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:1215. Needs next-pass proof rows for plugin and collab substrate impact. |
| Regression-proof testing strategy | 0.20 | 0.86 | Existing hard-cut tests prove instance primitive writers and stale state mirrors are gone in /Users/zbeyens/git/slate-v2/packages/slate/test/public-field-hard-cut-contract.ts:33 and runtime absence checks at :120; state/tx contracts prove grouped reads/writes in /Users/zbeyens/git/slate-v2/packages/slate/test/state-tx-public-api-contract.ts:29 and :51. New guards are still needed for no public Editor value export and no public transform-registry export. |
| Research evidence completeness | 0.15 | 0.88 | Full corpus evidence exists for Lexical, ProseMirror, and Tiptap in docs/research/sources/editor-architecture/read-update-runtime-corpus-ledger.md:25, :59, and :95; this pass corrected stale primitive-method wording in docs/research/decisions/slate-v2-architecture-verdict-after-human-stress-sweep.md and docs/research/decisions/slate-v2-read-update-runtime-architecture.md. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.87 | The plan keeps app-visible API surfaces small and composable: type-only Editor, pure data namespaces, state / tx, and extension groups. Runtime-owned render-shell DX remains governed by docs/research/decisions/editor-node-dx-should-use-runtime-owned-shells-and-spec-first-renderers.md:19, but this pass does not execute that render API. |
Weighted total after Pass 1: 0.874.
Completion threshold is not met:
0.92Status: complete for Pass 2 only. Completion remains pending.
Pass 2 pressure-tested the two unresolved boundaries from Pass 1:
Verdict:
Editor.replace and Editor.reset die with the public Editor value.editor.replace and editor.reset are not normal app-author APIs.editor.update((tx) => tx.value.replace(input)).state and tx groups where those groups exist.Why this is the right boundary:
Editor.replace or
editor.replace repeats the same mistake as primitive mutation helpers:
mutation outside the transaction vocabulary.createTestEditor(input) or internal seedEditor(editor, input) helper is
cleaner than preserving public replacement helpers for fixture convenience.after, before, range, string, and above are real
Slate vocabulary, but teaching them through static Editor.* or instance
methods keeps too many read paths alive. The final docs target is grouped
state/tx reads.Updated score: 0.886.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.90 | No React runtime code changes are planned in Pass 2. The boundary still supports narrow reads because app docs route through state/tx rather than static editor namespace. |
| Slate-close unopinionated DX | 0.20 | 0.89 | BaseEditor currently exposes replace, reset, and many query methods at /Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:271 and :313; Pass 2 decides these are not the normal app-doc path. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.88 | Full-document replacement maps to transaction metadata already represented by replaceSnapshot and reason: 'replace' in /Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts:1717; public collab replay remains tx.operations.replay(...). |
| Regression-proof testing strategy | 0.20 | 0.88 | Existing tests heavily use Editor.replace for seeding, including /Users/zbeyens/git/slate-v2/packages/slate/test/state-tx-public-api-contract.ts:17; Pass 2 adds explicit test-helper and tx-value replacement proof requirements instead of leaving fixture convenience implicit. |
| Research evidence completeness | 0.15 | 0.88 | No new external research was needed for this boundary pass; the current state/tx decision remains the naming authority. Pass 3 still must re-read the research/live-source layer before closure scoring can rise. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.88 | Pass 2 reduces normal app-facing choices: no static Editor, no normal editor.replace, no docs-first instance query path when a state/tx group exists. |
Weighted total: 0.886.
Plan delta from Pass 2:
tx.value.replace(input) as the public full-document replacement shape.editor.replace / editor.reset from normal app API target.Next owner:
.tmp/slate-v2/packages/slate: introduce the
non-public replacement/test-helper substrate and internal transform-registry
boundary before cutting the public Editor value.active goal state back to pending.active goal state for execution instead of review..tmp/slate-v2/packages/slate/test/public-surface-contract.ts.Editoreditor.replace / editor.resetexport interface EditorInterface or export const Editor in public
sourcebun test ./packages/slate/test/public-surface-contract.ts from
.tmp/slate-v2.tx.value.replace(input) and state/tx contract coverage.Editor value export, public EditorInterface name,
public transform-registry exports, and instance editor.replace /
editor.reset.slate/internal and the
slate-react runtime facade.Editor.* state/write
teaching and stale onValueChange / onSelectionChange docs.bun test ./packages/slate/test/state-tx-public-api-contract.ts ./packages/slate/test/public-surface-contract.ts
passed with 163 pass.bun check passed; bun test reported 1007 pass, 95 skip, 0 fail;
slate-react vitest reported 19 passed, 113 tests passed.config/bun-test-setup.ts compatibility bridge that rewrote
legacy fixture imports from slate to slate/internal./Users/zbeyens/git/slate.slate/internal imports where they exercise internal Editor helpers.bun test ./packages/slate/test/public-surface-contract.ts ./packages/slate/test/index.spec.ts ./packages/slate-hyperscript/test/index.spec.ts ./packages/slate-history/test/index.spec.ts ./packages/slate/test/escape-hatch-inventory-contract.ts
passed with 1167 pass, 95 skip, 0 fail.bun check passed; bun test reported 1007 pass, 95 skip, 0 fail;
slate-react vitest reported 19 passed, 113 tests passed.Status: complete for Pass 3 only. Completion remains pending.
What changed:
state / tx naming with the public
Editor value hard-cut consequencetx.value.replace is a target API and is not implemented yetLive-source findings:
BaseEditor still exposes overrideable query helpers, replace, reset,
subscribe, extend, and lifecycle methods in
/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:229.EditorInterface starts at
/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:798
and still includes editor-state reads, primitive writes, extension
registration, replacement, reset, subscribe, and update.export const Editor: EditorInterface still exists at
/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:1345.packages/slate/src/core/index.ts publicly exports
./transform-registry, and that file exports
getEditorTransformRegistry / setEditorTransformRegistry.getUpdateView currently gives tx.value.get() but not
tx.value.replace(...) in
/Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts:756
and :780.replaceSnapshot already has the right internal replacement substrate:
transaction authority replace, runtime-id reseeding, selection reset, and
marks reset in
/Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts:1717.Editor.* in user paths, including
saving, collaboration subscription, markdown shortcuts, hovering toolbar,
forced layout, review comments, and generated example state setup.Research findings:
docs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.md
remains the naming authority: public reads are state, public writes are
tx, and extension namespaces attach to state.<plugin> and tx.<plugin>.Editor value hard-cut target and says
tx.value.replace is required implementation work, not current capability.docs/research/decisions/slate-v2-read-update-runtime-architecture.md now
includes transaction-owned value replacement in the public API target.docs/research/sources/editor-architecture/read-update-runtime-corpus-ledger.md
still covers the needed external systems: Lexical for read/update lifecycle,
ProseMirror for transaction-owned state/selection/metadata, and Tiptap for
extension/product-DX pressure.Updated score: 0.899.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.90 | No React runtime changes enter this pass. The source refresh still supports narrow lifecycle access instead of static editor reads in render paths. |
| Slate-close unopinionated DX | 0.20 | 0.90 | Research now says type-only Editor, state / tx, pure data namespaces, and tx.value.replace; live docs/examples still need migration away from Editor.*. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.90 | Existing migration-backbone tests prove extension namespaces and operation replay, but still use Editor for fixture reads/seeding. The plan now calls that out as migration debt rather than acceptable public API. |
| Regression-proof testing strategy | 0.20 | 0.89 | The live grep shows broad Editor.* use in tests/docs/examples, so guard coverage must include export tests, docs/example grep guards, and seed helper migration. |
| Research evidence completeness | 0.15 | 0.91 | The compiled state/tx and read/update decisions were refreshed, and the existing full corpus ledger remains sufficient for this specific namespace cut. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.89 | Pass 3 narrows author-facing choices further but does not touch render components or hooks. |
Weighted total: 0.899.
Plan delta from Pass 3:
tx.value.replace(input) as target APItx.value.replace is not yet implementedEditor.* migration as an explicit closure blockerStatus: complete for Pass 4 only. Completion remains pending.
Hard pressure verdict:
Editor value is a DX and architecture win, not a direct
runtime speed win. Do not sell it as perf magic.Editor.* and instance replacement helpers. A hidden public escape hatch
would make the cleanup mostly branding.Editor.* / value-import hits, and the queried test paths had
930 hits for Editor.replace, Editor.getSnapshot, Editor.string,
runtime-id reads, and transform-registry access.Performance pressure:
tx.value.replace(input) should delegate to the existing replaceSnapshot
substrate or a shared internal replacement helper. It must not route through a
public Editor.replace shim.tx.value.replace(input) should preserve the current replacement behavior:
clone input children, reseed runtime ids from the previous index, set
selection and marks, and publish a replacement commit.getStateView / getUpdateView currently allocate and freeze grouped API
objects per read/update. This is acceptable for the namespace plan only if the
implementation does not add per-node/per-operation wrapper allocation on top.
If a later benchmark shows this view construction is hot, cache stable group
wrappers by editor/version in the runtime, not in React components.Editor.* reads inside render paths, but the
public namespace cut does not require React code edits.DX pressure:
editor.read((state) => ...) and editor.update((tx) => ...).editor.subscribe(...) as an advanced runtime/collab bridge, but use
the instance method directly. Do not keep Editor.subscribe(editor, ...).editor.extend(...) and defineEditorExtension(...) for extension
registration. Do not move extension registration into a static Editor
namespace.Node, Path, Point, Range,
Element, or Text would be fake consistency and worse DX.Migration pressure:
createTestEditor(input), seedEditor(editor, input), and
snapshotOf(editor) or equivalent non-public test utilities.tx.value.replace(input) contract tests.Editor.replace, editor.replace, Editor.getSnapshot, Editor.string,
Editor.subscribe, Editor.bookmark, and predicate reads to state / tx,
pure data helpers, or direct advanced instance bridge calls.EditorRuntime, EditorStatic, EditorQuery, or
compatibility aliases. That is the same public shape in a fake mustache.Regression pressure:
Editor, while import type { Editor } remains valid.editor.update((tx) => tx.value.replace(input)) updates children, selection,
marks, runtime ids, commit reason/classes/tags, and subscriber output.editor.replace and editor.reset
are not public app-author methods.getEditorTransformRegistry / setEditorTransformRegistry are unavailable
from public package entrypoints.slate-browser generated
rows; keep bun test:integration-local as closure/release proof, not the
first iteration gate.Simplicity pressure:
Updated score: 0.910.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.91 | React 19.2 research says external-store subscriptions and background UI help projection, but core invalidation remains Slate-owned. Pass 4 keeps the namespace cut out of React hot paths. |
| Slate-close unopinionated DX | 0.20 | 0.91 | The plan keeps Slate data helpers and read/update lifecycle while rejecting static editor-state helpers, product command sugar, and compatibility namespaces. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.91 | The plan keeps extension namespaces, commit metadata, deterministic operations, and tx.operations.replay(...), and explicitly rejects current-version adapter promises. |
| Regression-proof testing strategy | 0.20 | 0.91 | Pass 4 names export, tx replacement, write boundary, transform-registry, docs/example grep, and focused browser proof gates. |
| Research evidence completeness | 0.15 | 0.91 | Pass 4 relies on refreshed state/tx and React 19.2 research plus live source/test/docs grep evidence; no new corpus gap was found. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.90 | Public author choices are reduced to lifecycle, pure data helpers, extension registration, and advanced runtime subscription. Render/component DX remains out of scope. |
Plan delta from Pass 4:
tx.value.replace and state/tx view
allocationeditor.subscribe as an advanced instance bridge, not a static
namespace survivorStatus: complete for Pass 5 only. Completion remains pending.
Ledger verdict:
Editor value cut.EditorInterface cut.tx.value.replace(input) replacement target.editor.subscribe(...) as an advanced instance bridge for adapters.
Static Editor.subscribe(editor, ...) dies.Accepted steelman rows:
Cut public Editor value.
Editor.* is the most Slate-looking API in the repo.
Cutting it makes migration feel less like Slate and more like a new editor.Editor.string(editor, range).Editor as read-only. That still leaves two
read paths and keeps static editor-state access alive.state / tx optional theater.Editor.string(editor, at) becomes
editor.read((state) => state.text.string(at)); writes become
editor.update((tx) => ...); pure data namespaces remain.Cut public EditorInterface.
EditorStateView and
EditorUpdateTransaction augmentation instead of dumping methods on the
editor/static namespace.state.<plugin> and tx.<plugin>, not a global editor bag.Editor plus state/tx extension group
augmentation types.Editor value import.Hide transform registry.
tx.tx; core can
still import an internal registry.getEditorTransformRegistry / setEditorTransformRegistry.Keep pure data namespaces.
Editor but keeping Node / Range /
Path is inconsistent.state / tx. That makes
pure data utilities harder to use and couples them to editor runtime.Cut public replacement helpers.
editor.update((tx) => tx.value.replace(input)) is longer.editor.replace(input) is great fixture and load-state DX.editor.replace as advanced. That preserves a
write outside the write vocabulary.tx.value.replace; tests use non-public
seed helpers.Instance query methods.
editor.string([]) is shorter and more Slate-like
than editor.read((state) => state.text.string([])).state / tx; any remaining direct
instance queries must be explicitly advanced/internal until a focused
query-surface pass removes or groups them.Keep editor.subscribe(...) as an advanced bridge.
Editor.subscribe dies, why keep instance
subscription at all?onChange React prop is nicer for app authors.editor.subscribe.editor.subscribe without
Editor value import.No compatibility namespace.
EditorStatic or EditorCompat would
make migration safer.Editor.Updated score: 0.920.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.91 | Ledger keeps React out of the editor engine and treats namespace cleanup as architecture/DX, not a render-speed claim. |
| Slate-close unopinionated DX | 0.20 | 0.93 | Ledger keeps Slate model/data helpers while cutting only editor-state static value and wrong write paths. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.93 | Ledger preserves extension namespaces, editor.subscribe for adapters, deterministic operations, and no current-version adapter promise. |
| Regression-proof testing strategy | 0.20 | 0.92 | Ledger ties each hard cut to export/type/behavior/grep contracts and fixture-helper migration. |
| Research evidence completeness | 0.15 | 0.91 | No new research gap; ledger is grounded in refreshed state/tx decisions and live source/test/docs evidence. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.92 | Ledger rejects compatibility namespaces and command/chain sugar, keeping the public surface small. |
Plan delta from Pass 5:
Editor value, EditorInterface, transform registry,
replacement-helper, pure-data-namespace, subscribe-bridge, and no-compat
decisionsStatus: complete for Pass 6 only. Completion remains pending.
High-risk trigger:
Editor.* callsitesBlast radius:
/Users/zbeyens/git/slate-v2/packages/slate/src/Users/zbeyens/git/slate-v2/packages/slate/test/Users/zbeyens/git/slate-v2/packages/slate-dom/test/Users/zbeyens/git/slate-v2/packages/slate-react/test/Users/zbeyens/git/slate-v2/site/examples/ts/Users/zbeyens/git/slate-v2/docsPre-mortem:
Export/type churn becomes chaotic.
Editor value breaks hundreds of tests, and the fix is
a rushed EditorCompat namespace.tx.value.replace corrupts runtime identity or commit metadata.
replaceSnapshot and prove children, selection, marks, runtime ids,
reason/classes/tags, and subscriber output.Collaboration/adapters lose a clean observation path.
Editor.subscribe gets conflated with killing
subscription entirely, so persistence/collab examples move into React-only
props.editor.subscribe as the advanced headless adapter
bridge and prove operation replay plus subscriber metadata without any
public Editor value import.Expanded proof plan:
Editorimport type { Editor } worksEditorInterfaceeditor.replace / editor.resetEditorRuntime,
EditorStatic, EditorQuery, or EditorCompateditor.read((state) => ...) covers value, selection, marks, text, nodes,
points, ranges, and schemaeditor.update((tx) => ...) covers tx-local reads and writestx.value.replace(input) preserves replacement semantics and commit
metadataeditor.subscribe observes commits without static Editortx.operations.replay(...) remains deterministic for collab importEditor.replace /
Editor.getSnapshotstate / tx, pure data helpers, or explicit advanced
instance bridgesEditor.* editor-state reads/writestx.value.replaceeditor.subscribeeditor.update((tx) => ...)slate-browser contract rowsbun test:integration-local as closure/release gate onlyRollback/hard-cut answer:
Editor value.tx.value.replace substrateHigh-risk verdict:
Updated score: 0.928.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.92 | Proof plan explicitly forbids broad React subscriptions and new hot-path wrapper allocation. |
| Slate-close unopinionated DX | 0.20 | 0.93 | High-risk pass preserves type-only Editor, pure data helpers, state / tx, editor.subscribe, and no product command sugar. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.94 | Proof plan requires extension namespace, deterministic operation replay, commit metadata, and headless subscription proof. |
| Regression-proof testing strategy | 0.20 | 0.94 | Proof plan now names unit/type, behavior, migration, docs/example, browser, and performance gates. |
| Research evidence completeness | 0.15 | 0.91 | No new research gap; deliberate pass uses refreshed research and live-source evidence. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.92 | Plan stays minimal and refuses compatibility/product-command surfaces. |
Plan delta from Pass 6:
Status: complete for Pass 7 only. Completion remains pending until the
closure pass runs.
Revision decisions:
Instance query methods are no longer a vague bridge.
BaseEditor where state/tx groups
cover the same read.read, update, subscribe, extend, and schema definition.Ref lifecycle helpers are deferred out of this plan.
pathRef, pointRef, rangeRef, and ref-set ownership need their own
focused design because they are live handles, not simple committed reads.editor.refs or another namespace while solving
the public Editor value cut.Initial value ergonomics are deferred out of this plan.
createEditor({ initialValue }) is not required to cut the public
Editor value.tx.value.replace(input).editor.subscribe policy is final for this plan.
editor.subscribe.Completion gates now match the accepted proof plan.
Updated score: 0.936.
| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.93 | Revision keeps React subscriptions out of scope and forbids render-path Editor.* reads. |
| Slate-close unopinionated DX | 0.20 | 0.95 | Instance-query policy is now explicit: state/tx is normal DX, pure data helpers stay, refs/initialValue are deferred. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.94 | Revision preserves substrate-only migration proof and rejects adapter promises. |
| Regression-proof testing strategy | 0.20 | 0.95 | Closure gates now require export/type, replacement, write-boundary, registry, grep, extension, replay, and subscription proof. |
| Research evidence completeness | 0.15 | 0.92 | No contradiction remains in the research layer for this namespace decision. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.93 | Revision avoids extra namespaces and keeps raw Slate minimal. |
Plan delta from Pass 7:
editor.subscribe as an advanced adapter bridgeStatus: complete. This Ralplan is ready for user review.
Closure verdict:
Editor value should be hard cut.Editor remains type-only.editor.read((state) => ...) and
editor.update((tx) => ...).tx.value.replace(input).EditorInterface dies.Editor.* editor-state reads/writes.Closure gate check:
| Gate | Result |
|---|---|
score at least 0.92 | pass: 0.936 |
no dimension below 0.85 | pass |
| pass-state ledger complete | pass |
| high-risk deliberate pass complete | pass |
| objection ledger rows accepted or revised into plan | pass |
| no public API maybe language | pass |
| Plate/slate-yjs migration-backbone answers | pass |
| public export/test/doc acceptance criteria named | pass |
| deferred scope explicit | pass |
| completion files synchronized | pass |
Final score: 0.936.
| Dimension | Weight | Final Score | Reason |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.93 | Plan avoids React hot-path churn and treats this cut as lifecycle/API cleanup, not runtime magic. |
| Slate-close unopinionated DX | 0.20 | 0.95 | One lifecycle, type-only Editor, pure data helpers, no product commands, no compatibility namespace. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.94 | Extension namespaces, deterministic operations, commit metadata, replay, and subscription bridge are preserved. |
| Regression-proof testing strategy | 0.20 | 0.95 | Export/type, replacement, write-boundary, registry, grep, extension, replay, subscription, and focused browser gates are named. |
| Research evidence completeness | 0.15 | 0.92 | Research layer is refreshed and no contradiction remains for this namespace decision. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.93 | Public surface stays minimal and composable without UI/product leakage. |
Plan delta from Pass 8:
pending to ready-for-reviewKeep:
editor.read / editor.update as lifecyclestate / tx as grouped editor-state accessEditorCommit as local runtime factCut:
txExternal evidence:
const editor = createEditor();
editor.read((state) => {
const text = state.text.string([]);
const selection = state.selection.get();
const isVoid = state.schema.isVoid(element);
});
editor.update((tx) => {
tx.value.replace({
children,
selection: null,
marks: null,
});
tx.text.insert("x");
tx.nodes.set({ type: "heading" }, { at: target });
tx.operations.replay(operations, { tag: "remote" });
});
Top-level exports:
createEditorisEditordefineEditorExtensionEditor, EditorStateView, EditorUpdateTransaction, and related
public typesNon-public or internal exports:
Editor valueEditorInterfacegetEditorTransformRegistrysetEditorTransformRegistryeditor.replace / editor.reset as public app-author methodsInternal runtime may keep:
createEditorreplaceSnapshot implementation used by tx.value.replace and
test seeding helpersInternal runtime must not leak:
getEditorTransformRegistry through packages/slate/src/core/index.tsEditor value through packages/slate/src/interfaces or root index barrelsPreferred implementation shape:
src/internal/editor-kernel.ts
src/internal/transform-registry.ts
src/test-utils/seed-editor.ts
The exact paths can change, but the public/private boundary cannot.
This lane does not change React render contracts directly.
Carry-forward law:
Editor value to read stateAny docs or examples touched by this lane must use:
const selected = useEditorState((state) => state.selection.get());
or a target-scoped hook, not a static Editor.* read.
Plate does not need current-version adapter support from this plan.
Plate needs:
state.<plugin> and tx.<plugin> extension groupsThe Editor static value cut helps Plate by removing another place plugins
could dump methods. Product commands should live in Plate extension layers, not
raw Editor.*.
slate-yjs does not need a current adapter fixture from this plan.
It needs:
tx.operations.replay(...)The public Editor value cut is acceptable if collaboration code can replay
through transaction APIs and use public type/data helpers without importing a
static editor-state namespace.
| Surface | Contract |
|---|---|
| Public export surface | Type test proves import { Editor } from 'slate' is not a value export while import type { Editor } works. |
| Public static namespace | Runtime/module test proves no public Editor.* static object exists from root package exports. |
| Transform registry | Export audit proves getEditorTransformRegistry and setEditorTransformRegistry are not public root exports. |
| Normal reads | Public behavior tests use editor.read((state) => state.*) for selection, text, nodes, schema. |
| Normal writes | Public behavior tests use editor.update((tx) => tx.*) for text, nodes, marks, selection, operations. |
| Document replacement | Public behavior test uses editor.update((tx) => tx.value.replace(input)); export/type tests prove Editor.replace, Editor.reset, editor.replace, and editor.reset are not normal public app APIs. |
| Tests/fixtures | Test helpers seed and inspect editors without public Editor.replace / Editor.getSnapshot dependence where possible. |
| Docs/examples | Grep guard blocks first-party user-facing docs/examples from teaching Editor.* editor-state reads or writes. |
No browser behavior should change from cutting a static namespace.
Browser proof is required only if implementation touches:
If examples are migrated, run focused example smoke plus the existing generated
stress family for any affected example. Full bun test:integration-local
remains a closure/release gate, not the first iteration gate.
| Lens | Applicability | Result |
|---|---|---|
| Vercel React best practices | applied | React 19.2 supports external stores and scheduling, but this API cut is mainly about keeping public access paths narrow. No new React work until implementation touches React files. |
| performance-oracle | applied | Static namespace removal reduces broad API surface but does not prove runtime speed. Implementation must avoid wrapper allocations on hot tx paths. |
| tdd | applied | First implementation slice should start with public export and public API contract tests before code changes. |
| build-web-apps:shadcn | skipped | No UI/editor chrome is being designed in this lane. |
| react-useeffect | skipped | No effects or browser subscriptions are changed by the plan itself. |
Trigger:
Blast radius:
/Users/zbeyens/git/slate-v2/packages/slate/src/Users/zbeyens/git/slate-v2/packages/slate-dom/src/Users/zbeyens/git/slate-v2/packages/slate-react/src/Users/zbeyens/git/slate-v2/site/examples/ts/Users/zbeyens/git/slate-v2/docsEditor valuePre-mortem:
BaseEditor and tx types reference
typeof Editor.* everywhere.EditorInternal, recreating the
same bad API under a worse name.Proof plan:
import type { Editor } works, value import fails in type tests or
export inventoryEditor.* editor-state useRollback/hard-cut answer:
Hard cuts:
export const Editorexport interface EditorInterface as public APIcore/transform-registryeditor.replace / editor.reset as app-author APIsEditor.* as app author APIEditorRuntime, EditorQuery, or
EditorStaticRejected alternatives:
Editor but remove write methods. Rejected because static reads still
split the mental model.Editor to EditorApi. Rejected because it preserves the shape with
worse legacy recognition.getEditorTransformRegistry public as an advanced escape hatch.
Rejected because it exposes the write kernel directly.replace/reset as direct editor methods. Rejected because replacement is
a write and belongs to the update transaction.| Change | Pain | Strong objection | Steelman antithesis | Tradeoff tension | Why keep | Evidence | Rejected alternative | Migration answer | Docs/example answer | Regression proof | Ecosystem answer | Verdict |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
Cut public Editor value | legacy Slate user, test author | "Editor.* is familiar Slate. Why delete the name people know?" | Familiarity lowers migration friction and preserves old docs examples. | More code churn and test helper work. | The rewrite already chose state / tx; keeping Editor.* makes that choice optional theater. | Editor value mixes reads/writes at /Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:798 and :1345. | Keep static reads only. | Use editor.read / editor.update; pure data namespaces stay. | Transform and editor docs teach the lifecycle once. | export contract plus docs/examples grep. | Plate/Yjs use substrate APIs, not static editor helpers. | keep |
Cut public EditorInterface | plugin author | "I need a stable interface to augment." | Interface augmentation can be convenient. | Extensions need new typed registration patterns. | Public EditorInterface currently exists to type the static value and leaks its mixed shape. | EditorInterface includes writes, reads, extension registration, replace/reset at /Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:798. | Keep interface but hide value. | Use Editor, EditorStateView, EditorUpdateTransaction, and extension group augmentation. | Extension docs show state / tx groups. | type tests for extension namespaces. | Plate gets namespace augmentation without monkeypatching editor methods. | keep |
| Hide transform registry | core/runtime maintainer | "Core needs easy access to transform methods." | A public registry is a simple escape hatch. | Internal imports need cleanup. | Public write-kernel access bypasses the whole transaction/public API story. | Root core barrel exports transform-registry at /Users/zbeyens/git/slate-v2/packages/slate/src/core/index.ts:10. | Document as internal advanced. | Core imports from internal path; apps use tx. | No public docs. | export audit prevents root access. | Collab replays through tx.operations.replay. | keep |
| Keep pure data namespaces | raw Slate user | "If Editor dies, should Node and Range die too?" | Consistency might suggest cutting every namespace. | Too much churn if pure helpers move. | Pure data namespaces are not editor-state access paths and do not split read/write lifecycle. | Root exports editor implementation and interfaces, but pure data helpers are separate concepts. | Move all helpers under editor state. | Keep pure helpers; move editor-state helpers into state/tx. | Docs explain data helpers vs editor lifecycle. | type/import tests. | Plate/Yjs keep path/range/node utilities. | keep |
| Cut public replacement helpers | app author, test author | "Replacing the whole document should be easy." | editor.replace(input) is short and test-friendly. | Public replacement through tx is more verbose; fixtures need helpers. | Replacement is a write and must share transaction tags, commit metadata, runtime ids, and listener behavior. | replaceSnapshot already runs with replace authority in /Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts:1717; tests use Editor.replace mostly as seed setup. | Keep editor.replace as advanced. | Use tx.value.replace(input) in app code and non-public seed helpers in tests. | Saving/loading docs show tx.value.replace. | state/tx replacement contract plus export guard. | Plate/Yjs can map remote/full reloads to transaction replacement without static helpers. | keep |
| Reclassify instance queries | app author | "editor.string([]) is shorter than editor.read((state) => state.text.string([]))." | Legacy Slate query helpers are familiar and compact. | More callback ceremony for simple reads. | One read lifecycle is the only way to stop stale reads and teach tx-local freshness. | BaseEditor currently exposes query helpers at /Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:271; state groups already expose text/nodes/points/ranges/schema. | Keep instance queries as normal public API. | Docs use state/tx; internal code can keep bridge helpers until migration. | Concepts explain state/tx reads once. | docs/examples grep plus state group tests. | Plate can build command sugar above state/tx without relying on instance query methods. | keep |
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| 1. Current-state read and initial score | complete | live Editor export, transform registry export, state/tx tests, research corrections | created this plan and corrected stale research wording | closure score below threshold | Pass 2 |
| 2. Intent/boundary and decision brief pressure | complete | live BaseEditor replace/reset/query surface; replaceSnapshot transaction authority; docs/examples Editor.replace usage | decided tx.value.replace, test seed helper boundary, and non-normal instance query status | closure score below threshold | Pass 3 |
| 3. Research and live-source refresh | complete | refreshed state/tx and read/update research; live source still lacks tx.value.replace; docs/examples still teach Editor.* | recorded target-vs-current gap and docs/example migration blocker | closure score below threshold | Pass 4 |
| 4. Performance/DX/migration/regression/simplicity pressure | complete | React 19.2 perf research; live docs/examples/test hit counts; getStateView / getUpdateView allocation shape; slate-browser gate scripts | tightened performance constraints, migration order, proof rows, and simplicity cuts | closure score below threshold | Pass 5 |
| 5. Slate maintainer and steelman ledger | complete | expanded steelman rows for public Editor, EditorInterface, transform registry, data namespaces, replacement, instance queries, subscribe bridge, and no-compat aliases | accepted seven decisions and revised instance queries into a closure blocker | none | Pass 6 |
| 6. High-risk deliberate pass | complete | public API/export/type blast radius; fixture/docs/example/collab failure scenarios; expanded proof plan | added proof matrix and split-execution remediation answer | none | Pass 7 |
| 7. Revision pass | complete | instance-query blocker resolution; ref and initial-value deferrals; subscribe bridge policy; substrate-only migration proof | removed maybe language and aligned closure gates | none | Pass 8 |
| 8. Closure score and final gates | complete | final scorecard, gate check, synchronized completion files | marked Ralplan ready for user review | none | none |
Added:
Editor value hard cutEditor value and public EditorInterfaceEditor, isEditor, state / tx, and
pure data namespacesRevised:
editor.updatetx.value.replace(input) plus non-public test seed helperstx.value.replace(input) is required
implementation work, not a live capability yetEditor.* usage reclassified as a closure blocker, not just
cleanupDropped:
Editor.*editor.replace / editor.reset as normal app APIsEditorRuntime / EditorStatic / EditorQuery compatibility namespaceStrengthened:
Deferred:
Deferred out of this plan:
pathRef, pointRef, rangeRef, and ref sets).
They are live handles and need a focused design. This plan does not introduce
editor.refs or another namespace.createEditor({ initialValue }) ergonomics. Initialization sugar is
not required for the public Editor value hard cut.Open plan questions:
Editor value hard cut.What would change the decision:
Editor value breaks the operation/collaboration
substrate in a way state / tx cannot solveCurrent expectation: none of those will hold.
Editor, type-only
Editor, and no transform-registry root export.typeof Editor.* type references with per-function/internal types.Editor.* usages with instance reads, state/tx, pure
helpers, or internal helpers.tx.value.replace(input) and migrate app-facing replacement examples
away from Editor.replace / editor.replace.state / tx group reads where available; leave
internal bridge methods only where source migration requires a separate
phase.Editor.* editor-state reads/writes.Editor value and transform registry do
not return.bun check, and completion-check.From /Users/zbeyens/git/slate-v2:
bun test ./packages/slate/test/public-field-hard-cut-contract.ts
bun test ./packages/slate/test/state-tx-public-api-contract.ts
bun test ./packages/slate/test/write-boundary-contract.ts
bun check
From /Users/zbeyens/git/plate-2:
bun run completion-check
Additional planned guards:
rg -n "export const Editor|interface EditorInterface|getEditorTransformRegistry|setEditorTransformRegistry" packages/slate/src
rg -n "import \\{[^}]*Editor|Editor\\." docs site/examples/ts packages/slate/test packages/slate-react/test
The second grep needs allowlists for type-only imports, pure legacy fixture snapshots during migration, and internal test helpers.
When this Ralplan reaches done, the handoff must list:
Editor value and EditorInterface cutsDo not set done until:
0.920.85active goal state is synchronizedbun run completion-check passes after status is set appropriately