docs/plans/2026-05-07-slate-v2-architecture-deslop-review-ralplan.md
Do not rewrite Slate v2 again.
The current architecture is the right backbone: Slate model/operations, a small
public editor instance, editor.read / editor.update, typed state / tx
extension namespaces, a runtime-owned React/input shell, deterministic commits,
and conservative browser proof. A whole rewrite would mostly re-open decisions
that the live source has already resolved.
The cleanest next move is bounded deslop, not architecture replacement:
Editor helper table ugly and explicitly internal;Editor.*, primitive writers, SlateSpacer, or command/chain
sugar as normal public Slate API;slate/internal only where it hides public
API expectations;Final answer: the architecture is strong enough to keep. Refactor only the remaining legacy-teaching seams and internal-helper test debt. No rewrite.
Intent:
Desired outcome:
In scope:
slate API surface;slate/internal helper boundary;slate-react render shell, selector hooks, runtime input ownership, and
Mobile/IME owner graph;Non-goals:
Decision boundaries:
slate/internal;Unresolved user-decision points:
Principles:
Top drivers:
Editor as type-only from
/Users/zbeyens/git/slate-v2/packages/slate/src/index.ts:6;BaseEditor exposes only read, subscribe, update, and extend at
/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:480;Editor value export at
/Users/zbeyens/git/slate-v2/packages/slate/test/public-surface-contract.ts:320
and :337;slate-react no longer exports SlateSpacer from the public package index,
and the surface test guards that at
/Users/zbeyens/git/slate-v2/packages/slate-react/test/surface-contract.tsx:204;useEditableRootRuntime and owner engines
at
/Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/runtime-root-engine.ts:106,
:199, :246, and :283.Viable options:
| Option | Verdict | Why |
|---|---|---|
| Whole rewrite again | reject | It would discard a now-good public lifecycle and re-open solved API/runtime decisions. |
| Keep architecture with no cleanup | reject | Internal helper usage in tests and docs can still blur the public story. |
| Add raw Slate command/chain sugar | reject | Tiptap-style sugar belongs in product layers such as Plate, not raw Slate core. |
| Keep architecture and run bounded deslop slices | choose | It preserves the proven substrate and targets only remaining ambiguity. |
Chosen option:
Consequences:
createEditor,
editor.read, editor.update, editor.extend, and defineEditorExtension;slate/internal, but only when explicitly proving
internals;| Dimension | Weight | Score | Evidence |
|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.94 | React is kept as projection, while useEditableRootRuntime owns stable runtime refs and engines in /Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/runtime-root-engine.ts:106, :188, :199, :246, and :283. Large-surface render budgets exist in /Users/zbeyens/git/slate-v2/playwright/stress/generated-editing.test.ts:221, :947, and :1020. |
| Slate-close unopinionated DX | 0.20 | 0.93 | The public editor instance is locked to extend, read, subscribe, and update in /Users/zbeyens/git/slate-v2/packages/slate/test/public-surface-contract.ts:132; root exports keep Editor type-only in /Users/zbeyens/git/slate-v2/packages/slate/src/index.ts:6. |
| Plate and slate-yjs migration-backbone shape | 0.15 | 0.94 | Extension namespaces are typed as state and tx groups in /Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:433, :441, and :477; current Plate/Tiptap command sugar is deliberately rejected as raw Slate law. |
| Regression-proof testing strategy | 0.20 | 0.93 | Public-surface guards, render-void contracts, runtime owner audits, generated stress rows, and Mobile/IME proof rows now exist. Exact device claims remain non-claims without matching artifacts. |
| Research evidence completeness | 0.15 | 0.94 | Fresh local source checks covered Lexical dirty/composition/update logic, ProseMirror transaction/replace-range strategy, and Tiptap extension/command DX. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.93 | renderVoid receives content-only props and runtime owns hidden DOM at /Users/zbeyens/git/slate-v2/packages/slate-react/test/surface-contract.tsx:437 and :485; public hooks expose selector-style APIs from /Users/zbeyens/git/slate-v2/packages/slate-react/src/index.ts:77. |
Weighted total: 0.94.
Keep:
slate;Editor;editor.read((state) => ...);editor.update((tx) => ...);defineEditorExtension plus typed state / tx extension groups;Cut from normal public API:
Editor value;SlateSpacer;slate/internal;| System | Source | Mechanism | Avoids | Steal | Reject | Slate target | Verdict |
|---|---|---|---|---|---|---|---|
| Lexical | /Users/zbeyens/git/lexical/packages/lexical/src/LexicalUpdates.ts:243 and :965 | read/update discipline, dirty leaves/elements, composition key, transform loop | whole-tree work and composition corruption | dirty runtime buckets, update tags, composition-specific proof | class node model and $function public style | Slate runtime dirty ids plus read / update lifecycle | agree |
| Lexical events | /Users/zbeyens/git/lexical/packages/lexical/src/LexicalEvents.ts:161 and :179 | root event table plus beforeinput when available | scattered browser policy | centralized input ownership and composition-aware event routing | copying Lexical command registry as raw public API | Slate root runtime and event engines | partial |
| ProseMirror | /Users/zbeyens/git/prosemirror/state/src/transaction.ts:26 and :67 | transaction carries doc changes, selection mapping, and metadata | stale selection after edits | tx-local selection/read authority and metadata | integer position model | Slate path/range tx with commit metadata | agree |
| ProseMirror replace | /Users/zbeyens/git/prosemirror/transform/src/replace.ts:334 | fit slices by target depth and schema constraints | broad post-hoc normalization after paste/replace | bulk fragment fitting strategy for large paste/replace | full schema-first identity | Slate fragment insertion fast paths with explicit proof | partial |
| Tiptap | /Users/zbeyens/git/tiptap/packages/core/src/Extension.ts:23 and /Users/zbeyens/git/tiptap/packages/extension-code-block/src/code-block.ts:187 | extension configs expose command DX over ProseMirror | raw engine complexity in app code | composable extension ergonomics for Plate | raw Slate editor.commands / chain().focus().run() | Plate owns product command sugar over Slate state/tx | diverge |
Strategy:
Accepted current shape:
createEditor, isEditor, defineEditorExtension, elementProperty;Editor;Node, Path, Point, Range, Element, Text,
Operation, Scrubber;read, update, subscribe, extend;Deslop target:
slate/internal;slate/internal
imports;Keep the runtime owner graph:
useEditableRootRuntime orchestrates root refs and engine wiring;Do not split runtime files merely because they are large. Split only if a test or review proves an owner violation, duplicated policy body, or hot-path subscription leak.
Keep:
useEditorSelector, useEditorState, useNodeSelector,
useTextSelector, useElementSelected;renderVoid;SlateElement, SlateText, SlateLeaf, and placeholder primitives as
normal public building blocks.Reject:
selected / focused void props;SlateSpacer.Plate should migrate product APIs onto:
state / tx extension groups;Raw Slate should not freeze current Plate editor.api / editor.tf or Tiptap
command/chain sugar as core law.
slate-yjs should use:
No current adapter support is required by this review.
ClawSweeper related-issue pass: skipped for this pass.
Reason: this review changes no Slate v2 implementation, no issue claim, no PR fixed/improved count, and no public behavior surface. Existing issue-ledger owners remain:
Issue matrix:
| Issue | Cluster | Claim | Why | Proof route | Live ledger sync | PR line |
|---|---|---|---|---|---|---|
| none | architecture review | Not claimed | No behavior changed in this pass. | no-code review | unchanged | unchanged |
Reference sync:
docs/slate-issues/gitcrawl-live-open-ledger.md: unchanged, no issue status
change.docs/slate-v2/ledgers/fork-issue-dossier.md: unchanged, no reviewed issue
section added.docs/slate-v2/ledgers/issue-coverage-matrix.md: unchanged, no fixed or
related issue row added.docs/slate-v2/references/pr-description.md: unchanged, no PR claim/API
text change.| Surface | Current proof | Deslop stance |
|---|---|---|
| Public core API | /Users/zbeyens/git/slate-v2/packages/slate/test/public-surface-contract.ts:132 and :337 | Keep; add only if a new public surface appears. |
| State/tx writes | /Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.ts:461 | Keep; do not add parallel public writers. |
| Void/render DX | /Users/zbeyens/git/slate-v2/packages/slate-react/test/surface-contract.tsx:437 and :485 | Keep; no public spacer. |
| Input/IME runtime | /Users/zbeyens/git/slate-v2/packages/slate-react/src/editable/runtime-root-engine.ts:199 and Mobile/IME browser rows | Keep; exact device claims need device proof. |
| Large rendering | /Users/zbeyens/git/slate-v2/packages/slate-react/test/rendering-strategy-and-scroll.tsx:386 and generated stress budgets | Keep; budgets can be made more release-grade later. |
Keep the existing strategy:
Future deslop should not delete browser rows just because they look repetitive. They are behavior locks.
| Lens | Applicability | Findings | Plan delta |
|---|---|---|---|
| Vercel React best practices | applied | React remains projection; runtime subscriptions are selector/source oriented. | No rewrite; split only proven owner leaks. |
| performance-oracle | applied | Dirty runtime ids, large-doc budgets, and staged/virtualized proof exist. | Add release-grade p95/p99 budgets later, not a rewrite. |
| performance | applied | Large repeated editor surfaces are in scope. | Keep cohort metrics; no broad perf claim without budget proof. |
| tdd | applied | Behavior locks exist for public API, render, runtime, Mobile/IME. | Deslop slices need focused regression proof before cleanup. |
| build-web-apps:shadcn | skipped | No UI app surface changed in this review. | No delta. |
| react-useeffect | applied | Runtime effects synchronize DOM/browser systems. | No effect rewrite without concrete leak. |
Triggered because the review covers public API, runtime, browser behavior, and migration backbone.
Failure scenarios:
Proof plan:
Rollback answer:
Hard cuts:
Editor static helper value;SlateSpacer;editor.commands / editor.chain;Rejected alternatives:
Editor.* as friendly docs syntax;| Change / stance | Likely objection | Answer | Verdict |
|---|---|---|---|
Keep slate/internal static Editor table | "This is legacy API hiding under a new path." | It is explicitly internal and not exported from root; tests and internals still need a helper table. Public guards reject root export. | keep |
| No whole rewrite | "The runtime is still big." | Size is not the failure criterion. Owner leakage, subscription fanout, or behavior regressions are. Existing runtime owner tests and browser rows are more valuable than churn. | keep |
| No command/chain sugar | "Tiptap DX is nicer." | Product sugar belongs in Plate. Raw Slate needs primitive, unopinionated state/tx. | keep |
| Bounded test deslop | "Tests using internal helpers look messy." | Internal tests may be messy when proving internals. Public API tests and docs should be stricter. | keep |
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| 1. Current-state read | complete | live public API, render DX, runtime owner graph, prior closed plans | created this plan | none | pass 2 |
| 2. Ecosystem research refresh | complete | Lexical, ProseMirror, Tiptap local source checks | added strategy table | none | pass 3 |
| 3. Deslop pressure | complete | public/internal helper grep, docs/test surface grep | chose bounded cleanup over rewrite | no code edits in this skill | pass 4 |
| 4. Risk and closure score | complete | scorecard, issue no-claim accounting, maintainer objections | set score 0.94 and closed review | none | user review / later Ralph if desired |
Added:
Dropped:
Strengthened:
slate/internal helper use is acceptable only as internal proof support.None for this review.
What would change the decision:
No implementation starts from this Ralplan. Later Ralph/deslop work, if desired:
slate/internal.For any later cleanup slice:
bun --filter slate test:node -- public-surface-contract.ts;bun --filter slate-react test:vitest -- surface-contract.tsx;bun --filter slate-react test:vitest -- kernel-authority-audit-contract.ts;Accepted decisions:
Editor; normal editor instance stays read,
update, subscribe, extend.tx; no public primitive writer exports.slate/internal static table as internal/test owner.renderVoid; runtime owns spacers and hidden
anchors.| Gate | Result |
|---|---|
score at least 0.92 | pass: 0.94 |
no dimension below 0.85 | pass |
| live source cited for current shape | pass |
| ecosystem synthesis complete | pass |
| issue ledger accounting explicit | pass: no claims changed |
| high-risk deliberate mode complete | pass |
| deslop scope bounded | pass |
| no implementation edits from Ralplan | pass |
| final user-review handoff available | pass |
Completion verdict: done.
Started: 2026-05-08T00:15:49+08:00.
Task statement:
Desired outcome:
Known facts / evidence:
0.94;Constraints:
slate/internal, public Editor.*
helper values, public primitive writers, SlateSpacer, or command/chain
sugar as normal raw Slate API;slate/internal
when proving internal behavior;site/out and .next output are not source owners.Unknowns / open questions:
Likely touchpoints:
.tmp/slate-v2/docs;.tmp/slate-v2/site/examples;Execution ledger:
| Time | Pass | Owner | Evidence | Result | Next |
|---|---|---|---|---|---|
| 2026-05-08T00:15:49+08:00 | deslop-pass | public docs/examples | completion state reset to pending; active goal state refreshed | started | grep source docs/examples and patch only real public-teaching drift |
| 2026-05-08T00:21:19+08:00 | deslop-pass | public docs/examples | removed slate/internal from .tmp/slate-v2/site/examples/ts/forced-layout.tsx; replaced stale Editor.children prose in .tmp/slate-v2/docs/libraries/slate-react/annotations.md; source grep clean; bun typecheck:site, public-surface contract, bun check, and focused forced-layout Playwright stress row passed | phase 1 complete | phase 2 test-helper audit |
| 2026-05-08T00:22:15+08:00 | deslop-pass | package tests | completion state moved to phase 2 | in progress | grep package tests for public-vs-internal helper ambiguity |
| 2026-05-08T00:29:59+08:00 | deslop-pass | package tests | moved public read/update and generic API tests off slate/internal; focused public/helper tests passed | phase 2 complete | phase 3 internal helper naming audit |
| 2026-05-08T00:29:59+08:00 | deslop-pass | internal helper naming | verified source uses InternalEditor internally and root slate exports Editor type-only; slate/internal keeps the compatibility alias | phase 3 complete, no code edit | phase 4 runtime owner audit |
| 2026-05-08T00:29:59+08:00 | deslop-pass | runtime owner audit | no failing runtime proof named an owner leak; existing kernel authority inventory remains the owner lock | phase 4 skipped by plan condition | phase 5 performance budget audit |
| 2026-05-08T00:29:59+08:00 | deslop-pass | performance budgets | repeated-surface render budgets already exist in .tmp/slate-v2/playwright/stress/generated-editing.test.ts; no extra budget edit needed for this cleanup slice | phase 5 complete, no code edit | release discipline and closeout |
| 2026-05-08T00:29:59+08:00 | debug + verification-sweep-pass | closeout | removed stale editor.operations from .tmp/slate-v2/packages/slate-dom/test/bridge.ts; refreshed classified escape-hatch counts; bun test:release-discipline, bun lint:fix, bun check, and focused forced-layout Playwright row passed in .tmp/slate-v2 | execution done | completion state done |